容器

沙盒,把应用装起来的技术。这样一来,应用之间不仅没有互相干扰,还能方便搬运。是理想的 PaaS 状态。

但沙盒了哪些东西?

对于应用而言,静态表现为文件,存放于磁盘上。而动态表现为进程,变成了计算机里的数据和状态的。

所以说,容器其实是特殊的进程而已

容器技术的核心

通过约束和修改进程的动态表现,创造出一个边界。

  • CGroups:制造约束,隔离
  • Namespace:修改进程视图,限制

Namespace 技术

领导给组长划分一个圈,圈内组长管工仔。业绩由组长向上汇报,对工仔来说,这个圈就是公司,组长是编号NO1的人物。但其实在这个公司,组长编号可能只是NO199,而领导才是真正NO1。

这个“障眼法”有什么用?

让工仔看到某些指定的内容,保持专注。

这是一个Linux容器最基本的实现原理。/proc/<PID>/ns
障眼法种类有:

  • PID,CLONE_NEWPID,进程编号
  • Netork,CLONE_NEWNET,网络栈
  • IPC,CLONE_NEWIPC,信号量、消息队列、共享内存
  • User,CLONE_NEWUSER,用户和用户组
  • Mount,CLONE_NEWNS,挂载点
  • UTS,CLONE_NEWUTS,主机名和域名
  • CGROUP,CLONE_NEWCGROUP,资源限制

使用clone()系统调用创建一个进程时,可以实施不同的障眼法

# PID Namespace
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);

对比虚拟机,这种隔离有哪些优缺点?

  • 虚拟机里运行一个完整的GuestOS;而容器无需
  • 虚拟机隔离更加彻底;而容器隔离不彻底,比如:时间,/proc
  • 虚拟机更牢固;容器更容易越狱
虽然确实有Seccomp等技术,对容器发起的所有系统调用进行过滤和甄别来进行安全加固,但因为又多一层过滤,性能一定有影响。何况默认情况下,都不知道到底该开启哪些系统调用,禁止哪些系统调用

CGroups 技术

有了隔离,但当前组与其他组之间依旧是平等竞争关系。谁都不想自己组内的资源被他人挪用吧。

Linux Control Group,限制一个进程组能够使用的资源上限,包括CPU、内存、磁盘、网络带宽

CGroups 暴露给用户的操作接口是文件系统,以文件和目录的方式组织在操作系统的/sys/fs/cgroup/路径下。

ls -l /sys/fs/cgroupmount -t cgroup都能展示出子系统

  • CPU子系统
  • 内存子系统
  • IO子系统
  • 等等...

在每类子系统下创建一个目录,那么这个目录称为一个控制组。你会发现控制组内自动生成该子系统对应的资源限制文件。

实际上,CGroups 就是 一个子系统目录 + 一组资源限制文件 的组合。非常易用。

# 进入CPU子系统
cd /sys/fs/cgroup/cpu/
# 创建一个控制组
mkdir -pv container
# 运行一个吃CPU的进程,并得到PID。可top看效果
while :; do :; done &
[1] 226
# 查看并修改限制,1ms = 1000us
cat container/cpu.cfs_quota_us
-1
cat container/cpu.cfs_period_us
100000
echo 20000 > container/cpu.cfs_quota_us
# 将所需进程PID写入tasks。再用top看效果
echo 226 > container/tasks

CGroups不完善的地方

  • /proc 文件系统
    Linux下/proc记录着当前内核运行状态的一系列特殊文件,即整个公司的资料库。容器内应该仅能查询容器自身的数据。
1.lxcfs
2.Cgroup Namespace

容器技术的本质

容器技术非常重要的概念,容器是一个 “单进程” 模型。不是说容器不支持运行多进程,而是更希望容器本身和应用有相同的生命周期

避免容器是正常运行的,但应用早已挂了,这种情况。同时也便于后续容器的编排

深入理解容器镜像

Mount Namespace,对容器进程视图的改变,一定是伴随着挂载操作才能生效的。

如果将宿主机系统目录挂载到容器里使用,还谈不上独立和隔离。容器对系统目录的修改也很有可能毁掉宿主机。那该怎么办?

改变进程的根目录到你指定的位置:chroot

