容器化-Docker容器技术实现

  时过许久,再看起之前写的《初识Docker》的文章觉得略显单薄,只能作为想要简单了解Docker的非常入门的文章。最近又是看了很多前辈的文章,也是看了很多相关资料。最后决定还是把Docker的内容再次整理一篇文章,因为原本是要记录一下从Docker到k8s的网络问题,不过觉得Docker的网络以及其实现对后面的文章还是有很多关联性的。干脆这次是通过容器化作为前缀,准备整理一系列的文章,那么第一篇就是从Docker容器技术开篇吧。

一、Docker背后的技术依赖  

  Docker是一个在操作系统之上,通过进程隔离实现的的一个虚拟化产物,本质上还是宿主机上的不同进程,但是通过不同级别的资源隔离,在宿主机之外看起来好像是多个独立物理主机,进而提高了原宿主机的资源利用率,将宿主资源进行更细粒度的划分和使用。所以这里就涉及到一些核心技术分别来说明一下。

  • change root (chroot) : 最开始在1979年UNIX提出了chroot的概念,同样在Linux系统中,系统的默认目录就是/的根路径,这样所有的用户就都可以访问到这个目录下的内容。但是如果我们想要进行虚拟化的隔离,就要有各自独立的空间。所以chroot技术就是用来解决这个问题的,chroot能够改变当前系统的根目录,通过改变当前系统的根目录,我们就能够限制用户的权利,在新的根目录下并不能够访问到/根目录,也就建立起了一个完全隔离的目录结构。所以通过chroot为虚拟化技术提供了物理环境的隔离支持。
  • namespace : 进程隔离就是抽象出多个轻量级的内核(容器进程),这些进程可以充分利用宿主机的资源,宿主机有的资源这些进程都有共有,但是彼此之间是完全隔离开,进程所属的资源同样也是隔离开的,所以namespace就是用来隔离进程资源的。Linux Namespace提供了6项资源隔离,基本上涵盖了一个小型操作系统的运行要素,包括主机名,用户权限,文件系统,网络,进程号,进程间通信。
Namespace 系统调用参数 隔离内容 内核版本
UTS CLONE_NEWUTS 主机名和域名 2.6.19
IPC CLONE_NEWIPC 信号量、消息队列和共享内存 2.6.19
PID CLONE_NEWPID 进程编号 2.6.24
Network CLONE_NEWNET 网络设备、网络栈、端口等 2.6.29
Mount CLONE_NEWNS 挂载点 2.4.19
User CLONE_NEWUSER 用户和用户组 3.8

   Linux中源码中是通过clone()函数来创建一个子进程,在这里也可以理解为创建一个容器。

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

  这里面的flags对应的字段就是通过表格中的系统调用参数作为入参,来进行对应资源的隔离操作。对应的Docker中就是当我们执行docker run 或者docker start的时候,Docker的实现方法的底层都会调用这个clone()函数来创建子进程用于运行容器。在创建过程中会设置容器的主机名,IPC,用户和网络等信息,达到与主机资源隔离的目的。

  • control groups(cgroups) : 通过namespace和chroot技术在逻辑上实现了进程的隔离,但是这并不能保证在物理资源上的隔离,比如CPU和内存的使用。因为一个宿主机上的容器之间是相互不可见的,所以就可能出现一个容器在运行一个CPU密集形的任务,那这个时候其他容器就会收到影响而无法正常工作。所以cgroups就是用来解决容器之间物理资源的隔离的。cgroups是Google在2007补充到Linux内核之中的。

  cgroups可以把一系列的及其子任务整合到按资源进行划分的不同的组中,从而为系统资源管理提供一个统一的框架。通俗一点就是cgroups可以限制进程使用的资源(CPU ,内存,IO等)

