掘金 后端 ( ) • 2024-06-23 14:31

作为一个程序员,如果你想在不同的操作系统环境中安装Vim编辑器,需要执行不同的命令。例如,在Ubuntu中,你需要执行 apt-get install vim,而在CentOS中,则需要执行 yum install vim

安装一个小软件尚且如此复杂,如果你想将自己写的代码部署到不同操作系统的服务器上,所依赖的软件和配置就更多了。你需要针对每个环境单独编写一套部署脚本。这实在是让人头疼。

那么,有没有更好的解决方案呢? 当然有,没有什么问题是加一层中间层不能解决的。如果有,那就再加一层。这次,我们要加的中间层是Docker。

Docker可以帮助我们创建一个统一的环境,不论是在开发、测试还是生产阶段,都可以保持一致的配置和依赖。通过Docker,你可以打包你的应用程序及其所有的依赖项到一个容器中,然后在任何支持Docker的平台上运行这个容器。

这样,无论你在什么操作系统上部署,都无需担心环境差异导致的问题,大大简化了部署流程,提高了效率。Docker,作为我们解决这些问题的利器,值得每个程序员掌握和使用。

1711119585064.jpeg

准确来说是 Docker 容器

1711119585064.jpeg

Docker 是什么?

我们经常能听到程序员发出这样的感叹:"我的程序在我自己的环境里运行得很好,为什么到了你那里就出问题了呢?" 这里面涉及到两个关键概念 —— "程序"和"环境"。

程序是运行在操作系统之上的,而操作系统上装有各种版本的依赖库和配置,这些都是程序运行所必需的。我们将这些程序所依赖的元素统称为"环境"。

"环境"的设置是影响程序运行的关键因素,如果环境设置不一致,即使是同一份程序代码,运行结果也可能会大相径庭。这就是程序员们经常遇到的"在我这儿好使,到你那儿就不行"的原因。

因此,为了避免由于环境差异带来的问题,我们需要一个工具来统一管理和配置这些"环境",让程序在不同的设备和操作系统间也能保持一致性的运行效果。这就是Docker的主要作用,他能帮助我们创建一个统一的环境,让我们摆脱环境差异引发的困扰。

1711119585064.jpeg

程序的运行是依赖于特定环境的,如果环境发生变化,程序可能无法正常运行。想象一下,如果我们能够将程序和其依赖环境一起打包,然后将这个包交给其他人运行,那么问题不就迎刃而解了吗?

Docker就是这样一款工具软件,它能够将程序和其所需环境一同打包,方便分享和运行。这样一来,无论是在何种操作系统或设备上,只要有Docker,我们就能确保程序能够顺利运行。

那么Docker是如何做到这一点的呢?让我们一起探讨一下。

基础镜像是什么

如果环境不同,会导致程序运行的结果出现差异。因此,我们首先需要解决的核心问题,就是如何实现环境的统一

在众多环境元素中,最为关键的就是操作系统。比如,我们需要选择是使用 CentOS 还是 Ubuntu,然后确保所有的程序都在这个统一的操作系统上进行运行。另外,我们知道操作系统可以分为用户空间和内核空间,应用程序主要在用户空间运行。因此,我们并不需要完整的操作系统,只要能利用到操作系统的用户空间部分,就足以构建出满足应用运行的环境。

随后,我们需要统一程序语言的依赖环境。例如,如果要运行 Python 应用,就需要安装 Python 解释器;如果要运行 Java 应用,就需要安装 JVM;而如果需要运行 Go 应用,那么可能就不需要安装额外的环境了。

当我们确定了基础操作系统和程序语言后,就可以将它们的文件系统、依赖库、配置等相关内容一起打包,形成一个类似于压缩包的文件。这就是我们所说的基础镜像(Base Image)。

1711119585064.jpeg

Dockerfile 是什么

仅有基础镜像并不足够,我们在实际的操作中,往往还需要安装一些额外的依赖,比如执行yum install gcc命令来安装GCC,或者创建一些必要的文件夹。最重要的,我们需要运行我们的目标应用程序

由于在 Linux 系统中,所有的操作基本上都可以通过命令行来完成,因此我们可以将这些需要执行的操作按序列化的方式列出,形成一个类似待办事项清单(todo list)的文件。

这份待办事项清单,其实就是要求在基础镜像的基础上,按照列表中的顺序依次执行每一条命令。

