Docker 源码分析(八):Docker Container 网络(下)

阅读数:6333 2015 年 2 月 16 日

1.Docker Client 配置容器网络模式

Docker 目前支持 4 种网络模式,分别是 bridge、host、container、none,Docker 开发者可以根据自己的需求来确定最适合自己应用场景的网络模式。

从 Docker Container 网络创建流程图中可以看到,创建流程第一个涉及的 Docker 模块即为 Docker Client。当然,这也十分好理解,毕竟 Docker Container 网络环境的创建需要由用户发起,用户根据自身对容器的需求,选择网络模式,并将其通过 Docker Client 传递给 Docker Daemon。本节,即从 Docker Client 源码的角度,分析如何配置 Docker Container 的网络模式,以及 Docker Client 内部如何处理这些网络模式参数。

需要注意的是:配置 Docker Container 网络环境与创建 Docker Container 网络环境有一些区别。区别是:配置网络环境指用户通过向 Docker Client 传递网络参数,实现 Docker Container 网络环境参数的配置,这部分配置由 Docker Client 传递至 Docker Daemon,并由 Docker Daemon 保存;创建网络环境指,用户通过 Docker Client 向 Docker Daemon 发送容器启动命令之后,Docker Daemon 根据之前保存的网络参数,实现 Docker Container 的启动,并在启动过程中完成 Docker Container 网络环境的创建。

以上的基本知识,理解下文的 Docker Container 网络环境创建流程。

1.1 Docker Client 使用

Docker 架构中,用户可以通过 Docker Client 来配置 Docker Container 的网络模式。配置过程主要通过 docker run 命令来完成,实现配置的方式是在 docker run 命令中添加网络参数。使用方式如下(其中 NETWORKMODE 为四种网络模式之一,ubuntu 为镜像名称,/bin/bash 为执行指令):

docker run -d --net NETWORKMODE ubuntu /bin/bash

运行以上命令时,首先创建一个 Docker Client,然后 Docker Client 会解析整条命令的请求内容,接着解析出为 run 请求,意为运行一个 Docker Container,最终通过 Docker Client 端的 API 接口,调用 CmdRun 函数完成 run 请求执行。(详情可以查阅《Docker 源码分析》系列的第二篇——Docker Client 篇)。

Docker Client 解析出 run 命令之后,立即调用相应的处理函数 CmdRun 进行处理关于 run 请求的具体内容。CmdRun 的作用主要可以归纳为三点:

  • 解析 Docker Client 传入的参数,解析出 config、hostconfig 和 cmd 对象等;
  • 发送请求至 Docker Daemon,创建一个 container 对象,完成 Docker Container 启动前的准备工作;
  • 发送请求至 Docker Daemon,启动相应的 Docker Container(包含创建 Docker Container 网络环境创建)。

1.2 runconfig 包解析

CmdRun 函数的实现位于./docker/api/client/commands.go。CmdRun 执行的第一个步骤为:通过 runconfig 包中 ParseSubcommand 函数解析 Docker Client 传入的参数,并从中解析出相应的 config,hostConfig 以及 cmd 对象,实现代码如下:

config, hostConfig, cmd, err := runconfig.ParseSubcommand
(cli.Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", 
"Run a command in a new container"), args, nil) 

其中,config 的类型为 Config 结构体,hostConfig 的类型为 HostConfig 结构体,两种类型的定义均位于 runconfig 包。Config 与 HostConfig 类型同用以描述 Docker Container 的配置信息,然而两者之间又有着本质的区别,最大的区别在于两者各自的作用范畴:

  • Config 结构体:描述 Docker Container 独立的配置信息。独立的含义是:Config 这部分信息描述的是容器本身,而不会与容器所在 host 宿主机相关;
  • HostConfig 结构体:描述 Docker Container 与宿主机相关的配置信息。

1.2.1 Config 结构体

Config 结构体描述 Docker Container 本身的属性信息,这些信息与容器所在的 host 宿主机无关。结构体的定义如下:

type Config struct {
	Hostname        string
	Domainname      string
	User            string
	Memory          int64  // Memory limit (in bytes)
	MemorySwap      int64  // Total memory usage (memory + swap); set `-1' to disable swap
	CpuShares       int64  // CPU shares (relative weight vs. other containers)
	Cpuset          string // Cpuset 0-2, 0,1
	AttachStdin     bool
	AttachStdout    bool
	AttachStderr    bool
	PortSpecs       []string // Deprecated - Can be in the format of 8080/tcp
	ExposedPorts    map[nat.Port]struct{}
	Tty             bool // Attach standard streams to a tty, including stdin if it is not closed.
	OpenStdin       bool // Open stdin
	StdinOnce       bool // If true, close stdin after the 1 attached client disconnects.
	Env             []string
	Cmd             []string
	Image           string // Name of the image as it was passed by 
the operator (eg. could be symbolic)
	Volumes         map[string]struct{}
	WorkingDir      string
	Entrypoint      []string
	NetworkDisabled bool
	OnBuild         []string
}

Config 结构体中各属性的详细解释如下表:

Config 结构体属性名

类型

代表含义

Hostname

string

容器主机名

Domainname

string

域名名称

User

string

用户名

Memory

int64

容器内存使用上限(单位:字节)

MemorySwap

int64

容器所有的内存使用上限(物理内存 + 交互区),关闭交互区支持置为 -1

CpuShares

int64

容器 CPU 使用 share 值,其他容器的相对值

Cpuset

string

CPU 核的使用集合

AttachStdin

bool

是否附加标准输入

AttachStdout

bool

是否附加标准输出

AttachStderr

bool

是否附加标准错误输出

PortsSpecs

[]string

目前已被遗弃

ExposedPorts

map[nat.Port]struct{}

容器内部暴露的端口号

Tty

bool

是否分配一个伪终端 tty

OpenStdin

bool

在没有附加标准输入时,是否依然打开标准输入

StdinOnce

bool

若为真,表示第一个客户关闭标准输入后关闭标准输入功能

Env

[]string

容器进程运行的环境变量

Cmd

[]string

容器内通过 ENTRYPOINT 运行的指令

Image

string

容器 rootfs 所依赖的镜像名称

Volumes

map[string]struct{}

容器需要从 host 宿主机上挂载的目录

WorkingDir

string

容器内部的指定工作目录

Entrypoint

[]string

覆盖镜像属性中默认的 ENTRYPOINT

NetworkDisabled

bool

是否关闭容器网络功能

OnBuild

[]string

 

1.2.2 HostConfig 结构体

HostConfig 结构体描述 Docker Container 与宿主机相关的属性信息,结构体的定义如下:

