Docker 系列<二> Docker 架构


本文基于 docker 版本:

Client: Docker Engine - Community
Version: 19.03.8

Server: Docker Engine - Community
Version: 19.03.8

Cgroups & Namespace

在介绍 Docker 之前,我打算先说一下 linux 的 cgroups 和 namespace

Cgroups 是 Control Groups 的简称, 是 linux 内核提供的一个功能。它允许将进程分组,然后可以限制和监控各种资源的使用情况。

➜  ~ cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset  6       2       1
cpu     7       60      1
cpuacct 7       60      1
blkio   4       60      1
memory  10      82      1
devices 5       60      1
freezer 9       2       1
net_cls 12      2       1
perf_event      8       2       1
net_prio        12      2       1
hugetlb 11      2       1
pids    2       68      1
rdma    3       1       1

通过查看 cgroups 文件,我们可以看到 cgroups 可以管控的系统资源,包括 cpu、memory、io、网络等等。

也就是说,通过 cgroups,我们可以对一组进程进行内核资源管控。


Namespace 能够对全局的系统资源,如 pid ,网络,用户,主机名等,进行抽象隔离,这使得其中的进程看起来,就好像是有自己的全局资源一样,也意味着不同 namespace 中的进程,可以拥有一样的 pid ,端口。一个 namespace 中的进程,其所有的 “全局资源” 是对同 namespace 中的其他进程可见的,但对非 namespace 中的进程不可见。

我们可以查看 /proc/[pid]/ns 目录:

➜  ~ sudo ls -lh /proc/16310/ns
[sudo] password for dylan:
total 0
lrwxrwxrwx 1 root root 0 Apr 18 02:30 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Apr 18 02:30 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Apr 18 02:30 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Apr 18 02:30 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 Apr 18 02:30 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Apr 18 02:30 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Apr 18 02:30 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Apr 18 02:30 uts -> 'uts:[4026531838]'

同样能看到,ipc, mnt, net, pid 等公共资源的抽象。


总结来说, namespace 让你能用什么, cgroups 让你能用多少。

Docker 架构

Docker Engine Components Flow

这是一张 Docker 引擎的功能描述图。通过这张图,我们能看到 Docker 的基本结构划分以及包含的功能:

  • cli : docker 提供的命令行交互工具,通过 cli 提供的命令,可以管理网络(network)、容器(container)、镜像(image)、数据卷(data volume)
  • rest api : 提供 HTTP API 接口给 cli
  • server :我们常说的 docker daemon 守护进程,这是一个长期运行的进程,会处理来自 cli 的请求

Docker Architecture Diagram

这张 Docker 的基础架构图,可以很好的告诉我们它的运行机制:

  1. client 端通过 cli 执行 docker build/pull/run... 等命令,发送给 dockerd(docker daemon , 后简称为 dockerd) 。 cli 和 dockerd 之间可以通过 rest api 连接。 支持的连接协议有: linux 中分别为 tcp, unix, fd, windows 中为 tcp, npipe (源码: daemon/listeners::Init)
  2. dockerd 收到命令后(假如命令为 docker run nginx) , dockerd 一看本地并没有 nginx 镜像,就会去 docker registry 拉取 nginx 镜像(这里没有指定 nginx 版本,则会拉取 latest 版本)
  3. 拉取 nginx 镜像后,则会创建 nginx 容器的运行时。

通过 docker info 命令,可以看到它的详细配置,其中有一个 Docker Root Dir ,为 docker 所有文件包括 image、container、overlay 等的存储位置,默认为 /var/lib/docker 。如果要修改,可以通过在 /etc/docker/daemon.json 中增加 "graph":"YOUR_PATH" 然后重启 dockerd 即可(注意要做好数据备份工作!!!):

{
  "registry-mirrors": ["https://xxx.mirror.aliyuncs.com"],
  "graph": "YOUR_PATH"
}

Docker 各组件架构

我们以一个简单的流程,通过串联 Docker 源码,来讲述 Docker 的整个工作流程。

Dockerfile

Dockerfile , 包括 Docker Cli Command 等内容,会放到下一篇文章。这里我们先准备一个异常简单的 Dockerfile, 同时把这个 Dockerfile 命名为 dockerfile.min :

From alpine
CMD echo "Hello From iyuhp"

