本博客为个人学习极客时间中张磊课程时所作的笔记,仅作交流,不得作为商用

上一篇深入剖析kubernetes系列学习之**背景(一)

目录

什么是进程

进程的边界

Linux的障眼法

Namespace技术

虚拟机与容器的差异

限制的艺术

容器即“进程”


什么是进程

什么是进程?相信很多人都能回答上来,对于进程,其静态表现就是程序,大部分时间都安安静静待在磁盘上;而一旦运行起来,即其动态表现,程序被翻译、解释并运行,然后从磁盘空间或I/O设备加载相关数据,可能还要访问网络资源,接着系统根据程序运行计算机指令......也就是说,进程是程序运行起来后计算机执行环境的总和,而这,也是容器的基础——因为容器,其实是一种特殊的进程。

进程的边界

容器之所以被称为特殊的进程,是因为容器这个进程是有边界的。上一篇博客提到容器是一种沙盒技术,即能够向集装箱一样把应用装起来,这样应用与应用之间因为存在边界而不至于互相干扰,而被装进集装箱的应用也能被方便的搬运——这就是PaaS最理想的状态。容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其人为打造“边界”。在Linux中,Cgroup技术是用来制造约束的主要手段,而Namespace技术则是修改进程视图的主要方法,即让进程看到的资源信息与实际资源信息不同,俗称“障眼法”。

Linux的障眼法

我们可以通过创建一个Docker容器来体验Linux的障眼法,本文环境为CentOS 7+,通过yum安装docker,简明流程如下:

1.更新yum源

#yum -y update

2.添加yum仓库

# cat >/etc/yum.repos.d/docker.repo <<-EOF

然后输入

[dockerrepo]
name=Docker Repository
baseurl=https://yum.dockerproject.org/repo/main/centos/7
enabled=1
gpgcheck=1
gpgkey=https://yum.dockerproject.org/gpg
EOF

3.安装Docker包

#yum install -y docker-engine

4.检查docker-selenux有没有安装,如果未安装,执行(一般情况下应该安装了)

# yum install -y docker-selinux

5.启动服务

# systemctl start docker.service

首先创建一个容器(第一次创建会从远端拉去镜像,然后启动并进入容器):

# docker run -it busybox /bin/sh
/ # 

其中,

-i: --interactive(相互作用的)   Keep STDIN open even if not attached(即使没有连接,也要保持STDIN打开)

-t:   --tty                          Allocate a pseudo-TTY(分配一个 冒充的终端设备)

这条指令的意思是:启动容器、在容器中执行/bin/sh,为我分配命令行终端与容器交互。

执行ps指令查看进程信息:

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps
/ # 

可以发现/bin/sh进程的PID为1,说明/bin/sh与ps进程都被Docker隔离在了一个与宿主机(运行容器的机器)完全不同的环境中。

如何做到的呢?

其实很简单,Linux系统会为每个进程分配唯一的PID号,就像员工工牌,如果PID=100意味着你是公司第100号员工,1号员工就是老板,每个新员工入职时都会分配工号并递增。Docker做的就是在101号员工入职时给他施“障眼法”,使其看不到前面的100号员工,使其错误的以为自己是公司第1号员工(其实在真实的公司中他还是第101号)——这就是Linux的Namespace技术所做的事情。

Namespace技术

Namespace技术所做的“障眼法”,在容器上的具体表现就是实现了资源的隔离。Namespace主要隔离六项资源:

Namespace

系统调用参数

隔离内容

UTS

CLONE_NEWUTS

主机名与域名

IPC

CLONE_NEWIPC

信号量、消息队列和共享内存

PID

CLONE_NEWPID

进程编号

Network

CLONE_NEWNET

网络设备、网络栈、端口等等

Mount

CLONE_NEWNS

挂载点(文件系统)

User

CLONE_NEWUSER

用户和用户组

以创建新进程PID Namespace为例,在Linux系统中创建线程的系统调用是clone(),其函数定义如下:

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

具体调用以上代码会创建新进程并返回进程号pid,如:

int pid = clone(main_function, stack_size, SIGCHLD, NULL); 

