7K字带你深入浅出Docker技术

Docker是什么

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。

E506BFAF-560F-4AA3-95C6-98C604EC69B2_1_201_a.jpeg

容器技术

容器使软件应用程序与操作系统脱钩,从而为用户提供了一个干净而最小的Linux环境,同时在一个或多个隔离的“容器”中运行其他所有内容。容器的目的是启动一组有限的应用程序或服务(通常称为微服务),并使它们在独立的沙盒环境中运行。

这种隔离可防止在给定容器中运行的进程监视或影响在另一个容器中运行的进程。同样,这些容器化服务不会影响或干扰主机。能够将分散在多个物理服务器上的许多服务整合为一个的想法,是数据中心选择采用该技术的众多原因之一。

容器VS虚拟机

首先我们来看下面一张图,下面是虚拟机的实现方式

1630839530493_61cb27a5d0f2149a41488d8c9f932bf0.png

虚拟机实现资源隔离的方法是利用在主操作系统上运行独立的从操作系统,上图所示,在最底层运行的是我们所熟知的服务器,通常服务器上会运行一个主操作系统.

虚拟机的Guest OS即为虚拟机安装的操作系统,它是一个完整操作系统内核;虚拟机的Hypervisor层可以简单理解为一个硬件虚拟化平台,他负责协调宿主机上的硬件资源分配与管理.一个比较经典的虚拟机软件就是Parallels Desktop.

下面再来看看Docker的实现方式

1630839542476_a5f81adc1406b44da7941fa87b179b61.png

Docker容器技术的实现要比虚拟机技术的实现减少一层,由于Docker不需要Hypervisor实现硬件资源虚拟化,运行在Docker容器上的程序直接使用的都是实际物理机的硬件资源。因此在CPU、内存利用率上Docker将会在效率上有优势.

最后做一个简单的比较.

特性 容器 虚拟机
启动 秒级 分钟级
硬盘使用 一般为MB 一般为GB
性能 接近原生 弱于
系统支持量 单机支持上千个容器 一般是几十个

Docker的使用

可能有部分读者朋友们没有直接的使用过docker.我们先来举一个例子.在以前没有docker的情况下,假使我们想在linux环境下运行一个mysql,可能我们先要下载压缩包,解压,编译,设置等等流程,十分复杂.

但是当我们使用docker运行mysql时,只需要先运行docker pull mysql就会自动下载最新的mysql镜像.

再运行docker run mysql以后,就可以直接运行,如果你想再运行一份mysql实例,只需要再运行一次docker run的命令,并设置新的端口就能运行,这么一看是不是感觉docker是不是非常好用.

docker的功能还远远不止这些,接下来让我们一起走进docker,看看docker是怎么实现的容器技术.

Docker基础概念

运行流程

流程.jpeg

在Docker运行的流程图中,我们可以简单的把image理解为可执行程序,Container就是运行起来的进程。Registry就是代码管理平台.

那么写程序需要源代码,那么“写”image的"源代码"就是dockerfile,docker就是"编译器"。

因此我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置,之后把dockerfile交给“编译器”docker进行“编译”,生成"可执行程序"image,之后就可以运行这个image了,image运行起来后就是Docker container

Image(镜像)

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。 镜像不包含任何动态数据,其内容在构建之后也不会被改变。

首先来看一个比较简单的例子:

1625490629341_9e19801dc41a18fb83bf93c00a5db13f.png

上图是一个由debian系统作为基础镜像的简历样例,可以看到中间层就是基础的镜像,我们并没有对镜像进行任何定制化的操作,运行起来后就生成了一个容器,容器才是可写的对象.

对于Linux而言,内核启动后,会挂载root文件系统为其提供用户空间支持。而Docker镜像(Image),就相当于是一个root文件系统。

当然,Docker能实现的功能远不止如此,下面我们再来看看如何使用DockerFile构建一个定制化镜像:

 
 

sql

复制代码

FROM debian RUN apt-get install emacs RUN apt-get install apache2 CMD ["/bin/bash"]

1625490629343_a52602982fa713b466af861a0644f458.png

Docker设计时,充分利用Union FS的技术,将其设计为分层存储的架构。镜像实际是由多层文件系统联合组成。

镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。

分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

但是在构建镜像时也要格外的注意,比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。所以在构建镜像时,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

Container(容器)

容器 (container) 的定义和镜像 (image) 几乎一模一样,唯一区别在于容器的最上面那一层是可读可写的。

广义上我们可以将容器理解为,容器 = 镜像 + 读写层。

Repository(仓库)

镜像构建完成后,可以很容易的在当前宿主上运行,但是如何在其他服务器上运行这个镜像,那么我们就需要一个集中存放镜像文件的场所.这时就引出了Docker Repository的概念.

Docker Registry (仓库注册服务器)是一个集中的存储、分发镜像的服务。Docker 仓库的概念跟 Git 类似,注册服务器可以理解为 GitHub 这样的托管服务。实际上,一个 Docker Registry 中可以包含多个仓库 (Repository) ,每个仓库可以包含多个标签 (Tag),每个标签对应着一个镜像。所以说,镜像仓库是 Docker 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签.。