docker build

之后进行构建 :

➜  docker docker build . -t test/min:v0.1 -f ./dockerfile.min
Sending build context to Docker daemon  1.448MB
Step 1/2 : From alpine
 ---> a187dde48cd2
Step 2/2 : CMD echo "Hello From iyuhp"
 ---> Running in 392cd9c26109
Removing intermediate container 392cd9c26109
 ---> ae661fc7deae
Successfully built ae661fc7deae
Successfully tagged test/min:v0.1

在这一步, docker cli 运行命令 docker build args... 命令, 这个命令是怎么处理的呢?

这里要注意下,目前 docker cli 还在 这里

docker cli 使用 spf13/cobra (我们执行 docker –help 时候输出的一系列说明,就是这个东西搞的),初始化时,会把一系列的命令初始化进去:

docker cli 部分命令

我们去看下 NewBuildCommand :

// NewBuildCommand creates a new `docker build` command
func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
    options := newBuildOptions()
    cmd := &cobra.Command{
        Use:   "build [OPTIONS] PATH | URL | -",
        Short: "Build an image from a Dockerfile",
        Args:  cli.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            options.context = args[0]
            return runBuild(dockerCli, options)
        },
    }
    // ...
    return cmd
}

这个方法里, 当收到 docker build ... 命令时,会执行 RunE 方法 , 然后执行 runBuild 方法:

// cli/command/image/build.go:228
func runBuild(dockerCli command.Cli, options buildOptions) error {
    // 如果设置了 DOCKER_BUILDKIT 环境变量为 1, 则使用 buildkit 构建镜像
    buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
    if buildkitEnabled {
        return runBuildBuildKit(dockerCli, options)
    }
    // 否则通过 dockerd 构建镜像
    response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
    // ...
}

// client/image_build.go:20
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
    // ...
    serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
    return types.ImageBuildResponse{
        Body:   serverResp.body,
        OSType: osType,
    }, nil
}

可以看到,cli 通过 rest api 发送给了 docker daemon。


docker 封装了 api 层,也就是第一张图中的 REST API 部分。关于这一部分,可以在 api/server/router 目录中找到。docker 现在提供两种构建方式:

  • 基于 docker daemon 的第一代构建技术

  • 基于 bulidkit 的构建技术,不依赖 docker daemon(竞品有 google 家的 kaniko 以及 img, img 没有大厂背书)

  • docker 目前通过 BuilderBuildKit 来区分。在构建时, 加上 BuilderBuildKit=1 docker build ... 则可使用 buildkit:

    ➜  docker DOCKER_BUILDKIT=1 docker build . -t test/min:v0.1 -f ./dockerfile.min
    [+] Building 0.0s (5/5) FINISHED
     => [internal] load build definition from dockerfile.min     0.0s
     => => transferring dockerfile: 41B     0.0s
     => [internal] load .dockerignore     0.0s
     => => transferring context: 2B     0.0s
     => [internal] load metadata for docker.io/library/alpine:latest     0.0s
     => CACHED [1/1] FROM docker.io/library/alpine     0.0s
     => exporting to image         0.0s
     => => exporting layers      0.0s
     => => writing image sha256:13f822e3f1827d48d690e86dd2f30f1690e72f42f947db54b04b63597e0c2952 0.0s
     => => naming to docker.io/test/min:v0.1     0.0s
    

镜像构建完成后,通过 docker images 可以找到:

➜  docker docker images
REPOSITORY       TAG                 IMAGE ID            CREATED            SIZE
test/min         v0.1                13f822e3f182        3 weeks ago         5.6MB

docker run

到这一步, docker build 就执行完毕。现在我们来 docker run 一波, 看看会发生什么 :

➜  docker docker run 13f822e3f182
Hello From iyuhp

嗯,输出了我们希望输出的内容,说明我们构建的镜像是 OK 的。

同样的, docker cli 通过 docker run... 命令,调用 rest api 。不过这个命令需要分两步执行: docker createdocker start...

// cli/command/container/run.go:96
func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error {
    // create container
    createResponse, err := createContainer(ctx, dockerCli, containerConfig, &opts.createOptions)
    // start container
    if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
        return runStartContainerErr(err)
    }
    // ...
}

我们先看看 docker create 都做了哪些事情:

