容器简介
容器是一种打包应用及其运行环境的方式,为应用打包所有软件及其所依赖的环境,并且可以实现跨平台部署,它是一系列内核特性的统称。
容器技术优缺点
轻量化: 相比虚拟机,容器提供了更小的镜像,因此可以更快速地对容器进行构建和启动。容器更适合需要批量快速上线和快速弹性伸缩的应用。
细粒度(资源管控): 容器是一个沙箱运行进程,这个沙箱起到了细粒度管控资源的作用。在创建容器时,可以指定CPU、内存及I/O资源。在运行容器时强制执行这些资源限制,可防止容器占用其他资源。
高性能(资源利用率高): 容器使用更轻量级的运行机制,它是一种操作系统级别的虚拟化机制。由于容器是以进程形态运行的,因此其性能更接近裸机的性能。对于对性能有较高要求的应用,如高性能计算等,容器更为合适。
环境一致性: 容器实现了操作系统的解耦,它打包了整个操作系统,保证应用运行的本地环境和远端环境的高度一致性,从而保证一次容器打包可以到处运行,对跨平台、不同环境的应用部署有显著的帮助。
管理便捷性: 使生命周期管理,包括迁移、扩展、运维等更加便捷。
但不可否认,安全隔离一直是容器技术的一大弊端。容器只是运行在宿主机上的一种特殊进程,因此多个容器之间共享的还是同一台宿主机的操作系统内核,从而大大增加了安全攻击面。
容器的组成
容器=cgroup+namespace+rootfs+容器引擎(用户态工具)
基本技术
一个应用程序的运行环境的总和(内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的集合)被称为一个进程。容器技术的核心就是通过约束和修改进程的动态表现,从而为其创造出一个逻辑的“边界”。
容器技术本质上为应用解决了两个核心问题:应用的资源隔离限制和应用的可移植性(即在新的环境中可以直接运行)。容器将替代进程,成为今后主流的应用运行形态。
namespace
namespace是Linux用来隔离系统资源的方式,它使得PID、IPC、network等系统资源不再是全局性的,而是属于特定namespace 的,其中的进程好像拥有独立的“全局”系统资源。每个namespace里面的资源对其他namespace都是透明的、互不干扰的,改变一个namespace中的系统资源只会影响当前namespace中的进程,对其他namespace中的进程没有影响。
创建namespace的三种方法
- 在用fork或clone系统调用创建新进程时,可通过特定的选项控制,是与父进程共享命名空间,还是建立新的命名空间。
- setns系统调用让进程加入已经存在的namespace中,Docker exec就采取了该方法。
- unshare系统调用让进程离开当前的namespace,加入新的namespace中。
PID namespace
如果在调用clone时设定了CLONR_NEWPID,就会创建一个新的PID namespace,创建的新进程将会成为该namespace里的第一个进程,在当前PID namespace中这个进程的PID从1开始,该namespace内的进程都将以该进程为父进程,当该进程结束时,其中所有的进程都会结束。
PID namespace 是有层次的,新创建的 namespace 将会是创建该 namespace 的进程所属的namespace的子namespace。子namespace中的进程对父namespace是可见的,一个进程将拥有不止一个PID,其所在的namespace及所有直系祖先namespace中都将有一个PID。系统启动时,内核将创建一个默认的PID namespace,该namespace是所有以后创建的namespace的祖先,因此系统的所有进程在该namespace内都是可见的。
IPC namespace
在一个IPC(Inter-Process Communication 进程间通信) namespace中创建的IPC object对该 namespace 内的所有进程可见,但是对其他 namespace 中的进程不可见,这就使得不同namespace之间的进程不能直接通信。
PID namespace 和 IPC namespace 可以组合使用,只需在调用 clone 系统时同时指定CLONE_NEWPID和CLONE_NEWIPC,这样新创建的namespace就既是一个独立的PID命名空间,又是一个独立的IPC命名空间。不同namespace中的进程彼此不可见,也不能互相通信,这样就实现了进程间的隔离。
mount namespace
每个进程都存在于一个mount namespace中,mount namespace为进程提供了一个文件层次视图,用于让被隔离的进程只看到当前namespace里的挂载点信息。只有在“挂载”这个操作发生之后,进程的视图才会被改变,而在此之前新创建的容器会直接继承宿主机的各个挂载点。
如果不设定这个flag,子进程和父进程将共享一个mount namespace,其后子进程调用mount或umount将会对该namespace内的所有进程可见。如果子进程在一个独立的mount namespace中,就可以调用mount或umount建立一个新的文件层次视图,mount、unmount只对该namespace内的进程可见。该flag配合chroot、pivot_root系统调用,可以为进程创建一个独立的目录空间,chroot实现目录独享,mount namespace实现挂载点独享。
network namespace
如果在调用 clone 时设定了 CLONE_NEWNET,就会创建一个新的 network namespace。network namespace为进程提供了一个完全独立的网络协议栈视图,其包括网络设备接口、IPv4和IPv6协议栈、IP地址路由表、防火墙规则、Socket等。一个network namespace提供了一个独立的网络环境,就跟一个独立的系统一样。**一个物理设备只能存在于一个network namespace中,但它可以从一个 namespace 移动到另一个namespace 中。虚拟网络设备(Virtual Network Device)提供了一种类似于管道的抽象,可以在不同的 namespace 之间建立隧道。利用虚拟网络设备,我们可以建立某个 namepace 与其他 namespace 中物理设备的桥接。**当一个 network namespace 被销毁时,物理设备会被自动移回初始的 network namespace,即系统最开始的namespace中。
UTC namespace
如果在调用clone时设定了CLONE_NEWUTS,就会创建一个新的UTS namespace。一个 UTS namespace 就是一组被 uname 返回的标识符。新的 UTS namespace中的标识符通过复制调用进程所属的namespace的标识符来初始化,clone出来的进程可以通过相关系统调用改变这些标识符,比如调用sethostname来改变该namespace的主机名。
总结来说,Linux中的每个进程都包含以上多种namespace。
一个容器就是一个虚拟的运行环境,它对容器里的进程是透明的,进程会以为自己是直接在一个系统上运行的。实际上,容器在创建容器进程时,指定了这个进程所需启用的一组namespace参数,这样容器进程就只能“看到”当前namespace所限定的资源、文件、设备、状态或配置,而对于宿主机及其他不相关的应用,它就完全看不到了。这时,容器进程就会觉得自己是各自PID namespace里的第1号进程,只能看到各自mount namespace里挂载的目录和文件,只能访问各自network namespace里的网络设备,就好像运行在一个“容器”里面。
Linux namespace机制本身就是为实现容器虚拟化而开发的,它实际上修改了应用进程看待整个系统资源的“视角”,即它的“视线”被namespace做了限制,只能看到某些指定的内容。但对于宿主机来说,这些被隔离的进程与其他进程并没有太大的区别,所以 namespace 提供了一套轻量级、高效率的系统资源隔离方案,其远比传统的虚拟化技术开销小。不过,它也不是完美的,它为内核的开发带来了更大的复杂性,在隔离性和容错性上与传统的虚拟化技术相比也有差距。
cgroup(control group)
cgroup是Linux内核中的一项功能,它可以对进程进行分组,并在分组的基础上限制进程组能够使用的资源上限(如 CPU 时间、系统内存、网络带宽等)。cgroup的作用和namespace不一样,namespace是为了隔离进程之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。
cgroup实现了一个通用的进程分组框架,而不同资源的具体管理则是由各个cgroup子系统实现的:
子系统 | 描述 |
---|---|
devices | 设备权限控制 |
cpuset | 分配指定的cpu和内存节点 |
cpu | 控制cpu占用率 |
cpuacct | 统计cpu使用情况 |
memory | 限制内存的使用上限 |
freezer | 暂停cgroup中的进程 |
net_cls | 配置(tc)限制网络带宽 |
net_prio | 设置进程的网络流量优先级 |
huge_tlb | 限制HugeTLB的使用 |
pref_event | 允许perf工具基于cgroup分组做性能检测 |
cgroup和进程的关系?
Cgroup和进程之间的关系是,Cgroup可以将一组进程组绑定到一个特定的Cgroup中,并对这些进程组的资源使用进行限制和控制。这样,Cgroup实际上是对一组进程进行资源管理的工具。
当一个进程被创建时,它可以被分配到特定的Cgroup中,这样就可以受到该Cgroup所施加的资源限制和控制。这样就实现了对进程的资源隔离和管理。
rootfs
为了实现应用运行环境的一致性,容器使用了 rootfs 技术,这使得容器镜像中打包的内容不只有应用本身,还包括整个操作系统的文件和目录,即应用及其所需的依赖都被封装在一起,实现了应用环境的强一致性。
容器就是一个进程,所以可以通过chroot为容器进程提供一个新的根目录及新的文件系统。为了能够让容器的根目录看起来更像是一个真实的操作系统的根目录,一般会在容器启动时在其根目录下挂载一个完整的操作系统的文件系统,比如Ubuntu 16.04的ISO。这样在容器启动之后,在容器内执行“ls/”命令就可以查看整个根目录下的内容,也就是Ubuntu系统的所有目录和文件。
在Linux中有一个chroot命令,它的作用就是将进程的根目录变更到指定的位置(change root file system)
这个被挂载在容器根目录下,用来为容器进程提供隔离后运行环境的文件系统,就是容器镜像,被称为rootfs(根文件系统)。rootfs只是一个操作系统的文件系统,其中包括文件、配置和目录等,但并不包括操作系统内核。
同一台宿主机上的所有容器,都共享宿主机操作系统的内核。这就意味着,如果容器中的应用程序需要配置内核参数、加载额外的内核模块,以及与内核进行直接的交互等,那么这些都是对宿主机操作系统内核的操作,其对于该宿主机上的所有容器来说是全局操作。
镜像分层
Docker镜像的制作并没有沿用以前制作rootfs的标准流程,而是在镜像的设计过程中引入了层(layer)的概念。用户制作镜像的每一步操作都会生成一个层,整个文件系统的增量机制是基于UnionFS的。UnionFS是Linux内核中的一项技术,它将多个处于不同位置的目录联合挂载到同一个目录下。而Docker就是利用这种联合挂载的能力,将容器镜像里的多层内容呈现为统一的rootfs的。在Docker中使用的UnionFS是通过aufs来实现的,虽然aufs还未进入Linux内核主干,但是它在Ubuntu、Debain等发行版本中均有使用。
以Docker为例,其镜像主要分为三层:
- 只读层:同期的rootfs最下面五层,以增量的方式分别包含整个文件系统;
- 可读/写层:容器的rootfs最上面的一层,在没有写入文件之前,这个层是空的。一旦在容器里进行了写操作,由此产生的内容就会以增量的方式出现在这一层中。可读/写层就是专门用来存放修改 rootfs 后产生的增量内容的——无论是增加、删除还是修改产生的增量内容。当使用完这个被修改过的容器之后,还可以使用“docker commit”和“push”命令保存这个被修改过的可读/写层,而只读层里的内容不会有任何变化,这就是增量rootfs的好处。
- init层:这是 Docker/Kubernetes 单独生成的一个内部层,专门用来存放/etc/hosts、/etc/resolv.conf等配置信息。这些文件本来属于只读层,但是在启动容器时每次都会自动写入一些指定的参数,比如hostname,所以理论上需要在可读/写层对它们进行修改。但这些修改往往只对当前的容器有效,并不希望执行“docker commit”命令时,需将这些信息连同可读/写层一起提交,所以设置了额外的 init 层,init 层的内容在执行“docker commit”命令时会被忽略。
容器创建原理
pid = clone(fun, stack, flags, clone_arg);
(flags: CLONE_NEWPID | CLONE_NEWNS |
CLONE_NEWUSER | CLONE_NEWNET |
CLONE_NEWIPC | CLONE_NEWUTS |
…)
通过clone系统调用,并传入各个Namespace对应的clone flag,创建了一个新的子进程,该进程拥有自己的Namespace。根据以上代码可知,该进程拥有自己的pid、mount、user、net、ipc、uts namespace。
echo $pid > /sys/fs/cgroup/cpu/tasks
echo $pid > /sys/fs/cgroup/cpuset/tasks
echo $pid > /sys/fs/cgroup/blkio/tasks
echo $pid > /sys/fs/cgroup/memory/tasks
echo $pid > /sys/fs/cgroup/devices/tasks
echo $pid > /sys/fs/cgroup/freezer/tasks
将代码一中产生的进程pid写入各个Cgroup子系统中,这样该进程就可以受到相应Cgroup子系统的控制。
fun()
{
…
pivot_root("path_of_rootfs/", path);
…
exec("/bin/bash");
…
}
该fun函数由上面生成的新进程执行,在fun函数中,通过pivot_root系统调用,使进程进入一个新的rootfs,之后通过exec系统调用,在新的Namespace、Cgroup、rootfs中执行“/bin/bash”程序。
原文地址:https://blog.csdn.net/mashaokang1314/article/details/134646937
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_1285.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!