type HostConfig struct {
	Binds           []string
	ContainerIDFile string
	LxcConf         []utils.KeyValuePair
	Privileged      bool
	PortBindings    nat.PortMap
	Links           []string
	PublishAllPorts bool
	Dns             []string
	DnsSearch       []string
	VolumesFrom     []string
	Devices         []DeviceMapping
	NetworkMode     NetworkMode
	CapAdd          []string
	CapDrop         []string
	RestartPolicy   RestartPolicy
}

Config 结构体中各属性的详细解释如下表:

HostConfig 结构体属性名

类型

代表含义

Binds

[]string

从宿主机上绑定到容器的 volumes

ContainerIDFile

string

文件名,文件用以写入容器的 ID

LxcConf

[]utils.KeyValuePair

添加自定义的 lxc 选项

Privileged

bool

是否赋予该容器扩展权限

PortBindings

nat.PortMap

容器绑定到 host 宿主机的端口

Links

[]string

添加其他容器的链接到该容器

PublishAllPorts

bool

是否向宿主机暴露所有端口信息

Dns

[]string

自定义的 DNS 服务器地址

DnsSearch

[]string

自定义的 DNS 查找服务器地址

VolumesFrom

[]string

从指定的容器中挂载到该容器的 volumes

Devices

[]DeviceMapping

为容器添加一个或多个宿主机设备

NetworkMode

NetworkMode

为容器设置的网络模式

CapAdd

[]string

为容器用户添加一个或多个 Linux Capabilities

CapDrop

[]string

为容器用户禁用一个或多个 Linux Capabilities

RestartPolicy

RestartPolicy

当一个容器异常退出时采取的重启策略

1.2.3 runconfig 解析网络模式

讲述完 Config 与 HostConfig 结构体之后,回到 runconfig 包中分析如何解析与 Docker Container 网络模式相关的配置信息,并将这部分信息传递给 config 实例与 hostConfig 实例。

runconfig 包中的 ParseSubcommand 函数调用 parseRun 函数完成命令请求的分析,实现代码位于./docker/runconfig/parse.go#L37-L39,如下:

func ParseSubcommand(cmd *flag.FlagSet, args []string,
   sysInfo *sysinfo.SysInfo) (*Config, *HostConfig, *flag.FlagSet, error) {
		return parseRun(cmd, args, sysInfo)
}

进入 parseRun 函数即可发现:该函数完成了四方面的工作:

  • 定义与容器配置信息相关的flag 参数
  • 解析 docker run 命令后紧跟的请求内容,将请求内容全部保存至 flag 参数中,余下的内容一个为镜像 image 名,另一个为需要在容器内执行的 cmd 命令;
  • 通过 flag 参数验证参数的有效性,并处理得到 Config 结构体与 HostConfig 结构体需要的属性值;
  • 创建并初始化 Config 类型实例 config、HostConfig 类型实例 hostConfig,最总返回 config、hostConfig 与 cmd。

本文主要分析 Docker Container 的网络模式,而 parseRun 函数中有关容器网络模式的 flag 参数有 flNetwork 与 flNetMode,两者的定义分别位于./docker/runconfig/parse.go#L62./docker/runconfig/parse.go#L75,如下:

flNetwork = cmd.Bool([]string{"#n", "#-networking"}, true, "Enable networking for this container")

flNetMode = cmd.String([]string{"-net"}, "bridge", "Set the Network mode for the container\n'bridge': creates a new network stack for the container on the docker bridge\n'none': no networking for this container\n'container:<name|id>': reuses another container network stack\n'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure.")

可见 flag 参数 flNetwork 表示是否开启容器的网络模式,若为 true 则开启,说明需要给容器创建网络环境;否则不开启,说明不给容器赋予网络功能。该 flag 参数的默认值为 true,另外使用该 flag 的方式为:在 docker run 之后设定 --networking 或者 -n,如:

docker run --networking true ubuntu /bin/bash

另一个 flag 参数 flNetMode 则表示为容器设定的网络模式,共有四种选项,分别是:bridge、none、container:<name|id> 和 host。四种模式的作用上文已经详细解释,此处不再赘述。使用该 flag 的方式为:在 docker run 之后设定 --net,如:

Docker run --net host ubuntu /bin/bash

用户使用 docker run 启动容器时设定了以上两个 flag 参数(--networking 和 --net),则 runconfig 包会解析出这两个 flag 的值。最终,通过 flag 参数 flNetwork,得到 Config 类型实例 config 的属性 NetworkDisabled;通过 flag 参数 flNetMode,得到 HostConfig 类型实例 hostConfig 的属性 NetworkMode。

函数 parseRun 返回 config、hostConfig 与 cmd,代表着 runconfig 包解析配置参数工作的完成,CmdRun 得到返回内容之后,继续向下执行。

1.3 CmdRun 执行

在 runconfig 包中已经将有关容器网络模式的配置置于 config 对象与 hostConfig 对象,故在 CmdRun 函数的执行中,更多的是基于 config 对象与 hostConfig 参数处理配置信息,而没有其他的容器网络处理部分。

CmdRun 后续主要工作是:利用 Docker Daemon 暴露的 RESTful API 接口,将 docker run 的请求发送至 Docker Daemon。以下是 CmdRun 执行过程中 Docker Client 与 Docker Daemon 的简易交互图。

图 1.1 CmdRun 中 Docker Client 与 Docker Daemon 交互图

具体分析 CmdRun 的执行流程可以发现:在解析 config、hostConfig 与 cmd 之后,Docker Client 首先发起请求 create container。若 Docker Daemon 节点上已经存在该容器所需的镜像,则立即执行 create container 操作并返回请求响应;Docker Client 收到响应后,再发起请求 start container。若容器镜像还不存在,Docker Daemon 返回一个 404 的错误,表示镜像不存在;Docker Client 收到错误响应之后,再发起一个请求 pull image,使 Docker Daemon 首先下载镜像,下载完毕之后 Docker Client 再次发起请求 create container,Docker Daemon 创建完毕之后,Docker Client 最终发起请求 start container。

总结而言,Docker Client 负责创建容器请求的发起。关于 Docker Container 网络环境的配置参数存储于 config 与 hostConfig 对象之中,在请求 create container 和 start container 发起时,随请求一起发送至 Docker Daemon。

2.Docker Daemon 创建容器网络流程

Docker Daemon 接收到 Docker Client 的请求大致可以分为两次,第一次为 create container,第二次为 start container。这两次请求的执行过程中,都与 Docker Container 的网络相关。以下按照这两个请求的执行,具体分析 Docker Container 网络模式的创建。Docker Daemon 如何通过 Docker Server 解析 RESTful 请求,并完成分发调度处理,在《Docker 源码分析》系列的第五篇——Docker Server 篇中已经详细分析过,本文不再赘述。

