Docker笔记(5):Docker底层依赖的核心技术

1. 基础架构

Docker采用了标准的C/S架构,包括客户端和服务端两大部分,同时通过镜像仓库来存储镜像,架构如下图。客户端和服务端既可以运行在一个机器上,也可通过 socket或者 RESTful API来进行通信。

Docker笔记(5):Docker底层依赖的核心技术_第1张图片

(1)服务端

Docker服务端一般在宿主机后台运行,dockerd组件作为服务端接受来自客户的请求,并通过 containerd组件处理这些请求(创建、运行、分发容器)。服务端主要包括四个组件:

  • dockerd:为客户端提供RESTful API ,响应来自客户端的请求,采用模块化的架构,通过专门的Engine模块来分发管理各个来自客户端的任务,可以单独升级。
  • docker-proxy:dockerd 的子进程,负责配置容器的端口映射规则,只有当启动容器并且使用端口映射时候才会执行。
  • containerd:dockerd 的子进程,提供gRPC接口响应来自dockerd 的请求,管理 runC镜像和容器环境,可以单独升级。
  • containerd-shim:containerd的子进程,为runC容器提供支持,同时作为容器内进程的根进程 。

Docker服务端默认监听本地的unix: ///var/run/docker.sock套接字,默认只允许本地的root用户访问。可以通过-H选项来修改监听的方式。Ubuntu系统中,Docker服务端的默认启动配置文件在/etc/default/docker。

(2)客户端

Docker客户端则为用户提供一系列可执行命令,用户用这些命令实现与Docker服务端的交互。用户使用的Docker可执行命令即为客户端程序。与Docker服务端不同的是,客户端发送命令后,等待服务端返回,一旦收到返回后,客户端立刻执行结束并退出。用户执行新的命令,需要再次调用客户端命令。同样,客户端默认通过本地的unix: ///var/run/docker.sock套接字向服务端发送命令。如果服务端没有监听到默认套接字,则需要客户端在执行命令的时候显式指定。

(3)镜像仓库

镜像是使用容器的基础,Docker使用镜像仓库( Registry)在大规模场景下存储和分发镜像 。

2.命名空间

命名空间(Namespace)是Linux内核针对实现容器虚拟化而引入的一个强大特性。每个容器都可以拥有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。

在操作系统中,内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU等资源,所有的资源都是应用进程直接共享的。要想实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC等等的相互隔离。前者相对容易实现一些,后者则需要宿主主机系统的深入支持。

随着 Linux系统对于命名空间功能的逐步完善,Linux软件工程师已经可以实现上文所述的所有需求,让某些进程在彼此隔离的命名空间中运行。虽然,这些进程都共用一个内核和某些运行时环境(runtime,例如一些系统命令和系统库),但是彼此是不可见的,它们都认为自己是独占系统的。Docker容器每次启动时候,通过调用setNamespaces()方法来完成对各个命名空间的配置。

(1)进程命名空间

Linux通过进程命名空间管理进程号,对于同一进程(同一个task struct ),在不同的命名空间中,看到的进程号不相同 。每个进程命名空间有一套自己的进程号管理方法。进程命名空间是一个父子关系的结构,子空间中的进程对于父空间是可见的 。新fork出的一个进程,在父命名空间和子命名空间将分别对应不同的进程号。

例如,查看docker相关进程:

ps -ef |grep docker

img

进程号1553对应docker主进程,它作为父进程启动了 docker-containerd 进程,进程号为1654。启动一个ubuntu1604容器,执行如下命令,此时 , docker-containerd 进程作为父进程, 会为新建的容器启动一个 docker-containerd-shim 进程,作为该容器内所有进程的根进程。

img

进入容器内部查看容器内部的进程:

docker exec -it ubuntu1604 bash -c 'ps -ef'

img

在容器内的进程空 间中 则把 docker-containerd-shim 进程作为 0 号根进程,bash命令为1号进程,容器内只能看到 docker- containerd』iim 进程往下的子进程空 间,而无法获知宿主机上的进程信息。启动多个容器时,宿主机与容器内进程空间的关系如图。

Docker笔记(5):Docker底层依赖的核心技术_第2张图片

(2) IPC 命名空间

容器中的进程交互采用了Linux常见的进程间交互方法(Interprocess Communication,IPC),包括信号量、消息队列和共享内存等方式。PID命名空间和IPC命名空间可以组合起来一起使用,同一个 IPC 命名空间内的进程可以彼此可见,允许进行交互;不同空间的进程则无法交互 。

(3)网络命名空间

有了进程命名空间后,不同命名空间中的进程号可以相互隔离,但是网络端口还是共享本地系统的端口。通过网络命名空间,可以实现网络隔离。一个网络命名空间为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口、IPv4和IPv6协议栈、IP路由表、防火墙规则、sockets等,这样每个容器的网络就能隔离开来。

Docker采用虚拟网络设备(Virtual network device,VND)的方式,将不同命名空间的网络设备连接到起。默认情况下,Docker在宿主机上创建多个虚机网桥(如默认的网桥 docker0),容器中的虚拟网卡通过网桥进行连接。

使用 docker network ls 命令可以查看到当前系统中的网桥。

(4)挂载命名空间

