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

【云原生•容器】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里:

go 复制代码
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服务使修改生效

go 复制代码
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和端口信息:

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

go 复制代码
[root@localhost ~]# export DOCKER_HOST="tcp://192.168.31.150:2370"

go 复制代码
注意:
-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指令可以创建运行容器,如下:

go 复制代码
docker run -d --name nginx_from_sdk -p 18080:80 nginx

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

go 复制代码
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又是怎么回事呢?下文我们继续聊起。

相关推荐
Sheffield11 小时前
Docker的跨主机服务与其对应的优缺点
linux·网络协议·docker
Lee川12 小时前
深度拆解:基于面向对象思维的“就地编辑”组件全模块解析
javascript·架构
勤劳打代码12 小时前
Flutter 架构日记 — 状态管理
flutter·架构·前端框架
AI攻城狮16 小时前
OpenClaw 里 TAVILY_API_KEY 明明写在 ~/.bashrc,为什么还是失效?一次完整排查与修复
人工智能·云原生·aigc
子兮曰17 小时前
后端字段又改了?我撸了一个 BFF 数据适配器,从此再也不怕接口“屎山”!
前端·javascript·架构
Sheffield19 小时前
Alpine是什么,为什么是Docker首选?
linux·docker·容器
卓卓不是桌桌20 小时前
如何优雅地处理 iframe 跨域通信?这是我的开源方案
javascript·架构
Qlly20 小时前
DDD 架构为什么适合 MCP Server 开发?
人工智能·后端·架构
马艳泽20 小时前
win10下运行Start Broker and Proxy报错解决
docker
阿里云云原生1 天前
零配置部署顶级模型!函数计算一键解锁 Qwen3.5
云原生