ArchSummit全球架构师峰会全新主题——「智能进阶·架构重塑」>>> 了解详情
写点什么

Docker 源码分析(七):Docker Container 网络 (上)

  • 2015-01-24
  • 本文字数:7961 字

    阅读完需:约 26 分钟

1. 前言 (什么是 Docker Container)

如今,Docker 技术大行其道,大家在尝试以及玩转 Docker 的同时,肯定离不开一个概念,那就是“容器”或者“Docker Container”。那么我们首先从实现的角度来看看“容器”或者“Docker Container”到底为何物。

逐渐熟悉 Docker 之后,大家肯定会深深得感受到:应用程序在 Docker Container 内部的部署与运行非常便捷,只要有 Dockerfile,应用一键式的部署运行绝对不是天方夜谭; Docker Container 内运行的应用程序可以受到资源的控制与隔离,大大满足云计算时代应用的要求。毋庸置疑,Docker 的这些特性,传统模式下应用是完全不具备的。然而,这些令人眼前一亮的特性背后,到底是谁在“作祟”,到底是谁可以支撑 Docker 的这些特性?不知道这个时候,大家是否会联想到强大的 Linux 内核。

其实,这很大一部分功能都需要归功于 Linux 内核。那我们就从 Linux 内核的角度来看看 Docker 到底为何物,先从 Docker Container 入手。关于 Docker Container,体验过的开发者第一感觉肯定有两点:内部可以跑应用(进程),以及提供隔离的环境。当然,后者肯定也是工业界称之为“容器”的原因之一。

既然 Docker Container 内部可以运行进程,那么我们先来看 Docker Container 与进程的关系,或者容器与进程的关系。首先,我提出这样一个问题供大家思考“容器是否可以脱离进程而存在”。换句话说,能否创建一个容器,而这个容器内部没有任何进程。

可以说答案是否定的。既然答案是否定的,那说明不可能先有容器,然后再有进程,那么问题又来了,“容器和进程是一起诞生,还是先有进程再有容器呢?”可以说答案是后者。以下将慢慢阐述其中的原因。

阐述问题“容器是否可以脱离进程而存在”的原因前,相信大家对于以下的一段话不会持有异议:通过 Docker 创建出的一个 Docker Container 是一个容器,而这个容器提供了进程组隔离的运行环境。那么问题在于,容器到底是通过何种途径来实现进程组运行环境的“隔离”。这时,就轮到 Linux 内核技术隆重登场了。

说到运行环境的“隔离”,相信大家肯定对 Linux 的内核特性 namespace 和 cgroup 不会陌生。namespace 主要负责命名空间的隔离,而 cgroup 主要负责资源使用的限制。其实,正是这两个神奇的内核特性联合使用,才保证了 Docker Container 的“隔离”。那么,namespace 和 cgroup 又和进程有什么关系呢?问题的答案可以用以下的次序来说明:

(1) 父进程通过 fork 创建子进程时,使用 namespace 技术,实现子进程与其他进程(包含父进程)的命名空间隔离;

(2) 子进程创建完毕之后,使用 cgroup 技术来处理子进程,实现进程的资源使用限制;

(3) 系统在子进程所处 namespace 内部,创建需要的隔离环境,如隔离的网络栈等;

(4) namespace 和 cgroup 两种技术都用上之后,进程所处的“隔离”环境才真正建立,这时“容器”才真正诞生!

从 Linux 内核的角度分析容器的诞生,精简的流程即如以上 4 步,而这 4 个步骤也恰好巧妙的阐述了 namespace 和 cgroup 这两种技术和进程的关系,以及进程与容器的关系。进程与容器的关系,自然是:容器不能脱离进程而存在,先有进程,后有容器。然而,大家往往会说到“使用 Docker 创建 Docker Container(容器),然后在容器内部运行进程”。对此,从通俗易懂的角度来讲,这完全可以理解,因为“容器”一词的存在,本身就较为抽象。如果需要更为准确的表述,那么可以是:“使用 Docker 创建一个进程,为这个进程创建隔离的环境,这样的环境可以称为 Docker Container(容器),然后再在容器内部运行用户应用进程。”当然,笔者的本意不是想否定很多人对于 Docker Container 或者容器的认识,而是希望和读者一起探讨 Docker Container 底层技术实现的原理。

