这里写目录标题
简介
- 云原生离不开容器技术,这里再深入了解一下 Docker
- 基础部分可以看这个系列的文章
微服务
- 传统架构(单体分层)与微服务的对比
- 分离微服务的建议
通讯
- 微服务之间的通讯
- 点对点模式
- 网关模式(常见)
- 点对点模式
Docker
- docker 技术是基于 Linux 内核技术实现的
- namespace 技术出现的比较早了(进程隔离),Cgroup 技术是谷歌推出的 kernel patch(做资源管控),再基于 Union FS 技术推出了独创的 docker 镜像方式,实例化镜像为 container,再使用
- 虚拟机和容器的对比
- 虚拟机:需要再安装操作系统,调用链长,启动慢,占用资源多
- 容器,就是一个进程
- 虚拟机:需要再安装操作系统,调用链长,启动慢,占用资源多
- 基操部分请移步文章开头的链接
- docker 大势已去,k8s 和 podman 学起来,过渡起来还是很自然的
容器隔离技术
- 容器标准
- docker 的创举是 image specification(镜像协议),解决了业界一直头疼的应用分发问题
- 传统模式:运维/脚本控制,拉取 war 包(应用分发),部署重启;每家公司都要建立这样的系统,重复劳动,还会有环境问题等等
- docker:image repo就是 file server,docker 就是 agent,直接拉取(分发)了完整的运行环境,而且由于是分层机制,很多拉取过的内容可以复用,也就是只做增量拉取;统一了环境(归功于Runtime Specification),节省了开销(创举)
- docker 带来了一系列便捷,后续会体会到
Namespace
- 一种 Linux kernel 提供的资源隔离方案
- 看下源码,进程/线程都是 task
- 进程创建/加入 namespace 的三种方法
- k8s 支持的 ns 类型
- 详解各 namespace,简而言之,xxx namespace == 隔离 xxx
- 查看 namespace
# 查看当前系统的 ns lsns lsns -t net/pid/mnt # 查看某个进程的 ns ls -ls /proc/<pid>/ns/ # 进入某个 ns, -n 查看网络 # 首先是要获取 pid nsenter -t <pid> -n ip addr
- 一般情况下,先用
docker inspect
找到 PID,这个 Pid 是主机的(对应容器里的 PID 是 1);然后再用这个 PID 进入容器创建的 ns 查看其网络,很常用
- PID 来自主机,也就是在主机的 ns 里,再进入这个 pid 加入的其他 ns? 还是说这个 PID 就得从主机找?还是说 ns 之间的父子关系决定的?应该就是 PID 特殊的地方,各 ns 之间相关联的点
- ns 是不可以嵌套的,在 k8s 可以,后续介绍
- 练习,使用
unshare
,让进程到新的 ns 下执行
Cgroups
- ns 可以把进程塞到一个隔离环境运行,但是要完整模拟一个运行环境,还缺少资源管控
- Control Groups 就补充了这项功能
- 控制组有层级关系,类似树的结构,子控制组继承父控制组的属性(资源配额、限制等)
- 子系统(subsystem), 一个子系统其实就是一种资源的控制器,比如memory子系统可以控制进程内存的使用
- 子系统会加入到某个层级,该层级的所有控制组,均受到这个子系统的控制
- 真正起作用的是子系统,组成控制组的各层级需要附加子系统才能完成任务
- 源代码
- 特性:可配额,可度量;指明了资源的配额限制,进程可以加入到某个控制组,也可以迁移到另一个控制组
- systemd 在启动其他服务/进程的时候也会为其配置 Cgroup(进程和子系统的关联关系,加入层级树)
- 如何配置关联关系呢?通过配置文件,在
/sys/fs/cgroup/
目录下(Ubuntu),定义了各子系统目录,将进程号加入- 注:这个层级(树)是 Cgroups 之间的关系,相互有影响,但不是某个具体Cgroup的限制策略
- 后面有相关练习
- 参考资料
- 注:这个层级(树)是 Cgroups 之间的关系,相互有影响,但不是某个具体Cgroup的限制策略
- 来看看有哪些子系统
- CPU 子系统
- 关于 CFS 调度器
- 关于虚拟运行时间 vruntime
- 更详尽的内容需要深入了解 Linux 进程调度
- 进程调度虽然是比较大的话题,但涉及到红黑树算法等等内容,是技术进阶必不可少的
- 参考资料,带宽控制
- 练习:通过 CPU 子系统,控制 CPU 使用率
- 任务步骤
- 准备代码,启动进程
package main // 两个协程,死循环,吃满两个CPU func main() { go func() { for { } }() for { } }
- 查看相关文件,可以看到默认值;将要管理的进程号写到
cgroup.procs
- top 命令观察到 CPU 占用 200%(占2个),因为 quota 没做限制(-1),直接占满 2 个 CPU
- 限制 quota 为 10000,观察到只占用 1 个 CPU 的 10%
- 限制改为 150000,可以看到现在占用 1.5 个 CPU;(quota/period)
- 任务步骤
- memory 子系统也是类似的
- 练习:使用 memory 子系统,限制资源使用率(配额)
- 任务步骤
- 相关代码,执行 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
- 应用到 Docker Image
- 对比两个 Dockerfile
- 其中每一条命令都对应一层
- 关于基础镜像,只会引入 rootfs,不同 Base Image 会有些许差别
- 但我们关注的是 Kernel,不同发行版基本一致;因此 Docker 可以复用主机的 Kernel(没有bootfs),但是容器会加载镜像的 rootfs(readonly),不依赖宿主机,然后再挂载一个可写的文件系统(FS,readwrite)
- 写操作只改变联合挂载上去的FS层(整个容器使用了基于Union FS技术的Overlay2,提供了联合挂载功能),一旦在容器里做了写操作,修改产生的内容就会以增量的方式出现在这个层中;参考
- 小结:对原镜像内容的增删改都会增量到读写层,不会改变原镜像内容,docker commit 只会提交可读写层,从而保证镜像的共享特性
- 对比两个 Dockerfile
- 容器存储驱动,现在的主流是 OverlayFS
OverlayFS
- 所谓的存储驱动就是文件系统
- 上图的含义是,当上下层有相同的文件,最终在合并层,只使用上层(容器可写层)的文件
- 以下实验可以证明
# 在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
也能看到
- 以下实验可以证明
- 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 停止了支持
- 如图,中间也没有提到 daemon 和 shim,异类
- Docker 引擎架构
- 早期的 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
- 注: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 作为底层容器运行时
- containerd 可以直接运行 docker (也是OCI)格式的镜像,完整的 docker 系统其实加了很多其他东西,占用不少资源
- containerd 和 podman 的对比
- 因为它实现了 CRI(Container Runtime Interface),k8s 也可以将 containerd 作为底层容器运行时
网络
- docker 的网络类型
Null 模型
- 是一个空实现,启动后需要通过命令为容器配置网络
- 接下来一步步手动配置网络
- 启动 nginx 容器:
docker run --network=none --name none-nginx -d nginx
- 创建 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
- 启动 nginx 容器:
- 当然,也可以用
docker network
配置,更快捷 - 可以先看下面的 Bridge 部分,会更容易理解这里
Bridge
- 网桥是工作在数据链路层的,docker 会自动创建名为
docker0
的网桥设备,可以理解为一台交换机- 通过命令可以看到,这个 veth49ee0ab 就是运行的 nginx 容器的接口
- 会给容器分配 ip,可以进入 ns 查看
-p
参数其实是调用 iptables 实现的,iptables-save -t nat
- 通过命令可以看到,这个 veth49ee0ab 就是运行的 nginx 容器的接口
- 上面的网络配置都是 docker 驱动默认帮我们搞定的,如果也想手动实现,可以参考:
Underlay
- 上面是容器和 host 通信,同一主机的不同容器间通信;如果是跨节点容器间通信呢?
- 可以采用 Underlay 的方式
- 简而言之,就是将 host 的一部分 ip 预留给容器,创建容器时直接分配 host 网段的 ip
- 还有一种方式是
Overlay
,也叫隧道- 修改容器发送出的数据包,增加头部,目标 ip 改为另一个 host
- 到达目标 host,去掉增加的部分,路由到指定容器
- 常用工具:flannel
- flannel packet sample
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:
- 构建上下文(Build context)
- 目前:docker 做 image build,podman+k8s 做容器管理
- 看一份 build 日志
- 也就是说,更稳定的层应该写在 Dockerfile 前面,避免影响后续 cache
- 为了有效减少镜像层级,推出了多段构建,前面那段准备文件,只要后面这段(层数就很少了);其实前面那段写成脚本也行
LABEL
可以配合 label filter 过滤镜像docker images -f label=xxx
;关于RUN
:
- 关于
EXPOSE
- 关于
ADD
,别用 URL 获取 remote 文件,不太可控
- 关于
ENTRYPOINT
,让容器更有意义,面向对象(应用),而不是面向系统
- 其他指令
- 最佳实践
- 上面说的同一镜像多进程,指使用该镜像的容器中,启动了多个进程;应该只有一个主进程,推荐
- tag
小结
- 整理了 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
- podman 也可以管理
- 接下来就是 kubernetes 的学习