docker系列-docker.sock探究

预备知识

搞清楚/var/run/docker.sock参数的前提是了解docker的client+server架构,如下图

可见在电脑上运行的docker由client和server组成,我们输入docker version命令实际上是通过客户端将请求发送到同一台电脑上的Doceker Daemon服务,由Docker Daemon返回信息,客户端收到信息后展示在控制台上。

可是这个又跟docker.sock有什么关系呢?别急,我们再了解一下docker一些比较重要的组件

Docker 的主要组件

安装 docker ,其实是安装了 docker 客户端、dockerd 等一系列的组件,其中比较重要的有下面几个。

Docker CLI(docker)
docker 程序是一个客户端工具,用来把用户的请求发送给 docker daemon(dockerd)。

该程序的安装路径为:

1
/usr/bin/docker

Dockerd
docker daemon(dockerd),一般也会被称为 docker engine。

该程序的安装路径为:

1
/usr/bin/dockerd

Containerd
在宿主机中管理完整的容器生命周期:容器镜像的传输和存储、容器的执行和管理、存储和网络等。

该程序的安装路径为:

1
/usr/bin/docker-containerd

Containerd-shim
它是 containerd 的组件,是容器的运行时载体,主要是用于剥离 containerd 守护进程与容器进程,引入shim,允许runc 在创建和运行容器之后退出,并将 shim 作为容器的父进程,而不是 containerd 作为父进程,这样做的目的是当 containerd 进程挂掉,由于 shim 还正常运行,因此可以保证容器不受影响。此外,shim 也可以收集和报告容器的退出状态,不需要 containerd 来 wait 容器进程。我们在 docker 宿主机上看到的 shim 也正是代表着一个个通过调用 containerd 启动的 docker 容器。

该程序的安装路径为:

1
/usr/bin/docker-containerd-shim

RunC
RunC 是一个轻量级的工具,它是用来运行容器的,容器作为 runC 的子进程开启,在不需要运行一个 Docker daemon 的情况下可以嵌入到其他各种系统,也就是说可以不用通过 docker 引擎,直接运行容器。docker是通过Containerd调用 runC 运行容器的

该程序的安装路径为:

1
/usr/bin/docker-runc

从 hello world 开始

我们通过hello-world镜像分析来进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[vagrant@docker-host docker]$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

上面的输出信息指出,hello-world 容器的运行经历了如下四步:

  1. Docker 客户端向 docker daemon 发送请求
  2. Docker daemon 从 Docker Hub 上拉取镜像
  3. Docker daemon 使用镜像运行了一个容器并产生了输出
  4. Docker daemon 把输出的内容发送给了 docker 客户端

这是一个很抽象也很容器理解的过程,但是我们还想知道更多:docker daemon 是如何创建并运行容器的?
其实容器部分的操作和管理都被 dockerd 外包给 containerd 了,下图描述了运行一个容器时各个组件之间的关系:

进入正题

Docker Daemon 的连接方式

  1. UNIX 域套接字

    默认就是这种方式, 会生成一个 /var/run/docker.sock 文件, UNIX 域套接字用于本地进程之间的通讯, 这种方式相比于网络套接字效率更高, 但局限性就是只能被本地的客户端访问。

  2. tcp 端口监听

    服务端开启端口监听 dockerd -H IP:PORT , 客户端通过指定IP和端口访问服务端 docker -H IP:PORT 。通过这种方式, 任何人只要知道了你暴露的ip和端口就能随意访问你的docker服务了, 这是一件很危险的事, 因为docker的权限很高, 不法分子可以从这突破取得服务端宿主机的最高权限。

什么是unix socket?

