Docker容器的原理与实践 (下)

Docker原理分析

Docker架构


镜像原理

镜像是一个只读的容器模板,含有启动docker容器所需的文件系统结构及内容
Docker以镜像和在镜像基础上构建的容器为基础,以容器开发、测试、发布的单元将应用相关的所有组件和环境进行封装,避免了应用在不同平台间迁移所带来的依赖问题,确保了应用在生产环境的各阶段达到高度一致的实际效果。

主要特点

  • 分层
    镜像采用分层构建,每个镜像由一系列的镜像层组成, 当需要修改容器内的某个文件时,只对处于最上方的读写层进行变动,不覆盖下面已有文件系统的内容。 当提交这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。

+ bootfs  
主要包含bootloader和kernel, bootloader主要是引导加载kernel, 当容器启动成功后,kernel被加载到内存中后而引导文件系统则会被卸载unmount
+ rootfs  
是容器在启动时内部进程可见的文件系统,通常包含一个操作系统运行所需的文件系统
    + 传统linux在内核启动时首先会挂载一个只读的rootfs,检测器完整性之后再切换为读写模式
    + docker在挂载rootfs时也将其设为只读模式,挂载完毕后利用联合挂载技术在已有的只读rootfs上再挂载一个读写层。
    + 只有运行中文件系统发生变化,才会把变化的内容写到读写层,并隐藏只读层中的老版本文件
    + rootfs包含的就是典型Linux系统中的 /dev,/proc,/bin, /etc等标准目录和文件。
  • 写时复制
    • 可以在多个容器之间共享镜像,每个容器启动时不需要单独复制一份镜像文件
    • 将所有镜像层以只读方式挂载到一个挂载点,在上面覆盖一个可读写的容器层。
    • 写时复制配合分层机制减少了镜像对磁盘空间的占用和容器启动时间
  • 内容寻址
    • 根据内容来索引镜像和镜像层
    • 是对镜像层的内容计算检验和,生成一个内容哈希值作为镜像层的唯一标识
    • 对于来自不同构建的镜像层,只要拥有相同的内容哈希,也能被不同镜像共享
  • 联合挂载
    可以在一个挂载点挂载多个文件系统,将挂载点的原目录与被挂在内容进行整合,最终可见的文件系统将包含整合后各层的文件和目录
    • 读写层处于容器文件系统的最顶层,其下可能联合挂载多个只读层。

存储管理

为了适应不同平台不同场景的存储需求,Docker提供了各种基于不同文件系统实现的存储驱动来管理实际的镜像文件

元数据管理

镜像在设计上将元数据和文件存储完全隔离。Docker管理元数据采用的也正是从上至下repository、image、layer是3个层次。 所以repository与image两个元数据并无物理上的镜像文件与之对应,layer则存在物理上的镜像文件与之对应。

  • 仓库元数据
    文件中存储了所有版本镜像的名字和tag以及对应的镜像ID(image/aufs)
  • 镜像元数据
    文件中存储了镜像架构、操作系统、默认配置、该镜像的容器ID和配置,构建镜像的历史信息以及rootfs组成(image/aufs/imagedb/content/sha256)
  • 分层元数据
    • 镜像层
      描述不可改变的镜像层(image/aufs/layerdb/sha256)
    • 容器层
      描述可读写的容器层(image/aufs/layerdb/mounts/),读写层的ID也对应容器的ID

存储驱动

为支持写时复制特性,根据不同操作系统底层的支持提供不同的存储驱动

  • aufs(advanced multi layered unification file system)
    是一种支持联合挂载的文件系统,支持不同目录挂载到同一个目录,挂载对用户来说是透明的。
    • 文件操作
        • 从最顶层的读写层开始向下寻找,本层没有则根据层与层之间的关系到下一层找
        • 如果文件不存在则在读写层新建一个,否则向上面一样从顶层开始查找,找到后复制到读写层进行修改
        • 如果文件仅仅在读写层则直接删除;否则需要删除读写层的备份,再在读写层中创建whiteout文件来标志这个文件不存在,而不会真正删除底层文件(会有.wh.开头的隐藏文件)
        • 如果这个文件在读写层存在对应的whiteout文件则先将whiteout文件删除再新建
  • btrfs
  • zfs
  • devicemapper
  • overlay
    overlayFS是一种新型联合文件系统,允许一个文件系统与另外一个文件系统重叠,在上层文件系统中记录更改,而下层的文件系统保持不变。
  • vfs

    存储目录