为了方便操作,cgroups通过一个伪文件系统的方式来实现,并且提供API,用户对文件系统的操作就是对cgroups的操作。从具体的实现上来看,cgroups给每个执行任务挂了一个钩子,当任务执行过程中遇到对资源的分配使用时,就会触发钩子上的资源检查函数从而完成对资源的限制和优先级划分。

  总结一下就是cgroups具有一下四点作用:

  1. 资源限制:可以对任务使用的资源总额进行限制,超过一定的限制就可以触发对应动作
  2. 优先级分配:通过分配的CPU时间片和IO带宽的大小,实际上就是控制了任务运行的优先级
  3. 资源统计:可以统计资源的使用情况,比如CPU的使用时长等,可以用于计费统计
  4. 任务控制:cgroups可以对任务执行挂起和恢复等操作

   cgroups在设计之初就根据不同的资源分为了不同的子系统,一个子系统的本质就是一个资源控制器。比如CPU资源对应CPU子系统,负责控制CPU时间片的分配。内存对应的是内存子系统,负责限制内存的使用量。一个子系统或者多个子系统可以组成一个cgroup。cgroups中的资源都是通过cgroup为单位来实现的,一个进程可以加入到一个cgroup中,也可以从一个cgroup移动到另一个cgroup中。

  在环境中我们每创建一个容器,Linux就会为容器创建一个cgroup目录,以容器的 ID 命名,目录在 /sys/fs/cgroup/ 中。在目录下就可以看到我们对某种资源的限制文件,就是一个文件系统,所以我们前面说到用户对文件系统的操作就是对cgroups的操作。

 

  总结:这里我们介绍Docker在底层依赖Linux提供的chroot,namespace和cgroups技术来实现,可能其他文章中提到的Linux Container(LXC),其实LXC指的是Linux的虚拟化技术,而本文中提到chroot等都是LXC实际依赖的技术,其实可以说是更佳具体的LXC。这里通过更加具体的Linux底层技术依赖,是希望能给帮助大家更好的理解LXC到底做了什么能够支持Docker的,底层的原理到底是什么,而不是只记住了LXC这个名词。