对于 Docker Container 或者容器有了更加具体的认识之后,相信大家的眼球肯定会很快定位到 namespace 和 cgroup 这两种技术。Linux 内核的这两种技术,竟然能起到如此重大的作用,不禁为之赞叹。那么下面我们就从 Docker Container 实现流程的角度简要介绍这两者。

首先讲述一下 namespace 在容器创建时的用法,首先从用户创建并启动容器开始。当用户创建并启动容器时,Docker Daemon 会 fork 出容器中的第一个进程 A(暂且称为进程 A,也就是 Docker Daemon 的子进程)。Docker Daemon 执行 fork 时,在 clone 系统调用阶段会传入 5 个参数标志 CLONE_NEWNS、CLONE_NEWUTS、CLONE_NEWIPC、CLONE_NEWPID 和 CLONE_NEWNET(目前 Docker 1.2.0 还没有完全支持 user namespace)。Clone 系统调用一旦传入了这些参数标志,子进程将不再与父进程共享相同的命名空间(namespace),而是由 Linux 为其创建新的命名空间(namespace),从而保证子进程与父进程使用隔离的环境。另外,如果子进程 A 再次 fork 出子进程 B 和 C,而 fork 时没有传入相应的 namespace 参数标志,那么此时子进程 B 和 C 将会与 A 共享同一个命令空间(namespace)。如果 Docker Daemon 再次创建一个 Docker Container,容器内第一个进程为 D,而 D 又 fork 出子进程 E 和 F,那么这三个进程也会处于另外一个新的 namespace。两个容器的 namespace 均与 Docker Daemon 所在的 namespace 不同。Docker 关于 namespace 的简易示意图如下:

图 1.1 Docker 中 namespace 示意图

再说起 cgroup,大家都知道可以使用 cgroup 为进程组做资源的控制。与 namespace 不同的是,cgroup 的使用并不是在创建容器内进程时完成的,而是在创建容器内进程之后再使用 cgroup,使得容器进程处于资源控制的状态。换言之,cgroup 的运用必须要等到容器内第一个进程被真正创建出来之后才能实现。当容器内进程被创建完毕,Docker Daemon 可以获知容器内进程的 PID 信息,随后将该 PID 放置在 cgroup 文件系统的指定位置,做相应的资源限制。

可以说 Linux 内核的 namespace 和 cgroup 技术,实现了资源的隔离与限制。那么对于这种隔离与受限的环境,是否还需要配置其他必需的资源呢。这回答案是肯定的,网络栈资源就是在此时为容器添加。当为容器进程创建完隔离的运行环境时,发现容器虽然已经处于一个隔离的网络环境(即新的 network namespace),但是进程并没有独立的网络栈可以使用,如独立的网络接口设备等。此时,Docker Daemon 会将 Docker Container 所需要的资源一一为其配备齐全。网络方面,则需要按照用户指定的网络模式,配置 Docker Container 相应的网络资源。

2.Docker Container 网络分析内容安排

Docker Container 网络篇将从源码的角度,分析 Docker Container 从无到有的过程中,Docker Container 网络创建的来龙去脉。Docker Container 网络创建流程可以简化如下图:

图 2.1 Docker Container 网络创建流程图

Docker Container 网络篇分析的主要内容有以下 5 部分:

(1) Docker Container 的网络模式;

(2) Docker Client 配置容器网络;

(3) Docker Daemon 创建容器网络流程;

(4) execdriver 网络执行流程;

(5) libcontainer 实现内核态网络配置。