/var/lib/docker  
               /aufs                                aufs驱动工作的目录
                    /diff                           mount-id容器层,文件系统所有层的存储目录(下载的镜像内容就保存在这里)和容器层文件的差异变化会在这里出现。
                         /mount-id-init             最后一个只读层,用于挂载并重新生成dev/etc所列文件,这些文件与容器内的环境息息相关,但不适合被打包作为镜像的文件内容,
                                                    又不应该直接修改在宿主机文件上,所以设计了mountID-init层单独处理这些文件。这一层只在容器启动时添加。
                    /layers                         mount-id存储上述所有aufs层之间的关系等元数据,记录该层所依赖的所有其它层
                    /mnt                            aufs文件系统的挂载点,graphdriver会将diff中属于容器镜像的所有层目录以只读方式挂到mnt 
               /container                           容器配置文件目录
               /image 
                     /aufs                          存储镜像与镜像层元数据信息,真正的镜像层内容保存在aufs/diff
                          /imagedb/content          存储所有镜像的元数据
                          /layerdb                  存储所有镜像层和容器层的元数据
                                  /mounts           存储容器层元数据
                                         /init-id   init层id,这个layer存放的位置
                                         /mount-id  mount层id,这个layer存放的位置
                                         /parent    父layer的chain-id
                                  /share256         存储镜像层元数据
                                         /cache-id  该层数据存放的位置,在对应驱动目录下
                                         /diff      标识每一个layer
                                         /parent    标识父layer的chain-id
                                         /size      存放layer的数据大小
                                         /tar-split.json.gz 存放layer层的json信息
                          /repositories.json        记录镜像仓库中所有镜像的repository和tag名
               /volumes                             volumes的工作目录,存放所有volume数据和元数据

读写层、volumes、init-layer、只读层这几部分结构共同组成了一个容器所需的文件系统。
diff-id:通过docker pull下载镜像时,镜像的json文件中每一个layer都有一个唯一的diff-id
chain-id:chain-id是根据parent的chain-id和自身的diff-id生成的,假如没有parent,则chain-id等于diff-id,假如有parent,则chain-id等于sha256sum( “parent-chain-id diff-id”)
cache-id:随机生成的6416进制数。cache-id标识了这个layer的数据具体存放位置

数据卷

volume是存在于一个或多个容器中的特定文件或文件夹,这个目录以独立联合文件系统的形式存在于宿主机中

  • 特点
    • 容器创建时就会初始化,在容器运行时就可以使用其中的文件
    • 能在不同的容器中共享和重用
    • 对volume中的数据操作不会影响到镜像本身
    • 生命周期独立,即使删除容器volume依然存在

Namespace

同一个namespace下的进程可以感知彼此的变化,而对外界进程一无所知

  • UTS 隔离主机名与域名
  • IPC 隔离信号量、消息队列和共享内存
  • PID 隔离进程编号
    • 不同的PID namespaces会形成一个层级体系
    • 每个pid namespace的第一个进程 pid 1会像传统linux的init进程号一样拥有特权
  • Net 隔离网络设备、网络栈、端口
  • Mount 隔离挂载点(文件系统)
  • User 隔离用户和用户组

例子

mkdir newroot
cd newroot
cp -r /bin/ bin
chroot newroot
exit

Cgroup

根据需求把一系列的系统任务和子任务整合到按资源划分等级的不同组内,从而为系统资源管理提供一个统计的框架

主要作用

  • 资源限制
  • 优先级分配
  • 资源统计
  • 任务控制

主要特点

  • cgroup的api是以一个为文件系统的方式实现,用户态的程序可以通过文件操作实现cgroup的组织管理
  • 组织管理操作单元可以细粒度到线程级别,可以创建和销毁cgroup从而实现资源再分配和管理
  • 所有资源管理的功能都以子系统的方式实现,接口统一
  • 子任务创建之初与父任务处于同一个cgroups控制组

相关术语

task

表示系统的一个进程或线程

cgroup

按某种资源控制标准划分而成的任务组,包含一个或多个子系统

  • 实现形式表现为一个文件系统mount -t cgroup
  • docker实现
    • 会在单独挂载了每一个子系统的控制组目录下创建一个名为docker的控制组
    • 在docker控制组里面再为每个容器创建一个容器id为名称的容器控制组
    • 容器里的所有进程号都会写到该控制组tasks中,并在控制组文件cpu.cfs_quota_us中写入预设的限制参数值
  • 工作原理
    • 本质上来说,cgroups是内核附加在程序上的一系列钩子,通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的
    • 进程所需内存超过它所属cgroup最大限制以后,如果设置了oom control,进程会收到oom信号并结束,否则进程会挂起,进入睡眠状态,直到cgroup中的其他进程释放了足够的内存资源为止

subsystem