这种方式的优势在于,它以清晰明了的形式,将需要执行的操作一一罗列出来,使得后续的操作过程更为简洁,易于管理。

# 指定基础镜像
FROM python:3.9

# 设置工作目录
WORKDIR /app

# 复制依赖文件到容器中
COPY requirements.txt .

RUN yum install gcc
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 将当前目录下的所有文件复制到容器的 /app 目录下
COPY . /app

# 设置容器启动时执行的命令
CMD ["python", "app.py"]


这个 Dockerfile 文件定义了一个 Docker 容器镜像的构建步骤。它从指定的基础镜像开始,逐步添加依赖和配置,最终准备好一个可以运行目标应用程序的环境。下面是每一行的详细解释:

# 指定基础镜像
FROM python:3.9

这一行指定了基础镜像为 Python 3.9 的官方镜像。基础镜像是构建容器的起点,包含了预安装的 Python 3.9 环境。

# 设置工作目录
WORKDIR /app

这一行设置了工作目录为 /app。所有后续的命令(如 COPYRUN)都会相对于这个目录执行。

# 复制依赖文件到容器中
COPY requirements.txt .

这一行将宿主机当前目录中的 requirements.txt 文件复制到容器的工作目录 /app 中。

RUN yum install gcc

这一行使用 yum 包管理器安装 GCC 编译器。GCC 可能是编译某些 Python 包所需的依赖。

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

这一行运行 pip 命令,按照 requirements.txt 文件中的内容安装 Python 依赖包。--no-cache-dir 参数用于防止 pip 缓存下载的包,从而减少镜像的大小。

# 将当前目录下的所有文件复制到容器的 /app 目录下
COPY . /app

这一行将宿主机当前目录中的所有文件复制到容器的 /app 目录中。这包括应用程序的源代码、配置文件等。

# 设置容器启动时执行的命令
CMD ["python", "app.py"]

这一行指定了容器启动时默认执行的命令。这里是运行 python app.py,即启动应用程序。CMD 指令允许在启动容器时覆盖命令,但在大多数情况下,容器会执行这里指定的命令。

综上所述,这个 Dockerfile 文件通过以下步骤来构建一个容器镜像:

  1. 从 Python 3.9 基础镜像开始。
  2. 设置工作目录为 /app
  3. 复制依赖文件 requirements.txt 到工作目录。
  4. 安装 GCC 编译器。
  5. 使用 pip 安装 Python 依赖包。
  6. 复制应用程序的所有文件到工作目录。
  7. 设置容器启动时运行 python app.py 以启动应用程序。

容器镜像 是什么

需要注意的是,Dockerfile 只是一份描述了我们需要完成哪些操作的指南,但它并不会立即开始执行这些操作。当我们通过命令行执行 docker build 命令时,Docker 程序会按照 Dockerfile 的指引,逐一构建我们所需的环境和应用程序。这个过程的最终成果就是将这一整套环境和应用程序打包成一个类似于“压缩包”的文件,也就是我们所说的容器镜像(container image)。

1711119585064.jpeg

只需将容器镜像上传至任意一台服务器,然后对该"压缩包"执行"解压"操作,我们就可以同时获得运行环境和程序的能力,这真是太完美了! 然而,此刻我们还面临一个问题,那就是如何将容器镜像有效地分发到众多服务器上呢?

Registry 是什么

当然,我们可以逐一将容器镜像传输到每一台服务器上,但这将给发送方的网络带宽带来极大压力。那是否有更优的解决方案呢?答案是肯定的。我们可以借鉴 GitHub 代码仓库的方式。通常情况下,我们会使用 'git push' 命令将代码上传到 GitHub,然后那些需要的人可以通过 'git pull' 命令将代码从 GitHub 拉取到自己的机器上。

1711119585064.jpeg

Docker同样采用了镜像仓库的方式,通过使用docker push命令,我们可以将镜像推送到存储库,在需要的时候,使用docker pull命令将镜像拉取到本地机器。负责处理这些镜像存储库的推送和拉取操作的服务,就是我们所说的Docker Registry。凭借Docker Registry的功能,我们可以构建各种官方或私有的镜像存储库。例如,官方的DockerHub或者是清华大学的Tuna等非官方镜像库。通常,大型公司内部也会有自己专用的镜像存储库。

