1.容器主要特性
隔离性:可以在一个隔离环境运行,所以它的所有依赖都需要在这个隔离环境里面存在
可配额:依赖于Cgroups,来控制这个容器所能消耗的资源
便携性:因为之前有了隔离性,所以你现在的容器镜像就可以在任何地方重放了,
安全性:1.因为隔离性好 2.Linux本身有一些安全保障的技术来保证它的安全。
2.Linux内核代码中的Namespace实现
3.Linux对Namespace操作方法
一个进程在初创的时候,它就是有天然的namespace,Linux操作系统里面Pid为1的进程systemd,就是这个这个操作系统的第一个进程.systemd本身这个进程有自己的Namespace,那就是默认的namespace,也就是我们常说的主机Namespace,那么当其他的进程被init system拉起来以后,在Linux里面这个进程关系都是树状的,所有的进程其实都是通过其他进程fork出来的。当systemd去fork其他进程的时候,会把自己的namespace复制到其他进程上面去。所以默认情况下,新的子进程会跟主机共用同一个Namespace。所以默认情况下我们感受不到Namespace,因为它们共用同一个Pid的Namespace,共用网络Namespace,共用User NameSpace。
clone可以在复制进程的时候,通过flags这个参数设置新建的Namespace类型。
4.隔离性--Linux Namespace
每个用户进程其实都可以拥有自己的Namespace,主机这边有自己的主机Namespace,每个进程可以有自己的进程的Namespace,且彼此隔离。每一个进程其实都是一个自己封闭的运行空间。
进程都是主机的Namespace fork出来的,主机里面的这些进程其实也是主机fork出来的进程,所以所有的容器内部里面的进程,在主机上你都可以通过ps看到。但是在外面和容器里面的namespace看到的pid号是不一样的。它们中间是有一个映射关系。我们在容器的pid namespace里面看到的是它自己的pid号,容器启动的时候,会有一个ENTRYPOINT进程,这个进程就是我们在Pid namespace里面看到的1的进程号,所有其他的进程都是那个Pid fork 出来的。
network namespace:如果每一个进程拥有了独立的network namespace,那么这个进程它的网络身份就跟主机是不一样的。
IPC namespace:信号量和共享内存共享。不在同一个ipc namespace之间的进程是不能做IPC的。如果你希望它们之间通过IPC去通信。那么必须放在同一个ipc namespace里面。
mnt namespace:每个进程都有自己的文件系统。
uts namespace:每个进程都有自己的主机域名加上一个独立的IP
user namespace:每个进程都有自己的用户管理系统,它的这个用户也是隔离的。
5.关于namespace的常用操作
6.Cgroups
CPU、内存、磁盘等这些资源在Cgroup里面由不同的子系统管理起来。
6.1Cgroups代码实现
6.2可额度/可度量
6.2.1 CPU子系统
CPU子系统用来控制一个进程能占多少CPU。它通过了两种手段来控制,一种手段是通过cpu_shares这种相对时间,另外一种手段是通过这种绝对时间
cpu.shares:假设机器上有3个CPU,我定义了两个CGroup,那么我把第一个进程放在第一个CGroup,把第二个进程放在第二个CGroup。那这个时候进程和CGroup就产生关系了。接下来我把第一个CGroup里面的cpu_shares设成512,把第二个CGroup里面的cpu_shares设成1024,这就意味着操作系统去调用这些进程的时候,它对时间片分配就会按照1:2去分配。
绝对值是按照两个配置文件去配置的,一个是cfs_period_us,还有一个是cfs_quota_us。period是分母,quota是分子。
7.Liunx调度器
Linux里面提供了多个调度器,最高优先级的是RT,就是realtime的调度器,这个调度器基本上是轮训的,就是说如果你有多个进程是用RT调度器去调度,那么它就会轮流地去调度。次优的是CFS调度器,一般普通的用户进程都是用CFS调度器的,这是一个完全公平的调度器,它引入了一个叫做虚拟运行时间(vruntime)的概念。
7.1CFS调度器
进程的权重越大,获取的vruntime越多
7.2vruntime红黑树
vruntime最小的进程放在树的左边,vruntime最大的进程放在右边。每次做调度的时候,会从最左边取这个最小值,谁的vruntime到了,它就去运行谁。
7.3CFS进程调度
CFS本身会维护一个时钟周期,在时钟周期开始时,会调用_schedule()来调度进程运行。_schedule()函数调用pick_next_task()选一个vruntime值最小的那一个。既然调度了进程,那么就要做上下文切换,通过context_switch()切换到新的地址空间。在时钟周期结束了之后,调度器会更新这个进程的vruntime。此时红黑树会通过插入、反转、重新排序,会把一个更紧迫的进程丢在最左边。那么每次进程调度的时候,它永远从最左边去vruntime最小的这个进程进行调度。
其实vruntime跟这个cpu_shares产生了关联关系,cpu_shares越大的这种进程,它能占用的cpu时间就越多。
7.3.1 CPU子系统实战
首先创建一个cpudemo的cgroup: cd /sys/fs/cgroup/cpu/ mkdir cpudemo
运行一个程序,记录其中的进程号
cd cpudemo 并执行 echo 进程号 > cgroup.procs 把进程号加入到这个cgroup里面。那这个进程就被这个cgroup控制了。
8 cpuacct子系统
9 Memory子系统
10 Cgroup driver
Cgroup本身可以有不同的driver,比如说docker,它用的是cgroupfs作为驱动,整个操作系统它是用的systemd的Cgroup driver
11 文件系统
Docker的文件系统是利用了Union FS。什么是Union FS呢,它其实是通过某些技术,把不通的目录mount到同一个虚拟目录里面去,那不同目录在这个新的虚拟目录里面又可以有独立的权限,比如说可以设定readonly,readwritre。通过这种方式就会把多个子目录,多个不同来源的这种目录模拟成一个完整的操作系统或者说文件系统。
11.1 容器镜像
Docker它开创性地提出了一种容器镜像的概念。它利用源代码的形式(dockerFile),允许开发人员去定义一个面向应用的容器镜像构建的源代码。
假设说对于同一个用户或者在同一台机器上面,如果我们需要构建多个镜像,而这多个镜像它的基础镜像又是一样的,那么两个镜像可以共用这些基础镜像,相当于只保存了一份基础镜像。
11.2 Docker的文件系统
一般一个Linux会分为两个主要组成部分,一个是bootfs,一个是rootfs。bootfs分为2个子模块,一个叫bootloader,一个叫kernel。bootloader的主要作用是引导操作系统的启动,它主要其实就是把kernel加载出来就完成任务了。当kernel被加载到内存以后,整个bootfs会被umount掉。接下来的事情就是加载rootfs,rootfs就是我们日常常见的像/dev,/proc,/bin,/etc这些文件。
11.3 Docker的启动
Docker初始化也是将rootfs以readOnly方式去加载,但是它检查完了之后并不是把rootfs直接变成可写,而是在这个readonly的文件层基础之上,再添加新的层。所以它首先去加在Base Image,比如说Ubuntu,然后加在完成以后,Ubuntu这一层就变成readonly了,然后在这个基础之上再加一个jre层,然后再加比如说我们后面的这些指令层,那么它这样一层一层的堆叠,那么下面的层级永远都是只读,当这些所有层级加在完毕以后,它会把最上面的一层变成readwrite,那么所有你针对这个容器内部的文件修改,事实上都是在最上面这一层的修改。它并不会动到下面readonly的这些层的
11.4写操作
一个镜像是可以被多个容器使用的,而且一个镜像里面的不同层也是被多个镜像共享的,所以有了写时复制技术。它确保了下面的基础镜像层永远是不会被修改的。那么无论你通过这个基础镜像启动了多少个容器,那它底层的基础镜像层都是一致的,也就是只有一份拷贝,不需要为不同的容器进程、容器实例在复制一份出来。
11.5 容器存储驱动
11.6 OverlayFS
UNIONFS是把多个目录组织到一个虚拟目录里面去,使得这个虚拟目录包含来自不同源的内容,并且这个虚拟目录,它最终在Docker里面就作为rootfs供这个容器进程使用的。
11.6.1 OverlayFS的演示
11.7 OCI容器标准
12 Docker引擎架构
Docker daemon是Docker本身的一个后台的服务端,它事实上本身就是一个Rest API。
containerd是真正的控制运行时进程的这样一个后台程序,其本身也是一个可以独立运行的组件。containerd真正地会去启动一个containerd的shim进程,再通过runc(底层运行时的一个接口)去启动容器进程。
早起的时候没有containerd和shim,直接是由daemon来拉起进程的,那这样会有一个什么样的问题呢?docker daemon是所有容器进程的父进程。当你去升级Docker或者重启Docker的时候,父进程就不存在了,那个时候所有的子进程都会被重启,这样我们就无法轻易去升级Docker的。containerd作为一个更轻量级的容器管理进程,当去启动一个应用进程的时候,不是直接去启动的,而是通过shim进程,shim进程相当于用来维护这个容器进程生命周期的。这个shim的进程在运行起来以后,它会把这个shim进程交给操作系统的systemd,这样containerd下面是不挂任何子进程的,containerd就可以随便的去升级和重启