资源调度器,cpu子系统可以控制cpu分配时间(/sys/fs/cgroup/cpu/docker/)

  • blkio 可以为块设备设定输入、输出限制,比如物理驱动设备(磁盘、固态硬盘、USB)
  • cpu 使用调度程序控制任务对CPU的使用
  • cpuacct 自动生成cgroup中任务对cpu资源使用情况的报告
  • cpuset 为cgroup中的任务分配独立的cpu和内存
  • devices 可以开启或关闭cgroup中任务对设备的访问
  • freezer 可以挂起或恢复cgroup中的任务
  • memory 可以设定cgroup中任务对内存使用量的限定
  • perf_event 使用后使cgroup中的任务可以进行统一的性能测试
  • net_cls 通过使用等级识别符标记网络数据包,从而允许linux流量控制程序(traffic controller)识别从具体cgroup中生成的数据包

hierachy

层级是由一系列cgroup以一个树状结构排列而成,每个层级通过绑定对应的子系统进行资源控制

依赖其他的内核能力

  • seccomp(secure computing mode)
    安全计算模式,这个模式可以设置容器在对系统进行调用时进行一些筛选,也就是所谓的白名单。 是一种简洁的sandboxing机制。能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system calls),即read(), write(), exit()和sigreturn(),否则进程便会被终止。
  • SELinux
    安全增强式Linux(SELinux, Security-Enhanced Linux)是一种强制访问控制(mandatory access control)的实现
  • Netlink
    用来让不同的容器之间进行通信,可用于进程间通信,Linux内核与用户空间的进程间、用户进程间的通讯
  • Netfilter
    Linux内核中的一个软件框架,用于管理网络数据包。
    不仅具有网络地址转换(NAT)的功能,也具备数据包内容修改、以及数据包过滤等防火墙功能。
  • AppArmor
    类似于selinux,主要的作用是设置某个可执行程序的访问控制权限,可以限制程序 读/写某个目录/文件,打开/读/写网络端口等等
  • capability
    Linux把原来和超级用户相关的高级权限划分成为不同的单元,称为Capability,可以单独启用或者关闭, 它打破了UNIX/LINUX操作系统中超级用户/普通用户的概念,由普通用户也可以做只有超级用户可以完成的工作

网络原理

一些概念

  • net namespace
    隔离网络栈,有自己的接口、路由、防火墙规则
  • bridge
    相当于交换机,为连接在其上的设备转发数据帧
  • veth
    相当于交换机上的端口,是一对虚拟网卡,用于不同网络空间进行通信的方式,从一张veth网卡发出的数据包可以直接到达它的peer veth
  • gateway
    就是一个网络连接到另一个网络的“关口”,与本地网络连接的机器会把向外的流量传递到此地址中从而使那个地址成为本地子网以外的IP地址的"网关".
  • iptables
    linux内核的包过滤系统。在3、4层提供规则链对包进行标记、伪装、转发等

两个例子

通过虚拟网卡实现2个namespace通信

 

1、创建虚拟网络环境
ip netns add net0
ip netns add net1

2、在虚拟的net0环境中执行
ip netns exec net0 ifconfig -a
ip netns exec net0 ping localhost
ip netns exec net0 ip link set lo up
ip netns exec net0 ping localhost

3、创建一对虚拟网卡
ip link add type veth

4、把veth0移动到net0环境里面,把veth1移动到net1环境里面
ip link set veth0 netns net0
ip link set veth1 netns net1

4、配置虚拟网卡
ip netns exec net0 ip link set veth0 up
ip netns exec net0 ip address add 10.0.1.1/24 dev veth0
ip netns exec net1 ip link set veth1 up
ip netns exec net1 ip address add 10.0.1.2/24 dev veth1

5、测试
ip netns exec net0 ping -c 3 10.0.1.2
ip netns exec net0 ping -c 3 10.0.1.1
ip netns exec net1 ping -c 3 10.0.1.1

通过网桥实现多个namespace通信及外网访问原理

1、创建虚拟网络环境并连接网线
ip netns add net0
ip netns add net1
ip netns add bridge

ip link add type veth
ip link set dev veth0 name net0-bridge netns net0
ip link set dev veth1 name bridge-net0 netns bridge

ip link add type veth
ip link set dev veth0 name net1-bridge netns net1
ip link set dev veth1 name bridge-net1 netns bridge

2、在bridge中创建虚拟网桥
ip netns exec bridge brctl addbr br
ip netns exec bridge ip link set dev br up
ip netns exec bridge ip link set dev bridge-net0 up
ip netns exec bridge ip link set dev bridge-net1 up
ip netns exec bridge brctl addif br bridge-net0
ip netns exec bridge brctl addif br bridge-net1
ip netns exec bridge brctl show

