Kubernetes系列(2)——容器

半日闲 2020年12月03日 50次浏览

前言

Kubernetes被称之为容器编排引擎,那么由此可见,底层技术便是容器技术了!
容器的出现解决了程序运行时,环境的不一致问题,使得“build once,run anyway”成为可能。容器将应用程序从底层的主机设备中解耦,这使得在不同的云或者OS环境中部署变得更加容易!

容器与虚拟机

实际使用中,很多人往往不清楚容器和虚拟机的本质区别,如下
image.png
简单来讲,容器可以粗浅的理解为轻量虚拟机,但是主要的区别还是在于容器使用的是宿主机的内核,但是虚拟机与宿主机之间存在一个Hypervisor,使得虚拟机可以使用自己的内核。

容器镜像

镜像是一个随时可以运行的软件包, 包含运行应用程序所需的一切:代码和它需要的所有运行时、应用程序和系统库,以及一些基本设置的默认值。

容器运行时

运行时是负责运行容器的软件,而Kubernetes支持多个运行时,例如Docker,containerd,CRI-O以及任何实现Kubernetes 的CRI。

容器技术与底层实现

以下容器以Docker为示例 

资源隔离

Docker使用了namespace实现与宿主机的资源隔离,使用的namespace分为的6种,分别是

  • UTS namespace:实现主机名和域名的隔离,使得容器都拥有自己的主机名和域名,可以被看作一个独立的网络节点
  • IPC namespace:实现信号量、消息队列和共享内存的隔离,其包含了系统IPC标示符以及实现POSIX消息队列的文件系统,使得同一个IPC命名空间下的进程彼此可见,不同的则相互不可见
  • PID namespace:实现进程PID编号的隔离,不同的PID命名空间下的进程可以有相同的PID,每个PID命名空间都有独立的计数程序
  • Network namespace:实现网络资源的隔离,这里的隔离并非真正意义的网络隔离,而是把容器的网络独立出来,如同一个独立的网络实体来与外部通信
  • Mount namespace:实现挂载点的隔离,不同Mount命名空间下的文件结构发生变化互不影响
  • User namespace:实现安全相关的标示符和属性的隔离,包括用户ID、用户组ID、root目录、密钥key以及特殊权限等,该命名空间技术支持进程在容器内外拥有不同级别的权限

Network namespace的简单测试

我们使用veth-pair来实现容器内部与宿主机网络的互通,veth-pair是 一对虚拟设备,且总是成对出现 ,其基本原理如下
image.png
如此,这2个namespace便可以借助veth-pair来进行通信

## 首先,我们先增加一个网桥lxcbr0,模仿docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #为网桥设置IP地址

## 接下来,我们要创建一个network namespace - ns1

# 增加一个namesapce 命令为 ns1 (使用ip netns add命令)
ip netns add ns1 

# 激活namespace中的loopback,即127.0.0.1(使用ip netns exec ns1来操作ns1中的命令)
ip netns exec ns1   ip link set dev lo up 

## 然后,我们需要增加一对虚拟网卡

# 增加一个pair虚拟网卡,注意其中的veth类型,其中一个网卡要按进容器中
ip link add veth-ns1 type veth peer name lxcbr0.1

# 把 veth-ns1 按到namespace ns1中,这样容器中就会有一个新的网卡了
ip link set veth-ns1 netns ns1

# 把容器里的 veth-ns1改名为 eth0 (容器外会冲突,容器内就不会了)
ip netns exec ns1  ip link set dev veth-ns1 name eth0 

# 为容器中的网卡分配一个IP地址,并激活它
ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up

# 上面我们把veth-ns1这个网卡按到了容器中,然后我们要把lxcbr0.1添加上网桥上
brctl addif lxcbr0 lxcbr0.1

# 为容器增加一个路由规则,让容器可以访问外面的网络
ip netns exec ns1     ip route add default via 192.168.10.1

以上便是基于Network namespace的简单测试了,由此我们明白了Docker的基本网络原理。

因此 生产环境中,有时候我们可以为正在Debug的容器添加1个额外的网卡 ,如下