2.1 创建容器并配置网络参数

Docker Daemon 首先接收并处理 create container 请求。需要注意的是:create container 并非创建了一个运行的容器,而是完成了以下三个主要的工作:

  • 通过 runconfig 包解析出 create container 请求中与 Docker Container 息息相关的 config 对象;
  • 在 Docker Daemon 内部创建了与 Docker Container 对应的 container 对象;
  • 完成 Docker Container 启动前的准备化工作,如准备所需镜像、创建 rootfs 等。

创建容器过程中,Docker Daemon 首先通过 runconfig 包中 ContainerConfigFromJob 函数,解析出请求中的 config 对象,解析过程代码如下:

config := runconfig.ContainerConfigFromJob(job)

至此,Docker Client 处理得到的 config 对象,已经传递至 Docker Daemon 的 config 对象,config 对象中已经含有属性 NetworkDisabled 具体值。

处理得到 config 对象之后,Docker Daemon 紧接着创建 container 对象,并为 Docker Container 作相应的准备工作。具体的实现代码位于./docker/daemon/create.go#L73-L78,如下:

if container, err = daemon.newContainer(name, config, img); err != nil {
	return nil, nil, err
}
if err := daemon.createRootfs(container, img); err != nil {
	return nil, nil, err
}

与 Docker Container 网络模式配置相关的内容主要位于创建 container 对象中。newContainer 函数的定义位于./docker/daemon/daemon.go#L516-L550,具体的 container 对象如下:

container := &Container{
	ID:              id,
	Created:         time.Now().UTC(),
	Path:            entrypoint,
	Args:            args, //FIXME: de-duplicate from config
	Config:          config,
	hostConfig:      &runconfig.HostConfig{},
	Image:           img.ID, // Always use the resolved image id
	NetworkSettings: &NetworkSettings{},
	Name:            name,
	Driver:          daemon.driver.String(),
	ExecDriver:      daemon.execDriver.Name(),
	State:           NewState(),	
}

在 container 对象中,config 对象直接赋值给 container 对象的 Config 属性,另外 hostConfig 属性与 NetworkSeeetings 属性均为空。其中 hostConfig 对象将在 start container 请求执行过程中被赋值,NetworkSettings 类型的作用是描述容器的网络具体信息,定义位于./docker/daemon/network_settings.go#L11-L18,代码如下:

type NetworkSettings struct {
		IPAddress   string
		IPPrefixLen int
		Gateway     string
		Bridge      string
		PortMapping map[string]PortMapping // Deprecated
		Ports       nat.PortMap
}

Networksettings 类型的各属性的详细解释如下表:

NetworkSettings 属性名称

类型

含义

IPAddress

string

IP 网络地址

IPPrefixLen

int

网络标识位长度

Gateway

string

网关地址

Bridge

string

网桥地址

PortMapping

map[string]PortMapping

端口映射

Ports

nat.PortMap

端口号

总结而言,Docker Daemon 关于 create container 请求的执行,先实现了容器配置信息从 Docker Client 至 Docker Daemon 的转移,再完成了启动容器前所需的准备工作。

2.2 启动容器之网络配置

创建容器阶段,Docker Daemon 创建了容器对象 container,container 对象内部的 Config 属性含有 NetworkDisabled。创建容器完成之后,Docker Daemon 还需要接收 Docker Client 的请求,并执行 start container 的操作,即启动容器。

启动容器过程中,Docker Daemon 首先通过 runconfig 包中 ContainerHostConfigFromJob 函数,解析出请求中的 hostConfig 对象,解析过程代码如下:

hostConfig := runconfig.ContainerHostConfigFromJob(job) 

至此,Docker Client 处理得到的 hostConfig 对象,已经传递至 Docker Daemon 的 hostConfig 对象,hostConfig 对象中已经含有属性 NetworkMode 具体值。

容器启动的所有工作,均由以下的 Start 函数来完成,代码位于./docker/daemon/start.go#L36-L38,如下:

if err := container.Start(); err != nil {
		return job.Errorf("Cannot start container %s: %s", name, err)
	}

Start 函数实现了容器的启动。更为具体的描述是:Start 函数实现了进程的启动,另外在启动进程的同时为进程设定了命名空间(namespace),启动完毕之后为进程完成了资源使用的控制,从而保证进程以及之后进程的子进程都会在同一个命名空间内,且受到相同的资源控制。如此一来,Start 函数创建的进程,以及该进程的子进程,形成一个进程组,该进程组处于资源隔离和资源控制的环境,我们习惯将这样的进程组环境称为容器,也就是这里的 Docker Container。

回到 Start 函数的执行,位于./docker/daemon/container.go#L275-L320。Start 函数执行过程中,与 Docker Container 网络模式相关的部分主要有三部分:

  • initializeNetwork(),初始化 container 对象中与网络相关的属性;
  • populateCommand,填充 Docker Container 内部需要执行的命令,Command 中含有进程启动命令,还含有容器环境的配置信息,也包括网络配置;
  • container.waitForStart(),实现 Docker Container 内部进程的启动,进程启动之后,为进程创建网络环境等。

2.2.1 初始化容器网络配置

容器对象 container 中有属性 hostConfig,属性 hostConfig 中有属性 NetworkMode,初始化容器网络配置 initializeNetworking() 的主要工作就是,通过 NetworkMode 属性为 Docker Container 的网络作相应的初始化配置工作。

Docker Container 的网络模式有四种,分别为:host、other container、none 以及 bridge。initializeNetworking 函数的执行完全覆盖了这四种模式。

initializeNetworking() 函数的实现位于./docker/daemon/container.go#L881-L933

2.2.1.1 初始化 host 网络模式配置

Docker Container 网络的 host 模式意味着容器使用宿主机的网络环境。虽然 Docker Container 使用宿主机的网络环境,但这并不代表 Docker Container 可以拥有宿主机文件系统的视角,而 host 宿主机上有很多信息标识的是网络信息,故 Docker Daemon 需要将这部分标识网络的信息,从 host 宿主机添加到 Docker Container 内部的指定位置。这样的网络信息,主要有以下两种:

  • host 宿主机的主机名(hostname);
  • host 宿主机上 /etc/hosts 文件,用于配置 IP 地址以及主机名。

其中,宿主机的主机名 hostname 用于创建 container.Config 中的 Hostname 与 Domainname 属性。

另外,Docker Daemon 在 Docker Container 的 rootfs 内部创建 hostname 文件,并在文件中写入 Hostname 与 Domainname;同时创建 hosts 文件,并写入 host 宿主机上 /etc/hosts 内的所有内容。

