重新理解docker

Posted by Run-dream Blog on July 31, 2021

定义

从目的上来说,容器其实是一种沙盒技术。容器的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”

从操作系统的角度来说,Docker实例,就是一个特殊的被限制隔离的进程。而Docker引擎,则是用来创建和管理Docker实例的进程。

实现

glibc 提供了 clone 的方法来新建进程, 函数声明为:

   int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
                 /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

其中 flags 可以用来控制子进程的属性。

对于 linux 内核来说,其任务结构体中,就包含了这些属性


struct task_struct {
    /* Namespaces: */
	struct nsproxy			*nsproxy;
    /* Control Group info protected by css_set_lock: */
	struct css_set __rcu		*cgroups;
	/* cg_list protected by css_set_lock and tsk->alloc_lock: */
	struct list_head		cg_list;
};

struct nsproxy {
	atomic_t count;
	struct uts_namespace *uts_ns;
	struct ipc_namespace *ipc_ns;
	struct mnt_namespace *mnt_ns;
	struct pid_namespace *pid_ns_for_children;
	struct net 	     *net_ns;
	struct time_namespace *time_ns;
	struct time_namespace *time_ns_for_children;
	struct cgroup_namespace *cgroup_ns;
};

namespace

Namespace 机制对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号。

当 调用 clone 时传入的flags 包含 CLONE_NEWPID (since Linux 2.6.24)时,linux 就会在在新的PID名称空间创建进程。

同理还有以下 flags

  • CLONE_NEWNS
  • CLONE_NEWUSER
  • CLONE_NEWUTS

cgroup

Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

在 linux 下,执行

# 查看 cgroup 管理的资源
ls /sys/fs/cgroup/

# 输出
# blkio cpu cpuacct cpu,cpuacct cpuset devices freezer memory net_cls net_cls, net_prio, perf_event pids systemd

可以看到 cgroup 管理的资源类型:

  • blkio,为块设备设定I/O 限制,一般用于磁盘等设备;
  • cpuset,为进程分配单独的 CPU 核和对应的内存节点;
  • memory,为进程设定内存使用的限制。

以 cpu 为案例继续执行

# 查看 cpu 目录
ls /sys/fs/cgroup/cpu
# 输出
# cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us  cpu.shares notify_on_release cgroup.procs      cpu.cfs_quota_us  cpu.rt_runtime_us cpu.stat  tasks docker

cfs_periodcfs_quota, 这两个参数需要组合使用,可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间.

可以看到有一个 docker 的目录,这就是 docker 的控制组。

可以直接在这个目录下, 新建目录,也就是新增控制组。

也可以通过修改这些文件的内容来设置限制。

最后通过将 tasks 文件的内容改成要限制的进程的pid,就可以应用此限制。

rootfs

rootfs 就是挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,也就是所谓的容器镜像。

Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

docker 执行隔离需要对 docker 进程的根目录进行隔离,也就是 chroot

需要明确的是,rootfs 并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。

由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

Docker 公司在实现 Docker 镜像时并没有沿用以前制作 rootfs 的标准流程,而是基于 UnionFS(aufs/overlayfs ) ,将多个不同位置的目录联合挂载(union mount)到同一个目录下, 引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。

具体来说,是包含了

  • 只读层 ro + wh

    对应操作系统的基础文件

  • Init 层 ro + wh

    Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。

  • 可读写层 rw

    rootfs 最上面的一层。

其中:

  • ro 只读

    也就是 readonly

  • wh 白障

    也就是 whiteout 删除的时候在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。

  • rw 可读写

总结

对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

  1. 启用 Linux Namespace 配置;
  2. 设置指定的 Cgroups 参数;
  3. 切换进程的根目录(Change Root)

因此,docker 命令的本质也就是

  • docker build

    加载当前目录下的 Dockerfile 文件,然后按照顺序,执行文件中的原语, 建立 UnonFS 目录。

  • docker run

    启用 Linux Namespace 配置,设置指定的 Cgroups 参数,切换进程的根目录

  • docker exec

    加入到一个已经存在的 Namespace 当中

参考资料

深入解析kubenetes

bootlin