无需Docker, 5分钟徒手DIY 一个Linux容器

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介:

容器技术,其优势以及应用场景想必大家都不陌生。本文旨在用DIY一个简陋的Linux容器作为例子,让感兴趣的同学大致体会容器是如何建成的。

什么是容器?

提到容器最容易想到的大概就是Docker了, 然而Docker只是容器的一种实现以及提供丰富的周边工具,本质上也是利用了Linux primitives构建Linux container。如果要求我只能用一句话来定义容器,那么我会说:容器是 一个隔离的进程工作室 (an isolated process workspace)。这么定义当然太笼统,展开细节可以轻松讲几篇文章,我们这里聚焦最浅显的概念:隔离 (isolation),普遍的观点是将隔离分为两类:

  1. Namespace isolation: 容器内的进程看到的pid, 文件系统,hostname, IP, 网卡都是独立于host的,在namespace内无法修改其他namespaces的内容。
  2. Resource isolation: 容器内的进程系统资源的使用可以限制,例如容器内的进程们CPU/Memory使用不能超过quota等

DIY一个Linux容器

接下来我们将step by step讲解如何用Linux自带的工具将进程 (processes)放入容器 (an isolated workspace), 注: 整个流程参考了Eric Chiang的containers from scratch, 原文中有几个地方我没有直接走通,这里加了一些个人的剪裁和修改。我的环境是Ali Linux 4.4.114-1.al7.x86_64, 接下来的步骤无需安装Docker也不需要安装其他工具

Namespace隔离

1. 准备容器文件系统 (类似Docker image)

container filesystem本质上就是一个tarball, 有兴趣的话可以看一下Redbeard的minimal containers 视频,核心是一句话:A CONTAINER FILESYSTEM ISN'T MAGIC, IT'S JUST A TARBALL

# 下载+解压container filesystem tarball
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# wget https://github.com/ericchiang/containers-from-scratch/releases/download/v0.1.0/rootfs.tar.gz
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# tar -xzf rootfs.tar.gz

[root@iZ2zea1bcc0lrqq26qw1esZ ~]# ls rootfs
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

可以看到 rootfs下面看到的文件结构和Liunx / 下看到的很相似。

2. 创建一个简陋的容器

我们的目的是进入rootfs,并且以rootfs为新的root (/), 这样进程只能看到rootfs中的文件, Linux自带的chroot工具可以很容易达到这种效果

# Mount rootfs/proc,这样在rootfs子文件系统中的进程可以做`ps aux`这样的命令
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# mount -t proc proc rootfs/proc

# 用chroot将 /bin/bash 进程看到的root限制在dir rootfs, 恭喜你进入了你自建的隔离非常简陋的容器!
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# chroot rootfs /bin/bash

# 现在bash shell中的root (/) 已经变成了 host上面的“/root/rootfs”
root@iZ2zea1bcc0lrqq26qw1esZ:/# bin/ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  test.py  tmp  usr  var

# 这时文件系统隔离有了,但是进程隔离还没有, 下面的命令可以看到机器上所有的进程,我们容器里的shell甚至可以去kill其他进程,不理想!
root@iZ2zea1bcc0lrqq26qw1esZ:/# bin/ps aux

# 退出该容器
exit

3. 隔离PID

为了限制/bin/bash进程能看到的pids,我们需要为bash进程创建pid namespace。用 Linux unshare command 可以方便地为进程创造不同类型的namespace

# 下面的命令为 /bin/bash进程创建了一个新的process namespace
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# unshare -p -f --mount-proc=$PWD/rootfs/proc chroot rootfs /bin/bash

# 现在shell里面只能看到本pid namespace的两个进程了,注意shell进程是pid 1, 这里和Docker的主进程为pid 1完全一样
root@iZ2zea1bcc0lrqq26qw1esZ:/# bin/ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  20272  3284 ?        S    19:08   0:00 /bin/bash
root         2  0.0  0.0  17500  2084 ?        R+   19:08   0:00 bin/ps aux

上面几步之后,我们基本有了一个对filesystm和pid有隔离的容器,这个容器距离严谨的namespace隔离还有很长的路要走,例如创建Network namespace, 修改hostname, username space (不允许容器用户为root), 这里假设我们的隔离的需求很简单只是mount + pid。