二、Docker的Iptables

  这里先了解一下Docker对Iptables的相关操作为后面我们讨论Docker的网络做一下铺垫。首先我们通过Docker的官方文档先来了解一下Docker对Iptables的介绍:

      

   通过这个介绍我们可以知道Docker会在宿主机上定义两个Iptables chain , 分别是DOCKER 和 DOCKER-USER,然后Docker本身的一些规则是定义在DOCKER这个链里面的,然后如果我们想要执行一些规则在Docker本身规则之前,那么就需要在DOCKER-USER中进行定义。但是我们知道在Linux中已经为我们定义了4张表和5个Chain : prerouting , input ,forward , output 和 postrouting 。这里取用朱双印博文中的一张图片进行解释说明(想要深入了解Iptable的同学可以去:https://www.zsythink.net/archives/1199)

 

   也就是说这五个chain已经是固定好的了,那么Docker自定义添加的两条chain是在什么阶段执行相应的规则的呢?我们知道自定义的chain是不能被直接使用的,需要被默认链引用才能够使用,所以也就是说Docker本身自定义的chain需要在默认的chain去找调用,然后就才能知道具体都做了什么操作了以及在什么场景下发挥作用。

   下面在我自己的本地环境我们安装一个redis,然后把端口映射到宿主机上,通过这个例子俩看看iptables到地有什么变化。

 首先我们启动了一个redis-server,并且将6379端口暴露出去,然后看一下nat表有什么变化:

  这里我们可以看到在prerouting中出现了一条对DOCKER chain 的引用,也就是会将请求交由DOCKER chain来处理,那么在DOCKER里面我们就可以看到一条对应DNAT的规则,将请求本机6379端口的请求进行DNAT转换,把目标IP改成Docker容器的IP地址。然后因为docker0网桥的存在我们知道这个地址是Docker里面的容器,进而通过docker0这个网桥将请求转发到对应的容器中,进而实现了把容器内的服务暴露给公网的目的。Docker的其他网络实现基本也都是借助于Iptables来实现的,只是在不同阶段会在不同的chain中添加对应的规则。Iptables中有的四张表之前的优先级关系分别为: raw  >  mangle  > nat  > filter ,这里再引用博文中的一张图来加强说明 : 

 

三、Docker的网络

   通过上面的介绍我们知道,Docker通过namespace完成了与宿主机的隔离,但是这个Docker容器最终还是要通过宿主机与外界完成网络交互,否则容器能够实现的价值就会比较有限。所以这里一起来了解一下Docker的网络结构里面用到了哪些技术。

通过Docker的官方文档可以了解到,现在的网路模式已经从原来的四种模式改为五种了,而且支持插件的方式拓展网络模式。主要是:brigde,host,none,overlay 和 macvlan这五种。

这里Overlay网络实际上是目前最主流的容器跨节点数据传输和路由方案。但是本文重点是通过默认的bridge的模式来了解一下Docker的网络处理流程。

这里我们通过:ip a 命令可以看到,在Docker服务器启动之后,就会在宿主机上创建一个叫做docker0的虚拟网桥,那么随后在该宿主机上启动的全部docker容器在选择了Bridge的网络模式的情况下都是与该网桥进行链接来完成网络请求的。

在默认情况下,每创建一个容器都会创建一对虚拟veth对共同组成数据通道,其中一个会放在创建的容器中叫做eth0,另外一个会加入到docker0的网桥中。

 

首先我们通过brctl show命令可以看到现在的网桥情况,这里面的interfaces就是不同容器中的veth的名称。

下面来看一张示例图来加强理解:

   Docker在Bridge模式中会为每一个容器分配一个IP地址并将docker0的IP地址设置为容器的默认网关,网桥docker0通过iptables中的规则与宿主机上的网卡相连。所有符合条件的请求都会通过iptables转发到docker0并由网桥分发给对应的容器。

 

说到这里我们先停顿一下,这里我本人想要提一个问题:为什么Docker中的容器要通过docker0网桥来完成网络请求的转发?

1.docker0虚拟网桥

  首先说一下虚拟网桥,它是一种虚拟的网络设备,具备网络设备的所有特性,可以配置IP,Mac。除此之外虚拟网桥也可以作为一个交换机。对于普通的网络设备,就像是一个管道,只有两端,数据从一端进从另一端出。但是虚拟网桥有多个出口,数据可以从多个端口进从多个端口出。基于这个特性虚拟网桥可以接入其他的网络设备,而它本身长长作为主设备,其他设备作为从设备。这样的效果就等同于物理交换机的端口连接了一根网线与网卡相连。

 2.veth对  

  veth对是一种虚拟设备接口,都是成对出现的。是专门为LXC而创建的,它的作用很简单就是要把从一个network namespace发出的数据包转发到另一个namespace中。因为他们是成对出现的,在正确的创建和配置之后,向一端输入数据,veth会改变数据的方向并将其送入到内核网络的子系统中,完成数据的注入,而在另外一端就能够读取到此数据了。

3.专用地址

  在网络地址为IPV4的环境下,IP地址不够分配的情况比较严重,所以就出现了专用网络这个概念。就是会选择一些IP地址作为本机构内部使用的本地地址,这些地址只能用于机构内部的网络通信,而不能用于互联网上的网络通信,同时也有规定在互联网上的所有路由器的设备对目的地是专用地址的数据报一律不进行转发,目前来说有三个默认的专用地址块:

  • 10.0.0.0 到 10.255.255.255 (或记为:10.0.0.0/8,又称作24位块)
  • 172.16.0.0 到 172.31.255.255 (或记为:172.16.0.0/12,又称作20位块)
  • 192.168.0.0 到 192.168.255.255 (或记为:192.168.0.0/16,又称作16位块)

  这三个专用地址其实分别相当于一个A类网络,16个连续的B类网络和256个连续的C类网络,而我们也知道docker默认给容器分配的网络地址都是172.17.x.x。而docker0虚拟网桥的地址172.17.0.1,同时每个容器的网关的地址也就是docker0虚拟网桥的地址,所以说如果直接从容器发出网络请求,容器本地的IP地址是无法被路由器转发和处理的。

4.iptables的转发

  这里的转发我们分为两种情况:

  • 从公网发送请求到容器:如果我们想要把docker容器内的服务公开出去,那么就需要通过-p命令把容器的端口映射到主机上,这个操作就会在iptables中添加一条规则,比如这个:
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8090 -j DNAT --to-destination 172.17.0.2:8090

这条规则的意思就是:如果宿主机的eth0网卡收到目标端口为8090的请求,会进行DNAT转换,将目标IP转换成172.17.0.2(也就是容器的IP),然后转发数据报因为这个地址是专用地址,所有不会再转发出去,通过docker0网桥的信息可以知道这个地址是Docker容器内部的,所以会通过docker0网桥进行转发,这样外界就就可以与容器进行网络通信了。

  • 从容器发送请求到公网:Docker服务在创建了docker0虚拟网桥之后,会在iptables的postrouting的规则表中添加一条如下的规则:
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

简单的说就是:如果源地址为172.17.0.0/16的包(也就是从Docker容器产生的包),并不是从docker0网卡发出的,需要进行源地址转换,也就是SNAT,最终结果就是在通过eth0网卡转发数据报的时候将源地址改为网卡的地址,而不是docker容器的地址。 

所以容器内部的数据报想要在互联网上通信,首先要通过veth对的一端,将数据报发送到docker0网桥,docker0网桥在收到数据报的后会将数据转发到eth0然后进行SNAT操作,最后将数据报发送出去。 

posted @ 2021-05-17 21:16  SyrupzZ  阅读(252)  评论(0)    收藏  举报