Docker Container 网络创建过程中,networkdriver 模块使用并非是重点,故分析内容中不涉及 networkdriver。这里不少读者肯定会有疑惑。需要强调的是,networkdriver 在 Docker 中的作用:第一,为 Docker Daemon 创建网络环境的时候,初始化 Docker Daemon 的网络环境(详情可以查看《Docker 源码分析》系列第六篇),比如创建 docker0 网桥等;第二,为 Docker Container 分配 IP 地址,为 Docker Container 做端口映射等。而与 Docker Container 网络创建有关的内容极少,只有在桥接模式下,为 Docker Container 的网络接口设备分配一个可用 IP 地址。

本文为《Docker 源码分析》系列第七篇——Docker Container 网络(上)。

3.Docker Container 网络模式

正如在上文提到的,Docker 可以为 Docker Container 创建隔离的网络环境,在隔离的网络环境下,Docker Container 独立使用私有网络。相信很多的 Docker 开发者也是体验过 Docker 这方面的网络特性。

其实,Docker 除了可以为 Docker Container 创建隔离的网络环境之外,同样有能力为 Docker Container 创建共享的网络环境。换言之,当开发者需要 Docker Container 与宿主机或者其他容器网络隔离时,Docker 可以满足这样的需求;而当开发者需要 Docker Container 与宿主机或者其他容器共享网络时,Docker 同样可以满足这样的需求。另外,Docker 还可以不为 Docker Container 创建网络环境。

总结 Docker Container 的网络,可以得出 4 种不同的模式:bridge 桥接模式、host 模式、other container 模式和 none 模式。以下初步介绍 4 中不同的网络模式。

3.1 bridge 桥接模式

Docker Container 的 bridge 桥接模式可以说是目前 Docker 开发者最常使用的网络模式。Brdige 桥接模式为 Docker Container 创建独立的网络栈,保证容器内的进程组使用独立的网络环境,实现容器间、容器与宿主机之间的网络栈隔离。另外,Docker 通过宿主机上的网桥 (docker0) 来连通容器内部的网络栈与宿主机的网络栈,实现容器与宿主机乃至外界的网络通信。

Docker Container 的 bridge 桥接模式可以参考下图:

图 3.1 Docker Container Bridge 桥接模式示意图

Bridge 桥接模式的实现步骤主要如下:

(1) Docker Daemon 利用 veth pair 技术,在宿主机上创建两个虚拟网络接口设备,假设为 veth0 和 veth1。而 veth pair 技术的特性可以保证无论哪一个 veth 接收到网络报文,都会将报文传输给另一方。

(2) Docker Daemon 将 veth0 附加到 Docker Daemon 创建的 docker0 网桥上。保证宿主机的网络报文可以发往 veth0;

(3) Docker Daemon 将 veth1 添加到 Docker Container 所属的 namespace 下,并被改名为 eth0。如此一来,保证宿主机的网络报文若发往 veth0,则立即会被 eth0 接收,实现宿主机到 Docker Container 网络的联通性;同时,也保证 Docker Container 单独使用 eth0,实现容器网络环境的隔离性。

Bridge 桥接模式,从原理上实现了 Docker Container 到宿主机乃至其他机器的网络连通性。然而,由于宿主机的 IP 地址与 veth pair 的 IP 地址均不在同一个网段,故仅仅依靠 veth pair 和 namespace 的技术,还不足以是宿主机以外的网络主动发现 Docker Container 的存在。为了使得 Docker Container 可以让宿主机以外的世界感知到容器内部暴露的服务,Docker 采用 NAT(Network Address Translation,网络地址转换)的方式,让宿主机以外的世界可以主动将网络报文发送至容器内部。

具体来讲,当 Docker Container 需要暴露服务时,内部服务必须监听容器 IP 和端口号 port_0,以便外界主动发起访问请求。由于宿主机以外的世界,只知道宿主机 eth0 的网络地址,而并不知道 Docker Container 的 IP 地址,哪怕就算知道 Docker Container 的 IP 地址,从二层网络的角度来讲,外界也无法直接通过 Docker Container 的 IP 地址访问容器内部应用。因此,Docker 使用 NAT 方法,将容器内部的服务监听的端口与宿主机的某一个端口 port_1 进行“绑定”。

如此一来,外界访问 Docker Container 内部服务的流程为:

