Docker

简介

  • 云原生离不开容器技术,这里再深入了解一下 Docker
  • 基础部分可以看这个系列的文章

微服务

  • 传统架构(单体分层)与微服务的对比
    1
    2
  • 分离微服务的建议
    3

通讯

  • 微服务之间的通讯
    • 点对点模式
      1
    • 网关模式(常见)
      2

Docker

  • docker 技术是基于 Linux 内核技术实现的
    1
  • namespace 技术出现的比较早了(进程隔离),Cgroup 技术是谷歌推出的 kernel patch(做资源管控),再基于 Union FS 技术推出了独创的 docker 镜像方式,实例化镜像为 container,再使用
  • 虚拟机和容器的对比
    • 虚拟机:需要再安装操作系统,调用链长,启动慢,占用资源多
      2
    • 容器,就是一个进程
      3
  • 基操部分请移步文章开头的链接
  • docker 大势已去,k8s 和 podman 学起来,过渡起来还是很自然的

容器隔离技术

  • 容器标准
    1
  • docker 的创举是 image specification(镜像协议),解决了业界一直头疼的应用分发问题
    • 传统模式:运维/脚本控制,拉取 war 包(应用分发),部署重启;每家公司都要建立这样的系统,重复劳动,还会有环境问题等等
    • docker:image repo就是 file server,docker 就是 agent,直接拉取(分发)了完整的运行环境,而且由于是分层机制,很多拉取过的内容可以复用,也就是只做增量拉取;统一了环境(归功于Runtime Specification),节省了开销(创举)
    • docker 带来了一系列便捷,后续会体会到

Namespace

  • 一种 Linux kernel 提供的资源隔离方案
    1
  • 看下源码,进程/线程都是 task
    2
  • 进程创建/加入 namespace 的三种方法
    2
  • k8s 支持的 ns 类型
    3
  • 详解各 namespace,简而言之,xxx namespace == 隔离 xxx
    4
    5
  • 查看 namespace
    # 查看当前系统的 ns
    lsns
    lsns -t net/pid/mnt
    # 查看某个进程的 ns
    ls -ls /proc/<pid>/ns/
    # 进入某个 ns, -n 查看网络
    # 首先是要获取 pid
    nsenter -t <pid> -n ip addr
    
    7
  • 一般情况下,先用 docker inspect 找到 PID,这个 Pid 是主机的(对应容器里的 PID 是 1);然后再用这个 PID 进入容器创建的 ns 查看其网络,很常用
    6
  • PID 来自主机,也就是在主机的 ns 里,再进入这个 pid 加入的其他 ns? 还是说这个 PID 就得从主机找?还是说 ns 之间的父子关系决定的?应该就是 PID 特殊的地方,各 ns 之间相关联的点
  • ns 是不可以嵌套的,在 k8s 可以,后续介绍
  • 练习,使用 unshare,让进程到新的 ns 下执行
    8

