代码炼金工坊

容器化技术初探索笔记(一)Namespace

前言

虽然作为一个开发人员,快速布置好开发环境是一个无需多言的基本功,但是每切换到一个新的主机环境,想要完全重现自己熟悉的环境依旧是一个耗时麻烦的工作。于是在过去两年中我逐渐习惯于把docker作为自己的开发环境——得益于可定制化的开箱即用配置我再也不用操心每一步动作该如何因地制宜。

那么容器这种技术到底是如何实现的,原理是什么?在使用的过程中我的好奇心越来越重,终于买了一本《自己动手写Docker》1的书,尝试对容器化技术的实现进行初步的了解。

环境

在此先说明下自己使用的开发环境,以便保证读者获得行为一致的结果:

yuchanns@yuchanns-NUC8i7BEH
---------------------------
OS: Ubuntu 20.04 LTS x86_64
Host: NUC8i7BEH J72992-307
Kernel: 5.4.0-28-generic
Uptime: 16 hours, 47 mins
Packages: 1622 (dpkg), 6 (snap)
Shell: zsh 5.8
Terminal: /dev/pts/2
CPU: Intel i7-8559U (8) @ 4.500GHz
GPU: Intel Iris Plus Graphics 655
Memory: 955MiB / 15881MiB

书籍中所使用的内核版本较旧,因此部分设置有所改变,本文以Ubuntu 20.04LTS为准进行学习。

概念

LXC,全称Linux ContainerLinux容器化,是一种利用Linux内核本身提供的Namespace和Cgroup功能实现的进程层面的资源隔离的虚拟化技术。

Namespace

在编程语言中(如C++、PHP等),有一种命名空间的概念。在不同命名空间中,相同名称的函数、类等是相互隔离独立的东西。一般的操作系统进程,他们共享了外部的系统资源,例如uid、mount、ipc等等。而LXC可以以上述命名空间的形式将这些共享的部分独立开来实现进程隔离的目的。

Cgroup

此外,对于上述的隔离进程,我们还需要限制其资源的使用,例如可使用的cpu核心数、占用内存等等资源,则是通过Cgroup来实现的。

需要明确一点概念,这两者是Linux内核提供的功能,理论上只要实现了系统调用的相关接口,任何语言都可以在Linux环境下实现LXC,并不仅限于C或者Go而已。

MobyLinuxVM

刚接触到LXC的概念时,有点令我很疑惑——因为无论在Mac还是Windows下,我都曾使用过Docker。而上文却提到Namespace和Cgroup是Linux独有的技术,难道Docker打破了这种限制使用了另外的技术实现?

其实很容易发现,当我们在Windows10下使用Docker时,Docker会提示我们需要开启Windows的HyperV或者安装VirtualBox等虚拟机软件才能使用Docker。经过一番搜索,我了解到无论在Windows还是MacOS下,Docker首先会创建一个MobyLinux虚拟机,用以模拟出Linux底层内核环境,再此基础上再使用runC等容器引擎实现进程隔离。通过一系列的复杂转换,作为使用者的我们基本上是感觉不出有什么差别。只有共享磁盘I/O给人感觉性能异常低下(尤其是使用了oh-my-zsh!)。

在这里小小地提一句Windows下进入MobyLinux的方法2

#get a privileged container with access to Docker daemon
docker run --privileged -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker alpine sh

#run a container with full root access to MobyLinuxVM and no seccomp profile (so you can mount stuff)
docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine /bin/sh

#switch to host FS
chroot /host

这也让我稍微理解了Docker的开源项目后来改名时为什么叫Moby3

实现流程(namespace)

先从Namespace说起,不包含Cgroup。

流程图概览

st=>start: 创建进程,传入命令cmd
uns=>operation: 调用unshare设置
使进程的namespace隔离
self=>parallel: 在进程中调用自身,
创建出子进程(容器)
init=>subroutine: 在容器中进行一些
mount初始化操作
execve=>subroutine: 调用内核级函数execve
替换初始进程为cmd
e=>end: 等待子进程cmd
执行结束(wait)

st->uns(right)->self(path1, bottom)->init->execve(right)->e
self(path2, right)->e

其中,unsharemountexecve均属于Linux内核提供的系统调用,与语言无关,只需要使用的语言实现了这些接口就能进行调用。可以通过man命令查看相关的描述。

手册

实现

书中使用Golang实现了Namespace隔离,笔者此处不赘述,可以参考yuchanns/toybox

本文从php这门脚本语言来实现相应的功能。从不同的语言角度来看,实际上有助于读者理解这些命令的作用,分辨清楚语言和系统调用的边界。

#!/usr/bin/php
<?php
$command = "/bin/sh";

if ($argv[0] == __FILE__) {
    // 在容器中进行挂载等初始化操作
    // 首先设置递归根目录私有化
    system("mount --make-rprivate /");
    // 然后挂载/proc目录
    // noexec (手册)不允许在/proc中直接执行任何二进制文件。
    // nosuid (手册)在/proc执行程序时禁止使用set-user-ID和set-group-ID或file capabilities。
    // nodev (手册)禁止创建和访问/proc
    system("mount -t proc proc /proc -o noexec,nosuid,nodev");
    // 系统调用execve将init替换为command
    pcntl_exec($command, [], getenv());
} else {
    pcntl_unshare(CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWNS | CLONE_NEWNET);

    // 注意:第一个参数需要使用数组的形式传递
    // 直接执行自身脚本,生成子进程容器,并定位标准IO
    $process = proc_open([__FILE__], [
        STDIN,
        STDOUT,
        STDERR,
    ], $pipe, null, getenv());

    if (!is_resource($process)) exit(-1);
    // 等待容器进程退出
    pcntl_wait($status);
}

值得注意的地方有:

上述脚本运行之后,我们将进入到容器中。在容器中使用topps -ef可以看到自身成为了pid为1的初始进程。这是CLONE_NEWNSCLONE_NEWPID的功劳:

# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 22:03 pts/0    00:00:00 /bin/sh
root           6       1  0 22:03 pts/0    00:00:00 ps -ef

当然,在上述脚本中,我们还隔离了UTS、进程IPC通信和网络等资源,使用ifconfig可以看到与宿主机相反,容器中什么网络设备都没有;在容器中创建ipc Message Queues,宿主机并不能看到创建的消息队列:

# ipcmk -Q
Message queue id: 0
# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0xa8dd3a61 0          root       644        0            0           

读取进程uts的符号链接指向,宿主的父进程与容器本身的进程也是不同的,在容器中修改hostname不会影响到宿主机的hostname:

# hostname -b toybox
# hostname
toybox # 宿主机输出yuchanns-NUC8i7BEH

本文使用代码可在yuchanns/php-toybox获取。

下一篇文章开始记录实现内存和cpu资源限制的过程。


  1. 《自己动手写Docker》 ↩︎

  2. How can I ssh into the Beta’s MobyLinuxVM ↩︎

  3. moby/moby ↩︎