# neodocker **Repository Path**: Ya_Qia/neodocker ## Basic Information - **Project Name**: neodocker - **Description**: docker的简易实现版本 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-12-22 - **Last Updated**: 2024-12-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # neodocker 一个简单的docker实现,目前仅支持x86-64指令集架构 项目地址:[https://gitee.com/Ya_Qia/neodocker](https://gitee.com/Ya_Qia/neodocker) ## 运行方式 ### 依赖 本项目依赖于`glibc`、`make`、`libtar`、`libcap`及`libnl`,请在确保安装好相应软件后运行。 ### 编译及运行 执行: ```bash make # 编译 sudo ./daemon/bin/neodockerd # 运行daemon make run # 运行示例代码 ``` ## 实现思路 主要分为如下部分:前端交互、daemon程序框架、namespace实现、cgroups实现、网络实现。 后端架构整体采用C/S模式,客户端(多例程序)发送对应请求,由服务端(单例程序)接收请求并完成响应。 ### 使用C/S模式的必要性(选型原因) 使用C/S模式的最主要目的是防止容器核心代码多例执行,并提供对以普通用户运行的容器的支持。 前端直接使用函数调用的方式创建、删除容器时需以root用户权限执行,这将导致容器不再属于该用户。 为了解决该问题,可以将前后端分离,前端调用以普通用户的权限在前台执行,后端则以超管权限在后台执行,前端以类似RPC的方式传输数据确定后端要执行的代码。 ### C/S模式的好处(选型效果) 1. Server是单例程序,不会有多个进程同时运行导致并发问题; 2. Server以非阻塞形式运行,吞吐量大; 3. 将内部执行过程进一步抽象,对用户隐藏。 ### 各模块内容 #### 前端交互 #### daemon程序框架 daemon程序将执行的session id独立于执行终端,并将自身的标准输入禁用,标准输出和标准错误重定向到文件`.log`和`.errlog`中。 daemon程序是调用容器系统调用的服务端,它负责监听由它自身创建的一个Unix socket套接字,当监听到连接请求后完成一次对应操作。 完成操作的过程是非阻塞的,每个操作对应的函数都会额外fork一个新的进程进行事物处理,最后返回结果也由子进程直接完成。 除此之外,daemon还会初始化cgroups的文件系统。 ##### parse_create API 使用fork创建一个子进程用于处理执行流程,父进程负责监控其执行到的位置并在容器创建时为容器所对应的第一个进程ID创建网络设备。 ##### parse_attach API attach在fork后的子进程需要将自己从后台运行的daemon进程恢复为前台运行,为此需要打开、重定向对应的文件描述符(stdin重新打开,stdout和stderr重定向到传入的终端中)。 这种方式获得的前台进程会由于缺少控制终端而在执行bash等shell程序时出现警告信息(没有控制终端导致由终端输入的信号无法正确发送到attach的容器进程中去),后续会将attach改造为利用重定向的中间文件来传输输入、输出数据的形式。 ##### parse_delete API 在fork后直接调用cntr_delete,并在执行完成后将返回值发送回前端。 > 注:上述三者均使用了 > ```c > waitpid(-1, NULL, WNOHANG); > ``` > 来保证daemon的非阻塞性:不等待请求退出直接回到主循环,加速执行过程。 #### namespace实现 namespace的创建、修改和删除是容器创建(create)、进程插入(attach)和删除(remove)的核心操作,主要通过`clone()`系统调用和`setns()`系统调用实现对namespace的创建、修改,namespace的删除是由操作系统自动完成的:该namespace最后一个进程退出后且没有额外的对namespace虚拟文件系统的链接的情况下namespace会自动被删除。 通过namespace隔离,可以保证进程、挂载点、主机名、文件系统、网络设备等一系列操作系统级资源的隔离。截图如下: - 文件系统隔离 ![文件系统隔离](./images/fs.png) - 进程号隔离 ![进程号隔离](./images/pid.png) - 主机名隔离 ![主机名隔离](./images/hostname.png) - 网络设备隔离 ![网络设备隔离](./images/net_isolation.png) ##### cntr_create API 这个API完成以某个待执行命令为pid=1的进程执行创建容器的过程。 在执行基本的namespace创建之前,先生成一个随机数代表容器id,将容器打包好的系统镜像单独持久化到包含这个id的路径(不允许多个容器共享一个路径,这样会导致文件系统不隔离)。 在容器打包中,需要将环境变量单独持久化为一个文件,在`cntr_create`中解析其中的环境变量并传递给容器内的进程使用。 在使用`clone`创建容器内进程前,先创建pipe管道作为外部和内部进程同步的控制(这是因为容器namespace会隔离IPC,无法使用信号的方式完成同步)。 `clone`执行时,子进程有7个新的namespace:pid、uts、cgroups、ipc、network、mount、user,但time不会做隔离(与docker默认情况一致)。 > 注:`clone`传入的子进程入口函数是`child_func`,它完成了hostname设置、`chroot`、挂载`procfs`、设置独立进程组、限制进程`capability`并最后调用`execvpe`执行所需执行的命令。 用clone创建容器内进程后,利用clone返回的子进程PID获取得到其在`procfs`中的`uid_map`和`gid_map`,利用这两个映射文件完成容器内部的用户映射,可以支持主机的非特权用户映射为容器内的特权用户,保证容器执行的隔离性和安全性更高(优于docker实现)。 最后,将元数据信息持久化到容器所在目录的上级目录,方便`cntr_attach`时获取。 ##### cntr_attach API 这个API完成待执行的新命令加入容器的操作。 先访问持久化数据获得容器内pid=1的进程在外部的pid号,获取环境变量信息,将进程并入对应容器的6种namespace中(这里排除了user namespace,user namespace之后再进入,因为user namespace做了`capability`权限管理,限制权限后无法执行`chroot`等必要系统调用)。 `fork`一个子进程,其中`chroot`、挂载`procfs`后再进入user namespace,进入后执行`setuid`和`setgid`将用户改为内部映射的root。 虽然其他API都是非阻塞实现,但`cntr_attach`是阻塞实现,因为它必须等待内部执行的命令退出或者接收到外部信号。 ##### cntr_delete API 先获取持久化数据,其中包含了内部pid=1的进程在主机中的pid号,先执行`killpg`将它及所有子进程组成的进程组杀死,后删除所有容器持久化文件数据。 #### cgroups实现 Cgroup部分使用cgroup来实现对容器的CPU利用率、内存空间大小、CPU数量等系统资源进行限制,来达到限制容器的可使用资源。 本模块是基于croup v2实现的。cgroup v2 是 cgroup 的第二代版本,也称为 unified hierarchy(统一层次结构)。它在 Linux 内核 4.5 版本中引入,大多数linux均支持该版本。 在cgroup v2中,根 cgroup(/sys/fs/cgroup) 是整个 cgroup 层次结构的顶级目录,也称为根节点。它是整个层次结构的起点,所有其他 cgroups 都是其子 cgroups。根 cgroup 通常被挂载到文件系统中的 cgroup2 目录下。因此在本模块中,我们仿照docker的实现,首先会在根cgroup中创建一个名为neodocker.slice的子cgroup,该cgroup作为整个容器引擎的"根cgroup",我们会在neodocker.slice中为每个容器创建一个专有的子cgroup来达到分别控制每个容器的资源限制。 容器的cgroup层次结构如下所示 ``` /sys/fs/cgroup ├── system.slice ├── user.slice ├── init.scope ├── neodocker.slice │   ├── 21113313123.slice │   ├── 23313123.slice │   ├── 5566334.slice │   └── 993313.slice ``` 在本模块中主要实现了以下功能: ##### cgroup_init API 对cgroup进行初始化,通过检查Linux系统上的根cgroup的cgroup_subtree_control来检查根cgroup的控制器(controllers)是否向子集合开启,如果未开启相对应的控制器(例如CPU、memory、cpuset),为容器创建的子cgroup无法控制相对应的资源。因此会向cgroup_subtree_control中写入缺失的controllers。并创建neodocker.slice,作为容器引擎的根cgroup。 ##### create_cgroup API 为容器新建一个属于自身的cgroup,在neodocker.slice为新建的容器创建属于它的子cgroup,名称为容器id.slice。 ##### delete_cgroup API 删除容器对应的cgroup,在容器退出时将容器对应的cgroup进行删除。 ##### apply_cgroup 将容器对应的进程号加入到cgroup中达到资源控制的效果。具体来说创建容器时会将第一个命令的进程号,在attach进入容器时会将attach创建的bash命令进程号加入,由于cgroup具有传递性,linux会自动将其生成的子进程加入cgroup中。 ##### update_cgroup 更新指定容器的资源限制,例如可以修改cpu的利用率,该更新是实时的,不需要重新启动容器。目前支持的是修改CPU利用率、修改可用内存空间限制、修改可用的cpu数量。 ##### cgroup_exit API cgroup模块的退出处理,在容器引擎退出时进行调用,会删除neodocker.slcie以及它所创建的所有cgroup。 ##### 资源限制测试: 我们首先创建了一个容器了一个容器,容器中写了一个test程序,test程序主要的功能是创建了4个执行死循环的线程。因此如下图所示,每个线程都会占用一个CPU核的全部利用率。 ![no-limit](images/cgroup-no-limit.png) 然后我们调用update命令,将总体的cpu利用率限制在10%,我们可以看到4个test线程的利用率被限制了,全部加起来的利用率是10%。 ![limit](images/cgroup-limit.png) #### 网络实现 网络部分具有以下功能:容器与主机之间的网络互联、容器之间的网络互联,以及容器与外部Internet的互联。 在技术实现方面,网络部分主要使用了`libnl`库、`iptables`命令行接口和`ip`命令行接口。`libnl`库提供了一套接口,用于在Linux系统上基于Netlink协议进行通信。该库包含多个子库,用于实现不同的功能。其中,`libnl-route`模块提供了操作网络设备、路由功能、IP地址和邻居功能的接口,可用于创建网桥和`veth`设备,并设置相关的IP地址和启用网络设备。 `iptables`是Linux内核集成的IP数据包过滤系统,用于在Linux系统上更好地控制IP数据包和防火墙配置。通过配置`iptables`,可以允许容器之间的连接,并在`nat`表的`POSTROUTING`链中添加`MASQUERADE`规则,实现网络地址转换(NAT),从而使容器可以访问外部Internet。 `ip`命令是Linux的网络配置工具,用于创建网络设备、分配IP地址、管理路由表和控制网络设备的状态。在该系统中,主要使用`ip`命令来添加默认路由。 网络采用网桥+veth实现,其架构图如下图所示。 ![](./images/network.png) 容器和网桥neodocker0通过veth连接,所有的接入网桥的设备组成一个二层局域网可以互相连接。 容器对外网的访问首先容器的默认网关为10.0.0.1,对外网的请求会转发给10.0.0.1网桥,在此过程中包会被iptables所拦截并进行NAT转发实现容器对外网的访问。 ##### set_link_ipv4 API 根据名称获取网络设备并为网卡绑定ip地址。 ##### set_link_up API 根据名称来打开网络设备。 ##### create_netns_host_bridge API 创建一个名称为`neodocker0`的网络设备并设定相关`iptables`条目。 ##### create_netns_host_veth API 利用`libnl`库来创建一对`veth`并且根据传入的子`pid`编号获取`net namespace`并将`veth`的一端放入到`namespace`中。 ##### create_netns_container API 这部分代码在容器内部执行,其主要功能为设置`namespace`中`veth`位于容器另一端的虚拟网卡`eth0`的`ip`地址、默认路由和dns地址。 其中更改dns主要修改容器内部的`/etc/resolv.conf`实现,将宿主机的dns配置复制到容器内部即可。 ##### 连通性测试 ![](./images/ping_test.png) 上述是在容器2上进行的ping测试,可以看到容器2的ip为`10.0.0.2/24`,实现了对容器3地址为`10.0.0.3/24`的ping测试,宿主级+网关`10.0.0.1/24`的ping测试,外网Internet中`baidu.com`的ping测试。 ## 常见问题 ### 为什么以非root模式运行的容器procfs和sysfs所有者都显示为nobody? 因为Linux内核不允许对procfs和sysfs的实际所有者进行变更,它们的所有者都指向真正UID=0的用户。非root模式运行会将当前用户映射为root,自然把旧的UID=0的用户给覆盖掉了。 ## 性能测试部分 ### UnixBench 我们使用了UnixBench性能测试工具来与docker的性能进行对比。UnixBench(Unix Benchmark)是一款用于在Unix和类Unix操作系统上进行性能基准测试的工具。它的目的是评估系统的整体性能,包括CPU、内存、文件系统等方面。UnixBench提供了一套包含多个测试项目的测试套件,每个项目都针对系统的不同方面进行测量。 UnixBench包含以下子测试: - Dhrystone(整数运算性能): 该测试主要评估系统的整数计算性能。 - Whetstone(浮点运算性能): 这个测试用于衡量系统的浮点运算性能。 - File Copy(文件拷贝性能): 通过模拟大量小文件和大文件的复制操作,评估文件系统的性能。 - Pipe Throughput(管道吞吐量): 通过测试管道操作的性能,评估进程间通信的效率。 - Process Creation(进程创建性能): 评估系统在创建新进程时的性能表现。 - System Call Overhead(系统调用开销): 测试系统调用的性能开销。 - Memory Copy(内存拷贝性能): 评估系统内存拷贝操作的性能。 - Execl Throughput(执行性能): 通过测试系统在执行程序时的性能。 UnixBench执行这些测试,并生成一个综合的分数,该分数反映了系统在各个方面的性能。通常,UnixBench的分数越高,表示系统性能越好。UnixBench测试会分为单进程模式和多进程模式,多进程模式会测试运行当前计算机的最大核心数量的测试用例。 我们首先比较在单进程模式下比较。首先是docker的测试结果 ![docker-single](images/docker-single.png) 我们的容器引擎的测试结果 ![neodocker-single](images/neodocker-single.png) 为方面对比,将以上测试结果的分数(INDEX)做成表格,如下所示,分数越高代表性能越好。 | tests | docker | neodocker | |-------------------------------------------|----------|--------------| | Dhrystone 2 using register variables | 6312.9 | 6838.5 | | Double-Precision Whetstone | 621.2 | 772.2 | | Execl Throughput | 1275.0 | 1418.3 | | File Copy 1024 bufsize 2000 maxblocks | 2034.2 | 3155.7 | | File Copy 256 bufsize 500 maxblocks | 1247.8 | 1963.4 | | File Copy 4096 bufsize 8000 maxblocks | 3993.8 | 5299.1 | | Pipe Throughput | 1456.6 | 1604.2 | | Pipe-based Context Switching | 506.1 | 453.6 | | Process Creation | 755.1 | 675.1 | | Shell Scripts (1 concurrent) | 1474.8 | 1471.3 | | Shell Scripts (8 concurrent) | 12005.8 | 12607.8 | | System Call Overhead | 966.7 | 1115.7 | | System Benchmarks Index Score | 1673.6 | 1908.4 | 通过以上表格可以看到,大部分测试的性能是相差无几的,而在File Copy系列测试中,我们的得分要比docker的性能要好,这是因为docker的文件系统overlayfs的存在,存在多个文件系统层,每个层都需要进行层叠和合并操作,因此会损失一些读写性能。 多进程的对比结果如下: - docker结果: ![docker-multiple](images/docker-multiple.png) - 我们的容器引擎测试结果 ![neodocker-multiple](images/neodocker-multiple.png) 可以看到是与单进程的结果测试一致的。