Cgroups

  • ns 可以把进程塞到一个隔离环境运行,但是要完整模拟一个运行环境,还缺少资源管控
  • Control Groups 就补充了这项功能
    1
  • 控制组有层级关系,类似树的结构,子控制组继承父控制组的属性(资源配额、限制等)
  • 子系统(subsystem), 一个子系统其实就是一种资源的控制器,比如memory子系统可以控制进程内存的使用
    • 子系统会加入到某个层级,该层级的所有控制组,均受到这个子系统的控制
    • 真正起作用的是子系统,组成控制组的各层级需要附加子系统才能完成任务
  • 源代码
    2
  • 特性:可配额,可度量;指明了资源的配额限制,进程可以加入到某个控制组,也可以迁移到另一个控制组
    3
  • systemd 在启动其他服务/进程的时候也会为其配置 Cgroup(进程和子系统的关联关系,加入层级树)
  • 如何配置关联关系呢?通过配置文件,在 /sys/fs/cgroup/ 目录下(Ubuntu),定义了各子系统目录,将进程号加入
    • 注:这个层级(树)是 Cgroups 之间的关系,相互有影响,但不是某个具体Cgroup的限制策略
      3-1
    • 后面有相关练习
    • 参考资料
  • 来看看有哪些子系统
    4
  • CPU 子系统
    5
  • 关于 CFS 调度器
    6
  • 关于虚拟运行时间 vruntime
    7
  • 更详尽的内容需要深入了解 Linux 进程调度
    8
  • 进程调度虽然是比较大的话题,但涉及到红黑树算法等等内容,是技术进阶必不可少的
    9
  • 参考资料带宽控制
  • 练习:通过 CPU 子系统,控制 CPU 使用率
    • 任务步骤
      10
    • 准备代码,启动进程
      package main
      // 两个协程,死循环,吃满两个CPU
      func main() {
      	go func() {
      		for {
      		}
      	}()
      	for {
      	}
      }
      
    • 查看相关文件,可以看到默认值;将要管理的进程号写到 cgroup.procs
      11
    • top 命令观察到 CPU 占用 200%(占2个),因为 quota 没做限制(-1),直接占满 2 个 CPU
      13
    • 限制 quota 为 10000,观察到只占用 1 个 CPU 的 10%
      12
    • 限制改为 150000,可以看到现在占用 1.5 个 CPU;(quota/period)
      14
  • memory 子系统也是类似的
    15
  • 练习:使用 memory 子系统,限制资源使用率(配额)
    • 任务步骤
      16
    • 相关代码,执行 go build malloc.go,或者写 Makefile
      // malloc.go
      package main
      
      //#cgo LDFLAGS:
      //char* allocMemory();
      import (
      	"time"
      	"fmt"
      	"C"
      )
      
      func main() {
      	for i:=1; i<1=0; i++ {
      		fmt.Printf("Allocating #{i*100}Mb memory, raw memory is #{i*100*1024*1025}\n")
      		C.allocMemory()
      		// 每分钟分配一次内存,一共10次
      		time.Sleep(time.Minute)
      	}
      }
      
      // malloc.c
      #include<stdlib.h>
      #include<stdio.h>
      #include<string.h>
      
      #define BLOCK_SIZE (100*1024*1024)
      
      char* allocMemory() {
      	char* out = (char*)malloc(BLOCK_SIZE);
      	memset(out, 'A', BLOCK_SIZE);
      	return out;
      }
      
  • Cgroup 目录的删除需要安装 cgroup-tools,执行 cgdelete cpu:cpudemo
  • Cgroup 管理器(driver),一般是 systemd,是如何划分层级(建立Hierarchy)的?
    • 注:systemd 负责初始化 Cgroups 的根目录,为每个 unit(进程) 配置 Cgroup
    • systemd 启动并管理所有其它进程
  • Ubuntu 虚拟机安装 k8s

Image

  • 联合文件系统 Union FS
    1
  • 应用到 Docker Image
    • 对比两个 Dockerfile
      2
    • 其中每一条命令都对应一层
    • 关于基础镜像,只会引入 rootfs,不同 Base Image 会有些许差别
      3
    • 但我们关注的是 Kernel,不同发行版基本一致;因此 Docker 可以复用主机的 Kernel(没有bootfs),但是容器会加载镜像的 rootfs(readonly),不依赖宿主机,然后再挂载一个可写的文件系统(FS,readwrite)
      4
    • 写操作只改变联合挂载上去的FS层(整个容器使用了基于Union FS技术的Overlay2,提供了联合挂载功能),一旦在容器里做了写操作,修改产生的内容就会以增量的方式出现在这个层中;参考
      5
    • 小结:对原镜像内容的增删改都会增量到读写层,不会改变原镜像内容,docker commit 只会提交可读写层,从而保证镜像的共享特性
      5-1
  • 容器存储驱动,现在的主流是 OverlayFS
    6
    7

OverlayFS

  • 所谓的存储驱动就是文件系统
    8
  • 上图的含义是,当上下层有相同的文件,最终在合并层,只使用上层(容器可写层)的文件
    • 以下实验可以证明
      # 在host上
      mkdir upper lower merged work
      echo "from lower" > lower/in_lower.txt
      echo "from upper" > upper/in_upper.txt
      echo "from lower" > lower/in_both.txt
      echo "from upper" > upper/in_both.txt	# 相同文件
      # 指定为使用overlay挂载
      sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work
      `pwd`/merged
      cat merged/in_both.txt	# 可以看到:from upper
      delete merged/in_both.txt
      delete merged/in_lower.txt
      delete merged/in_upper.txt
      
    • docker inspect 也能看到9
  • Overlay 的作用
    • 给每个容器安排不同的文件系统挂载点,再挂载一个可读写层;Overlay 是基于 Union FS 技术实现的,这个 mount 也就是 union mount,核心就是分层,每层的文件可以来自不同目录,最大的优势是共享
    • 将上下层文件合并(Merge readonly+readwrite)
  • Overlay 是容器存储驱动,也就是容器的文件系统,加上 ns 和 Cgroups,共同构建出完整的运行时环境,解决分发问题
    • 注:是为容器运行提供服务
    • 镜像文件也是基于 Union FS 分层设计,Overlay 创建容器时继续加“”,并且可以将下面的设置readonly并继续加层;后续会介绍镜像的 Dockerfile,和这里区别
  • 上面说的 Merge 侧重点在于
    • 镜像文件层提供基础文件系统(rootfs)和其他镜像层,节省空间
    • 上面的 rw 容器层只存放变化的部分
    • merge 就是将这两部分合并让整个 FS 看起来是一个整体,并不是真的合了一份同时有lower和upper的文件系统出来,也是链接,只不过同名文件取upper的