2.2.1.2 初始化 other container 网络模式配置

Docker Container 的 other container 网络模式意味着:容器使用其他已经创建容器的网络环境。

Docker Daemon 首先判断 host 网络模式之后,若不为 host 网络模式,则继续判断 Docker Container 网络模式是否为 other container。如果 Docker Container 的网络模式为 other container(假设使用的 -net 参数为 --net=container:17adef,其中 17adef 为容器 ID)。Docker Daemon 所做的执行操作包括两部分。

第一步,从 container 对象的 hostConfig 属性中找出 NetworkMode,并找到相应的容器,即 17adef 的容器对象 container,实现代码如下:

nc, err := container.getNetworkedContainer()

第二步,将 17adef 容器对象的 HostsPath、ResolveConfPath、Hostname 和 Domainname 赋值给当前容器对象 container,实现代码如下:

container.HostsPath = nc.HostsPath
container.ResolvConfPath = nc.ResolvConfPath
container.Config.Hostname = nc.Config.Hostname
container.Config.Domainname = nc.Config.Domainname
2.2.1.3 初始化 none 网络模式配置

Docker Container 的 none 网络模式意味着不给该容器创建任何网络环境,容器只能使用 127.0.0.1 的本机网络。

Docker Daemon 通过 config 属性的 DisableNetwork 来判断是否为 none 网络模式。实现代码如下:

if container.daemon.config.DisableNetwork {
		container.Config.NetworkDisabled = true
		return container.buildHostnameAndHostsFiles("127.0.1.1")
	}
2.2.1.4 初始化 bridge 网络模式配置

Docker Container 的 bridge 网络模式意味着为容器创建桥接网络模式。桥接模式使得 Docker Container 创建独立的网络环境,并通过“桥接”的方式实现 Docker Container 与外界的网络通信。

初始化 bridge 网络模式的配置,实现代码如下:

if err := container.allocateNetwork(); err != nil {
		return err
}
return container.buildHostnameAndHostsFiles(container.NetworkSettings.IPAddress) 

以上代码完成的内容主要也是两部分:第一,通过 allocateNetwork 函数为容器分配网络接口设备需要的资源信息(包括 IP、bridge、Gateway 等),并赋值给 container 对象的 NetworkSettings;第二,为容器创建 hostname 以及创建 Hosts 等文件。

2.2.2 创建容器 Command 信息

Docker 在实现容器时,使用了 Command 类型。Command 在 Docker Container 概念中是一个非常重要的概念。几乎可以认为 Command 是 Docker Container 生命周期的源头。Command 的概念会贯穿以后的《Docker 源码分析》系列,比如 Docker Daemon 与 dockerinit 的关系,dockerinit 和 entrypoint.sh 的关系,entrypoint.sh 与 CMD 的关系,以及 namespace 在这些内容中扮演的角色。

简单来说,Command 类型包含了两部分的内容:第一,运行容器内进程的外部命令 exec.Cmd;第二,运行容器时启动进程需要的所有环境基础信息:包括容器进程组的使用资源、网络环境、使用设备、工作路径等。通过这两部分的内容,我们可以清楚,如何启动容器内的进程,同时也清楚为容器创建什么样的环境。

首先,我们先来看 Command 类型的定义,位于./docker/daemon/execdriver/driver.go#L84,通过分析 Command 类型以及其他相关的数据结构类型,可以得到以下简要类型关系图:

图 2.1 Command 类型关系图

从 Command 类型关系图中可以看到,Command 类型中重新包装了 exec.Cmd 类型,即代表需要创建的进程具体的外部命令;同时,关于网络方面的属性有 Network,Network 的类型为指向 Network 类型的指针;关于 Docker Container 资源使用方面的属性为 Resources,从 Resource 的类型来看,Docker 目前能做的资源限制有 4 个维度,分别为内存,内存 +Swap,CPU 使用,CPU 核使用;关于挂载的内容,有属性 Mounts;等等。

简单介绍 Command 类型之后,回到 Docker Daemon 启动容器网络的源码分析。在 Start 函数的执行流程中,紧接 initializeNetworking() 之后,与 Docker Container 网络相关的是 populateCommand 环节。populateCommand 的函数实现位于./docker/daemon/container.go#L191-L274。上文已经提及,populateCommand 的作用是创建包 execdriver 的对象 Command,该 Command 中既有启动容器进程的外部命令,同时也有众多为容器环境的配置信息,包括网络。

本小节,更多的分析 populateCommand 如何填充 Command 对象中的网络信息,其他信息的分析会在《源码分析系列》的后续进行展开。

Docker 总共有四种网络模式,故 populateCommand 自然需要判断容器属于哪种网络模式,随后将具体的网络模式信息,写入 Command 对象的 Network 属性中。查验 Docker Container 网络模式的代码位于./docker/daemon/container.go#L204-L227,如下:

parts := strings.SplitN(string(c.hostConfig.NetworkMode), ":", 2)
	switch parts[0] {
	case "none":
	case "host":
		en.HostNetworking = true
	case "bridge", "": // empty string to support existing containers
		if !c.Config.NetworkDisabled {
			network := c.NetworkSettings
			en.Interface = &execdriver.NetworkInterface{
				Gateway:     network.Gateway,
				Bridge:      network.Bridge,
				IPAddress:   network.IPAddress,
				IPPrefixLen: network.IPPrefixLen,
			}
		}
	case "container":
		nc, err := c.getNetworkedContainer()
		if err != nil {
			return err
		}
		en.ContainerID = nc.ID
	default:
		return fmt.Errorf("invalid network mode: %s", c.hostConfig.NetworkMode)
	}

populateCommand 首先通过 hostConfig 对象中的 NetworkMode 判断容器属于哪种网络模式。该部分内容涉及到 execdriver 包中的 Network 类型,可参见 Command 类型关系图中的 Network 类型。若为 none 模式,则对于 Network 对象(即 en,*execdriver.Network)不做任何操作。若为 host 模式,则将 Network 对象的 HostNetworking 置为 true;若为 bridge 桥接模式,则首先创建一个 NetworkInterface 对象,完善该对象的 Gateway、Bridge、IPAddress 和 IPPrefixLen 信息,最后将 NetworkInterface 对象作为 Network 对象的 Interface 属性的值;若为 other container 模式,则首先通过 getNetworkedContainer() 函数获知被分享网络命名空间的容器,最后将容器 ID,赋值给 Network 对象的 ContainerID。由于 bridge 模式、host 模式、none 模式以及 other container 模式彼此互斥,故 Network 对象中 Interface 属性、ContainerID 属性以及 HostNetworking 三者之中只有一个被赋值。当 Docker Container 的网络查验之后,populateCommand 将 en 实例 Network 属性的值,传递给 Command 对象。