// Create creates a new container from the given configuration with a given name.
func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr error) {
    // 1. 获取镜像 id
    if opts.params.Config.Image != "" {
        img, err = daemon.imageService.GetImage(opts.params.Config.Image)
    }
    // 2. 与镜像中的配置合并并验证
    if err := daemon.mergeAndVerifyConfig(opts.params.Config, img); err != nil {
        return nil, errdefs.InvalidParameter(err)
    }
    // 3. 配置 container 的 log driver,若未配置,则使用 daemon 的 log driver
    if err := daemon.mergeAndVerifyLogConfig(&opts.params.HostConfig.LogConfig); err != nil {
        return nil, errdefs.InvalidParameter(err)
    }
    // 4. 创建容器对象,包含容器配置,网络,Host,镜像等信息
    if container, err = daemon.newContainer(opts.params.Name, os, opts.params.Config, opts.params.HostConfig, imgID, opts.managed); err != nil {
        return nil, err
    }

    // 5. 增加读写层, 镜像层是只读的,增加读写层用来运行时的数据交互
    // Set RWLayer for container after mount labels have been set
    rwLayer, err := daemon.imageService.CreateLayer(container, setupInitLayer(daemon.idMapping))
    if err != nil {
        return nil, errdefs.System(err)
    }
    container.RWLayer = rwLayer
    // 6. 创建一些列文件夹,用来保存容器信息, /var/lib/docker/containers/[id] 目录下
    // checkpoints, hostconfig.json, hostconfig.json
    rootIDs := daemon.idMapping.RootPair()
    if err := idtools.MkdirAndChown(container.Root, 0700, rootIDs); err != nil {
        return nil, err
    }
    if err := idtools.MkdirAndChown(container.CheckpointDir(), 0700, rootIDs); err != nil {
        return nil, err
    }
    if err := daemon.setHostConfig(container, opts.params.HostConfig); err != nil {
        return nil, err
    }
    // 7. 注册到 dockerd 中
    if err := daemon.Register(container); err != nil {
        return nil, err
    }
    stateCtr.set(container.ID, "stopped")
    daemon.LogContainerEvent(container, "create")
    return container, nil
}

docker create 主要就是完成 container 配置的初始化以及注册到 dockerd


那么 docker start 做了什么呢?

  1. 根据给定的 name(这个 name 可以是 container id,或者 container name ,或者 short container id) 获取 container 对象

  2. 查看 container 的状态,如果在 Paused, Running, Dead, RemovalInProgress 状态,则返回错误

  3. 验证 container 配置文件

  4. 挂载 RWLayer 读写层(TODO: 如何挂载)

  5. 初始化网络,设置 Hostname

  6. 创建容器 spec

  7. 调用 libcontainer runc 创建并运行容器, runc 负责和 linux kernel 打交道,最开始提到的 cgroups 和 namespace ,都是由 runc 来创建的

这里的部分,本渣目前还没有理清除,可以参考 这里 , 以及该作者关于 Docker 的一系列文章


架构图

Docker Detail Architecture


Docker Image

镜像由 layer 构成,每一层 layer 对应 Dockerfile 中一个命令,比如:

From alpine
WORKDIR /iyuhp
ADD flag.txt .
RUN rm flag.txt
CMD echo "Hello From iyuhp"

当我们执行 docker build . 时, 可以看到输出:

➜  docker docker build . -t layer-test:v0.1 -f ./dockerfile.layer2
Sending build context to Docker daemon  1.451MB
Step 1/5 : From alpine
 ---> a187dde48cd2
Step 2/5 : WORKDIR /iyuhp
 ---> Running in 2aca1ee61533
Removing intermediate container 2aca1ee61533
 ---> 9a4c07535dd2
Step 3/5 : ADD flag.txt .
 ---> 10360de80e7c
Step 4/5 : RUN rm flag.txt
 ---> Running in 4065ee172091
Removing intermediate container 4065ee172091
 ---> 8a0e76ce41b7
Step 5/5 : CMD echo "Hello From iyuhp"
 ---> Running in c039ecab87dd
Removing intermediate container c039ecab87dd
 ---> 8b7004425461
Successfully built 8b7004425461
Successfully tagged layer-test:v0.1