1711119585064.jpeg

容器是什么

如今,我们已经解决了服务器间传输容器镜像的问题。我们可以在目标服务器上执行docker pull命令以获取容器镜像,然后再执行docker run命令来“解压缩”这个类似于压缩包的容器镜像,从而获得一个独立的环境和应用程序并进行运行。这样生成的独立环境和应用程序就构成了我们所称的“容器”。在一个操作系统上,我们可以同时运行多个这样的容器,而这些容器之间又是相互独立、相互隔离的。

1711119585064.jpeg

Docker 和虚拟机的关系?

看似熟悉,这个容器确实和我们使用vmware或kvm创建的传统虚拟机有些相似。然而,它们之间有一个关键的差异,那就是传统的虚拟机自带一个完整的操作系统,而容器并不包含完整的操作系统。实际上,容器的基础镜像只包含了操作系统的核心依赖库、配置文件以及其他必要组件。

容器通过利用名为Namespace的机制,使得它在运行时看起来就像是一个独立的操作系统。另外,容器还使用了一种名为Cgroup的技术来限制其可以使用的计算资源。这两种机制共同使得容器在运行时既有独立性,又能有效控制资源使用,从而表现出与虚拟机相似但更为高效的特性。

1711119585064.jpeg

所以说,容器本质上只是个自带独立运行环境的特殊进程,底层用的其实是宿主机的操作系统内核

1711119585064.jpeg

Docker 的架构原理

现在,让我们回到日常使用场景,聊聊 Docker 的架构原理。Docker 采用经典的客户端/服务器架构,其中客户端对应 Docker CLI,服务器对应 Docker Daemon。

当我们在命令行中输入 Docker 命令时,实际上就是在使用 Docker CLI 进行操作。Docker CLI 与 Docker Daemon 通信,Daemon 负责处理这些请求并执行相应的操作,从而实现容器的管理和运行。

1711119585064.jpeg

Docker-cli 会解析我们输入的 cmd 命令,然后调用 Docker daemon 守护进程提供的 RESTful API。接收到命令后,守护进程会根据指令进行创建和管理容器的操作。

更为详细地说,Docker Daemon 内部主要由 Docker Server 和 Engine 两个层次组成。Docker Server 本质上是一个 HTTP 服务,它的主要职责是提供对外的 API 接口,用于操作容器和镜像。当 Docker Server 接收到 API 请求时,它会将任务分发到 Engine 层。

Engine 层的主要职责是创建 Job,这些 Job 会负责实际执行各种任务。因此,整个 Docker 的运行流程是从 Docker-cli 输入命令开始,经过 Docker Daemon 的解析和分发,最终由 Engine 层的 Job 完成实际的运行任务。

1711119585064.jpeg

不同的 Docker 命令会执行不同类型的 Job 任务。

docker build

如果你执行的是 docker build 命令,Job 会根据 Dockerfile 的指令,类似于剥洋葱般一层又一层地构建容器镜像文件。

1711119585064.jpeg

docker pull/push

如果你执行的是 docker pull 或 push 等镜像推送或拉取操作,那么 Job 会与外部的 Docker Registry 进行交互,以上传或下载镜像。

1711119585064.jpeg

docker run

如果你执行的是 "docker run" 命令,Job 将基于镜像文件激活 containerd 组件,进而驱动 runC 组件创建并运行容器。

1711119585064.jpeg

Docker 到底是什么?

现在让我们重新理解这个概念,Docker 在本质上就是一个工具软件,它能够将程序和其运行环境打包并执行。更具体地说,Docker通过 Dockerfile 描述应用程序和其环境的依赖关系,使用 docker build 命令来构建镜像文件,通过 docker pull/push 命令与 Docker Registry 进行交互,实现镜像文件的存储和分发。而 docker run 命令则是基于镜像文件启动容器,并在该容器中运行程序及其相应的环境,从而有效解决了由环境依赖引发的各种问题。

1711119585064.jpeg

好的,至此,我们已经对 Docker 的基本架构和运行原理有了清晰的了解。 接下来,让我们探讨一下与 Docker 紧密相关的一些附属工具和技术。

Docker Compose 是什么?

现在我们已经知道 Docker 容器本身只是一个特殊的进程,但如果我们想要部署多个容器,并且对这些容器的启动顺序有一定要求,该怎么办呢?例如,一个博客系统需要先启动数据库,再启动身份验证服务,最后启动博客的 Web 服务。虽然可以逐个执行 docker run 命令来实现这一点,但是否有更优雅的解决方案呢?

