底层实现
Docker 底层的核心技术包括 Linux 上的命名空间(Namespaces)、控制组(Control groups)、Union 文件系统(Union file systems)和容器格式(Container format)。
传统的虚拟机通过在宿主机中运行 hypervisor 来模拟一整套完整的硬件环境提供给虚拟机的操作系统。虚拟机系统看到的环境是可限制的,也是彼此隔离的。这种直接的做法实现了对资源最完整的封装,但很多时候往往很良妃系统资源。
我们知道在操作系统,包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU等等,所有的资源都是应用进程直接共享。要想实现虚拟化,除了要实现堆内存、CPU、网络 IO、硬盘 IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC 等等的相互隔离。
随着 Linux 系统对于命名空间功能的完善实现,程序员已经可以实现上面的所有需求,让某些进程在彼此隔离的命名空间中运行。大家虽然都共用一个内存和某些运行时环境(例如一些系统命令和系统库),但是彼此却看不到,都以为系统中只有自己的存在。这种机制就是容器(Container),利用命名空间来做权限的隔离控制,利用 cgroups 来做资源分配。
基础架构
Docker 采用了 C/S 架构,包括客户端和服务端。Docker daemon 作为服务端接受来自客户的请求,并处理这些请求。客户端和服务端可以运行在一个机器上,也可以通过 socket 或者 RESTful API来进行通信。
命名空间
命名空间是 Linux 内核一个强大的特性,每个容器都有自己单独的命名空间,运行在其中的用用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。
pid 命名空间
隔离每个进程,不同的命名空间中可以存在相同的 pid。
net 命名空间
每个 net 命名空间有独立的网络设备,IP地址,路由表,/proc/net目录。Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一个 docker 网桥 docker0 连接在一起。
ipc 命名空间
容器中进程交互还是采用了 Linux 常见的进程间交互方法,包括信号量、消息队列和共享内存等。
mnt 命名空间
类似 chroot,将一个进程放到一个特定的目录执行。mnt 命名空间允许不同命名空间的进程看到的文件结构不同,这样进程对应的文件目录就被隔离开了。同 chroot 不同,每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point。
uts 命名空间
UTS(UNIX time-sharing System)命名空间允许每个容器拥有独立的 hostname 和 domain name,使其在网络上可以被视作一个独立的节点而非主机上的一个进程。
user 命名空间
每个容器可以有不同的用户和组 id,也就是说可以在容器内用容器内部的用户执行程序而非主键上的用户。
控制组
cgroup 是 linux 内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争。
控制组可以提供对容器的内存、CPU、磁盘 IO 等资源的限制和审计管理。
联合文件系统
联合文件系统是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。
联合文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像,可以制作各种具体的应用镜像。
另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时在加上自己独有的改动层,大大提高了存储的效率。
Docker 中使用的 AUFS(AnotherUnionFS) 就是一种联合文件系统。目前 Docker 支持的联合文件系统种类包括:AUFS、btrfs、vfs 和 DeviceMapper。
容器格式
最初,Docker 采用了 LXC 中的容器格式。Docker 从1.20版本开始支持新的 libcontainer格式,并作为默认选择。
网络实现
docker 的网络实现其实就是利用了 Linux 上的网络命名空间和虚拟网络设备(特别是 veth pair)。
基本原理
首先,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)来接受数据包;此外,如果不同子网之前要进行通信,需要路由机制。
Docker 中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接受接口的接受缓存中。对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是他不需要真正同外部网络设备通信,速度要快很多。
Docker 容器网络就利用了这项技术,它在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做 veth pair)。
创建网络参数
docker 创建一个容器的时候,会执行如下操作:
- 创建一对虚拟接口,分别放到本地主机和新容器中;
- 本地主机一端桥接到默认的 docker0 或指定网桥上,并具有一个唯一的名字,如 veth65f9
- 容器一端放到新容器中,并修改名字作为 eth0,这个接口只在容器的命名空间可见;
- 从网桥可用地址段中获取一个空闲地址分配给容器的 eth0,并配置默认路由到桥接网卡 veth65f9
完成这些之后,容器就可以使用 eth0 虚拟网卡来连接其他容器和其他网络。
可以在 docker run 的时候通过 --net 参数来指定容器的网络配置,有4个可选值:
- --net=bridge 这个是默认值,连接到默认的网桥
- --net-host 告诉 Docker 不要将容器网络放到隔离的命名空间中,此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。容器进程可以跟主机其他 root 进程一样可以打开低范围的端口,可以访问本地网络服务比如 D-bus,还可以让容器做一些影响整个主机系统的事情,比如重启主机。因此使用这个选项的时候要非常小心,如果进一步的使用
--privileged=true
,容器会被允许直接配置主机的网络堆栈。 - --net=container:NAME_or_ID 让 Docker 将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享 IP 地址和端口等网络资源,两者进程可以直接通过
lo
环回接口通信。 - --net=none 让 Docker 将新容器放到隔离的网络栈中,但是不进行网络配置。之后用户可以自己进行配置。
以下是通过 docker inspet
命令获取到的容器网络配置信息。
"NetworkMode": "default",
"PortBindings": {
"5000/tcp": [
{
"HostIp": "127.0.0.1",
"HostPort": "1234"
}
]
}
网络配置细节
用户使用 --net=none
后,可以自行配置网络,让容器达到跟平常一样具有访问网络的权限。通过这个过程,可以了解 Docker 配置网络的细节。