构建镜像时,docker 会对 Dockerfile 文件从上至下一行行执行,每次执行都会构建一层 layer (本质上也是一个 image),构建完毕后,该 layer 则不可更改(readonly)。

docker layer 的实现,基于 Union FS ,即传说中的联合文件系统。

如果该层的操作对象在上一层(上面 Dockerfile 的第四步) , docker 只会在该层将这个对象标记为删除,最后运行这个镜像的时候,这个对象你无法看到,表现为被删除。

而当我们 run image 的时候,docker 则会挂载一层 RWLayer,如果只需要读取底层 layer 的数据时,则直接去读取,但如果需要对底层数据做修改,则会先将该文件 copy 到 RWLayer ,再做修改,这就是常说的 COW (coyp-on-write) 技术。

Docker Registry

docker registry 是用来存储 docker image 的镜像仓库,类似于 github。通过 docker pulldocker push 等命令,可以从 registry 拉取镜像或者将我们本地构建的镜像推送到 registry。

docker 提供官方的 Docker Hub, 各大云厂商也都推出了自己的容器镜像服务。

当然,我们也可以基于 docker 提供的 registry 镜像,来构建我们自己的 docker registry,就好像我们搭建自己的 gitlab 一样。

Docker Network

docker 的网络实现了插件化,意味着,你可以基于 docker network interface 来实现自己的网络插件,或者使用其他的网络插件。

docker 内置了以下几种 network driver:

  • bridge:docker 默认的 network driver
  • host:共享宿主机的网络
  • overlay:可以实现不同 dockerd 之间的网络通信
  • macvlan:该模式下,可以为容器分配 mac 地址,使其在网络上显示为物理设备(不太清除…)
  • none:该模式下,将禁止网络连接

这里以 bridge 模式为例。

默认网络

我们通过 docker network ls 查看时,会发现 docker 已经帮我们创建了三个类型的网络:

➜  unionFS docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
4b3d0dc09100        bridge              bridge              local
847894dcaa28        host                host                local
f8c096c91de0        none                null                local

通过 docker network inspect [id/name] 可以查看该网络的具体信息(删除了部分内容):