答案是肯定的。我们可以通过一个 YAML 文件来清晰地定义要部署的容器、它们的启动顺序以及这些容器的资源分配(如 CPU 和内存等)。这种方法不仅更直观,而且更易于管理和维护。

version: "3.8"

services:
  A:
    image: "some-image-for-a"
    deploy:
      resources:
        limits:
          cpus: "0.50" # 限制 CPU 使用率为 50%
          memory: 256M # 限制内存使用量为 256MB

  B:
    image: "some-image-for-b"
    depends_on:
      - A

  C:
    image: "some-image-for-c"
    depends_on:
      - B

接着,我们只需通过一行 docker-compose up 命令,Docker 就会开始解析这个 YAML 文件,并按照指定的顺序一键部署所有的容器。这样我们就能非常便捷地完成一整套服务的部署。

这个过程,实际上就是 Docker Compose 所做的工作。Docker Compose 提供了一个简洁而高效的方式,让我们能够更方便地管理和运行多个容器。

1711119585064.jpeg

Docker Swarm 是什么?

Docker 的主要功能在于解决单个容器的部署问题。然而,当我们需要部署由多个容器组成的一整套服务时,Docker Compose 则能派上用场。

再往上走,我们就会遇到 Docker Swarm。Docker Swarm 是一个更高维度的解决方案,它主要解决的是这一整套服务在多台服务器上的集群部署问题。比如,当 A 服务器发生故障时,Docker Swarm 可以轻松地将服务迁移到 B 服务器上并重新部署一套。此外,Docker Swarm 还能根据需求对服务进行扩缩容,从而实现更加灵活和高效的资源管理。

1711119585064.jpeg

Docker 和 k8s 的关系是什么?

k8s它会在多台 Node 服务器上调度 Pod,进行部署和扩缩容。

1711119585064.jpeg

每个 Pod 内部可以含有多个 container,每个 container 本质上就是一个服务进程

1711119585064.jpeg

没错,Kubernetes(K8s)和 Docker Swarm 的功能确实很相似。实际上,Docker Swarm 是 Kubernetes 的竞争产品,因此它们的功能区别并不大。

回头看看 Docker 容器和 Kubernetes 之间的关系,思路会更加清晰。Docker 部署的容器,实际上就是 Kubernetes 调度的 Pod 内的容器。虽然它们都叫容器,本质上是相同的东西。只不过,Kubernetes 不仅支持 Docker 容器,还支持其他类型的容器。

Docker Compose 基于多个容器创建的一整套服务,其实就是 Kubernetes 中的 Pod。而 Docker Swarm 和 Kubernetes 做的事情本质上是一样的,都是在调度和管理 Pod。

再来看一下 Kubernetes 的官方定义,它是一个容器编排引擎。将其理解为以 API 编程的方式管理和安排各个容器的引擎,这个定义是不是就显得特别精辟了?

1711119585064.jpeg

现在,再回头看看 Docker 的图标,一个个集装箱放在一艘船上,这些集装箱代表着互相隔离的容器。而 Kubernetes(K8s)的图标是轮船的方向盘,寓意是 Kubernetes 控制着轮船的航向,实际上指的是调度容器。这一波联想非常形象地描述了它们各自的功能和角色。

1711119585064.jpeg

总结

  • Docker 本质上是一个将程序和环境打包并运行的工具软件,而 Docker 容器则是一个自带独立运行环境的特殊进程,底层实际上使用的是宿主机的操作系统内核
  • Docker 软件通过 Dockerfile 描述环境和应用程序的依赖关系,通过 docker build 构建镜像,通过 docker pull/push 与 Docker Registry 交互实现存储和分发镜像,并通过 docker run 命令基于镜像启动容器,从而在容器内运行程序及其对应的环境,解决环境依赖带来的各种问题。
  • Docker 解决的是单个容器的部署问题,Docker Compose 解决的是多个容器组成的一套服务的部署问题,Docker Swarm 解决的是多个容器组成的一套服务在多台服务器上的部署问题,而 Kubernetes (k8s) 是 Docker Swarm 的竞品,在更高层次上兼容了 Docker 容器,实现了容器的编排和调度。