(1) 外界访问宿主机的 IP 以及宿主机的端口 port_1;

(2) 当宿主机接收到这样的请求之后,由于 DNAT 规则的存在,会将该请求的目的 IP(宿主机 eth0 的 IP)和目的端口 port_1 进行转换,转换为容器 IP 和容器的端口 port_0;

(3) 由于宿主机认识容器 IP,故可以将请求发送给 veth pair;

(4) veth pair 的 veth0 将请求发送至容器内部的 eth0,最终交给内部服务进行处理。

使用 DNAT 方法,可以使得 Docker 宿主机以外的世界主动访问 Docker Container 内部服务。那么 Docker Container 如何访问宿主机以外的世界呢。以下简要分析 Docker Container 访问宿主机以外世界的流程:

(1) Docker Container 内部进程获悉宿主机以外服务的 IP 地址和端口 port_2,于是 Docker Container 发起请求。容器的独立网络环境保证了请求中报文的源 IP 地址为容器 IP(即容器内部 eth0),另外 Linux 内核会自动为进程分配一个可用源端口(假设为 port_3);

(2) 请求通过容器内部 eth0 发送至 veth pair 的另一端,到达 veth0,也就是到达了网桥(docker0)处;

(3) docker0 网桥开启了数据报转发功能(/proc/sys/net/ipv4/ip_forward),故将请求发送至宿主机的 eth0 处;

(4) 宿主机处理请求时,使用 SNAT 对请求进行源地址 IP 转换,即将请求中源地址 IP(容器 IP 地址)转换为宿主机 eth0 的 IP 地址;

(5) 宿主机将经过 SNAT 转换后的报文通过请求的目的 IP 地址(宿主机以外世界的 IP 地址)发送至外界。

在这里,很多人肯定会问:对于 Docker Container 内部主动发起对外的网络请求,当请求到达宿主机进行 SNAT 处理后发给外界,当外界响应请求时,响应报文中的目的 IP 地址肯定是 Docker 宿主机的 IP 地址,那响应报文回到宿主机的时候,宿主机又是如何转给 Docker Container 的呢?关于这样的响应,由于 port_3 端口并没有在宿主机上做相应的 DNAT 转换,原则上不会被发送至容器内部。为什么说对于这样的响应,不会做 DNAT 转换呢。原因很简单,DNAT 转换是针对容器内部服务监听的特定端口做的,该端口是供服务监听使用,而容器内部发起的请求报文中,源端口号肯定不会占用服务监听的端口,故容器内部发起请求的响应不会在宿主机上经过 DNAT 处理。

其实,这一环节的内容是由 iptables 规则来完成,具体的 iptables 规则如下:

复制代码
iptables -I FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

这条规则的意思是,在宿主机上发往 docker0 网桥的网络数据报文,如果是该数据报文所处的连接已经建立的话,则无条件接受,并由 Linux 内核将其发送到原来的连接上,即回到 Docker Container 内部。

以上便是 Docker Container 中 bridge 桥接模式的简要介绍。可以说,bridger 桥接模式从功能的角度实现了两个方面:第一,让容器拥有独立、隔离的网络栈;第二,让容器和宿主机以外的世界通过 NAT 建立通信。

然而,bridge 桥接模式下的 Docker Container 在使用时,并非为开发者包办了一切。最明显的是,该模式下 Docker Container 不具有一个公有 IP,即和宿主机的 eth0 不处于同一个网段。导致的结果是宿主机以外的世界不能直接和容器进行通信。虽然 NAT 模式经过中间处理实现了这一点,但是 NAT 模式仍然存在问题与不便,如:容器均需要在宿主机上竞争端口,容器内部服务的访问者需要使用服务发现获知服务的外部端口等。另外 NAT 模式由于是在三层网络上的实现手段,故肯定会影响网络的传输效率。

3.2 host 模式