➜  unionFS docker network inspect 4b3d0dc09100
[
    {
        "Name": "bridge",
        "Id": "4b3d0dc0910017d6bb7823e3415910ff7a7f9175d04cb29808ce627efcf9ad58",
        "Created": "2020-04-18T21:58:05.999876875+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Containers": {
            "8b87032b8281671c043a33202efa022ddaa398bc42719d1fa1c3e1ca3245970f": {
                "Name": "growing-uploader",
                "EndpointID": "227d88337cda67d632f48cf64a1b5e5c6607f560fe0e0dca8b09515d3fe29208",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        }
    }
]

这里向我们显示了该网络的:

  • Driver: bridge,网桥模式
  • subnet : 16位掩码
  • gateway : 172.17.0.1
  • containers :当前使用该网络的容器,这里能看到该容器的 id,ip 等信息
  • options : 网络的其他设置,包括挂载的网桥 docker0(该网桥是 docker 在安装时创建的), 网络的 mtu 为 1500 等等。这里说下 mtu,maximum transmission unit, 最大传输单元。如果我们的 mtu 设置的不合理,比如设置为 1400 ,那就有可能产生丢包问题

其中, bridge 是 docker 默认的网络模式以及使用的网络。当我们去运行容器时,如果未通过 --network 来指定使用的网络时,则会使用 bridge 网络。


创建网络

现在我们尝试创建一个自己的网络 ownbridge [1]

docker network create -d bridge --subnet 172.0.0.3/30 --gateway 172.0.0.1 ownbridge
  • -d 参数用来指定 network driver ,默认为 bridge
  • –sebnet 通过 cidr 格式指定网段
  • –gateway 指定网关
  • 更多参数可通过 docker network create -h 获取

现在,我们 build 一个 image ,然后使用上面的网络启动:

Dockerfile

FROM busybox
RUN mkdir /html \
        && echo "Hello World" > /html/index.html
EXPOSE 1234
CMD ["httpd",  "-f", "-p", "1234", "-h", "/html"]

docker build && docker run

  • --network 指定网络为 ownbridge
  • –rm : 容器停止时删除容器
  • -p : 将 1234 端口暴露到宿主机, -p 12306:1234 , 则会将容器端口 1234 映射到宿主机 12306 上
  • -d : 后台运行
# build
➜  docker docker build . -t simpelhttpd:v0.1
// ...
Successfully built 1e3ceb976b97
Successfully tagged simpelhttpd:v0.1

# run 通过 --network 指定网络
➜  docker docker run --rm -p 1234 --network ownbridge -d 1e3ceb976b97
1ad1f5236ab5f95b2bf235f33cff494b43a77b7e414b85b067416ad9715b039c

Curl

# 查看端口 可以看到是 0.0.0.0:32775 -> 1234/tcp 
# 即 docker 将容器内 1234 端口映射到了 32775 上
➜  docker docker ps                

# Curl
➜  docker curl localhost:32775
Hello World

输出了 “Hello World” , 就是我们在 Dockerfile 中写入到 index.html 中的内容


通信

现在我们看看,curl 是怎么访问到我们的容器的。在此之前,我们整理下目前的信息:

目标网络

容器信息:

"Gateway": "172.0.0.1",
"IPAddress": "172.0.0.2"

它关联的网桥:

176: br-814aff72d8df: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:83:a7:d1:16 brd ff:ff:ff:ff:ff:ff
    inet 172.0.0.1/30 brd 172.0.0.3 scope global br-814aff72d8df
       valid_lft forever preferred_lft forever
    inet6 fe80::42:83ff:fea7:d116/64 scope link
       valid_lft forever preferred_lft forever

在执行 curl 之前,宿主机经过 icmp + arp ,习得 arp 信息,放入本机的 arp 缓存表中。

访问前 arp 表:

➜  docker arp
Address                  HWtype  HWaddress           Flags Mask            Iface
_gateway                 ether   ef:ae:ae:ff:ff:ff   C                     eth0

这个时候,在 arp 表中没有找到,则会进行广播 arp 报文,报文结构一般为:

dest mac | source mac | type

type 为 1 时表示 arp 请求, dest mac 为全 f 时(ff.ff.ff.ff.ff.ff) 即进行广播,具体信息请参考 Ref 部分的引用。

访问后 arp 表:

➜  docker arp -nvvv
Address                  HWtype  HWaddress           Flags Mask            Iface
172.16.11.253            ether   ef:ae:ae:ff:ff:ff   C                     eth0
172.0.0.2                ether   02:42:ac:00:00:02   C                     br-814aff72d8df

这里习得 172.0.0.2 对应的 mac 地址为 02:42:ac:00:00:02 。我们知道二层网络是基于 mac 地址通信的,常见的两层交换机的作用,就是通过 mac 地址转发。


这里还要再说一句,在 bridge 模式下,docker 默认会为所有容器开放的端口起一个 docker-proxy 的进程,通过nat + iptables[2] 对该端口进行代理,该功能可以通过配置 userland-proxy 为 false 关闭。

通过 sudo iptables -t nat -nL 查看:

➜  docker sudo iptables -t nat -nL
Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:32775 to:172.0.0.2:1234

查看 iptables nat 表,能看到,所有访问 32775 端口的请求,都被转发到 172.0.0.2:1234 这儿了。


于是宿主机查询自己的路由表:

➜  docker route -nvvv
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.16.11.253   0.0.0.0         UG    100    0        0 eth0
172.0.0.0       0.0.0.0         255.255.255.252 U     0      0        0 br-814aff72d8df

第一条中的 0.0.0.0 意为默认路由,即所有不存在该路由表中的请求,都会由它路由到下一跳 172.16.11.253

而我们的 ip 为 172.0.0.2 ,根据路由表规则,会被网卡 br-814aff72d8df 处理。

docker 通过 veth pair [3],与 网卡 br-814aff72d8df 连接,也就是说,上面的请求,最终被发送给我们此行的目的地。


Network Flow

Container Network Flow

注释

[1]

--subnet 172.0.0.1/30

网络地址划分

我们知道,通常将 ip 地址分为 A 类、B 类、C 类地址,ip 地址可以用一个长 32 位的二进制数表示。这其中又分为网络地址 + 主机地址。

对于 A 类地址而言,其前八位表示网络地址,后 24 位表示主机地址。ip 寻址,就是先通过寻找网络地址,然后再寻找主机地址。

上面的划分,会导致 ipv4 资源的浪费,于是诞生了 CIDR , 上面的 172.0.0.1/30 就是其表现形式。这是什么含义呢?

前面的 ip 可以转化位一个 32 位的二进制数, 30 表示前 32 位为网络地址,将前 30 位置为 1, 即为它的子网掩码,这里为: 255.255.255.252,则主机地址只能是剩下的后两位表示,也就是总共可以产生 22 = 4 个 ip ,即 172.0.0.[0-3] ,通常,会有一些特殊的 ip 不会被分配,用来作为广播、网关等作用,所以通常会减去 2,作为实际可用的 ip 数。

如何判断两个 ip 是否在同一网段?

  • 对于 A, ip B 与掩码 A 做与运算,得到的与自己子网一致
  • 对于 B, ip A 与掩码 B 做与运算,得到的与自己子网一致

假设两个 ip A : 172.168.0.1/16, ip B : 172.168.3.1/24

于是我们得到:

  • ip A : 172.168.0.1
  • 掩码 A: 172.168.0.0
  • ip B:172.168.3.1
  • 掩码 B:172.168.3.0

则 A 运算后,得到子网 ip B & 掩码 A = 172.168.0.0/16

B 运算后, 得到子网 ip A & 掩码 B = 172.168.3.0/24

一致,所以他们在同一网段。

如果 A 变为 172.168.3.1/16, B 变成 172.168.0.1/24 , 则不在同一网段了。因为 B 运算后的结果为: 172.168.3.0/24 ,与自己的 172.168.0.0/24 不一致。

[2]

iptables 内置四张 table ,执行顺序为 raw > mangle > nat > filter

我们执行 sudo iptables -nL 时, 默认 table 为 filter。

docker 在做 port 映射时, 会分别向 nat 和 filter 两张表写入信息。我们在查找的时候,需要查看两张表的信息。

同理, 我们甚至可以手动修改 iptables ,来达到宿主机内不同容器间的通信。

[3]

如何查看 docker veth pair,这里提供两种方式

  1. 先查看容器内 veth pair,然后通过 ip a 查看:

    # 107a4446c1f5 对应具体的 container id
    ➜  docker docker exec -ti 107a4446c1f5 sh -c 'cat /sys/class/net/eth0/iflink'
    214
    # 这里看到是 214, 然后通过 ip link 查看
    ➜  docker ip link | grep 214:
    214: veth9ea7c64@if213: ...
    # 所以这里 veth9ea7c64 就是和上面 container 的 veth pair
  2. 通过 docker inspect + ethtool

    # 查找 net namespace
    ➜  docker docker inspect --format='{{ .NetworkSettings.SandboxKey}}' 107a4446c1f5
    /var/run/docker/netns/047bec4e1b1c
    
    # 去容器内部查看 veth
    ➜  docker sudo nsenter --net=/var/run/docker/netns/047bec4e1b1c ethtool -S eth0
    NIC statistics:
         peer_ifindex: 214
         rx_queue_0_xdp_packets: 0
         rx_queue_0_xdp_bytes: 0
         rx_queue_0_xdp_drops: 0
    
    # 通过 ip link 查看
    ➜  docker ip link | grep 214:
    214: veth9ea7c64@if213: ...

这个 nsenter 的命令对于调试容器貌似挺有用的,需要学习一波。

Reference

Cgroups

Namespace

InfoQ 源码分析

ARP Wiki

Read More

丢包问题


文章作者: peifeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 peifeng !
 上一篇
Docker 系列<完> Docker Commands 一览 Docker 系列<完>Docker Commands 一览
前面两篇文章分别介绍了 Docker 的安装和内部架构。本文将以爬取 bing 图片这个小例子展开,讲述一份 Dockerfile 的常用结构和命令,以及常用的 Docker 命令。
2020-04-21
下一篇 
Docker 系列<一> Docker 安装 Docker 系列<一>Docker 安装
你想在本地起一个 Java 服务,于是你安装了 JDK,你想在本地起一个 Golang 服务,于是你安装了 Golang ,你想起一个 Python 服务,于是你又安装了 Python,后来,你安装了 Docker ,妈妈再也不用担心你乱安装各种依赖了。
2020-04-17
  目录