作为一种容器虚拟化技术,Docker深度应用了操作系统的多项底层支持技术。
早期版本的Docker是基于已经成熟的Linux Container(LXC)技术实现的。自Docker 0.9版本起,Docker逐渐从LXC转移到新的libcontainer上,并且积极推动开放容器规范runc,视图打造更通用的底层容器虚拟化库。
从操作系统功能上看,目前Docker底层依赖的核心技术主要包括Linux操作系统的:

  • 命名空间(Namespace)
  • 控制组(Control Group)
  • 联合文件系统(Union File System)
  • Linux网络虚拟化支持

    17.1 基本架构

    Docker目前采用了标准的C/S架构。客户端和服务端既可以运行在一个机器上,也可以运行在不同机器上,通过socket或RESTful API来进行通信。
    第17章 Docker核心实现技术 - 图1

    1. 服务端

    Docker Daemon一般在宿主主机后台运行,作为服务端接受来自客户的请求,并处理这些请求(创建、运行、分发容器)。
    在设计上,Docker Daemon是一个模块化的架构,通过专门的Engine模块来分发管理各个来自客户端的任务。Docker服务端默认监听本地的unix:///var/run/docker.sock套接字,只允许本地的root用户或docker用户组成员访问。可以通过 -H 选项来修改docker daemon监听的目标。例如,让服务端监听本地的TCP连接1234端口:
    1. docker daemon -H 0.0.0.0:1234
    此外,Docker也支持通过HTTPS认证方式来验证访问。

    unix:///var/run/docker.sock是Unix域套接字,是本机进程间通信的一种方式。创建一个Unix域套接字时,需要指定一个文件地址,对于docker来说,这个地址就是/var/run/docker.sock。由于docker daemon监听这个套接字,因此直接向这个套接字写入命令即可操作Docker。因此在本机启动用于Docker管理的Container时,都会看到这样的参数:-v /var/run/docker.sock:/var/run/docker.sock,即将本机的docker.sock挂在到容器中,从而使容器可以直接向本机Docker发送命令。 参考: 高级进程间通信之UNIX域套接字 Docker Tips: 关于/var/run/docker.sock

对于不同OS,docker daemon启动时的初始配置文件位置不同,对于使用systemd管理启动服务的系统,配置文件在 /etc/systemd/system/docker.service.d/docker.conf

2. 客户端

Docker客户端为用户提供一系列可执行命令,用户用这些命令与Docker Daemon交互。客户端默认通过本地的unix:///var/run/docker.sock套接字向服务端发送命令。如果服务端没有监听在默认的地址,则需要客户端在执行命令时显式指定服务端地址。

3. 新的架构设计

原架构的缺点:Docker Daemon责任太重,既然响应API请求,又要管理容器。
新架构:在1.11.0+之后的版本中,管理容器的任务呗放到了一个单独的组件containerd中,减少了daemon的工作。


17.2 命名空间

命名空间(namespace)是Linux内核的一个强大特性。每个容器都拥有自己单独的命令控件,运行在其中的应用都像是在独立的操作系统环境中一样。命名空间机制保证了容器之间资源隔离,彼此互不影响。


17.3 控制组

控制组(CGroups)是Linux内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。控制组可以提供对容器的内存、CPU、磁盘IO等资源进行限制和计费管理。
具体来看,控制组提供:

  • 资源限制:比如容器能使用的内存限制。
  • 优先级:CPU优先级。
  • 资源审计:用来统计系统实际上把多少资源用到适合的目的上。
  • 隔离:为组隔离命名空间,这样一个组不会看到另一个组的进程、网络连接和文件系统。
  • 控制:挂起、恢复和重启动等操作。

Docker的控制组相关信息保存在 /sys/fs/cgroup/memory/docker/ 目录下。如下图:
image.png
其中白色的文件为全局配置,修改这些文件会影响所有Container。蓝色的文件夹代表每个容器,其中包含各个容器的配置。但是不应修改其中的文件,只是查看容器状态时可以读取其中文件。
可以在创建或启动容器时为每个容器指定资源限制,例如:

  • -c | —cpu-shares=XXX 调整容器使用CPU的权重。
  • -m | —memory=XXX 调整容器使用内存的大小。

17.4 联合文件系统

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

1. Docker存储

一个Docker镜像自身是由多个文件层组成的,每一层有唯一的编号(层ID)。可以通过 docker history 查看一个本地镜像由哪些层组成。例:
image.png
构成Docker镜像的各个文件层内容都是只读的、不可修改的。而当Docker利用镜像启动一个容器时,将在镜像文件系统的最顶端再挂在一个新的可读可写层给容器。容器中的内容更新只会发生在该读写层。当需要修改底层文件层中的某个文件时,需要先将该文件复制到读写层,然后在读写层修改(写时复制,copy on write)。当修改的目标文件较大时,性能比较差。因此,一般推荐奖容器修改的数据通过Volume方式挂在,而不是直接修改镜像内的数据。
另外,对于频繁启停的Docker容器,文件系统本身的IO性能也十分关键。
具体来说,Docker相关的所有文件都存储在 var/lib/docker 目录下,里面包含Docker镜像和容器运行相关的所有文件。
image.png
而镜像文件系统相关的内容,则根据使用的文件系统的不同,存储在不同的文件夹下,即/var/lib/docker/{driver-name}。如果使用的是aufs(ubuntu常用),则在aufs文件夹下;如果是deveice mapper,则在devicemapper文件系统下。不同文件系统内,存储镜像文件系统的方式不同。但通常都有一个/mnt/目录,代表各个容器运行时的最终挂载点。

2. 多种文件系统比较

Docker支持多种联合文件系统,在启动时按照以下优先级选择:

  • overlay2
  • aufs
  • devicemapper
  • btrfs
  • vfs

17.5 Linux网络虚拟化

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

veth pair参考:Linux 虚拟网络设备 veth-pair 详解,看这一篇就够了

1. 基本原理

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

2. 网络创建过程

在使用docker run命令启动容器的时候,可以通过 --net 参数来指定容器的网络配置:

  • --net=bridge :默认值,在Docker网桥docker0上位容器创建新的网络栈。(网桥docker0相当于一台交换机,每个容器相当于一台电脑,各个容器通过交换机连接在一起)
  • --net=container:NAME_or_ID :让Docker将新建容器的进程放到一个已存在的容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源列表,但会和已存在的容器共享IP地址和端口等网络资源,两者进程可以直接通过lo环回接口通信。
  • --net=host :告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络。此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。这样做存在很大的安全风险。
  • --net=none :让Docker将新容器放到隔离的网络栈中,但是不进行网络配置。之后用户可以自行进行配置。
  • --net=user_defined_network :用户先自行用network相关命令创建一个网络,然后通过这种方式将容器链接到指定的已创建网络上。

    3. 手动配置网络