Docker 架构

  • Docker 是容器化的开创者,但是 Google 和一些容器化大厂合作,组建了 OCI,确定了容器标准;加之 Docker 本身也存在一些问题,地位便逐步被 k8s 取代
    • OCI 定义了运行时标准/镜像标准,分发标准(解决了分发问题)
    • 由于没有实现 CRI,docker 需要额外的 docker-shim 和 docker.daemon ,会导致代码脆弱,所以 k8s 停止了支持
      6
    • 如图,中间也没有提到 daemon 和 shim,异类
      5
  • Docker 引擎架构
    1
  • 早期的 Docker 直接使用 docker.daemon 作为所有容器的父进程,一旦升级需要重启,所有容器出问题;父进程退出是会影响子进程的(init 会接管并回收)
    • 进程都是父进程 fork 出来的,子进程结束执行会告知父进程,父进程回收子进程的资源(调用 wait 检查其状态,然后让内核销毁其 PCB)
  • 后来采用了上图所示的,每个容器的父进程是 containerd fork 的一对一的 containerd-shim 进程,而 shim 进程的父进程是 init 进程(PID=1),不再是 containerd,因此容器不再受升级影响(shim不升级)
    • containerd 进程可以通过询问 shim 获取容器状态,也给 docker.daemon 提供 gRPC 接口
    • 如图,容器进程是 11172,shim 进程是 11149,它的父进程是 1
      2
    • 注:docker-shim 和 containerd-shim 不同,这和发展历史有关,这里 docker-shim 是指和 kubelet 交互的进程,containerd-shim 是做 k8s 运行时的叫法,如果还是在 docker 那套系统,也可以叫 docker-shim
    • 每启动一个容器都会起一个新的 containerd-shim 进程,通过指定三个参数:容器ID、bundle目录(镜像被解包,它的内容以文件系统bundle的形式提供给容器运行时,或者说提供给OverlayFS),运行时二进制(默认是runC),调用 runC 的 API 创建一个容器
    • runC 是 OCI 的 runtime specification 的具体实现;镜像标准除了分层还有哪些?docker 镜像也是遵循 OCI 标准的
    • runC 是实现 OCI 接口的最低级别的组件,为容器提供了所有的底层功能,与现有的低级 Linux 功能交互,如命名空间和控制组
  • containerd 是容器运行时,作用是:在宿主机中管理完整的容器生命周期,包括镜像的传输和存储、容器的执行和管理、存储和网络等
    • 因为它实现了 CRI(Container Runtime Interface),k8s 也可以将 containerd 作为底层容器运行时
      3
    • containerd 可以直接运行 docker (也是OCI)格式的镜像,完整的 docker 系统其实加了很多其他东西,占用不少资源
    • containerd 和 podman 的对比

网络

  • docker 的网络类型
    1
    2

Null 模型

  • 是一个空实现,启动后需要通过命令为容器配置网络
  • 接下来一步步手动配置网络
    • 启动 nginx 容器:docker run --network=none --name none-nginx -d nginx
      1
    • 创建 net ns,连接 ns 和 网桥
      # 创建网络命名空间
      mkdir -p /var/run/netns
      find -L /var/run/netns -type l -delete
      export pid=4763
      ln -s /proc/$pid/ns/net /var/run/netns/$pid
      ip netns list
      # 上述步骤可以直接通过 ip netns add 4763 命令完成
      # /proc/4763 是进程创建后就有的
      # 创建net ns就是在/var/run/netns下新建,将 /proc 目录下的文件链接过去(内核相关),就会将这个 ns 给这个进程
      # /proc 存储的是当前内核(进程)的运行状态
      
      # 创建 veth pair,也就是网线,两端:A&B
      ip link add A type veth peer name B
      # A 口接网桥 docker0
      brctl addif docker0 A
      # 点亮
      ip link set A up
      # B 口接创建的ns
      SETIP=172.17.0.10	# 给net ns配置ip等信息
      SETMASK=16
      GATEWAY=172.17.0.1
      ip link set B netns $pid
      ip netns exec $pid ip link set dev B name eth0
      ip netns exec $pid ip link set eth0 up
      ip netns exec $pid ip addr add $SETIP/$SETMASK dev eth0
      ip netns exec $pid ip route add default via $GATEWAY
      
    • 再次查看容器 ns 的网络配置,也可以这样进入 net ns:ip netns exec 4763 ip a
      2
  • 当然,也可以用 docker network 配置,更快捷
  • 可以先看下面的 Bridge 部分,会更容易理解这里