类似于 chroot,挂载(Mount,MNT)命名空间可以将一个进程的根文件系统限制到个特定的目录下。挂载命名空间允许不同命名空间的进程看到位于宿主机中的本地文件,每个命名空间中的进程所看到的文件目录彼此是隔离的。例如,不同命名空间中的进程,都认为自己独占了一个完整的根文件系统(rootfs),但实际上,不同命名空间中的文件彼此隔离,不会造成相互影响,同时也无法影响宿主机文件系统中的其他路径。

(5) UTS 命名空间

UTS (UNIX Time-sharing System)命名空间允许每个容器拥有独立的主机名和域名 ,从

而可以虚拟出一个有独立主机名和网络空间的环境 ,就跟网络上一台独立的主机一样 。

如果没有手动指定主机名称,Docker 容器的主机名就是返回的容器 ID 的前 6 字节前缀。

docker inspect -f {{".Config.HostName"}} ubuntu1604

Docker笔记(5):Docker底层依赖的核心技术_第3张图片

(6)用户命名空间

每个容器可以有不同的用户和组id,可以在容器内使用特定的内部用户执行程序,而非宿主机上存在的用户。每个容器内部都可以有最高权限的 root 帐号,但跟宿主主机不在一个命名空间 。通过使用隔离的用户命名空间 ,可以提高安全性,避免容器内的进程获取到额外的权限;同时通过使用不同用户也可以进一步在容器内控制权限 。

3.控制组

控制组(CGroups)是 Linux 内核的一个特性,主要用来对共享资源进行隔离、限制、审计等 。只有将分配到容器的资源进行控制,才能避免多个容器同时运行时对宿主机系统的资源竞争。每个控制组是一组对资源的限制,支持层级化结构 。控制组提供如下功能:

  • 资源限制( resource limiting):可将组设置一定的内存限制 。 比如:内存子系统可以为进程组设定一个内存使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发 Out of Memory 警告。
  • 优先级( prioritization ):通过优先级让一些组优先得到更多的 CPU 等资源 。
  • 资源审计( accounting):用来统计系统实际上把多少资源用到适合的目的上,可以使用 cpuacct 子系统记录某个进程组使用的 CPU 时间 。
  • 隔离( isolation ):为组隔离命名空间,这样使得一个组不会看到另一个组的进程,网络连接和文件系统。
  • 控制( control ):执行挂起 ,恢复和重启动等操作 。

Docker 容器每次启动时候,通过调用 func setCapabilities() error 方法来完成对各个命名空间的配置 。 安装 Docker后,用户可以在/sys/fs/cgroup/memory/docker/目录下看到对 Docker 组应用的各种限制项,包括全局限制和位于子目录中对于某个容器的单独限制,用户可以通过修改这些文件值来控制组,从而限制 Docker 应用资源 。同时,可以在创建或启动容器时为每个容器指定 资源的限 制。

4.联合文件系统

联合文件系统(UnionFS )是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息作为一次提交,并层层叠加,同时可以将不同目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果 。 联合文件系统是实现Docker 镜像的技术基础 。

Docker 镜像可以通过分层来进行继承 。 例如,用户基于基础镜像来制作各种不同的应用镜像。 这些镜像共享同一个基础镜像层,提高了存储效率。此外,当用户改变了一个Docker 镜像(比如升级程序到新的版本),则会创建一个新的层( layer ) 。因此,用户不用替换整个原镜像或者重新建立,只需要添加新层即可 。 用户分发镜像的时候,也只需要分发被改动的新层内容(增量部分) 。这让 Docker 的镜像管理变得十分轻量和快速 。

5.Linux 网络虚拟化

Docker的本地网络实现其实就是利用了 Linux上的网络命名空间和虚拟网络设备(特别是veth pair)。

1.基本原理

要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)与外界相通,并可以收发数据包;此外,如果不同子网之间要进行通信,还需要额外的路由机制。Docker中的网络接口默认都是虚拟接口。虚拟接口的最大优势就是转发效率极高。这是因为 Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无须通过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并无区别,只是它的速度要快得多。Docker 容器网络就很好地利用了 Linux 虚拟网络技术,它在本地主机和容器内分别创建一个虚拟接口veth,并连通(这样的一对虚拟接口叫做 veth pair)。

2.网络创建过程

Docker创建一个容器的时候,会具体执行如下操作:

  1. 创建一对虚拟接口veth pair,分别放到本地主机和新容器的命名空间中;
  2. 宿主机一端的虚拟接口连接到默认的 docker0网桥或指定网桥上,并具有一个以veth开头的唯一名字,如veth1234;
  3. 容器一端的虚拟接口将放到新创建的容器中,并修改名字作为eth0。这个接口只在容器的命名空间可见。
  4. 从网桥可用地址段中获取一个空闲地址分配给容器的eth0(例如172.17.0.2/16),并配置默认路由网关为 docker0网卡的内部接口 docker0的IP地址(例如172.17.42.1/16)。

完成这些之后,容器就可以使用它所能看到的eth0虚拟网卡来连接其他容器和访问外部网络。用户也可以通过docker network命令来手动管理网络。

6.总结

Docker的优秀特性跟操作系统自身的支持,特别是Linux上成熟的已有容器技术支持是分不开的。在实际使用 Docker容器的过程中,还将涉及如何调整系统配置来优化容器性能,这些都需要有丰富的 Linux系统运维知识和实践经验。通过runC等更通用的容器运行时技术标准,Docker 目前已经可以移植到 Linux 之外的多种平台上,这将使得它的应用范围更为广泛。通过引人插件化组件(如网络插件),Docker 还可以支持更丰富的功能。

你可能感兴趣的:(Docker)