【云原生•容器】Docker架构剖析,它还是从前那个Docker吗?
Docker架构
Docker采用client/server架构,客户端向服务器发送请求,服务器负责构建、运行和分发容器:
Docker架构说明:
我们日常使用各种docker命令,如docker run、docker pull等,其实就是在使用Docker客户端(Docker CLI);
客户端将用户输入指令解析发送到docker后端服务docker daemon;
比如是创建容器指令,daemon进程先去在本地镜像库中查找依赖的镜像文件(Image),如果不存在还需到远程仓库(Registry)拉取到本地;
镜像准备完成后,daemon进程根据用户指令参数就可以创建容器(Container)。
通信协议
Docker客户端和后端daemon进程默认运行在同一台服务节点上,并采用UNIX本地套接字方式进行通信,这样可以省略掉网络协议栈流程,性能和安全性更高。可以查看Docker启动日志:
从上面启动日志最后一行"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
❝2370是远程访问端口号,根据需要调整,默认使用2375端口。
2、重启Docker服务使修改生效
systemctl daemon-reload
systemctl restart docker
再查看docker启动日志:
从启动日志最后两行输出的信息来看,后端daemon进程将UNIX本地套接字和TCP两种网络监听方式都开启,下面就来看下有哪些方式可以远程访问Docker服务。
Rest API
Rest API方式通用性较高,基本所有服务进程都支持Rest API访问机制。比如之前使用 docker version 指令查看版本信息,现在可以直接通过浏览器或curl访问URL:http://192.168.31.150:2370/version
获取:
还比如 docker ps -a
指令查看容器信息,现在可以通过URL:http://192.168.31.150:2370/containers/json?all=true获取到:
之前通过docker cli方式执行的指令,现在都可以通过Rest API方式执行,具体Rest API接口及其参数可以查看官网:https://docs.docker.com/reference/api/engine/v1.43/ 。
Docker CLI
如果Docker CLI需要访问远端Docker服务,可以通过执行指令时添加 -H 或 --host 参数设置远端服务IP和端口信息:
如果不想在每次指令中都指定远程地址,也可以将其设置到环境变量中:
[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背后运行的机制,我重新给Docker绘制了张"自画像"可更好的反应Docker背后的真实面孔:
从上面架构图看较为复杂,涉及到组件包括: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的能力大不如从前。
OCI开发容器标准定义了容器运行时规范和容器镜像规范,那为啥又要搞出来个CRI规范呢?还有kubernetes最新版本已经弃用Docker又是怎么回事呢?下文我们继续聊起。