而如果我们在flags参数项指定CLONE_NEWPID参数,如:

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

这时,新创建的进程会“看到”全新的进程空间,在这个空间里,它看到自己的PID是1(当然,这只是障眼法),在宿主机真实进程空间里,这个进程的PID还是真实值。

这就是Linux容器最基本的原理,一句话——都是假象~看到一个说法,容器是没有上过历史课的进程,哈哈哈哈~

虚拟机与容器的差异

很多人把容器成为“轻量级”的虚拟化技术,这一不严谨的说法是把虚拟机的概念套在了容器上。虚拟机与容器对比如下图所示:

深入剖析kubernetes系列学习之何为容器(二)

虚拟机中,Hypervisor软件通过硬件虚拟化功能,模拟出来运行一个操作系统所需要的各种硬件,如CPU、内存、IO设备等,然后在虚拟硬件上安装新的操作系统,即Guest OS。但是反观Docker部分,Docker的位置与应用同级别并且靠边,这意味着用户在容器中的应用进程与宿主机上其他进程一样由宿主机操作系统统一管理,只不过说这些被隔离的进程拥有额外配置过的Namespace参数。Docker在这里起到的作用就是辅助和管理。

不难发现,虚拟机更消耗资源和性能,容器几乎没有额外资源占用情况。“敏捷”和“高性能”是容器相较于虚拟机最大的优势,但容器基于Linux Namespace的隔离机制存在一个主要问题:隔离不彻底。

最典型的的例子就是:时间。容器中的应用进程调用settimeofday(2)修改时间时,整个宿主机的时间也随之被修改。

解决方法就是:基于虚拟化或者独立内核技术的容器实现可以较好地在隔离和性能之间做出平衡。

限制的艺术

不得不说,Linux系统是真的狠,用“障眼法”迷惑了进程,还要将进程施展才华的舞台大小给限制住。

当然,这是必须的,想想如果不做资源限制,宿主机上随便一个容器里面的应用进程就能把宿主机的资源给耗尽,怎么感觉有点像农夫与蛇呢?

Linux的Cgroups(Control Group)技术是专门用来限制一个进程组能够使用资源(CPU、内存、磁盘、网络带宽等)上限的功能。

我们可以先看看Cgroups可以进行限制的资源种类:(CentOS 7+)

[[email protected] cgroup]# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup ......
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)

可以看到/sys/fs/cgroup下面有很多诸如cpuset、cpu、memory等子目录,或者叫子系统。

我们可以再具体查看该类资源被限制的方法:

[[email protected] cgroup]# cd cpu
[[email protected] cpu]# ll
total 0
-rw-r--r--  1 root root 0 Feb 20 22:18 cgroup.clone_children
--w--w--w-  1 root root 0 Feb 20 22:18 cgroup.event_control
-rw-r--r--  1 root root 0 Feb 20 22:18 cgroup.procs
-r--r--r--  1 root root 0 Feb 20 22:18 cgroup.sane_behavior
-r--r--r--  1 root root 0 Feb 20 22:18 cpuacct.stat
-rw-r--r--  1 root root 0 Feb 20 22:18 cpuacct.usage
-r--r--r--  1 root root 0 Feb 20 22:18 cpuacct.usage_percpu
-rw-r--r--  1 root root 0 Feb 20 22:18 cpu.cfs_period_us
-rw-r--r--  1 root root 0 Feb 20 22:18 cpu.cfs_quota_us
-rw-r--r--  1 root root 0 Feb 20 22:18 cpu.rt_period_us
-rw-r--r--  1 root root 0 Feb 20 22:18 cpu.rt_runtime_us
-rw-r--r--  1 root root 0 Feb 20 22:18 cpu.shares
-r--r--r--  1 root root 0 Feb 20 22:18 cpu.stat
drwxr-xr-x  2 root root 0 Feb 24 11:33 docker
-rw-r--r--  1 root root 0 Feb 20 22:18 notify_on_release
-rw-r--r--  1 root root 0 Feb 20 22:18 release_agent
drwxr-xr-x 50 root root 0 Feb 24 17:27 system.slice
-rw-r--r--  1 root root 0 Feb 20 22:18 tasks
drwxr-xr-x  2 root root 0 Feb 24 10:27 user.slice