mkdir -p $HOME/test
mkdir -p $HOME/test/{bin,lib64,lib}
T=$HOME/test
cp -v /bin/{bash,ls} ${T}/bin
list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
for i in $list; do cp -v "$i" "${T}${i}"; done
chroot ${T} /bin/bash

Mount Namespace基于对chroot的改良,成为Linux里的第一个Namespace。
为了更加真实,一般在容器的根目录下挂载一个完整的文件系统。随后在容器中ls看到的内容就像一个完整的系统。

这个挂载在容器上、用来为容器进程提供隔离后执行环境的文件系统,就是容器镜像,也叫作rootfs(根文件系统)

这里体现了容器的另一特性:一致性

rootfs里打包的不只应用,还有整个操作系统的文件和目录,这就是一个完整的依赖库嘛。所以轻松解决了线下线上环境不一致的问题。

每个应用涉及不同开发环境,那每个应用都要打包一份rootfs吗?能不能实现增量?

Docker引入分层概念,镜像制作的每一步都会生成一个layer,即一个增量rootfs。此处用到了一种Union File System(联合文件系统)的能力。

UnionFS,最主要的功能是将多个不通位置的目录联合挂载到同一个目录下
tree .
├── A
│  ├── a
│  └── x
└── B
  ├── b
  └── x
mkdir -p C
mount -t aufs -o dirs=./A:./B none ./C
tree ./C
├── a
├── b
└── x

Mount Namespace 和 rootfs 的结合使用,容器为进程构建出一个完善的文件系统隔离环境。


看清一个容器

运行一个Docker项目,为待创建的用户进程完成以下操作:

  • 启用 Linux Namespace 配置
  • 指定 CGroups 参数
  • 切换进程根目录(chroot)

By Jeff.W

Namespace 构建了四周围墙(进程隔离)
CGroups 构建了受控的天空优先使用阳光雨露(资源限制)
Mount Namespace 和 rootfs 构建了脚下的大地(文件系统隔离)
一切都是熟悉的,没有丝毫陌生感(容器一致性)

如何进入容器内部

docker exec命令如何进入容器里?

Nampespace创建的隔离空间虽然见不着,但一个进程的Namespace信息确实存在宿主机上,且以文件方式存在于/proc/<PID>/ns/下。通过加入某个进程已有的Namespace,达到“进入”这个进程所在容器的目的。这是docker exec的实现原理。

加入指定名称空间的小程序

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
 
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
 
int main(int argc, char *argv[]) {
    int fd;
    
    fd = open(argv[1], O_RDONLY);
    if (setns(fd, 0) == -1) {
        errExit("setns");
    }
    execvp(argv[2], &argv[2]); 
    errExit("execvp");
}

argv[1]当前进程要加入的Namespace文件路径,比如“/proc/1314/ns/net”;argv[2]表示运行的进程,比如“/bin/bash”。

gcc -o set_ns set_ns.c
./set_ns /proc/1314/ns/net /bin/bash
ifconfig

容器数据卷

使用Mount Namespacerootfs实现文件系统隔离,思考两个问题:

  1. 容器里新建文件,宿主机如何获取
  2. 宿主机文件,容器如何访问

这正是 Docker Volume 要解决的问题。允许将宿主机上指定的目录或文件,挂载到容器里面进行读取和修改。

Volume声明有两种,本质都是把宿主机目录挂载进容器/test目录

docker run -v /test ...
docker run -v /home:/test ...

第一种没声明宿主机目录,Docker默认使用宿主机上/var/lib/docker/volumes/[VOLUME_ID]/_data进行挂载。

那么,具体如何实现挂载?

容器进程被创建后,在执行chroot/pivot_root之前,容器进程一直可以看到宿主机的整个文件系统,包括容器镜像的各个层。这些层通过联合挂载在/var/lib/docker/aufs/mnt/,这样所需的rootfs就准备好了。而后在执行chroot之前,把Volume指定的宿主机目录(/home)挂载到指定容器目录(/test)在宿主机上对应的目录/var/lib/docker/aufs/mnt/[rwlayer_id]/test

容器进程,是Docker创建容器的初始化进程(dockerinit),而不是应用进程(ENTRYPOINT+CMD)。dockerinit负责完成根目录的准备、挂载设备和目录、配置hostname等一系列需要在容器内进行的初始化操作。最后调用execv(),让应用进程取代自己成为容器里PID=1的进程

标签: none

添加新评论