unix socket可以让一个程序通过类似处理一个文件的方式和另一个程序通信,这是一种进程间通信的方式(IPC)。
当你在host上安装并且启动好docker,docker daemon 会自动创建一个socket文件并且保存在/var/run/docker.sock目录下。docker daemon监听着socket中即将到来的链接请求(可以通过-H unix:///var/run/docker.sock设定docker daemon监听的socket文件,-H参数还可以设定监听tcp:port或者其它的unix socket),当一个链接请求到来时,它会使用标准IO来读写数据。

docker.sock 是docker client 和docker daemon 在localhost进行通信的socket文件。
可以直接call这个socket文件来拉去镜像,创建容器,启动容器等一系列操作。(其实就是直接call docker daemon API而不是通过docker client的方式去操控docker daemon)。

官方说明

我们从Docker Daemon进行,查看它的官方文档

上图是Docker Daemon的配置参数

翻译过来就是:–host=[]指定Docker守护程序将在何处侦听客户端连接。如果未指定,则默认为/var/run/docker.sock

所以docker客户端只要把请求发往这里,daemon就能收到并且做出响应。

按照上面的解释来推理:我们也可以向/var/run/docker.sock发送请求,也能达到docker psdocker images这样的效果;

验证

1.查看镜像:

  • docker-cli方式:

    1
    2
    3
    4
    5
    6
    [vagrant@docker-host ~]$ docker images
    REPOSITORY TAG IMAGE ID CREATED SIZE
    spring_demo latest b7c0355e0a01 6 days ago 151MB
    nginx latest f35646e83998 2 weeks ago 133MB
    hello-world latest bf756fb1ae65 9 months ago 13.3kB
    ampregistry:5000/sng-biz-base-alpine 2.1.0 54c2c81fffbe 17 months ago 137MB
  • 请求到Docker Daemon:

    该请求返回的是json串,使用| jq .是为了格式化json,方便查看

    jq是linux里面的一个json格式化工具,如果没有安装可以去掉后面的| jq .代码,拷贝返回的json串手动格式化也可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    [vagrant@docker-host ~]$ curl -s --unix-socket /var/run/docker.sock http:/images/json | jq .
    [
    {
    "VirtualSize": 151316494,
    "Size": 151316494,
    "Containers": -1,
    "Created": 1603359433,
    "Id": "sha256:b7c0355e0a01f296e6ac94071b19c227839c8c19fecb2376dd8677ecbbe48017",
    "Labels": null,
    "ParentId": "sha256:4a5e4dd8fb964affb9750f01f66fb5f381988fcdbf8928902ce3f77b14fe990a",
    "RepoDigests": null,
    "RepoTags": [
    "spring_demo:latest"
    ],
    "SharedSize": -1
    },
    {
    "VirtualSize": 132861270,
    "Size": 132861270,
    "Containers": -1,
    "Created": 1602578384,
    "Id": "sha256:f35646e83998b844c3f067e5a2cff84cdf0967627031aeda3042d78996b68d35",
    "Labels": {
    "maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
    },
    "ParentId": "",
    "RepoDigests": [
    "nginx@sha256:ed7f815851b5299f616220a63edac69a4cc200e7f536a56e421988da82e44ed8"
    ],
    "RepoTags": [
    "nginx:latest"
    ],
    "SharedSize": -1
    },
    {
    "VirtualSize": 13336,
    "Size": 13336,
    "Containers": -1,
    "Created": 1578014497,
    "Id": "sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b",
    "Labels": null,
    "ParentId": "",
    "RepoDigests": [
    "hello-world@sha256:8c5aeeb6a5f3ba4883347d3747a7249f491766ca1caa47e5da5dfcf6b9b717c0"
    ],
    "RepoTags": [
    "hello-world:latest"
    ],
    "SharedSize": -1
    }
    ]

2.查看容器:

先运行一个nginx容器:docker run --name mynginx -p 80:80 -d nginx

  • docker-cli方式:

    1
    2
    CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
    919b34a22018 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:80->80/tcp mynginx
  • 请求Docker Daemon:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    [vagrant@docker-host ~]$ curl -s --unix-socket /var/run/docker.sock http:/containers/json | jq .
    [
    {
    "Mounts": [],
    "NetworkSettings": {
    "Networks": {
    "bridge": {
    "DriverOpts": null,
    "MacAddress": "02:42:ac:11:00:02",
    "GlobalIPv6PrefixLen": 0,
    "GlobalIPv6Address": "",
    "IPv6Gateway": "",
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "89d9f74c2884582696971298095e85c9c7df332a356c4b3a4dc09807eb6d457f",
    "EndpointID": "53d712df1ad9bd9093b8492c1abace5fe72785cd80189aa4c64d7652dff38490",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16
    }
    }
    },
    "HostConfig": {
    "NetworkMode": "default"
    },
    "Status": "Up 11 minutes",
    "State": "running",
    "Id": "919b34a2201822a04fb1160c1c2369580d7b9ef9e7f1e9c77ad4da8761649fb9",
    "Names": [
    "/mynginx"
    ],
    "Image": "nginx",
    "ImageID": "sha256:f35646e83998b844c3f067e5a2cff84cdf0967627031aeda3042d78996b68d35",
    "Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
    "Created": 1603879410,
    "Ports": [
    {
    "Type": "tcp",
    "PublicPort": 80,
    "PrivatePort": 80,
    "IP": "0.0.0.0"
    }
    ],
    "Labels": {
    "maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
    }
    }
    ]

如果去看看 Engine API,你会发现其它的请求也都是用类似方式发送的,更多API可以参考官方文档,目前最新的版本是v1.40:https://docs.docker.com/engine/api/v1.40/

至此,我们对docker的client、server架构有了清楚的认识:Docker Daemon相当于一个server,监听来自/var/run/docker.sock的请求,然后做出各种响应,例如返回镜像列表,创建容器。

更多精彩内容:mrxccc