Docker架构
Docker采用C/S架构,通过socket/RESTful API进行通信。
服务端
Docker daemon作为服务端守护进程,接受处理客户端请求(创建、运行、分发容器)
默认监听本地unix:///var/run/docker.sock
套接字,只允许本地root访问,通过-H来修改监听方式。
# 监听本地TCP 1234端口
$ sudo docker -H 0.0.0.0:1234 -d &
服务端默认启动配置文件在/etc/default/docker
。
客户端
Docker客户端为用户提供一系列可执行命令,与Docker daemon交互,客户端发送命令后,等待服务端返回,一旦收到返回后,立刻执行并退出。
默认通过本地unix:///var/run/docker.sock
套接字向服务端发送命令,如果服务端没有监听默认套接字,需要在客户端显式指定:
docker -H tcp://127.0.0.1:1234 version
Docker核心概念
namespace
命名空间是linux实现容器虚拟化引入的特性。每个容器都可以拥有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样,保证了容器之间彼此互不影响。
在操作系统中,包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU等资源都是应用进程直接共享的。要实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还需要实现文件系统、网络、PID、UID、IPC等的相互隔离。前者容易实现,而后者需要宿主机系统的深入支持。
而linux的命名空间功能可以支持实现上述的需求。
- 进程命名空间
linux通过命名空间管理进程号,对于同一进程(同一个task_struct),在不同的命名空间中,看到的进程号不相同,每个进程命名空间有一套自己的进程号管理方法。进程命名空间是一个父子关系的结构,子空间中的进程对于父空间是可见的。新fork出的进程在父命名空间和子命名空间将分别有一个进程号来对应
# 例如查看Docker主进程的pid的进程号是10751
docker ps |grep docker
root 10751 1 0 Feb02 ? 00:00:30 /usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled --log-driver=journald --signature-verification=false --storage-driver overlay2
# pid=1是init进程,其他所有进程最初都有它管理产生
# 新建一个mysql容器
docker run -itd -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 --name mysql-latest mysql
docker ps |grep docker
root 10751 1 0 Feb02 ? 00:00:33 /usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled --log-driver=journald --signature-verification=false --storage-driver overlay2
root 13159 10751 0 02:44 ? 00:00:00 /usr/libexec/docker/docker-proxy-current -proto tcp -host-ip 0.0.0.0 -host-port 3306 -container-ip 172.17.0.2 -container-port 3306
# 查看新建容器进程的父容器,正是Docker主进程10751
-
网络命名空间
一个网络命名空间为进程提供了一个完全独立的网络协议栈的视图,包括网络设备接口、IPv4和IPv6协议栈、IP路由表、防火墙规则,sockets等。这样每个容器的网络就能隔离开来。Docker采用虚拟网络设备的方式,将不同命名空间的网络设备连接到一起,默认情况下,容器中虚拟网卡将本地主机上的docker0网桥连接在一起。 -
IPC命名空间
容器中进程交互还是采用了linux常见的进程间交互方法,包括信号量、消息队列和共享内存等。PID命名空间和IPC命名空间可以组合起来一起使用,同一个IPC名字空间内的进程可以彼此可见,允许进行交互,不同空间的进程则无法交互。 -
挂载命名空间
类似chroot,将一个进程方到一个特定的目录执行。挂载命名空间允许不同命名空间的进程看到文件结构不同,这样每个命名空间中的进程所看到的文件目录彼此被隔离。 -
UTS命名空间
UTS(UNIX Time-sharing System)命名空间允许每个容器拥有独立的主机名和域名,从而可以虚拟出一个有独立主机名和网络空间的环境,就和网络上一台独立的主机一样。
默认情况下,Docker容器的主机名就是返回的容器ID:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fdd435874ee2 mysql "docker-entrypoint..." 2 hours ago Up 15 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp mysql-latest
- 用户命名空间
每个容器可以有不同的用户和组id,也就是说可以在容器内使用特定的内部用户执行程序,而非本地系统上存在的用户。
每个容器内部都可以有root账号,跟宿主主机不在一个命名空间。
控制组
命名空间隔离运行环境,使得容器中的进程看起来还想再地理的环境中运行,但光有环境隔离还不够,因为这些进程还是可以不受限制使用系统资源。为了防止进程占用太多资源而影响其他进程,Docker使用Cgroup对容器中的进程做限制,就是限制进程组使用的资源上限,包括 CPU,内存,磁盘,网络带宽。
控制组(CGroups)是Linux内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源,Docker才能避免多个容器同时运行时的系统资源竞争。
控制组的设计目标是为不同的应用情况提供统一接口,从控制单一进程到系统级虚拟化。
具体来看,控制组提供一下功能:
- 资源限制:组可以设置为不超过设定的内存限制。
- 优先级:通过优先级让一些组优先得到更多的CPU等资源。
- 资源审计:用来统计系统实际上把多少资源用到适合的目的上,可以使用cpuacct子系统记录某个进程组使用的CPU时间。
- 隔离:为组隔离名字空间,这样一个组不会看到另一个组的进程、网络连接和文件系统。
- 控制:挂起、恢复和重启动等操作。
安装Docker后可以在/sys/fs/cgroup/memory/docker/目录下看到对Docker组应用的各种限制项:
ls /sys/fs/cgroup/memory/docker
联合文件系统
在容器内,应该看到完全独立的文件系统,而且不会受到宿主机以及其他容器的影响。这个独立的文件系统,就叫做容器镜像。它还有一个更专业的名字叫 rootfs.rootfs 中包含了一个操作系统所需要的文件,配置和目录,但并不包含系统内核。因为在 Linux 中,文件和内核是分开存放的,操作系统只有在开启启动时才会加载指定的内核。这也就意味着,所有的容器都会共享宿主机上操作系统的内核。
在 PaaS 时代,由于云端和本地的环境不同,应用打包的过程,一直是比较痛苦的过程。但有了 rootfs ,这个问题就被很好的解决了。因为在镜像内,打包的不仅仅是应用,还有所需要的依赖,都被封装在一起。这就解决了无论是在哪,应用都可以很好的运行的原因。
不光这样,rootfs 还解决了可重用性的问题,想象这个场景,你通过 rootfs 打包了一个包含 java 环境的 centos 镜像,别人需要在容器内跑一个 apache 的服务,那么他是否需要从头开始搭建 java 环境呢?docker 在解决这个问题时,引入了一个叫层的概念,每次针对 rootfs 的修改,都只保存增量的内容,而不是 fork 一个新镜像。
即联合文件系统是一个轻量级的分层文件系统,它支持将文件系统中的修改信息作为一次提交,并层层叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。联合文件系统是实现Docker镜像的技术基础。镜像可以通过分层来进行继承。例如,用户基于基础镜像来制作各种不同的应用镜像。这些镜像共享同一个基础镜像层,提高了存储效率。此外,当用户改变了一个Docker镜像,则一个新的层会创建。因此,用户不用替换整个原镜像或重新建立,只需要添加新层即可。用户分发镜像的时候,也只需要分发被改动的新层内容。
层级的想法,同样来自于 Linux,一个叫 union file system (联合文件系统)。它最主要的功能就是将不同位置的目录联合挂载到同一个目录下。对应在 Docker 里面,不同的环境则使用了不同的联合文件系统。比如 centos7 下最新的版本使用的是 overlay2,而 Ubuntu 16.04 和 Docker CE 18.05 使用的是 AuFS.
可以通过 docker info 来查询使用的存储驱动:
docker info |grep 'Storage Driver'
Storage Driver: overlay2
当Docker利用镜像启动一个容器时,将利用镜像分配文件系统并且挂载一个新的可读写的层给容器,容器会在这个文件系统中创建,并且这个可读写的层被添加到镜像中。
网络实现
Docker的网络实现其实就是利用了linux上的网络命名空间和虚拟网络设备。
- 基本原理
直观上看,要实现网络通信,机器需要至少一个网络接口(物理或虚拟接口)与外界相同,并可以收发数据包;此外,如果不同子网之间要进行通信,需要额外的路由机制。
Docker中的网络接口默认都是虚拟接口。虚拟接口的最大优势就是转发效率极高。这是因为linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口缓存中,而无需通过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并无区别,只是它速度快很多。
Docker容器网络就很好地利用了linux虚拟网络技术。它让本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通。
- 网络创建过程
Docker创建一个容器的时候,会具体执行如下操作:
- 创建一对虚拟接口,分别放到本地主机和新容器的命名空间中。
- 本地主机一端的虚拟接口连接到默认的docker0网桥或指定网桥上,并具有一个以veth开头的唯一名字,如veth1234。
- 容器一端的虚拟接口将放到新创建容器中,并修改名字作为eth0。这个接口只在容器的命名空间可见
- 从网桥可用地址段中获取一个空闲地址分配给容器eth0,并配置默认路由网关为docker0网卡的内部接口docker0的IP地址。
完成这些之后,容器就可以使用它所能看到的eth0虚拟网卡来连接其他容器和访问外部网络。
容器的实质是进程,但与直接在宿主执行的实例进程不同,容器进程属于自己的独立的命名空间。因此容器可以拥有自己的root文件系统、自己的网络配置、自己的进程空间、甚至自己的用户ID。
这样看来,容器只是一种被限制了的特殊进程。