Docker & Kubernetes
Docker
与虚拟机的对比
Docker 容器并非虚拟机,但可以比喻为更轻量级的虚拟机。使用虚拟机运行多个相互隔离的应用时(以下翻译参考自这里):
- 基础设施(Infrastructure) - 可以是个人电脑,数据中心的服务器,或者是云主机。
- 主操作系统(Host Operating System) - 运行的可能是 MacOS,Windows 或者某个 Linux 发行版。
- 虚拟机管理系统(Hypervisor) - 利用 Hypervisor(即上述的 ESXI),可以在主操作系统之上运行多个不同的从操作系统。
- 从操作系统(Guest Operating System) - 假设你需要运行 3 个相互隔离的应用,则需要使用 Hypervisor 启动 3 个从操作系统,也就是 3 个虚拟机。这些虚拟机都非常大,也许有 700MB,这就意味着它们将占用 2.1GB 的磁盘空间。更糟糕的是,它们还会消耗很多 CPU 和内存。
- 各种依赖 - 每一个从操作系统都需要安装许多依赖。如果你使用 Ruby 的话,应该需要安装 gems;如果使用其他编程语言,比如 Python 或者 Node.js,都会需要安装对应的依赖库。
- 应用 - 安装依赖之后,就可以在各个从操作系统分别运行应用了,这样各个应用就是相互隔离的。
使用 Docker 容器运行多个相互隔离的应用时:
- 基础设施(Infrastructure) - 同上
- 主操作系统(Host Operating System) - 所有主流的 Linux 发行版都可以运行 Docker。对于 MacOS 和 Windows,也有一些办法”运行” Docker。
- Docker 守护进程(Docker Daemon) - Docker 守护进程取代了 Hypervisor,它是运行在操作系统之上的后台进程,负责管理 Docker 容器。
- 各种依赖 - 对于 Docker,应用的所有依赖都打包在 Docker 镜像中,Docker 容器是基于 Docker 镜像创建的。
- 应用 - 应用的源代码与它的依赖都打包在 Docker 镜像中,不同的应用需要不同的 Docker 镜像。不同的应用运行在不同的 Docker 容器中,它们是相互隔离的。
虚拟机和 Docker 的一些对比:
Docker 守护进程可以直接与主操作系统进行通信,为各个 Docker 容器分配资源;它还可以将容器与主操作系统隔离,并将各个容器互相隔离。虚拟机启动需要数分钟,而 Docker 容器可以在数毫秒内启动。由于没有臃肿的从操作系统,Docker 可以节省大量的磁盘空间以及其他系统资源。但并不是说虚拟机就被取代了,因为两者有不同的使用场景。虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 Docker 通常用于隔离不同的应用,例如前端,后端以及数据库。
概念 | 描述 |
---|---|
Docker 镜像 (Images) | Docker 镜像是用于创建 Docker 容器的模板,比如 Ubuntu 系统镜像。 |
Docker 容器 (Container) | 容器是独立运行的一个或一组应用,是镜像运行时的实体。 |
Docker 客户端 (Client) | Docker 客户端通过命令行或者其他工具使用 Docker SDK 与 Docker 的守护进程通信。 |
Docker 主机 (Host) | 一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。 |
Docker Registry | Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。Docker Hub 提供了庞大的镜像集合供使用。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 < 仓库名 >:< 标签 > 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。 |
Docker Machine | Docker Machine 是一个简化 Docker 安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装 Docker,比如 VirtualBox、 Digital Ocean、Microsoft Azure。 |
Docker 指令
Docker 允许你在容器内运行应用程序, 使用 docker run
命令来在容器内运行一个应用程序:
# docker - Docker 的二进制执行文件。
# run - 与前面的 docker 组合来运行一个容器。
# ubuntu:15.10 - 指定要运行的镜像,Docker 首先从本地主机上查找镜像是否存在,如果不存在,Docker 就会从镜像仓库 Docker Hub 下载公共镜像。
# /bin/echo "Hello world" - 在启动的容器里执行的命令
# Docker 以 ubuntu15.10 镜像创建一个新容器,然后在容器里执行 bin/echo "Hello world",然后输出结果
$ docker run ubuntu:15.10 /bin/echo "Hello world"
也可以通过 docker 的两个参数 -i -t
(也可以写成 -it
),来实现命令行交互式运行,通过 exit
来退出容器。
- -t - 在新容器内指定一个伪终端或终端。
- -i - 允许你对容器内的标准输入 (STDIN) 进行交互
docker ps
可以来查看容器状态:
$ docker ps
CONTAINER ID IMAGE COMMAND ...
5917eac21c36 ubuntu:15.10 "/bin/sh -c 'while t…" ...
docker images
可以列出本地主机上的镜像:
# 同一仓库源可以有多个 TAG,代表这个仓库源的不同个版本
# 如 ubuntu 仓库源里,有 15.10、14.04 等多个不同的版本,我们使用 REPOSITORY:TAG 来定义不同的镜像。如 ubuntu:15.10
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 14.04 90d5884b1ee0 5 days ago 188 MB
php 5.6 f40e9e0f10c8 9 days ago 444.8 MB
nginx latest 6f8d099c3adc 12 days ago 182.7 MB
mysql 5.6 f2e8d6c772c0 3 weeks ago 324.6 MB
当我们在本地主机上使用一个不存在的镜像时 Docker 就会自动下载这个镜像。如果我们想预先下载这个镜像,我们可以使用 docker pull
命令来下载它。可以使用 docker search
命令来搜索镜像:
$ docker pull ubuntu:13.10
13.10: Pulling from library/ubuntu
6599cadaf950: Pull complete
23eda618d451: Pull complete
f0be3084efe9: Pull complete
52de432f084b: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:15b79a6654811c8d992ebacdfbd5152fcf3d165e374e264076aa435214a947a3
Status: Downloaded newer image for ubuntu:13.10
我们也可以自己通过 Dockerfile 创建适用的镜像,
docker push
可以推送到 Docker Hub。
容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过 -P
或 -p
参数来指定端口映射:
- -P - 容器内部端口随机映射到主机的高端口。
- -p - 容器内部端口绑定到指定的主机端口。
# 可以看到容器端口 5000 绑定主机端口 32768
$ docker run -d -P training/webapp python app.py
$ docker run -d -p 5000:5000 training/webapp python app.py
$ docker ps
CONTAINER ID IMAGE COMMAND ... PORTS NAMES
33e4523d30aa training/webapp "python app.py" ... 0.0.0.0:5000->5000/tcp berserk_bartik
fce072cc88ce training/webapp "python app.py" ... 0.0.0.0:32768->5000/tcp grave_hopper
Dockerfile
Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。下面以定制一个 nginx 镜像为例,在一个空目录下,新建一个名为 Dockerfile
文件,并在文件内添加以下内容:
FROM nginx
RUN echo '这是一个本地构建的 nginx 镜像' > /usr/share/nginx/html/index.html
我们先来看看 Dockerfile 支持的一些常用指令:
- FROM - 定制的镜像都是基于 FROM 的镜像,这里的 nginx 就是定制需要的基础镜像。后续的操作都是基于 nginx。
- RUN - 用于执行后面跟着的命令行命令。有以下俩种格式
- shell 格式 -
RUN <命令行命令>
,等同于在终端操作的 shell 命令 - exec 格式 -
RUN ["可执行文件", "参数1", "参数2"]
,如 RUN [”./test.php”, “dev”, “offline”] 等价于 RUN ./test.php dev offline
- shell 格式 -
- COPY - 从上下文目录中复制文件或者目录到容器里指定路径,路径不存在的话,会自动创建。
- ADD - 和 COPY 类似,可以支持自动解压,比如在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。但缺点是在不解压的前提下,无法复制 tar 压缩文件。目标路径>源文件>
- CMD - 和 RUN 类似,用于运行程序,但二者运行的时间点不同。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。
- CMD 在 docker run 时运行
- RUN 是在 docker build 时运行
- ENV - 设置环境变量,在后续的指令中可以使用这个环境变量。ARG 构建参数与 ENV 作用一致,不过作用域不一样,后者作用于 build 过程。构建好的镜像内不存在此环境变量。
- VOLUME - 定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。可以避免重要的数据,因容器重启而丢失,这是非常致命的。同时避免容器不断变大。
- WORKDIR - 指定提前创建好的工作目录。docker build 构建镜像过程中,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。
数据卷是一个可供一个或多个容器使用的特殊目录,特点是可以共享;修改会立马生效;更新不会影响镜像;默认会一直存在,即使容器被删除。数据卷的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中。创建数据卷命令
docker volume create my-vol
👈
注意:Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。例如:
FROM centos
RUN yum -y install wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN tar -xvf redis.tar.gz
以上执行会创建 3 层镜像。可简化为以下格式,以 &&
符号连接命令,这样执行后,只会创建 1 层镜像:
FROM centos
RUN yum -y install wget \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& tar -xvf redis.tar.gz
然后在 Dockerfile 文件的存放目录下,执行构建动作。即通过目录下的 Dockerfile 构建一个 nginx:v3(镜像名称:镜像标签):
# . 代表本次执行的上下文路径 - 是指 docker 在构建镜像,有时候想要使用到本机的文件(比如复制),docker build 命令得知这个路径后,会将路径下的所有内容打包。
$ docker build -t nginx:v3 .
由于 docker 的运行模式是 C/S。我们本机是 C,docker 引擎是 S。实际的构建过程是在 docker 引擎下完成的,所以这个时候无法用到我们本机的文件。这就需要把我们本机的指定目录下的文件一起打包提供给 docker 引擎使用。如果未说明最后一个参数,那么默认上下文路径就是 Dockerfile 所在的位置。
Docker Compose
Docker Compose 是用于定义和运行多容器 Docker 应用程序的工具,解决了容器与容器之间如何管理编排的问题。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。步骤如下:
- 使用 Dockerfile 定义应用程序的环境。
- 使用
docker-compose.yml
定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。 - 最后,执行
docker compose up
命令来启动并运行整个应用程序。
# yaml 配置实例
version: '3' # 指定本 yaml 依从的 compose 哪个版本制定的
services:
web:
build: . # 指定为构建镜像上下文路径
ports:
- "5000:5000"
volumes:
- .:/code
- logvolume01:/var/log
links:
- redis
redis:
image: redis
volumes:
logvolume01: {}
Compose 项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。
我们以这篇文章示例来看以下场景,用 Python 来建立一个能够记录页面访问次数的 web 网站。
# app.py
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host='redis', port=6379)
@app.route('/')
def hello():
count = redis.incr('hits')
return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
# dockerfile
FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]
# docker-compose.yml
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
redis:
image: "redis:alpine"
之后我们运行 compose 项目,此时访问本地 5000 端口,每次刷新页面,计数就会加 1:
docker compose up
Docker Machine
Docker Machine 是一种可以让您在虚拟主机上安装 Docker 的工具,并可以使用 docker-machine
命令来管理主机。Docker Machine 也可以集中管理所有的 docker 主机,比如快速的给 100 台服务器安装上 docker。Docker Machine 管理的虚拟主机可以是机上的,也可以是云供应商,如阿里云,腾讯云,AWS,或 DigitalOcean。使用 docker-machine 命令,您可以启动,检查,停止和重新启动托管主机,也可以升级 Docker 客户端和守护程序,以及配置 Docker 客户端与您的主机进行通信。
Docker Swarm
Docker Swarm 是 Docker 的集群管理工具。用户可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机,快速打造一套容器云平台。运行 Docker 的主机可以主动初始化一个 Swarm 集群或者加入一个已存在的 Swarm 集群,这样这个运行 Docker 的主机就成为一个 Swarm 集群的节点。swarm 集群由管理节点(manager)和工作节点(work node)构成:
- mananger - 负责整个集群的管理工作包括集群配置、服务管理等所有跟集群有关的工作。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为 leader,leader 通过 raft 协议实现。
- worker - 任务执行节点,管理节点将服务 (service) 下发至工作节点执行。管理节点默认也作为工作节点。你也可以通过配置让服务只运行在管理节点。
- 任务(Task)- 是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器。
- 服务(Services)- 是指一组任务的集合,服务定义了任务的属性。服务有两种模式:
- replicated services 按照一定规则在各个工作节点上运行指定个数的任务。
- global services 每个工作节点上运行一个任务
Kubernetes
Node & Pod
虽然官方已经提供了 Docker Swarm 来解决集群管理,但是目前业界更流行的是 Kubernetes(k8s)。Kubernetes 是 Google 团队发起并维护的基于 Docker 的开源容器集群管理系统。真正的生产型应用会涉及多个容器,这些容器必须跨多个服务器主机进行部署,因此可能会比较复杂。但 Kubernetes 有助于解决这一问题。Kubernetes 可以提供所需的编排和管理功能,以便您针对这些工作负载大规模部署容器。可以构建跨多个容器的应用服务、跨集群调度、扩展这些容器,并长期持续管理这些容器的健康状况。
建于 Docker 之上的 Kubernetes 可以构建一个容器的调度服务,其目的是让用户透过 Kubernetes 集群来进行云端容器集群的管理,而无需用户进行复杂的设置工作。系统会自动选取合适的工作节点来执行具体的容器集群调度处理工作。其核心概念是 Container Pod。一个 Pod 由一组工作于同一物理工作节点的容器构成。这些组容器拥有相同的网络命名空间、IP 以及存储配额,也可以根据实际情况对每一个 Pod 进行端口映射。此外,Kubernetes 工作节点会由主系统进行管理,节点包含了能够运行 Docker 容器所用到的服务。
- 节点(Node)- 一个节点是一个运行 Kubernetes 中的主机。
- 容器组(Pod)- 一个 Pod 对应于由若干容器组成的一个容器组,同个组内的容器共享一个存储卷(volume)。容器组是创建、调度、管理的最小单位。
- 容器组生命周期(pos-states)- 包含所有容器状态集合,包括容器组状态类型,容器组生命周期,事件,重启策略,以及 replication controllers。
- Replication Controllers- 主要负责指定数量的 pod 在同一时间一起运行。
- 服务(services)- 一个 Kubernetes 服务是容器组逻辑的高级抽象,同时也对外提供访问容器组的策略。
- 卷(volumes)- 一个卷就是一个目录,容器对其有访问权限。
- 标签(labels)- 标签是用来连接一组对象的,比如容器组。标签可以被用来组织和选择子对象。
- 接口权限(accessing_the_api)- 端口,IP 地址和代理的防火墙规则。
- web 界面(ux)- 用户可以通过 web 界面操作 Kubernetes。
- 命令行操作(cli)- kubectl 命令。
架构示意图
k8s 集群如下图:
- control-plane - 即控制平面。在这里,我们可以找到用于控制集群的 Kubernetes 组件以及一些有关集群状态和配置的数据。这些核心 Kubernetes 组件负责处理重要的工作,以确保容器以足够的数量和所需的资源运行。
- kube-apiserver - 是 Kubernetes 控制平面(control-plane)的前端,用于处理内部和外部请求。API 服务器会确定请求是否有效,如果有效,则对其进行处理。您可以通过 REST 调用、kubectl 命令行界面或其他命令行工具(例如 kubeadm)来访问 API。
- kube-scheduler - 调度程序会考虑容器集的资源需求(例如 CPU 或内存)以及集群的运行状况。随后,它会将容器集安排到适当的计算节点。
- kube-controller-manager - 控制器用于查询调度程序,并确保有正确数量的容器集在运行。如果有容器集停止运行,另一个控制器会发现并做出响应。控制器会将服务连接至容器集,以便让请求前往正确的端点。还有一些控制器用于创建帐户和 API 访问令牌。
- etcd - 配置数据以及有关集群状态的信息位于 etcd(一个键值存储数据库)中。etcd 采用分布式、容错设计,被视为集群的最终事实来源。
- kubelet - 每个计算节点(Node)中都包含一个 kubelet,这是一个与控制平面通信的微型应用。kubelet 可确保容器在容器集内运行。当控制平面需要在节点中执行某个操作时,kubelet 就会执行该操作。它是真正去运行这些 Pod 的组件。
- kube-proxy - 每个计算节点(Node)中还包含 kube-proxy,这是一个用于优化 Kubernetes 网络服务的网络代理。kube-proxy 负责处理集群内部或外部的网络通信——靠操作系统的数据包过滤层,或者自行转发流量。
其 master 和 node 的架构示意图如下:
接下来看个例子,看这些组件之间是如何相互沟通相互通信,协调来完成一次 Pod 的调度执行操作的:
- 用户可以通过 UI 或者 CLI 提交一个 Pod 给 Kubernetes 进行部署,这个 Pod 请求首先会通过 CLI 或者 UI 提交给 Kubernetes API Server,下一步 API Server 会把这个信息写入到它的存储系统 etcd,之后 Scheduler 会通过 API Server 的 watch 或者叫做 notification 机制得到这个信息:有一个 Pod 需要被调度。
- Scheduler 会根据它的内存状态进行一次调度决策,在完成这次调度之后,它会向 API Server report 说:“OK!这个 Pod 需要被调度到某一个节点上。”
- API Server 接收到这次操作之后,会把这次的结果再次写到 etcd 中,然后 API Server 会通知相应的节点进行这次 Pod 真正的执行启动。相应节点的 kubelet 会得到这个通知,kubelet 就会去调 Container runtime 来真正去启动配置这个容器和这个容器的运行环境,去调度 Storage Plugin 来去配置存储,network Plugin 去配置网络。
PaaS
PaaS 平台是一个通用的、基于 Web 的 Kubernetes 管理平台。通过可视化 Kubernetes 对象模板编辑的方式集成 CI/CD,降低业务接入成本,拥有完整的权限管理系统,适应多租户场景,是一款适合企业级集群使用的发布平台。更是整合了告警、日志、服务治理和分布式存储等强大功能,是一套完整的解决方案!
容器 Pod 是 k8s 中基本单位,负责装一个或多个容器。Node 实际上是对集群中服务器资源的抽象,一个 Node 可以是一台服务器 (实体的或者虚拟机),Node 上可以运行多个 Pod。总的来说通过将应用容器化,服务器资源抽象成 Node,k8s 负责将我的应用部署到 Node 上,并且能够在应用崩溃时自动重启等功能。
本身 k8s 是不提供 UI 界面的,PaaS 实际上是提供了一套 UI 界面让用户使用更加便利,并且提供了额外的功能。前端的应用部署在 PaaS 主要是执行了个 Nginx 服务,这个 Nginx 服务器存放了构建好的 html:
FROM harbor.paas.xxx.io/helper/compile:latest AS clone
ARG REPO
ARG BRANCHNAME
ARG DEPLOY_NAMESPACE
ENV COMMIT_ID VERSION
RUN mkdir -p /data/www
RUN url=VERSION && wget ${url} -q -O "/app.${url##*.}" && if [ "${url##*.}" = "gz"
];then mkdir -p /app && tar -xzvf /app.gz -C /app;fi
FROM harbor.paas.xxx.io/dev/centos7-openresty1158-dev:v1
RUN mkdir /data/log/nginx -p \
&& chown www -R /data/log/nginx
COPY --from=clone /app /data/www
EXPOSE 80
CMD [ "/opt/openresty/nginx/sbin/nginx", "-g", "daemon off;" ]
这是一个实际应用的 Dockerfile,可以看到里面逻辑实际上是从远程获取一个压缩包,然后把压缩包解压出来,并拷⻉到 nginx 的资源目录下,最后启动 nginx。随后我们会在 PaaS 平台里选择构建,这里会触发 PaaS 里的 Jenkins,根据 Dockerfile 构建生成镜像,并且上传镜像。当我们选择发布的时候,k8s 会根据我们选中的镜像,将其发布到集群中。这个过程概括下是对应的机器去拉取镜像,并且在本地启动实例 (一般用 Docker 去启动)。
关于 CICD 平台的内容可以查看这一章节 👈
参考链接
- 从零开始入门 K8s:详解 K8s 核心概念 By 李响