至此,populateCommand 关于网络方面的信息已经完成配置,网络配置信息已经成功赋值于 Command 的 Network 属性。。

2.2.3 启动容器内部进程

当为容器做好所有的准备与配置之后,Docker Daemon 需要真正意义上的启动容器。根据 Docker Daemon 启动容器流程涉及的 Docker 模块中可以看到,这样的请求,会被发送至 execdriver,再经过 libcontainer,最后实现真正启动进程,创建完容器。

回到 Docker Daemon 的启动容器,daemon 包中 start 函数的最后一步即为执行 container.waitForStart()。waitForStart 函数的定义位于./docker/daemon/container.go#L1070-L1082,代码如下:

func (container *Container) waitForStart() error {
		container.monitor = newContainerMonitor(container, container.hostConfig.RestartPolicy)

		select {
		case <-container.monitor.startSignal:
		case err := <-utils.Go(container.monitor.Start):
			return err
		}

		return nil
}

以上代码运行过程中首先通过 newContainerMonitor 返回一个初始化的 containerMonitor 对象,该对象中带有容器进程的重启策略(RestartPolicy)。这里简单介绍 containerMonitor 对象。总体而言,containerMonitor 对象用以监视容器中第一个进程的执行。如果 containerMonitor 中指定了一种进程重启策略,那么一旦容器内部进程没有启动成功,Docker Daemon 会使用重启策略来重启容器。如果在重启策略下,容器依然没有成功启动,那么 containerMonitor 对象会负责重置以及清除所有已经为容器准备好的资源,例如已经为容器分配好的网络资源(即 IP 地址),还有为容器准备的 rootfs 等。

waitForStart() 函数通过 container.monitor.Start 来实现容器的启动,进入./docker/daemon/monitor.go#L100,可以发现启动容器进程位于./docker/daemon/monitor.go#L136,代码如下:

exitStatus, err = m.container.daemon.Run(m.container, pipes, m.callback) 

以上代码实际调用了 daemon 包中的 Run 函数,位于./docker/daemon/daemon.go#L969-L971,代码如下:

func (daemon *Daemon) Run(c *Container, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) {
		return daemon.execDriver.Run(c.command, pipes, startCallback)
}

最终,Run 函数中调用了 execdriver 中的 Run 函数来执行 Docker Container 的启动命令。

至此,网络部分在 Docker Daemon 内部的执行已经结束,紧接着程序运行逻辑陷入 execdriver,进一步完成容器启动的相关步骤。

3.execdriver 网络执行流程

Docker 架构中 execdriver 的作用是启动容器内部进程,最终启动容器。目前,在 Docker 中 execdriver 作为执行驱动,可以有两种选项:lxc 与 native。其中,lxc 驱动会调用 lxc 工具实现容器的启动,而 native 驱动会使用Docker 官方发布的 libcontainer来启动容器。

Docker Daemon 启动过程中,execdriver 的类型默认为 native,故本文主要分析 native 驱动在执行启动容器时,如何处理网络部分。

在 Docker Daemon 启动容器的最后一步,即调用了 execdriver 的 Run 函数来执行。通过分析 Run 函数的具体实现,关于 Docker Container 的网络执行流程主要包括两个环节:

(1) 创建 libcontainer 的 Config 对象

(2) 通过 libcontainer 中的 namespaces 包执行启动容器

将 execdriver.Run 函数的运行流程具体展开,与 Docker Container 网络相关的流程,可以得到以下示意图:

图 3.1 execdriver.Run 执行流程图

3.1 创建 libcontainer 的 Config 对象

Run 函数位于./docker/daemon/execdriver/native/driver.go#L62-L168,进入 Run 函数的实现,立即可以发现该函数通过 createContainer 创建了一个 container 对象,代码如下:

container, err := d.createContainer(c) 

其中 c 为 Docker Daemon 创建的 execdriver.Command 类型实例。以上代码的 createContainer 函数的作用是:使用 execdriver.Command 来填充 libcontainer.Config。

libcontainer.Config 的作用是,定义在一个容器化的环境中执行一个进程所需要的所有配置项。createContainer 函数的存在,使用 Docker Daemon 层创建的 execdriver.Command,创建更下层 libcontainer 所需要的 Config 对象。这个角度来看,execdriver 更像是封装了 libcontainer 对外的接口,实现了将 Docker Daemon 认识的容器启动信息转换为底层 libcontainer 能真正使用的容器启动配置选项。libcontainer.Config 类型与其内部对象的关联图如下:

图 3.2 libcontainer.Config 类型关系图

进入 createContainer 的源码实现部分,位于./docker/daemon/execdriver/native/create.go#L23-L77,代码如下:

func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Config, error) {
	container := template.New()
	……
	if err := d.createNetwork(container, c); err != nil {
		return nil, err
	}
	……
	return container, nil
}

3.1.1 libcontainer.Config 模板实例

从 createContainer 函数的实现以及 execdriver.Run 执行流程图中都可以看到,createContainer 所做的第一个操作就是即为执行 template.New(),意为创建一个 libcontainer.Config 的实例 container。其中,template.New() 的定义位于./docker/daemon/execdriver/native/template/default_template.go,主要的作用为返回 libcontainer 关于 Docker Container 的默认配置选项。

Template.New() 的代码实现如下:

func New() *libcontainer.Config {
	container := &libcontainer.Config{
		Capabilities: []string{
			"CHOWN",
			"DAC_OVERRIDE",
			"FSETID",
			"FOWNER",
			"MKNOD",
			"NET_RAW",
			"SETGID",
			"SETUID",
			"SETFCAP",
			"SETPCAP",
			"NET_BIND_SERVICE",
			"SYS_CHROOT",
			"KILL",
			"AUDIT_WRITE",
		},
		Namespaces: map[string]bool{
			"NEWNS":  true,
			"NEWUTS": true,
			"NEWIPC": true,
			"NEWPID": true,
			"NEWNET": true,
		},
		Cgroups: &cgroups.Cgroup{
			Parent:          "docker",
			AllowAllDevices: false,
		},
		MountConfig: &libcontainer.MountConfig{},
	}

	if apparmor.IsEnabled() {
		container.AppArmorProfile = "docker-default"
	}

	return container
}

关于该 libcontainer.Config 默认的模板对象,从源码实现中可以看到,首先设定了 Capabilities 的默认项,如 CHOWN、DAC_OVERRIDE、FSETID 等;其次又将 Docker Container 所需要的设定的 namespaces 添加默认值,即需要创建 5 个 NAMESPACE,如 NEWNS、NEWUTS、NEWIPC、NEWPID 和 NEWNET,其中不包括 user namespace,另外与网络相关的 namespace 为 NEWNET;最后设定了一些关于 cgroup 以及 apparmor 的默认配置。