3、配置虚拟环境网卡
ip netns exec net0 ip link set dev net0-bridge up
ip netns exec net0 ip address add 10.0.1.1/24 dev net0-bridge

ip netns exec net1 ip link set dev net1-bridge up
ip netns exec net1 ip address add 10.0.1.2/24 dev net1-bridge

4、测试
ip netns exec net0 ping -c 3 10.0.1.2
ip netns exec net1 ping -c 3 10.0.1.1
ip netns exec bridge ping -c 3 10.0.1.2

5、需要给当前网络环境配置一个网卡ip不然没有网络路由网络不可达
ip netns exec bridge ip address add 10.0.1.3/24 dev br

6、想要ping宿主机网络怎么办?
ip netns exec bridge ping 192.168.99.100
ip link add A type veth peer name B
ip link set B netns bridge
ip netns exec bridge ip link set dev B up

ip link set A up

6、给AB网卡配置ip
ip netns exec bridge ip address add 172.22.0.11/24 dev B
ip address add 172.22.0.10/24 dev A

7、设置bridge默认网关
ip netns exec bridge route add default gw 172.22.0.10
ip netns exec bridge ping 192.168.99.100

8、设置net0默认网关
ip netns exec net0 ping 192.168.99.100
ip netns exec net0 route add default gw 10.0.1.3
ip netns exec net0 ping 192.168.99.100 不通

9、地址伪装
ip netns exec bridge iptables -t nat -A POSTROUTING -o B -j MASQUERADE
ip netns exec net0 ping 192.168.99.100

10、让虚拟网卡访问外网
ip netns exec net0 ping 8.8.8.8

iptables -t filter -I FORWARD -o A -j ACCEPT
iptables -t filter -I FORWARD -i A -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -s 172.22.0.0/24 -j MASQUERADE

11、vbox访问mac地址
ip netns exec bridge ping 192.168.99.1
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
ip netns exec bridge ping 192.168.99.1

影响网络连通的几个要素

  • 内核是否开启IP转发支持
    cat /proc/sys/net/ipv4/ip_forward
    sysctl -w net.ipv4.ip_forward=1
  • 防火墙转发规则是否打开
  • 是否开启IP地址伪装
  • 是否正确设置网关

CNM网络模型

  • 沙盒,一个沙盒包含了一个容器网络栈信息。沙盒可以对容器的接口、路由和DNS设置等进行管理。一个沙盒可以有多个端点和多个网络。
  • 端点,一个端点可以加入一个沙盒和一个网络。端点的实现可以使veth pair、open vSwitch内部端口。
  • 网络,一个网络是一组可以互相联通的端点。网络的实现可以是linux bridge、VLAN等。

内置驱动



  • bridge
    默认设置,libnetwork将创建出来的容器连接到docker网桥。其与外界使用NAT.

    操作例子

      docker network create br3
      docker run -itd --name busybox-bridge --net=br3 busybox
    
      docker run -itd --name busybox-bridge-none busybox
      docker network connect br3 busybox-bridge-none
      查看容器网络和宿主机网桥及其上端口
    
  • host
    libnetwork不为docker容器创建网络协议栈及独立的network namespace。

    • 容器使用宿主机的网卡、IP、端口、路由、iptable规则
    • host驱动很好的解决了容器与外界通信的地址转换问题,但也降低了容器与容器建、容器与宿主机之间的网络隔离性,引起网络资源竞争的冲突。

      例子

      docker run -itd --name busybox-host --net=host busybox
      
  • container
    • 指定新创建的容器和已存在的容器共享一个网络空间
    • 两个容器的进程可以通过lo网卡设备通信

      例子

      docker run -itd --name busybox busybox
      docker run -itd --name busybox-container --net=container:busybox-bridge busybox
      
  • none
    容器拥有自己的network namespace,但不进行任何网络配置。

    例子

docker run -itd --name busybox-none --net=none busybox
  • overlay
    • 使用标准的VXLAN
    • 使用过程中需要一个额外的配置存储服务如consul、etcd、Zookeeper
    • 需要在daemon启动的时候额外添加参数来指定配置存储服务地址
  • remote
    实现插件化,调用用户自行实现的网络驱动插件

DNAT来实现外部访问容器

docker run -itd -p 9901:991 --name busybox-dnat busybox

容器云平台


云平台厂家

如果你从头看到尾那真是太棒了,如果你也对docker技术感兴趣欢迎popo(hzchenzhiliang@corp.netease.com)联系交流。

相关阅读:Docker容器的原理与实践(上)

本文来自网易实践者社区,经作者陈志良授权发布。