【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?

Docker架构

Docker采用client/server架构,客户端向服务器发送请求,服务器负责构建、运行和分发容器:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_容器技术

Docker架构说明:

  • 我们日常使用各种docker命令,如docker run、docker pull等,其实就是在使用Docker客户端(Docker CLI);
  • 客户端将用户输入指令解析发送到docker后端服务docker daemon;
  • 比如是创建容器指令,daemon进程先去在本地镜像库中查找依赖的镜像文件(Image),如果不存在还需到远程仓库(Registry)拉取到本地;
  • 镜像准备完成后,daemon进程根据用户指令参数就可以创建容器(Container)。

通信协议

Docker客户端和后端daemon进程默认运行在同一台服务节点上,并采用UNIX本地套接字方式进行通信,这样可以省略掉网络协议栈流程,性能和安全性更高。可以查看Docker启动日志:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_Docker_02

从上面启动日志最后一行"API listen on /run/docker.sock"可以看到daemon进程采用UNIX本地套接字,套接字文件为:/run/docker.sock。

注意:/var/run/docker.sock和/run/docker.sock是一致的,因为/var/run目录链接到/run目录下:

[root@docker01 docker]# ls -lt /var/run lrwxrwxrwx. 1 root root 6 6月 17 2020 /var/run -> ../run [root@docker01 docker]# ls -lah /run/docker.sock srw-rw----. 1 root docker 0 8月 27 18:25 /run/docker.sock

注意:从上面看到/run/docker.sock文件属主为root,用户组为docker,并且只有属主用户和组用户有rw读写权限,这就是为啥非root用户运行docker会出错,除非创建的用户添加到docker属组里。

如何在容器里使用Docker?

后端daemon进程套接字文件为/var/run/docker.sock,只需要将该文件挂载到容器中,同时这个容器里安装了docker客户端,那它就可以做任何我们在宿主机上docker命令可以做的事情。

下面案例演示下:创建容器并挂载 /var/run/docker.sock 文件,然后在容器里就可以运行docker命令了,比如创建容器,而且这个容器是创建在了宿主机上,而不是这个container里:

1、使用docker镜像创建一个容器
# docker container run --rm -it -v /var/run/docker.sock:/var/run/docker.sock docker:latest sh

2、在刚创建的容器里,使用docker ps可以正常与宿主机上后端daemon进程交互,查看到当前有1个正在运行中容器
/ # docker ps
CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS                                   NAMES
3e72e6a9563d   docker:latest   "docker-entrypoint.s…"   14 seconds ago   Up 14 seconds                                           tender_kilby

3、在容器里使用docker run命令创建一个新容器
/ # docker container run --rm -d busybox:latest ping 1.1.1.1
b250c29051e51cb429e15471ed301c0806a29cef1ffbf388c46861a7f538f4f9

4、在容器里重新查看运行中容器,刚新建的容器也显示出来
/ # docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED              STATUS              PORTS                                   NAMES
b250c29051e5   busybox:latest   "ping 1.1.1.1"           4 seconds ago        Up 4 seconds                                                stoic_agnesi
3e72e6a9563d   docker:latest    "docker-entrypoint.s…"   About a minute ago   Up About a minute                                           tender_kilby

5、退出容器,在宿主机上使用docker ps查看运行中容器,和容器中使用docker ps命令查看内容一致
/ # exit
# docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED          STATUS         PORTS                                   NAMES
b250c29051e5   busybox:latest   "ping 1.1.1.1"           10 seconds ago   Up 9 seconds                                           stoic_agnesi
3e72e6a9563d   docker:latest   "docker-entrypoint.s…"   11 minutes ago   Up 11 minutes                                           tender_kilby

容器里使用Docker一般用于容器监控、或者DevOps场景下,比如Jenkins是以容器方式部署运行,这时可能就需要在容器里构建镜像、自动基于构建镜像部署运行等。

从上面得知,Docker客户端和后端daemon进程默认运行在同一台节点,并采用UNIX本地套接字方式通信,因为这样避免复杂的网络协议栈流程,性能和安全性更高;它们还可以运行在不同服务节点上进行远程通信,后端daemon进程除了支持上面说的UNIX本地套接字外,还可以支持Socket方式,下面就来说下Docker服务端远程访问有哪些方式。