Docker Container 中的 host 模式与 bridge 桥接模式有很大的不同。最大的区别当属,host 模式并没有为容器创建一个隔离的网络环境。而之所以称之为 host 模式,是因为该模式下的 Docker Container 会和 host 宿主机共享同一个网络 namespace,故 Docker Container 可以和宿主机一样,使用宿主机的 eth0,实现和外界的通信。换言之,Docker Container 的 IP 地址即为宿主机 eth0 的 IP 地址。

Docker Container 的 host 网络模式可以参考下图:

图 3.2 Docker Container host 网络模式示意图

上图最左侧的 Docker Container,即采用了 host 网络模式,而其他两个 Docker Container 依然沿用 brdige 桥接模式,两种模式同时存在于宿主机上并不矛盾。

Docker Container 的 host 网络模式在实现过程中,由于不需要额外的网桥以及虚拟网卡,故不会涉及 docker0 以及 veth pair。上文 namespace 的介绍中曾经提到,父进程在创建子进程时,如果不使用 CLONE_NEWNET 这个参数标志,那么创建出的子进程会与父进程共享同一个网络 namespace。Docker 就是采用了这个简单的原理,在创建进程启动容器的过程中,没有传入 CLONE_NEWNET 参数标志,实现 Docker Container 与宿主机共享同一个网络环境,即实现 host 网络模式。

可以说,Docker Container 的网络模式中,host 模式是 bridge 桥接模式很好的补充。采用 host 模式的 Docker Container,可以直接使用宿主机的 IP 地址与外界进行通信,若宿主机的 eth0 是一个公有 IP,那么容器也拥有这个公有 IP。同时容器内服务的端口也可以使用宿主机的端口,无需额外进行 NAT 转换。当然,有这样的方便,肯定会损失部分其他的特性,最明显的是 Docker Container 网络环境隔离性的弱化,即容器不再拥有隔离、独立的网络栈。另外,使用 host 模式的 Docker Container 虽然可以让容器内部的服务和传统情况无差别、无改造的使用,但是由于网络隔离性的弱化,该容器会与宿主机共享竞争网络栈的使用;另外,容器内部将不再拥有所有的端口资源,原因是部分端口资源已经被宿主机本身的服务占用,还有部分端口已经用以 bridge 网络模式容器的端口映射。

3.3 other container 模式

Docker Container 的 other container 网络模式是 Docker 中一种较为特别的网络的模式。之所以称为“other container 模式”,是因为这个模式下的 Docker Container,会使用其他容器的网络环境。之所以称为“特别”,是因为这个模式下容器的网络隔离性会处于 bridge 桥接模式与 host 模式之间。Docker Container 共享其他容器的网络环境,则至少这两个容器之间不存在网络隔离,而这两个容器又与宿主机以及除此之外其他的容器存在网络隔离。

Docker Container 的 other container 网络模式可以参考下图:

图 3.3 Docker Container other container 网络模式示意图

上图右侧的 Docker Container 即采用了 other container 网络模式,它能使用的网络环境即为左侧 Docker Container brdige 桥接模式下的网络。

Docker Container 的 other container 网络模式在实现过程中,不涉及网桥,同样也不需要创建虚拟网卡 veth pair。完成 other container 网络模式的创建只需要两个步骤:

(1) 查找 other container(即需要被共享网络环境的容器)的网络 namespace;

(2) 将新创建的 Docker Container(也是需要共享其他网络的容器)的 namespace,使用 other container 的 namespace。

Docker Container 的 other container 网络模式,可以用来更好的服务于容器间的通信。

在这种模式下的 Docker Container 可以通过 localhost 来访问 namespace 下的其他容器,传输效率较高。虽然多个容器共享网络环境,但是多个容器形成的整体依然与宿主机以及其他容器形成网络隔离。另外,这种模式还节约了一定数量的网络资源。但是需要注意的是,它并没有改善容器与宿主机以外世界通信的情况。

3.4 none 模式

Docker Container 的第四种网络模式是 none 模式。顾名思义,网络环境为 none,即不为 Docker Container 任何的网络环境。一旦 Docker Container 采用了 none 网络模式,那么容器内部就只能使用 loopback 网络设备,不会再有其他的网络资源。