ip link add peerA type veth peer name peerB 
brctl addif docker0 peerA 
ip link set peerA up 
ip link set peerB netns ${container-pid} 
ip netns exec ${container-pid} ip link set dev peerB name eth1 
ip netns exec ${container-pid} ip link set eth1 up ; 
ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1 ;

这样的话,我们就为正在运行的容器添加了1个叫做eth1的网卡
在Docker中,Docker使用容器进程的PID作为namespace的命令,如何查看某个容器的相关信息呢?如下

# 以Kubernetes中dns容器为例
kubectl get po -n kube-system | grep dns
# 找到该container的id
docker ps | grep coredns
# 找到该container所在的进程
ps aux | grep 40abab3ea75e

执行如下
image.png
在ns目录下,可以查看到该容器的namespace,如下
image.png

资源限制

Docker使用cgroup来对容器进行使用资源上的限制,实现组进程并管理它们的资源总消耗,分享可用的硬件资源到容器并限制容器的内存和CPU的使用。
cgroup提供4大功能,如下

  • 资源限制:对任务使用的资源总量进行限制,如应用在运行时超过上限配额就会给与提示
  • 优先级分配:通过分配的CPU时间片数量及磁盘IO带宽大小,实际上就相当于控制了任务的优先级
  • 资源统计:可以统计系统的资源使用量,如CPU、内存等使用情况
  • 任务控制:可以对任务进行挂起、恢复等操作

cgroup的实践

Linux通过namespace对容器的运行环境进行隔离,然后通过cgroup对容器使用的资源进行限制,那cgroup到底是什么呢?并且作用到了什么上面呢?
cgroup简单来讲,就是可以限制1个进程组所使用到的资源上限,包括Cpu,Mem,Disk,IO等。
通过mount -t cgroup可以查看到cgroup到底限制了哪些东西。如下
image.png
下面简单的使用下cgroup,了解下运作机制,以cpu的限制为例

  1. 在/sys/fs/cgroup/cpu目录下创建1个fake-container目录

系统会自动在fake-container目录下生成默认文件 ,如下
image.png

  1. 创建1个进程,使其占据100%的cpu
while : ; do : ; done &

创建后,可以看到进程号以及该进程的cpu使用率,如下
image.png

  1. 对该进程进行限制,如下
# tasks文件中默认为空
echo 27442 > tasks
echo 30000 > cpu.cfs_quota_us

tasks文件:要进行限制的进程号
cpu.cfs_quota_us文件:进程能使用的最大cpu
cpu.cfs_period_us文件:在该段时间内,进程能分配到的最大时间周期,默认为100000(100ms)
上述操作则表示,PID为27442的进程在每100ms的时间里,最多能使用30ms的cpu ,即cpu使用率30%

  1. 使用top命令查看该进程cpu使用率,如下

image.png
可以看到经过cgroup对进程的cpu进行限制后,便立即生效
通过如上测试,对于Docker而言,只需要在每个子系统下创建1个目录即可,然后向对应限制文件中写入限制值即可 ,至于如何写入,完全可以在docker run命令的时候,进行指定,举例如下

docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

即可看到docker目录下出现了1个文件夹,如下
image.png
可以看到当使用docker run命令运行了kubeaz容器后,在docker目录下出现了1个目录( 该目录名和该容器的container id一致 )

权限限制

从Linux内核2.2版本开始,Linux支持把超级用户不同单元的权限分离,可以单独的开启和禁止,即capability的概念。可以将capability赋给普通的进程,使其可以做root用户可以做的事情。内核在验证进程是否具有某项权限时,不再验证该进程的是特权进程(有效用户ID为0)和非特权进程(有效用户ID非0),而是验证该进程是否具有其进行该操作的capability。不合理的禁止capability,会导致应用崩溃。目前Docker默认启用一个严格capability限制权限,同时支持开发者通过命令行来改变其默认设置,保障可用性的同时又可以确保其安全。

镜像构建

容器的基础是依托于镜像之上的,然而进行的构建却是很简单,只需要1个Dockerfile即可,语法也不多。