Template.New() 函数最后返回类型为 libcontainer.Config 的实例 container,该实例中只含有默认配置项,其他的配置项的添加需要 createContainer 的后续操作来完成。

3.1.2 createNetwork 实现

在 createContainer 的实现流程中,为了完成 container 对象(类型为 libcontainer.Config)的完善,最后有很多步骤,如与网络相关的 createNetwork 函数调用,与 Linux 内核 Capabilities 相关的 setCapabilities 函数调用,与 cgroups 相关的 setupCgroups 函数调用,以及与挂载目录相关的 setupMounts 函数调用等。本小节主要分析 createNetwork 如何为 container 对象完善网络配置项。

createNetwork 函数的定义位于./docker/daemon/execdriver/native/create.go#L79-L124,该函数主要利用 execdriver.Command 中 Network 属性中的内容,来判断如何创建 libcontainer.Config 中 Network 属性(关于两中 Network 属性,可以参见图 3.1 和图 3.2)。由于 Docker Container 的 4 种网络模式彼此互斥,故以上 Network 类型中 Interface、ContainerID 与 HostNetworking 最多只有一项会被赋值。

由于 execdriver.Command 中 Network 的类型定义如下:

type Network struct {
	Interface      *NetworkInterface
	Mtu            int              
	ContainerID    string            
	HostNetworking bool             
}

分析 createNetwork 函数,其具体实现可以归纳为 4 部分内容:

(1) 判断网络是否为 host 模式;

(2) 判断是否为 bridge 桥接模式;

(3) 判断是否为 other container 模式;

(4) 为 Docker Container 添加 loopback 网络设备。

首先来看 execdriver 判断是否为 host 模式的代码:

if c.Network.HostNetworking {
	container.Namespaces["NEWNET"] = false
	return nil
}

当 execdriver.Command 类型实例中 Network 属性的 HostNetworking 为 true,则说明需要为 Docker Container 创建 host 网络模式,使得容器与宿主机共享同样的网络命名空间。关于 host 模式的具体介绍中,已经阐明,只须在创建进程进行 CLONE 系统调用时,不传入 CLONE_NEWNET 参数标志即可实现。这里的代码正好准确的验证了这一点,将 container 对象中 NEWNET 的 Namespace 设为 false,最终在 libcontainer 中可以达到效果。

再看 execdriver 判断是否为 bridge 桥接模式的代码:

if c.Network.Interface != nil {
		vethNetwork := libcontainer.Network{
			Mtu:        c.Network.Mtu,
			Address:    fmt.Sprintf("%s/%d", c.Network.Interface.IPAddress, 
c.Network.Interface.IPPrefixLen),
			Gateway:    c.Network.Interface.Gateway,
			Type:       "veth",
			Bridge:     c.Network.Interface.Bridge,
			VethPrefix: "veth",
		}
		container.Networks = append(container.Networks, &vethNetwork)
	}

当 execdriver.Command 类型实例中 Network 属性的 Interface 不为 nil 值,则说明需要为 Docker Container 创建 bridge 桥接模式,使得容器使用隔离的网络环境。于是这里为类型为 libcontainer.Config 的 container 对象添加 Networks 属性 vethNetwork,网络类型为“veth”,以便 libcontainer 在执行时,可以为 Docker Container 创建 veth pair。

接着来看 execdriver 判断是否为 other container 模式的代码:

if c.Network.ContainerID != "" {
		d.Lock()
		active := d.activeContainers[c.Network.ContainerID]
		d.Unlock()

		if active == nil || active.cmd.Process == nil {
			return fmt.Errorf("%s is not a valid running container to join", c.Network.ContainerID)
		}
		cmd := active.cmd

		nspath := filepath.Join("/proc", fmt.Sprint(cmd.Process.Pid), "ns", "net")
		container.Networks = append(container.Networks, &libcontainer.Network{
			Type:   "netns",
			NsPath: nspath,
		})
	}

当 execdriver.Command 类型实例中 Network 属性的 ContainerID 不为空字符串时,则说明需要为 Docker Container 创建 other container 模式,使得创建容器使用其他容器的网络环境。实现过程中,execdriver 需要首先在 activeContainers 中查找需要被共享网络环境的容器 active;并通过 active 容器的启动执行命令 cmd 找到容器第一进程在宿主机上的 PID;随后在 proc 文件系统中找到该进程 PID 的关于网络 namespace 的路径 nspath;最后为类型为 libcontainer.Config 的 container 对象添加 Networks 属性,Network 的类型为“netns”。

此外,createNetwork 函数还实现为 Docker Container 创建一个 loopback 回环设备,以便容器可以实现内部通信。实现过程中,同样为类型 libcontainer.Config 的 container 对象添加 Networks 属性,Network 的类型为“loopback”,代码如下:

container.Networks = []*libcontainer.Network{
		{
			Mtu:     c.Network.Mtu,
			Address: fmt.Sprintf("%s/%d", "127.0.0.1", 0),
			Gateway: "localhost",
			Type:    "loopback",
		},
	}

至此,createNetwork 函数已经把与网络相关的配置,全部创建在类型为 libcontainer.Config 的 container 对象中了,就等着启动容器进程时使用。

3.2 调用 libcontainer 的 namespaces 启动容器

回到 execdriver.Run 函数,创建完 libcontainer.Config 实例 container,经过一系列其他方面的处理之后,最终 execdriver 执行 namespaces.Exec 函数实现启动容器,container 对象依然是 namespace.Exec 函数中一个非常重要的参数。这一环节代表着 execdriver 把启动 Docker Container 的工作交给 libcontainer,以后的执行陷入 libcontainer。

调用 namespaces.Exec 的代码位于./docker/daemon/execdriver/native/driver.go#L102-L127,为了便于理解,简化的代码如下:

namespaces.Exec(container, c.Stdin, c.Stdout, c.Stderr, c.Console,
 c.Rootfs, dataPath, args, parameter_1, parameter_2) 

其中 parameter_1 为定义的函数,如下:

func(container *libcontainer.Config, console, rootfs, dataPath, 
init string, child *os.File, args []string) *exec.Cmd {
		c.Path = d.initPath
		c.Args = append([]string{
			DriverName,
			"-console", console,
			"-pipe", "3",
			"-root", filepath.Join(d.root, c.ID),
			"--",
		}, args...)

		// set this to nil so that when we set the clone flags anything else is reset
		c.SysProcAttr = &syscall.SysProcAttr{
			Cloneflags: uintptr(namespaces.GetNamespaceFlags(container.Namespaces)),
		}
		c.ExtraFiles = []*os.File{child}

		c.Env = container.Env
		c.Dir = c.Rootfs

		return &c.Cmd
	}