仓库又可以分为两种形式:

  • public(公有仓库)
  • private(私有仓库)

Docker Registry 公有仓库是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。

除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry 。Docker 官方提供了 Docker Registry镜像,可以直接使用做为私有 Registry 服务。当用户创建了自己的镜像之后就可以使用 push 命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 pull 下来就可以了。

Docker架构

流程图.jpeg

Docker是一个C/S模式的架构,后端是一个松耦合架构,模块各司其职。

  1. 用户的所有命令通过Docker ClientDocker Daemon建立通信,并发送请求给后者。
  2. Docker Daemon作为Docker架构中的主体部分,首先提供Server的功能使其可以接受Docker Client的请求;
  3. Engine执行Docker内部的一系列工作,每一项工作都是以一个Job的形式的存在。

在之前的基础概念中,我们已经了解了Registry,Container等概念.接下来就是一些Docker运行过程中的组件介绍.

Docker Client

  1. Docker Client是和Docker Daemon建立通信的客户端。用户使用docker命令后,Docker Client负责解析对应的命令以及参数,并向Docker Daemon服务端发起请求.
  2. Docker Client可以通过以下三种方式和Docker Daemon建立通信:
    1. tcp://host:port
    2. unix://path_to_socket
    3. fd://socketfd
  3. Docker Client发送容器管理请求后,由Docker Daemon接受并处理请求,当Docker Client接收到返回的请求相应并简单处理后,Docker Client一次完整的生命周期就结束了。一次完整的请求:发送请求→处理请求→返回结果,与传统的C/S架构请求流程一致.

Docker Daemon

image.png

Docker Daemon是docker的守护进程,也是docker运行时的核心.分别有两个部分组成.

  1. Docker Server
    1. Docker Server相当于C/S架构的服务端。功能为接受并调度分发Docker Client发送的请求。接受请求后,Server通过路由与分发调度,找到相应的Handler来执行请求。
    2. 在Server的服务过程中,Server在listener上接受Docker Client的访问请求,并创建一个全新的goroutine来服务该请求。在goroutine中,首先读取请求内容,然后做解析工作,接着找到相应的路由项,随后调用相应的Handler来处理该请求,最后Handler处理完请求之后回复该请求。
  2. Engine
    1. Engine是Docker架构中的运行引擎,通过执行Job的方式来管理所有的容器与镜像。
    2. Engine数据结构的设计与实现过程中,有一个handler对象。该handler对象存储的都是关于众多特定job的handler处理访问。举例说明,Engine的handler对象中有一项为:{“create”: daemon.ContainerCreate,},则说明当名为"create"的job在运行时,执行的是daemon.ContainerCreate的handler
  3. Job
    1. 一个Job可以认为是Docker架构中Engine内部最基本的工作执行单元。Docker可以做的每一项工作,都可以抽象为一个Job。无论是镜像的下载,容器的运行停止等等。Docker Server的运行过程实际也是一个Job,名为serveapi
    2. Job的概念与Unix中进程相仿。在Unix进程中,对每个进程都有名称,参数,环境变量,标准的输入输出,错误处理,返回状态等,在Docker的Job也都存在.

Graph

image.png

Graph中管理着所有本地已经下载的镜像.其中Graph DB中记录了所有镜像之间的依赖关系.

Driver

通过Driver驱动,Docker可以实现对Docker容器运行环境的定制,定制的维度主要有网络环境、存储方式以及容器执行方式。

image.png

Docker Driver的实现可以分为以下三类驱动:graphdriver、networkdriver和execdriver。

graphdriver

graphdriver主要用于完成容器镜像的管理,包括存储与获取。

image.png

graphdriver主要用于容器镜像的管理:

  1. 负责从Docker Registry下载镜像并进行存储,当用户下载指定的容器镜像时,graphdriver将容器镜像分层存储在本地的指定目录下.
  2. 负责从本地镜像存储目录中获取指定的容器镜像,并按特定规则为容器准备rootfs;
  3. 负责管理通过指定Dockerfile构建的全新镜像。

networkdriver

image.png

networkdriver的作用是完成Docker容器网络环境的配置,其中包括:

  1. Docker Daemon启动时为Docker环境创建网桥;
  2. Docker容器创建前为其分配相应的网络接口资源;
  3. Docker容器分配IP、端口并与宿主机做NAT端口映射,设置容器防火墙策略等

execdriver

image.png

execdriver作为Docker容器的执行驱动,负责创建容器运行时的命名空间,负责管理容器资源使用的统计与限制,负责容器内部进程的真正运行等

在Docker 0.9.0版本之前,只支持使用Linux的LXC驱动进行容器管理,在0.9.0版本之后默认使用native驱动实现,native驱动是docker项目下一个全新的子项目,去除了外部依赖.

libcontainer

image.png

ibcontainer是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的系统调用。

正是由于libcontainer的存在,才使得docker可以不需要依赖LXC或者其他包就可以完成对防火墙,namespaces等的操作。