上面三个标红的文件中,cfs_period_us文件和cfs_quota_us文件组合使用,可以限制tasks文件中的进程在cfs_period_us时间(单位:u微秒)内,只能被分配到总量为cfs_quota_us的CPU时间。

Docker如何限制容器的CPU资源呢?

你可以看到上面标紫色的docker目录吧,打开这个目录就一目了然了:

[[email protected] cpu]# cd docker/
[[email protected] docker]# ll
total 0
-rw-r--r-- 1 root root 0 Feb 24 10:30 cgroup.clone_children
--w--w--w- 1 root root 0 Feb 24 10:30 cgroup.event_control
-rw-r--r-- 1 root root 0 Feb 24 10:30 cgroup.procs
-r--r--r-- 1 root root 0 Feb 24 10:30 cpuacct.stat
-rw-r--r-- 1 root root 0 Feb 24 10:30 cpuacct.usage
-r--r--r-- 1 root root 0 Feb 24 10:30 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 Feb 24 10:30 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 Feb 24 10:30 cpu.cfs_quota_us     ##限制进程的CPU使用率
-rw-r--r-- 1 root root 0 Feb 24 10:30 cpu.rt_period_us
-rw-r--r-- 1 root root 0 Feb 24 10:30 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 Feb 24 10:30 cpu.shares
-r--r--r-- 1 root root 0 Feb 24 10:30 cpu.stat
-rw-r--r-- 1 root root 0 Feb 24 10:30 notify_on_release
-rw-r--r-- 1 root root 0 Feb 24 10:30 tasks                          ##存放docker容器进程PID

补充:

当然,cfs_period_us文件和cfs_quota_us文件里面的值可以使用默认值,也可在启动容器时指定,如docker启动容器时就可以指定

#docker run -it --cpu-period=100000 --cpu-quota=20000 busybox /bin/sh

那Docker如何限制容器的其他所有资源呢?相信你应该想到了:

[[email protected] cgroup]# find -type d -name "docker"
./hugetlb/docker
./cpuset/docker
./pids/docker
./freezer/docker
./perf_event/docker
./memory/docker
./devices/docker
./blkio/docker
./cpu,cpuacct/docker
./net_cls,net_prio/docker
./systemd/docker

Docker在所有资源目录下都建了名为docker的目录,并在其中完成资源限制。

可以看出,Cgroups设计的还是简单易用的,就是在子系统目录下新建一个目录以及相应的资源限制文件完成资源的限制,再在容器进程启动后,将进程PID写进tasks文件即可。

当然,Cgroups的限制能力也有缺陷,最多提及的是/proc文件系统问题,该目录下存储记录了当前内核运行状态,比如使用top命令查看CPU和内存数据,在容器里执行top命令时看到的居然是宿主机的CPU和内存信息,而不是当前容器的数据。怎么解决呢?给一下别人写的答案:

top 是从 /prof/stats 目录下获取数据,所以道理上来讲,容器不挂载宿主机的该目录就可以了。lxcfs就是来实现这个功能的,做法是把宿主机的 /var/lib/lxcfs/proc/memoinfo 文件挂载到Docker容器的/proc/meminfo位置后。容器中进程读取相应文件内容时,LXCFS的FUSE实现会从容器对应的Cgroup中读取正确的内存限制。从而使得应用获得正确的资源约束设定。kubernetes环境下,也能用,以ds 方式运行 lxcfs ,自动给容器注入争取的 proc 信息。

容器即“进程”

综上,你可以体会到,一个正在运行的docker容器,就是一个启动了多个Linux Namespace的应用进程,而这个进程使用的资源量受到Cgroups的限制。

容器是一个“单进程”模型。因此简单来看,在容器中没有办法同时运行两个不同的应用,除非能找到一个公共的PID=1的程序来充当两个不同应用的父进程,因此很多时候,人们选择systemd软件来代替应用本身作为容器的启动进程。当然,还有更好的容器设计模式来作为解决方法,如容器与应用同生命周期。

(完结)

相关文章: