点击蓝字,关注我们
背景简介
伴随着互联网的高速发展,云计算、云原生、微服务架构等诸多专业技术在各个互联网公司被应用的越来越多,目前各大互联网公司已经在大规模应用微服务架构且传统行业也逐渐接受了这种架构模式。云计算的到来推动云原生应用服务的落地实践,得益于云计算快速发展,基于云计算特性所设计的云原生应用相比传统的单体应用在安全性,扩展性,快速迭代,运维等各方便都有巨大的领先优势。
微服务架构使用更细粒度的服务和设计准则推动服务容器化,服务容器化(docker) 将每个微服务变成无状态服务(占用端口,文件占用等),将软件容器中的应用程序和进程作为独立的应用程序部署单元运行,并作为实现高级别资源隔离的机制。
Docker简介
Docker是基于Go语言实现的云开源项目,诞生于2013年初,最初发起者是dotCloud公司。Docker自开源后受到广泛的关注和讨论,目前已有多个相关项目,逐渐形成了围绕Docker的生态体系。dotCloud公司后来改名为Docker Inc,专注于Docker相关技术和产品的开发。Docker是一个开源的容器引擎,能够将应用程序和基础设施层隔离,并可以将基础设施当作程序一样进行管理。使用Docker容器可更快地打包、测试以及部署应用程序,减少从编写到部署运行代码的周期。
在云计算时代,docker容器依然成为了技术热潮之一,Docker容器的实现原理就是通过Namespace命名空间实现进程隔离、UnionFilesystem联合文件系统实现文件系统隔离、ControlGroups控制组实现资源隔离。
NameSpace
一个运行中的服务即一个进程,进程提供了服务运行需要的软硬件环境,在一台宿主机上同时启动多个服务时,可能会出现资源的争夺、进程互相影响等,因此通过Namespace可以将宿主机上同时运行的多个服务划分成独立的服务单独进程运行。
在Linux系统中创建一个新的 Docker 容器,并通过 exec 进入容器的 bash 并查询容器中的全部进程
[ ]
0245401a4fa04b010164652e1c28dd83f6cb1963eb1d8b9f258cbb8a2cd8a59e
[ ]
[ ]
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 06:35 pts/0 00:00:00 /bin/bash
root 16 0 0 06:36 pts/1 00:00:00 /bin/bash
root 30 16 0 06:37 pts/1 00:00:00 ps -ef
通过执行信息可以看到Docker 最开始执行的 /bin/bash,是容器内部的第 1 号进程(PID=1),容器共有三个进程在运行,启动时执行的 /bin/bash,以及后续进入容器执行命令/bin/bash与ps查询进程指令。当前容器内的进程与宿主机器中的进程隔离在不同的环境中,容器与宿主机的进程隔离便是采用Linux 系统中的 Namespace 机制实现。
Linux的Namespace机制包含clone_newcgroup、clone_newipc、clone_newnet、clone_newns、clone_newpid、clone_newuser、clone_newuts。进程的隔离采用的clone_newpid来实现。Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 等 Namespace,用来对各种不同的进程上下文进行隔离。
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
调用clone() 系统函数创建新进程,指定 CLONE_NEWPID 参数,新创建的进程将会使用一个全新的进程空间,进程空间的进程ID为1。但在宿主机的进程空间里,新创建的进程 PID 还是真实的数值。当多次执行 clone()函数调用,会创建多个 PID Namespace,每个 Namespace 里的应用进程都是当前容器里的第 1 号进程,既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 的具体信息。
创建docker容器时(执行docker run/docker start命令)通过createSpec方法设置进程间隔离的Spec,在setNamespaces方法传入进程、用户、网络等参数,实现与宿主机与容器的进程、用户、网络等隔离。
func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
s := oci.DefaultSpec()
// ...
if err := setNamespaces(daemon, &s, c); err != nil {
return nil, fmt.Errorf("linux spec namespaces: %v", err)
}
return &s, nil
}
//通过setNamespaces 方法设置进程、用户、网络、IPC 以及 UTS 相关的命名空间
func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {
if c.HostConfig.PidMode.IsContainer() {
ns := specs.LinuxNamespace{Type: "pid"}
pc, err := daemon.getPidContainer(c)
if err != nil {
return err
}
ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
setNamespace(s, ns)
} else if c.HostConfig.PidMode.IsHost() {
oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
} else {
ns := specs.LinuxNamespace{Type: "pid"}
setNamespace(s, ns)
}
return nil
}
ControlGroups
Linux ControlGroups 的全称是 Linux Control Group,主要作用是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等。此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。
在 Linux 中Cgroups 给用户暴露出来的操作接口是文件系统,以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。
[ ]
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
Cgroups 的每一个子系统都有独有的资源限制能力
blkio -- 为块设备设定输入/输出限制,比如物理设备(磁盘,固态硬盘,USB 等等)。
cpu -- 使用调度程序提供对 CPU 的 cgroup 任务访问。
cpuacct -- 自动生成 cgroup 中任务所使用的 CPU 报告。
cpuset -- 为 cgroup 中的任务分配独立 CPU(在多核系统)和内存节点。
devices -- 可允许或者拒绝 cgroup 中的任务访问设备。
freezer -- 挂起或者恢复 cgroup 中的任务。
memory -- 设定 cgroup 中任务使用的内存限制,并自动生成由那些任务使用的内存资源报告。
net_cls -- 使用等级识别符(classid)标记网络数据包,可允许 Linux 流量控制程序(tc)识别从具体 cgroup 中生成的数据包。
ns -- 命名空间子系统。
在/sys/fs/cgroup 目录下有很多 cpuset、cpu、 memory 等子目录(子系统),是当前主机可以被 Cgroups 进行限制的资源种类,在子系统对应的资源种类下,可以看到该类资源具体可以被限制的方法。
对 CPU 子系统来说,可以看到如下几个配置文件
[root@bj-sjhl-wx-zhaodexian-dev 14:28:03 /sys/fs/cgroup/cpu]
# cd /sys/fs/cgroup/cpu
[root@bj-sjhl-wx-zhaodexian-dev 14:28:10 /sys/fs/cgroup/cpu]
# ll
总用量 0
-rw-r--r-- 1 root root 0 1月 11 2021 cgroup.clone_children
--w--w--w- 1 root root 0 1月 11 2021 cgroup.event_control
-rw-r--r-- 1 root root 0 1月 11 2021 cgroup.procs
-r--r--r-- 1 root root 0 1月 11 2021 cgroup.sane_behavior
drwxr-xr-x 2 root root 0 3月 15 14:26 container
-r--r--r-- 1 root root 0 1月 11 2021 cpuacct.stat
-rw-r--r-- 1 root root 0 1月 11 2021 cpuacct.usage
-r--r--r-- 1 root root 0 1月 11 2021 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.rt_period_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.shares
-r--r--r-- 1 root root 0 1月 11 2021 cpu.stat
drwxr-xr-x 2 root root 0 3月 15 11:44 docker
-rw-r--r-- 1 root root 0 1月 11 2021 notify_on_release
-rw-r--r-- 1 root root 0 1月 11 2021 release_agent
drwxr-xr-x 60 root root 0 3月 14 15:54 system.slice
-rw-r--r-- 1 root root 0 1月 11 2021 tasks
drwxr-xr-x 2 root root 0 3月 10 18:40 user.slice
配置文件基础配置使用
在 /sys/fs/cgroup/cpu 目录下创建名称为container目录,操作系统会在新创建的 container 目录下自动生成该子系统对应的资源限制文件,目录container被称为一个“控制组”。
# cd /sys/fs/cgroup/cpu
[root@bj-sjhl-wx-zhaodexian-dev 14:25:51 /sys/fs/cgroup/cpu]
# mkdir container
[root@bj-sjhl-wx-zhaodexian-dev 14:26:05 /sys/fs/cgroup/cpu]
# ll
总用量 0
-rw-r--r-- 1 root root 0 1月 11 2021 cgroup.clone_children
--w--w--w- 1 root root 0 1月 11 2021 cgroup.event_control
-rw-r--r-- 1 root root 0 1月 11 2021 cgroup.procs
-r--r--r-- 1 root root 0 1月 11 2021 cgroup.sane_behavior
drwxr-xr-x 2 root root 0 3月 15 14:26 container
-r--r--r-- 1 root root 0 1月 11 2021 cpuacct.stat
-rw-r--r-- 1 root root 0 1月 11 2021 cpuacct.usage
-r--r--r-- 1 root root 0 1月 11 2021 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.rt_period_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 1月 11 2021 cpu.shares
-r--r--r-- 1 root root 0 1月 11 2021 cpu.stat
drwxr-xr-x 2 root root 0 3月 15 11:44 docker
-rw-r--r-- 1 root root 0 1月 11 2021 notify_on_release
-rw-r--r-- 1 root root 0 1月 11 2021 release_agent
drwxr-xr-x 60 root root 0 3月 14 15:54 system.slice
-rw-r--r-- 1 root root 0 1月 11 2021 tasks
drwxr-xr-x 2 root root 0 3月 10 18:40 user.slice
[root@bj-sjhl-wx-zhaodexian-dev 14:26:07 /sys/fs/cgroup/cpu]
# ls container/
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
查看container 控制组 CPU quota 限制(默认 -1),CPU period 默认 100 ms(100000 us)
[ ]
-1
[ ]
100000
限制container控制组进程每 100 ms 的时间只能使用 20 ms 的 CPU 时间,该控制组指定进程只能使用到 20% 的 CPU 带宽。将限制进程 PID 写入 container 组里的 tasks 文件,设置就会对限制进程生效。
[root@bj-sjhl-wx-zhaodexian-dev 14:26:07 /sys/fs/cgroup/cpu]
echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
[root@bj-sjhl-wx-zhaodexian-dev 14:26:07 /sys/fs/cgroup/cpu]
echo 限制容器进程PID > /sys/fs/cgroup/cpu/container/tasks
Linux Cgroups 的设计是一个子系统目录加上一组资源限制文件的组合。对于 Docker 容器项目资源限制需要在每个子系统下面,为每个容器创建一个控制组(创建一个新目录),在启动容器进程之后,将启动后容器的进程 PID 写入到对应控制组的 tasks 文件中使其生效。
UnionFS(Union File System)
Linux 的NameSpace和ControlGroups分别解决了容器资源隔离的问题,NameSpace实现了进程、网络以及文件系统的隔离,ControlGroups实现了 CPU、内存等资源隔离。docker通过Mount NameSpace在容器根目录下挂载了一个完整的操作系统的文件系统,用来为容器进程提供隔离后执行环境的文件系统(rootfs),称之为“容器镜像”。
容器镜像把应用程序运行需要的操作系统环境、开发语言的依赖库、系统lib库等都打包在一起,作为容器启动时的根目录,容器进程的各种依赖调用都在这个目录里来保障环境的一致性!为了解决在开发新应用或升级应用重复制作rootfs的问题,Docker 在镜像的设计中,通过AUFS(Advanced UnionFS联合文件系统) 将多个目录联合放在同一个目录下,用户制作镜像的每一步操作,都会生成一个层(docker-layer),也就是一个增量 rootfs。
除了 AUFS 之外,Docker 还支持了不同的存储驱动,包括aufs、devicemapper、overlay2、zfs、vfs 等等,最新的 Docker 版本中overlay2 取代了aufs 成为推荐的存储驱动,但是在没有overlay2 驱动的机器上仍然使用aufs 作为 Docker 的默认驱动。
镜像layer-case (centos系统,存储驱动默认使用overlay2)
[ ]
REPOSITORY TAG IMAGE ID CREATED SIZE
apisix-gateway v1 ccee202f7e5c 3 days ago 560MB
hub.xesv5.com/jichufuwuzhongtai-gateway/apisix v2.13LTS-debian 7994600d7480 5 weeks ago 556MB
php latest 13b9b1961ba3 5 months ago 484MB
ubuntu latest ba6acccedd29 7 months ago 72.8MB
centos latest 5d0da3dc9764 8 months ago 231MB
[ ]
[
{
: ,
...此处省略.....
: {
: {
: ,
: ,
: ,
:
},
:
},
: {
: ,
: [
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
]
}
}
]
通过以上执行结果RootFS/Layers可以看到apisix-gateway镜像由16个层组成,每一层都是一个增量 rootfs,在使用镜像时,Docker 会把这些增量联合挂载在一个统一的挂载点上。
容器镜像的组装过程,每一个镜像层都是建立在另一个镜像层之上,所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所有的容器都建立在底层服务(Kernel)上,包括命名空间、控制组、rootfs 等。