Docker Container

Docker Container(Docker容器)是Docker架构中服务交付的最终体现形式。Docker通过DockerDaemon的管理,libcontainer的执行,最终创建Docker容器。Docker容器作为一个交付单位,功能类似于传统意义上的虚拟机(Virtual Machine),具备资源受限、环境与外界隔离的特点。

流程梳理

看到这里,我相信各位读者朋友们应该已经对Docker基础架构有了一个大概得认知了,让我们以docker run为例子回顾一下 Docker 各个组件是如何协作的。

假设我们要运行一条: docker run -p 3306:3306 --name mysql -d mysql

容器启动过程如下:

  • Docker 客户端执行 docker run 命令
  • Docker daemon 通过graghdriverGragh中拉取最新的 mysql 镜像
  • Docker daemon 通过networkdriver建立端口映射
  • Docker daemon 通过execdriver启动容器

Docker核心技术实现

在了解了这么多Docker实现以后,我们可能还会有最后一些疑问,Docker是如何实现资源隔离的.

在日常使用 Linux 或者 macOS 时,我们并没有运行多个完全分离的服务器的需要,但是如果我们在服务器上启动了多个服务,这些服务其实会相互影响的,每一个服务都能看到其他服务的进程,也可以访问宿主机器上的任意文件,这是很多时候我们都不愿意看到的,我们更希望运行在同一台机器上的不同服务能做到完全隔离,就像运行在多台不同的机器上一样。

Docker实现容器之间的完全隔离一共使用到了三大技术:

Namespaces(命名空间)

命名空间 (namespaces) 是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法.

进程隔离

Docker Deamon启动的初期,会通过setNamespaces函数去创建一个新的命名空间.在创建命名空间使用的clone函数中传入CLONE_NEWPID参数就完成容器对宿主机之间的进程隔离.

image.png

文件资源隔离

在创建命名空间使用的clone函数中传入CLONE_NEWNS参数,子进程即可得到父进程挂载的拷贝.

当容器创建时,容器需要一个自己的rootfs来实现与别的容器文件资源隔离,所以当Docker创建容器时,会将容器需要的目录进行挂载,并改变容器能访问的根目录,将容器之间的文件系统隔离.

改变容器能够访问个文件目录的根节点,libcontaine 提供的了pivot_root 或者 chroot 函数。

网络隔离

当 Docker 启动时,会自动在主机上创建一个 docker0 虚拟网桥,实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。

image.png

当创建一个 Docker 容器的时候,同时会创建了一对veth pair接口。这对接口一端在容器内,即是容器内部的eth0;

veth pair是成对出现的一种虚拟网络设备接口,一端连着网络协议栈,一端彼此相连,彼此联通的这端数据互通。

image.png

另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker就创建了在主机和所有容器之间一个虚拟共享网络。

Control Groups(控制组)

Control Groups(简称 CGroups)能够隔离宿主机器上的物理资源,CGroup提供以下这些功能.

  • 限制进程组可以使用的资源数量(Resource limiting )。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会触发OOM(out of memory)。

  • 进程组的优先级控制(Prioritization )。比如:可以使用cpu子系统为某个进程组分配特定cpu share。

  • 记录进程组使用的资源数量(Accounting )。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间

  • 进程组隔离(Isolation)。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间。

  • 进程组控制(Control)。比如:使用freezer子系统可以将进程组挂起和恢复

物理资源隔离

CGroup通过多个子系统来控制系统资源的分配.

我们可以使用lssubsys -m查看当前系统下CGroup对应的子系统目录.

  • cpuset /sys/fs/cgroup/cpuset
  • cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
  • blkio /sys/fs/cgroup/blkio
  • memory /sys/fs/cgroup/memory
  • devices /sys/fs/cgroup/devices
  • freezer /sys/fs/cgroup/freezer
  • net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
  • perf_event /sys/fs/cgroup/perf_event
  • pids /sys/fs/cgroup/pids

在宿主机内,首先当Docker启动时,会在上述的所有子系统下创建docker文件夹.

当Docker创建容器时,会在docker文件夹下的task子目录下创建pid对应的新文件对容器资源进行分配以及管控.

image.png

UnionFS(联合文件系统)

Union文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下

2017-11-30-docker-filesystems.png

在Docker中,每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所以需要一个文件系统对所有的文件进行管理.

镜像管理

在Docker中目前使用了多种文件系统对镜像进行管理,包括当前主流的overlay2,aufs等等.

不同的存储驱动在存储镜像和容器文件时也有着完全不同的实现,有兴趣的读者可以在 Docker 的官方文档Docker storage drivers 中找到相应的内容。

总结

看到这里相信各位读者朋友们对Docker已经有了更为深刻的理解.

由于Docker更新至今,代码库太过庞大,也只能从低版本的Docker源码以及大佬们的Docker技术文章中窥其一二,如果有感兴趣的朋友也可以相互交流.

猜你喜欢

转载自blog.csdn.net/wdj_yyds/article/details/131790289