远程访问

Docker CLI和后端daemon进程默认情况下同服务器部署,采用 UNIX本地套接字方式通信,关闭了远程访问机制,下面介绍如何开启 Docker 远程访问。

1、编辑/usr/lib/systemd/system/docker.service

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_云原生_03

2370是远程访问端口号,根据需要调整,默认使用2375端口。

2、重启Docker服务使修改生效

systemctl daemon-reload
systemctl restart docker

再查看docker启动日志:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_容器技术_04

从启动日志最后两行输出的信息来看,后端daemon进程将UNIX本地套接字和TCP两种网络监听方式都开启,下面就来看下有哪些方式可以远程访问Docker服务。

Rest API

Rest API方式通用性较高,基本所有服务进程都支持Rest API访问机制。比如之前使用 docker version 指令查看版本信息,现在可以直接通过浏览器或curl访问URL:http://192.168.31.150:2370/version获取:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_Docker_05

还比如 docker ps -a 指令查看容器信息,现在可以通过URL:http://192.168.31.150:2370/containers/json?all=true获取到:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_容器技术_06

之前通过docker cli方式执行的指令,现在都可以通过Rest API方式执行,具体Rest API接口及其参数可以查看官网:https://docs.docker.com/reference/api/engine/v1.43/ 。

Docker CLI

如果Docker CLI需要访问远端Docker服务,可以通过执行指令时添加 -H 或 --host 参数设置远端服务IP和端口信息:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_Docker_07

如果不想在每次指令中都指定远程地址,也可以将其设置到环境变量中:

[root@localhost ~]# export DOCKER_HOST="tcp://192.168.31.150:2370"

注意:
-H参数也可以指定本地套接字文件,如:docker -H unix:///var/run/docker.sock ps -a
-H指定TCP协议,如:docker -H tcp://192.168.31.150:2370 ps -a,这种方式如果不指定端口,默认是2375,tcp://协议前缀可以省略。

SDK

对于开发来说,难免将docker和项目进行整合集成,官网提供SDK方式可以快速轻松地构建和扩展Docker应用,官网SDK支持Golang、Python,非官网支持语言较多,实在不支持的语言上面介绍过使用Rest API方式也很方便。

通过docker run指令可以创建运行容器,如下:

docker run -d --name nginx_from_sdk -p 18080:80 nginx

下面我们通过golang程序实现上述效果:

package main

import (
 "context"
 "fmt"
 "github.com/docker/go-connections/nat"
 "io"
 "os"

 "github.com/docker/docker/api/types/container"
 "github.com/docker/docker/api/types/image"
 "github.com/docker/docker/client"
)

func main() {
 ctx := context.Background()
 cli, err := client.NewClientWithOpts(
  client.FromEnv,
  client.WithAPIVersionNegotiation(),
  client.WithHost("tcp://192.168.31.150:2370"))
 if err != nil {
  panic(err)
 }
 defer cli.Close()

 imageName := "nginx"

 out, err := cli.ImagePull(ctx, imageName, image.PullOptions{})
 if err != nil {
  panic(err)
 }
 defer out.Close()
 io.Copy(os.Stdout, out)

 /**
 docker run -d --name nginx_from_sdk -p 18080:80 nginx
 */
 resp, err := cli.ContainerCreate(ctx, &container.Config{
  Image: imageName,
 }, &container.HostConfig{
  PortBindings: nat.PortMap{"80/tcp": []nat.PortBinding{{
   HostIP:   "0.0.0.0",
   HostPort: "18080",
  }}},
 }, nil, nil, "nginx_from_sdk")

 if err != nil {
  panic(err)
 }

 if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
  panic(err)
 }

 fmt.Println(resp.ID)
}

执行上述代码后,查看容器列表,发现已经按照指定信息创建对应容器:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_容器技术_08

核心架构剖析

上面官网Docker架构图较为粗糙,从业务功能上描述了大概流程,通过该架构图并不能真正了解Docker背后运行的机制,我重新给Docker绘制了张"自画像"可更好的反应Docker背后的真实面孔:

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_Docker_09

