Docker架构
Docker采用C/S架构,通过socket/RESTful API进行通信。
Docker daemon
Docker daemon作为服务端守护进程,接受处理客户端请求并管理Docker对象,例如images、containers、networks和volumes,同时守护进程还可以与其他守护进程通信以管理Docker服务。
默认监听本地unix:///var/run/docker.sock
套接字,只允许本地root访问,通过-H来修改监听方式。
# 监听本地TCP 1234端口
$ sudo docker -H 0.0.0.0:1234 -d &
服务端默认启动配置文件在/etc/default/docker
。
Docker client
Docker客户端为用户提供一系列可执行命令,与Docker daemon交互,客户端发送命令后,等待服务端返回,一旦收到返回后,立刻执行并退出。
默认通过本地unix:///var/run/docker.sock
套接字向服务端发送命令,如果服务端没有监听默认套接字,需要在客户端显式指定:
docker -H tcp://127.0.0.1:1234 version
Docker registries
Docker registries用于存储Docker images,Docker Hub是任何人都可以使用的公共registry,Docker默认的registry是Docker Hub。也可以注册私人registry。
当使用docker pull/run
或docker push
命令时,所需的image将从配置的registry中下载或上传。
Docker object
Docker中的images、containers、networks、volumes、plugins等都称为Docker object。
images
image是一个只读的的模板,它包含了创建Docker container的一些指令等说明。通常情况下,一个image是基于另一个image创建的,加上了一些特殊的定制环境。例如基础ubantu image和基于其创建的具有apache web服务的ubantu image。
可通过Dockerfile的方式创建镜像,Dockerfile创建镜像可理解为一个分层模型,Dockerfile中每一条指令都会创建一个层,当改变Dockerfile重建镜像时,只讲对应的层重建即可,这也是image轻量化的一个原因。
container
容器是一个镜像的可运行实例,可通过客户端对其进行创建,启动,停止或删除等一系列操作。一个容器可以连接一个或多个网络、一个或多个存储设备,当然也可以基于其当前状态创建一个新的镜像。
docker run -i -t ubantu /bin/bash
此命令可以创建并启动一个ubantu容器,它将执行以下操作:
- 如果本地没有ubantu镜像,docker将会从registry中下载一个ubantu镜像,和执行
docker pull ubantu
命令一样; - 创建一个容器,和执行
docker container create
命令一样; - docker为容器申请一个可读写文件系统,作为最后一层,这可以让运行中的容器在其本地文件系统中创建或修改文件和目录。
- 由于没有指定任何网络操作,docker会创建一个网络接口将容器连接到默认网络,包括为容器指定IP,默认情况下,容器可以通过宿主机网络连接到外部网络。
- docker启动容器并执行/bin/bash。
-i -t
的意思是以交互的方式运行容器并连接到当前终端,可以通过当前终端输入。 - 当输入
exit
到/bin/bash
,容器将会停止但不会删除。可以再次启动或删除。
Docker核心概念
namespace
命名空间是linux实现容器虚拟化引入的特性。创建容器时,docker为其创建一组命名空间。每个容器都可以拥有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样,保证了容器之间彼此互不影响。
在操作系统中,包括内核、文件系统、网络、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。
这样看来,容器只是一种被限制了的特殊进程。
Docker、containerd、docker-shim、runC之间的关系
-
docker:docker本身而言包括了docker client和dockerd,dockerd实属是对容器相关操作的api的最上层封装,直接面向操作用户;
-
containerd:dockerd实际真实调用的还是containerd的api接口(rpc方式实现),containerd是dockerd和runC之间的一个中间交流组件,理论上,不运行dockerd,也能够直接通过containerd来管理容器。
向上为Docker daemon提供gRPC接口,使得Docker Daemon屏蔽下面的结构变化,确保原有接口向下兼容。向下通过containerd-shim结合runC,使得引擎可以独立升级。
-
docker-shim:真实运行容器的载体,每启动一个容器都会起一个新的docker-shim的进程。它通过指定三个参数:容器ID、bundle目录(containerd对应某个容器生成目录,一般位于:/var/run/docker/libcontainerd/containerID,其中包括了容器配置和标准输入、标准输出、标准错误三个管道文件),运行时二进制(默认是runC),来调用runC的api创建一个容器。
主要作用是:
- 它允许容器运行时(即 runC)在启动容器之后退出,简单说就是不必为每个容器一直运行一个容器运行时(runC)
- 即使在 containerd 和 dockerd 都挂掉的情况下,容器的标准 IO 和其它的文件描述符也都是可用的
- 向 containerd 报告容器的退出状态
有了它就可以在不中断容器运行的情况下升级或重启 dockerd,对于生产环境来说意义重大。
- runC:是 Docker 公司按照 OCI 标准规范编写的一个操作容器的命令行工具,其前身是 libcontainer 项目演化而来,runC 实际上就是 libcontainer 配上了一个轻型的客户端,是一个命令行工具端,根据 OCI(开放容器组织)的标准来创建和运行容器,实现了容器启停、资源隔离等功能。Docker默认提供了docker-runc实现,事实上,通过containerd的封装,可以在Docker Daemon启动的时候指定runc的实现。
使用runC运行busybox
# mkdir /container
# cd /container/
# mkdir rootfs
准备容器镜像的文件系统,从 busybox 镜像中提取
# docker export $(docker create busybox) | tar -C rootfs -xvf -
# ls rootfs/
bin dev etc home proc root sys tmp usr var
# 有了rootfs之后,我们还要按照 OCI 标准有一个配置文件 config.json 说明如何运行容器,
# 包括要运行的命令、权限、环境变量等等内容,runc 提供了一个命令可以自动帮我们生成
# docker-runc spec
# ls
config.json rootfs
# docker-runc run simplebusybox #启动容器
/ # ls
bin dev etc home proc root sys tmp usr var
/ # hostname
runc
当Docker daemon启动之后,dockerd和docker-containerd进程一直存在。当启动容器之后,docker-containerd进程会创建docker-containerd-shim进程。最后docker-containerd-shim子进程,已经是实际在容器中运行的进程。
moby、docker-ce与docker-ee的区别
最早的时候docker就是一个开源项目,主要由docker公司维护。
2017年年初,docker公司将原先的docker项目改名为moby,并创建了docker-ce和docker-ee。
这三者的关系是:
- moby是继承了原先的docker的项目,是社区维护的的开源项目,谁都可以在moby的基础打造自己的容器产品
- docker-ce是docker公司维护的开源项目,是一个基于moby项目的免费的容器产品
- docker-ee是docker公司维护的闭源产品,是docker公司的商业产品。
moby project由社区维护, docker-ce project是docker公司维护,docker-ee是闭源的。
要使用免费的docker,从网页docker-ce上获取。
要使用收费的docker,从网页docker-ee上获取。