同样的,parameter_2 也为定义的函数,如下:

func() {
		if startCallback != nil {
			c.ContainerPid = c.Process.Pid
			startCallback(c)
		}
	}

Parameter_1 以及 parameter_2 这两个函数均会在 libcontainer 的 namespaces 中发挥很大的重要。

至此,execdriver 模块的执行部分已经完结,Docker Daemon 的程序运行逻辑陷入 libcontainer。

4.libcontainer 实现内核态网络配置

libcontainer 是一个 Linux 操作系统上容器的实现包。libcontainer 指定了创建一个容器时所需要的配置选项,同时它利用 Linux namespace 和 cgroup 等技术为使用者提供了一套 Golang 原生态的容器实现方案,并且没有使用任何外部依赖。用户借助 libcontainer,可以感受到众多操纵 namespaces,网络等资源的便利。

当 execdriver 调用 libcontainer 中 namespaces 包的 Exec 函数时,libcontainer 开始发挥其实现容器功能的作用。Exec 函数位于./libcontainer/namespaces/exec.go#L24-L113。本文更多的关心 Docker Container 的网络创建,因此从这个角度来看 Exec 的实现可以分为三个步骤:

(1) 通过 createCommand 创建一个 Golang 语言内的 exec.Cmd 对象;

(2) 启动命令 exec.Cmd,执行容器内第一个进程;

(3) 通过 InitializeNetworking 函数为容器进程初始化网络环境。

以下详细分析这三个部分,源码的具体实现。

4.1 创建 exec.Cmd

提到 exec.Cmd,就不得不提 Go 语言标准库中的包 os 以及包 os/exec。前者提供了与平台无关的操作系统功能集,后者则提供了功能集里与命令执行相关的部分。

首先来看一下在 Go 语言中 exec.Cmd 的定义,如下:

type Cmd struct {
    	Path string                   // 所需执行命令在系统中的路径 
    	Args []string                  // 传入命令的参数 
    	Env []string                   // 进程运行时的环境变量 
   		Dir string                     // 命令运行的工作目录 
   		Stdin io.Reader
    	Stdout io.Writer
    	Stderr io.Writer
    	ExtraFiles []*os.File             // 进程所需打开的文件描述符资源 
    	SysProcAttr *syscall.SysProcAttr   // 可选的操作系统属性 
    	Process *os.Process            // 代表 Cmd 启动后,操作系统底层的具体进程 
    	ProcessState *os.ProcessState    // 进程退出后保留的信息 
}

清楚 Cmd 的定义之后,再来分析 namespaces 包的 Exec 函数中,是如何来创建 exec.Cmd 的。在 Exec 函数的实现过程中,使用了以下代码实现 Exec.Cmd 的创建:

command := createCommand(container, console, rootfs, dataPath, 
os.Args[0], syncPipe.Child(), args) 

其中 createCommand 为 namespace.Exec 函数中传入的倒数第二个参数,类型为 CreateCommand。而 createCommand 只是 namespaces.Exec 函数的形参,真正的实参则为 execdriver 调用 namespaces.Exec 时的参数 parameter_1,即如下代码:

func(container *libcontainer.Config, console, rootfs, dataPath,
 init string, child *os.File, args []string) *exec.Cmd {
		c.Path = d.initPath
		c.Args = append([]string{
			DriverName,
			"-console", console,
			"-pipe", "3",
			"-root", filepath.Join(d.root, c.ID),
			"--",
		}, args...)

		// set this to nil so that when we set the clone flags anything else is reset
		c.SysProcAttr = &syscall.SysProcAttr{
			Cloneflags: uintptr(namespaces.GetNamespaceFlags(container.Namespaces)),
		}
		c.ExtraFiles = []*os.File{child}

		c.Env = container.Env
		c.Dir = c.Rootfs

		return &c.Cmd
	}

熟悉 exec.Cmd 的定义之后,分析以上代码的实现就显得较为简单。为 Cmd 赋值的对象有 Path,Args,SysProcAttr,ExtraFiles,Env 和 Dir。其中需要特别注意的是 Path 的值 d.initPath,该路径下存放的是 dockerinit 的二进制文件,Docker 1.2.0 版本下,路径一般为“/var/lib/docker/init/dockerinit-1.2.0”。另外 SysProcAttr 使用以下的代码来赋值:

&syscall.SysProcAttr{
		Cloneflags: uintptr(namespaces.GetNamespaceFlags(container.Namespaces)),
	}

syscall.SysProAttr 对象中的 Cloneflags 属性中,即保留了 libcontainer.Config 类型的实例 container 中的 Namespace 属性。换言之,通过 exec.Cmd 创建进程时,正是通过 Cloneflags 实现 Clone 系统调用中传入 namespace 参数标志。

回到函数执行中,在函数的最后返回了 c.Cmd,命令创建完毕。

4.2 启动 exec.Cmd 创建进程

创建完 exec.Cmd,当然需要将该执行命令运行起来,namespaces.Exec 函数中直接使用以下代码实现进程的启动:

if err := command.Start(); err != nil {
		return -1, err
	}

这一部分的内容简单直接, Start() 函数用以完成指定命令 exec.Cmd 的启动执行,同时并不等待其启动完毕便返回。Start() 函数的定义位于 os/exec 包。

进入 os/exec 包,查看 Start() 函数的实现,可以看到执行过程中,会对 command.Process 进行赋值,此时 command.Process 中会含有刚才启动进程的 PID 进程号,该 PID 号属于在宿主机 pid namespace 下,而并非是新创建 namespace 下的 PID 号。

4.3 为容器进程初始化网络环境

上一环节实现了容器进程的启动,然而却还没有为之配置相应的网络环境。namespaces.Exec 在之后的 InitializeNetworing 中实现了为容器进程初始化网络环境。初始化网络环境需要两个非常重要的参数:container 对象以及容器进程的 Pid 号。类型为 libcontainer.Config 的实例 container 中包含用户对 Docker Container 的网络配置需求,另外容器进程的 Pid 可以使得创建的网络环境与进程新创建的 namespace 进行关联。

namespaces.Exec 中为容器进程初始化网络环境的代码实现位于./libcontainer/namespaces/exec.go#L75-L79,如下:

if err := InitializeNetworking(container, command.Process.Pid, syncPipe, &networkState); err != nil {
		command.Process.Kill()
		command.Wait()
		return -1, err
	}