从上面架构图看较为复杂,涉及到组件包括:docker cli、daemon、containerd、containerd-shim、runc等,还涉及CRI、OCI两个容器运行时的协议规范,这套架构的背后还涉及到诸多历史原因,下面就来缕下它们的关系,可以对Docker的发展、容器技术的演进有更加清晰的认识。

2013年Docker强势崛起一下带火了容器技术,大家纷纷转入开始关注和使用容器技术,其中就包括Google这种科技巨佬,Google 开始希望和 Docker 公司合作共同推进一个中立的容器运行时(container runtime)库作为 Docker 项目的核心依赖。Docker公司当时风头正盛,这个提议显然会削弱自身的影响力,即使Google这种科技巨佬也没放在眼里,直接拒绝了这个提议,从此也开启了容器圈后续一系列政治斗争。

Google 紧接着联合 Red Hat、IBM 等几位巨佬认为运行时标准不能被 Docker 一家公司控制, 于是就撺掇着搞了开放容器标准(Open Contianer Initiative),简称 OCI。OCI 的提出意在将容器运行时和镜像的实现从 Docker 项目中完全剥离出来。这样做,一方面可以改善 Docker 公司在容器技术上一家独大的现状;另一方面也为其他玩家不依赖于 Docker 项目构建各自的平台层能力提供了可能。这就和之前Google希望和Docker合作共同推进一个中立的容器运行时库的诉求是一致的,合作不行那就联合其它大佬一起制定接口规范。

Docker公司迫于压力权衡后将 Libcontainer 捐出,并改名为 RunC 项目,作为OCI接口标准和规范的参考实现,其实OCI接口规范本身就是依据RunC项目制定的一套容器和镜像的标准和规范,毕竟当时Docker基本是容器生态的事实标准。

Docker最开始使用的容器运行时并没有自己开发,而是使用三方的LXC产品,但从0.9版本开始使用自己开发的Libcontainer取代LXC,LXC叫做LinuX Container,简称Linux的容器。

这还不够,为了彻底扭转 Docker 一家独大的局面,Google、Microsoft、IBM、Amazon、Red Hat等几位大佬又合伙成立了一个基金会叫 CNCF,全称 Cloud Native Computing Foundation(云原生计算基金会),CNCF 的目标很明确,在容器运行时基本处于被Docker垄断状态,Docker生态成为很多容器事实标准,那就换个赛道,Docker的致命问题就是更加偏向底层部署,缺乏平台化能力,对于生产环境下大规模部署稍显不足,CNCF基金会就从容器技术上层应用发力:基于容器部署,发展使用Kubernetes进行编排与调度,顺势发布了 Kubernetes 项目。

Docker 也并没有"坐以待毙",开始主动革新,Docker 从 1.1 版本起推动自身的重构,将容器运行时核心功能拆分到 Containerd 项目中,作为Docker的核心依赖,这样做也是为了推动 Docker Swarm 项目发展,以便在容器编排上重点发力,准备和kubernetes掰掰手腕,以提升Docker的平台化能力,结果直接被秒杀以惨败收场。后来又将Docker容器运行时核心依赖 Containerd 捐赠给 CNCF 社区,开始专注于自己商业化转型。至此,容器市场的格局发生重大改变,kubernetes成为容器圈的"新宠儿"。

经过Containerd、RunC等项目从Docker中分离出来形成一个个独立的开源项目,Docker中容器相关核心技术已被掏空,这也导致了Docker的影响力已被极大的削弱,见下图。如今容器已不再与Docker紧密耦合,我们可以使用Docker或者其他非Docker工具运行容器,Docker不再代表容器技术的发展。当前的Docker也是将Containerd、RunC等容器运行时核心依赖集成进来,自身主要关注的是上层功能封装集成、用户体验优化等,docker的能力大不如从前。

【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?(上)_Docker_10

OCI开发容器标准定义了容器运行时规范和容器镜像规范,那为啥又要搞出来个CRI规范呢?还有kubernetes最新版本已经弃用Docker又是怎么回事呢?下文我们继续聊起。