FROM:基础镜像来源
RUN:构建命令
COPY:复制文件到容器
ADD:同COPY
CMD:使用docker run运行容器时,容器内部运行的命令,可以被docker run时指定的命令覆盖
ENTRYPOINT:与CMD类似,但是不会被docker run指定的命令覆盖
ENV:容器内的环境变量
ARG:构建时的参数 
VOLUME:挂载卷
EXPOSE:容器暴露端口
WORKDIR:容器运行时的工作目录
USER:容器运行命令时的用户
HEALTHYCHECK:容器的健康检查
ONBUILD:延迟构建

具体我们直接来实践吧!
Dockerfile如下

[root@a Dockerfile_test]$ ls .
Dockerfile  index.html

# 所有需要ADD或者COPY到容器内的文件,都必须和Dockerfile文件处于同一目录等级
[root@a Dockerfile_test]$ cat Dockerfile
FROM busybox:latest
COPY index.html  /var/www/index.html
EXPOSE 80
ENTRYPOINT ["/bin/httpd","-h","/var/www/","-f"]

# 在Dockerfile文件所处的目录执行命令
[root@a Dockerfile_test]$ docker build . -t busybox:my

上述Dockerfile构建了1个开启httpd服务的busybox镜像
容器运行时,所运行的命令必须在前台执行,相当于卡住容器进程一样,否则docker run该容器的时候,运行的命令一结束,容器也就自动结束 

镜像背后的实现

使用docker build命令可以将镜像构建起来,然后使用docker run去运行,那么镜像到底本质上是什么呢?

镜像的实质

镜像本质上是一个tar包,使用分层技术来实现一次又一次的build,如下

docker save busybox:latest -o busybox.tar
tar xvf busybox.tar

image.png
我们可以看到镜像实际上可以被保存为1个文件并且是tar包。
查看解包出来的文件,我们可以看到整个镜像的构建参数
image.png
并且使用docker history busybox:latest,我们可以看到镜像的每一层的信息
image.png
在此,我们确认了镜像本质上是tar包的事实,那么镜像分层技术又是什么呢?

镜像分层技术

镜像分层技术是基于UnionFS的,所谓的UnionFs就是把不同物理位置上的目录合并mount到一个目录中,最典型的应用就是平时我们将CD/DVD进行mount到一个目录上,然后我们就可以读取CD/DVD中的内容。
1个普通的镜像,使用docker history可以看到镜像每一步构建的历史
image.png
通常,Dockerfile文件中的每一个RUN命令都会创建1个镜像层,而每一层就会有一个image id,并且这一层相对于前一层变更的大小都会显示出来,如上图所示。
镜像在build的过程中,并不会真正去复制前一个镜像,而是与前一个镜像做diff,只修改需要修改的地方。
简单的说 
启动容器的时候,最上层容器层是可写层,之下的都是镜像层,只读层。
当容器需要读取文件的时候
从最上层镜像开始查找,往下找,找到文件后读取并放入内存,若已经在内存中了,直接使用。(即,同一台机器上运行的docker容器共享运行时相同的文件)。
当容器需要添加文件的时候
直接在最上面的容器层可写层添加文件,不会影响镜像层。
当容器需要修改文件的时候
从上往下层寻找文件,找到后,复制到容器可写层,然后,对容器来说,可以看到的是容器层的这个文件,看不到镜像层里的文件。容器在容器层修改这个文件。
当容器需要删除文件的时候
从上往下层寻找文件,找到后在容器中记录删除。即,并不会真正的删除文件,而是软删除。这将导致镜像体积只会增加,不会减少。
综上,Docker镜像通过分层实现了资源共享,通过copy-on-write实现了文件隔离。
对于文件只增加不减少问题,我们应当在同一层做增删操作,从而减少镜像体积。

备忘

Docker技术通过LXC来实现轻量级的虚拟化,通过namespace进行隔离、cgroup进行资源限制、capability进行权限限制,以满足容器的安全隔离。然而由于Docker容器是共享Linux内核的,所以我们应该认识到容器并非严格全封闭,使用Docker容器一定需要注意保证内核的安全和稳定,需要配合必要的监控和容错。

参考 
cgroup参考:文章一文章二
namespace参考:文章一