InitializeNetworing 的作用很明显,即为创建的容器进程初始化网络环境。更为底层的实现包含两个步骤:

(1) 先在容器进程的 namespace 外部,创建容器所需的网络栈;

(2) 将创建的网络栈迁移进入容器的 net namespace。

IntializeNetworking 的源代码实现位于./libcontainer/namespaces/exec.go#L176-L187,如下:

func InitializeNetworking(container *libcontainer.Config, nspid int, pipe *syncpipe.SyncPipe, networkState *network.NetworkState) error {
	for _, config := range container.Networks {
		strategy, err := network.GetStrategy(config.Type)
		if err != nil {
			return err
		}
		if err := strategy.Create((*network.Network)(config), nspid, networkState); err != nil {
			return err
		}
	}
	return pipe.SendToChild(networkState)
}

以上源码实现过程中,首先通过一个循环,遍历 libcontainer.Config 类型实例 container 中的网络属性 Networks;随后使用 GetStrategy 函数处理 Networks 中每一个对象的 Type 属性,得出 Network 的类型,这里的类型有 3 种,分别为“loopback”、“veth”、“netns”。除 host 网络模式之外,loopback 对于其他每一种网络模式的 Docker Container 都需要使用;veth 针对 bridge 桥接模式,而 netns 针对 other container 模式。

得到 Network 类型的类型之后,libcontainer 创建相应的网络栈,具体实现使用每种网络栈类型下的 Create 函数。以下分析三种不同网络栈各自的创建流程。

4.3.1 loopback 网络栈的创建

Loopback 是一种本地环回设备,libcontainer 创建 loopback 网络设备的实现代码位于./libcontainer/network/loopback.go#L13-L15,如下:

func (l *Loopback) Create(n *Network, nspid int, networkState *NetworkState) error {
		return nil
}

令人费解的是,libcontainer 在 loopback 设备的创建函数 Create 中,并没有作实质性的内容,而是直接返回 nil。

其实关于 loopback 设备的创建,要回到 Linux 内核为进程新建 namespace 的阶段。当 libcontainer 执行 command.Start() 时,由于创建了一个新的网络 namespace,故 Linux 内核会自动为新的 net namespace 创建 loopback 设备。当 Linux 内核创建完 loopback 设备之后,libcontainer 所做的工作即只要保留 loopback 设备的默认配置,并在 Initialize 函数中实现启动该设备。

4.3.2 veth 网络栈的创建

Veth 是 Docker Container 实际使用的网络策略之一,其使用网桥 docker0 并创建 veth pair 虚拟网络设备对,最终使一个 veth 配置在宿主机上,而另一个 veth 安置在容器网络 namespace 内部。

libcontainer 中实现 veth 策略的代码非常通俗易懂,代码位于./libcontainer/network/veth.go#L19-L50,如下:

name1, name2, err := createVethPair(prefix)
	if err != nil {
		return err
	}
	if err := SetInterfaceMaster(name1, bridge); err != nil {
		return err
	}
	if err := SetMtu(name1, n.Mtu); err != nil {
		return err
	}
	if err := InterfaceUp(name1); err != nil {
		return err
	}
	if err := SetInterfaceInNamespacePid(name2, nspid); err != nil {
		return err
	}

主要的流程包含的四个步骤:

(1) 在宿主机上创建 veth pair;

(2) 将一个 veth 附加至 docker0 网桥上;

(3) 启动第一个 veth;

(4) 将第二个 veth 附加至 libcontainer 创建进程的 namespace 下。

使用 Create 函数实现 veth pair 的创建之后,在 Initialize 函数中实现将网络 namespace 中的 veth 改名为“eth0”,并设置网络设备的 MTU 等。

4.3.3 netns 网络栈的创建

netns 专门为 Docker Container 的 other container 网络模式服务。netns 完成的工作是将其他容器的 namespace 路径传递给需要创建 other container 网络模式的容器使用。

libcontainer 中实现 netns 策略的代码位于./libcontainer/network/netns.go#L17-L20,如下:

func (v *NetNS) Create(n *Network, nspid int, networkState *NetworkState) error {
		networkState.NsPath = n.NsPath
		return nil
}

使用 Create 函数先将 NsPath 赋给新建容器之后,在 Initialize 函数中实现将网络 namespace 的文件描述符交由新创建容器使用,最终实现两个 Docker Container 共享同一个网络栈。

通过 Create 以及 Initialize 的实现之后,Docker Container 相应的网络栈环境即已经完成创建,容器内部的应用进程可以使用不同的网络栈环境与外界或者内部进行通信。关于 Initialize 函数何时被调用,需要清楚 Docker Daemon 与 dockerinit 的关系,以及如何实现 Docker Daemon 进程与 dockerinit 进程跨 namespace 进行通信,这部分内容会在《Docker 源码分析》系列后续专文分析。

5. 总结

如何使用 Docker Container 的网络,一直是工业界倍加关心的问题。本文将从 Linux 内核原理的角度阐述了什么是 Docker Container,并对 Docker Container 4 种不同的网络模式进行了初步的介绍,最终贯穿 Docker 架构中的多个模块,如 Docker Client、Docker Daemon、execdriver 以及 libcontainer,深入分析 Docker Container 网络的实现步骤。

目前,若只谈论 Docker,那么它还是只停留在单 host 宿主机的场景上。如何面对跨 host 的场景、如何实现分布式 Docker Container 的管理,目前为止还没有一个一劳永逸的解决方案。再者,一个解决方案的存在,总是会适应于一个应用场景。Docker 这种容器技术的发展,大大改善了传统模式下使用诸如虚拟机等传统计算单位存在的多数弊端,却在网络方面使得自身的使用过程中存在瑕疵。希望本文是一个引子,介绍 Docker Container 网络,以及从源码的角度分析 Docker Container 网络之后,能有更多的爱好者思考 Docker Container 网络的前世今生,并为 Docker 乃至容器技术的发展做出贡献。

6. 作者介绍

孙宏亮,DaoCloud初创团队成员,软件工程师,浙江大学 VLIS 实验室应届研究生。读研期间活跃在 PaaS 和 Docker 开源社区,对 Cloud Foundry 有深入研究和丰富实践,擅长底层平台代码分析,对分布式平台的架构有一定经验,撰写了大量有深度的技术博客。2014 年末以合伙人身份加入 DaoCloud 团队,致力于传播以 Docker 为主的容器的技术,推动互联网应用的容器化步伐。邮箱:allen.sun@daocloud.io

7. 参考文献

http://docs.studygolang.com/pkg/os/exec/#Cmd

https://github.com/docker/libcontainer/tree/v1.2.0

https://github.com/docker/libcontainer/issues/323


感谢郭蕾对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论