Resource 隔离

CGroup (control group) 对于CPU, memory, 文件系统资源的隔离的技术已经很成熟,大多数容器也都选择用cgroup来做资源限制,对cgroup的细节不在这里介绍,下面的步骤会为上面创建好的container加上内存使用的限制

# 下面这些命令在host shell内执行,而不是刚才创建的container shell
# 创建一个叫做mygroup的memory cgroup
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# mkdir /sys/fs/cgroup/memory/mygroup

# 限制在这个cgroup里面所有的进程内存使用和不能超过100MB
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# echo "100000000" > /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes

# 找到我们的container在host上面的pid,这里是10028 (这里需要注意有两个bash进程,我们的容器进程是从unshare命令里面fork出来的,因此是10028而不是unshare 10027本身)
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# ps aux|grep bin/bash
root     10027  0.0  0.0 107904   704 pts/0    S    01:49   0:00 unshare -p -f --mount-proc=/root/rootfs/proc chroot rootfs /bin/bash
root     10028  0.0  0.0  20264  3068 pts/0    S+   01:49   0:00 /bin/bash
root     10031  0.0  0.0 112668  2216 pts/1    S+   01:49   0:00 grep --color=auto bin/bash

# 根据你找到pid替换$MY_PID
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# export MY_PID=10028

# 将shell pid加入mygroup, 加入之后,容器内所有的进程 (bash以及其子进程)都会受到内存使用100M的
[root@iZ2zea1bcc0lrqq26qw1esZ ~]# echo $MY_PID > /sys/fs/cgroup/memory/mygroup/tasks

# 下面这些命令在container shell内执行
# 测试一下效果,下面的python脚本会不断消耗内存
bin/cat <<EOF > ./test.py
a = ''
i=0
while True:
i += 1
print "%dmb" % (i*10,)
some_str = ' ' * 10000000
a = a + some_str
EOF

# 执行脚本,可以看到内存接近100M的时候脚本会被kill, 现在我们再也不用担心这个容器内的进程会用光host的内存啦, nice!
root@iZ2zea1bcc0lrqq26qw1esZ:/# /usr/bin/python test.py 
10mb
20mb
30mb
40mb
50mb
60mb
70mb
80mb
Killed

总结

到这里,我们DIY了一个文件系统,pid隔离,内存限制为100M的容器。通用的容器需要对网络,user namespace, 以及其他系统资源(CPU, disk等)做了更完整的隔离,这篇文章的目的不是鼓励大家去自建容器,而是通过一个简单的动手教程让大家熟悉Linux容器的构成和实现。Docker以及其他容器runtime都提供了强大的功能,但是归根溯源都还是将Linux已有的namespace工具,cgroup结合在一起搭建出了易用容器。欢迎对容器技术感兴趣的同学拍砖交流。

目录
相关文章
|
3天前
|
运维 Linux Docker
Docker笔记(个人向) 简述,最新高频Linux运维面试题目分享
Docker笔记(个人向) 简述,最新高频Linux运维面试题目分享
|
3天前
|
运维 Linux Docker
ChatGLM3在Docker环境部署,Linux运维免打包多渠道统计如何实现
ChatGLM3在Docker环境部署,Linux运维免打包多渠道统计如何实现
|
3天前
|
运维 Linux Docker
ChatGLM3在Docker环境部署(1),Linux运维电话面试技巧
ChatGLM3在Docker环境部署(1),Linux运维电话面试技巧
|
3天前
|
运维 网络协议 Linux
Docker网络_docker 网络,来看看这份超全面的《Linux运维面试题及解析》
Docker网络_docker 网络,来看看这份超全面的《Linux运维面试题及解析》
|
3天前
|
Java 虚拟化 Docker
Docker简介及用途,为什么要使用Docker?Docker容器和虚拟机的区别
Docker简介及用途,为什么要使用Docker?Docker容器和虚拟机的区别
|
3天前
|
存储 Linux Docker
CentOS7修改Docker容器和镜像默认存储位置
CentOS7修改Docker容器和镜像默认存储位置
|
3天前
|
Linux Docker 容器
更改docker容器中的时间而不影响宿主机
更改docker容器中的时间而不影响宿主机
|
3天前
|
Ubuntu Linux Go
docker容器启动报错
docker容器启动报错解决