Bridge

  • 网桥是工作在数据链路层的,docker 会自动创建名为 docker0 的网桥设备,可以理解为一台交换机
    • 通过命令可以看到,这个 veth49ee0ab 就是运行的 nginx 容器的接口
      1
    • 会给容器分配 ip,可以进入 ns 查看
      2
    • -p 参数其实是调用 iptables 实现的,iptables-save -t nat
      3
  • 上面的网络配置都是 docker 驱动默认帮我们搞定的,如果也想手动实现,可以参考:
    4

Underlay

  • 上面是容器和 host 通信,同一主机的不同容器间通信;如果是跨节点容器间通信呢?
  • 可以采用 Underlay 的方式
    5
  • 简而言之,就是将 host 的一部分 ip 预留给容器,创建容器时直接分配 host 网段的 ip
  • 还有一种方式是 Overlay,也叫隧道
    • 修改容器发送出的数据包,增加头部,目标 ip 改为另一个 host
    • 到达目标 host,去掉增加的部分,路由到指定容器
      6
    • 常用工具:flannel
      7
    • flannel packet sample
      8

Dockerfile

  • 说完了 NS,Cgroups,OverlayFS,容器基本就OK了
  • 说说镜像,主要就是制作镜像的 Dockerfile 文件了,基操请看文章开头提供的链接
    • 可以做镜像的不止是 Dockerfile,podman、buildah 也可以
    • 制作镜像时已经使用了容器
    • 最基础的容器镜像是 scratch,一个空文件夹(磁盘上的常规文件夹);如果想做一个最简单的镜像,只需要提供 rootfs(根文件系统) 即可;其实这就是深受喜爱的 apline 镜像
      FROM scratch
      ADD alpine-minirootfs-3.11.6-x86_64.tar.gz /
      CMD ["/bin/sh"]
      
    • rootfs:内核启动时所挂载(mount)的第一个文件系统,内核代码的镜像文件保存在根文件系统中
      • Linux系统启动时(BIOS),内核(镜像文件)被加载到内存中(grub),内核紧接着加载 rootfs 文件系统
      • 包含系统启动时所必须的目录和关键性的文件(描述硬件设备和系统配置信息)
      • 基于内存运行
      • Q:内核在rootfs中,内核怎么加载rootfs?
      • 至少包括以下目录
        /etc/:存储重要的配置文件。
        /bin/:存储常用且开机时必须用到的执行文件。
        /sbin/:存储着开机过程中所需的系统执行文件。
        /lib/:存储/bin/及/sbin/的执行文件所需的链接库,以及Linux的内核模块。
        /dev/:存储设备文件。
        
  • Docker 在管理和构建应用时,遵循了 12 Factor,其中的进程Factor:
    1
  • 构建上下文(Build context)
    2
  • 目前:docker 做 image build,podman+k8s 做容器管理
  • 看一份 build 日志
    3
    4
  • 也就是说,更稳定的层应该写在 Dockerfile 前面,避免影响后续 cache
  • 为了有效减少镜像层级,推出了多段构建,前面那段准备文件,只要后面这段(层数就很少了);其实前面那段写成脚本也行
    5
  • LABEL 可以配合 label filter 过滤镜像 docker images -f label=xxx;关于 RUN
    6
  • 关于 EXPOSE
    7
  • 关于 ADD,别用 URL 获取 remote 文件,不太可控
    8
    8
  • 关于 ENTRYPOINT,让容器更有意义,面向对象(应用),而不是面向系统
    9
  • 其他指令
    10
  • 最佳实践
    11
  • 上面说的同一镜像多进程,指使用该镜像的容器中,启动了多个进程;应该只有一个主进程,推荐
    12
  • tag
    13

小结

  • 整理了 Docker 用到的几个核心技术:Namespace,Cgroups,Union FS 和 Dockerfile
  • podman 流行起来,但用到的核心技术还是 NS,Cgroups,UnionFS;镜像可以来自 docker(build Dockerfile)/podman/buildah 等(都遵循OCI)
  • 目前主流的技术是 k8s,做容器编排管理
    • podman 也可以管理 pod,但是不如 k8s 完善,毕竟不是一个东西
    • podman 定位是 pod(container)+容器管理,平替 docker,所以先不细讲
    • k8s 和 podman 什么关系?没直接关系,但是 k8s 需要容器运行时(创建、管理),可以选择 podman/containerd
  • 接下来就是 kubernetes 的学习
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑞士_R

修行不易...

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值