可以说 none 模式为 Docker Container 做了极少的网络设定,但是俗话说得好“少即是多”,在没有网络配置的情况下,作为 Docker 开发者,才能在这基础做其他无限多可能的网络定制开发。这也恰巧体现了 Docker 设计理念的开放。

4. 作者介绍

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

5. 下期预告

下期内容为:Docker 源码分析(八):Docker Container 网络(下)


感谢郭蕾对本文的审校和策划。

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

2015-01-24 01:3210105

评论

发布
暂无评论
发现更多内容

购物中心的运营保障,数衍科技数据桥接服务系统升级

科技怪咖

新元联手倍市得,以数字化手段实现人才公租房项目满意度持续监测

科技怪咖

OceanBase 4.0:当我们谈单机分布式一体化架构时,我们在说什么?

OceanBase 数据库

数衍科技与超市发达成合作,共同探索数字小票的新应用

科技怪咖

【数据结构实践】从0到1带你利用Python实现自定义集合

迷彩

数据结构 集合运算 8月月更 自定义集合

一加和OPPO是什么关系?我来揭秘

Geek_8a195c

[CSS入门到进阶] 外国前端开发者说的 Intrinsic Ratios in css 是什么意思?

HullQin

CSS JavaScript html 前端 8月月更

基于STM32设计的拼图小游戏

DS小龙哥

8月月更

C/CPP中int和string的互相转换详解与多解例题分析

CtrlX

c c++ 后端 数据类型 8月月更

数据点按时间间隔以及数据值分割数据块

waitmoon

算法 SLO

defi质押dapp智能合约系统开发代码逻辑

开发微hkkf5566

自动化测试如何解决日志问题

老张

自动化测试 日志处理

老板问我要ROI,我让他先挑宽门or窄门

科技怪咖

CSDN 报告:阿里云容器服务成为中国开发者首选

阿里巴巴中间件

阿里云 云原生 云原生容器

皮皮APP夏日防溺水公益讲座 联动武汉长江救援队筑建生命安全线

联营汇聚

每日一R「14」错误处理

Samson

学习笔记 8月月更 ​Rust

腾讯云大数据平台 TBDS全面升级,加速构建安全可控的大数据生态

科技热闻

仅用3年!青软集团跃升华为云教育类目伙伴TOP2

科技怪咖

开源一夏 |分布式事务--TCC解决方案

六月的雨在InfoQ

开源 分布式事务 TCC 最终一致性 8月月更

ARMS实践|日志在可观测场景下的应用

阿里巴巴中间件

阿里云 云原生 可观测

风险组件已经升级到最新版本,仍然提示风险,如何快速解决——kaptcha 安全漏洞

墨菲安全

Kaptcha 漏洞修复 开源安全 漏洞检测 开源安全与治理

云途加油站 | 一文读懂 Dynatrace 与Amazon Lambda 的“双剑合璧心法”

亚马逊云科技 (Amazon Web Services)

数据库 Serverless Lambda

看准六点,帮你选对客户体验管理(CEM)系统

科技怪咖

活动预告(29日)|诚邀您参与AWS & 观测云「可观测性体验日」

观测云

直播预告 | PolarDB-X 动手实践系列—— PolarDB-X on OSS 冷热数据分离存储

阿里云数据库开源

数据库 阿里云 开源 分布式 PolarDB-X

分布式雪花算法

源字节1号

前端开发 后端开发

美团二面被pass,肝完这份1213 页 的算法刷题神册成功拿到字节offer

了不起的程序猿

Java 字节跳动 算法 java程序员 java编程

Python自学教程5-字符串有哪些常用操作

和牛

Python 测试 8月月更

解决 Flutter 嵌套过深,是选择函数还是自定义类组件?

岛上码农

flutter ios 前端 安卓开发 8月月更

Flu tter开发小技巧

坚果

开源 8月月更

合成资产赛道风云突变,Linear Finance有望成为最具潜力的黑马

鳄鱼视界

Docker源码分析(七):Docker Container网络 (上)_开源_孙宏亮_InfoQ精选文章