docker只是一个进程

有这样一个场景,我们要实现一个计算加法的程序,因为计算机只认识0和1,所以你不管用那种计算机编程语言,都需要编译成二进制文件,为了能让程序运行起来,我们需要给他提供数据,在cpu和内存的配合下实现加法运算,像这样一个程序运行起来后的计算机执行环境的总和,就是进程,容器被大家所认识的一个重要特性就是隔离性以及应用的打包也就是它的镜像管理,而它的隔离性的实现主要就是使用到了namespace和cgroup技术,我们用docker起一个容器,
在这里插入图片描述可以看到容器内的第一号进程是我们起容器时的 /bin/sh 命令,但是我们知道一般linux系统内的第一号进程是systemd,我们接下来看看宿主机上的进程信息
在这里插入图片描述我们使用inspect指令获取到容器在宿主机上的进程号,使用ps-ef可以看到当前容器的夫进程是containerd,docker默认使用containerd启动容器,所以可以把docker去掉直接使用containerd来启动容器,而containerd的父进程是systemd,之所以会发生这种情况是因为namespace技术,namespace是linux用来进程隔离的技术,系统创建进程的系统调用是clone,linux对namespace的操作还有,setns,unshare

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

上面代码可以创建一个新的进程并返回他的pid号

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

setns
该系统调用可以让调用进程加入某个已经存在的 Namespace 中:

Int setns(int fd, int nstype)

unshare
该系统调用可以将调用进程移动到新的 Namespace 下:

int unshare(int flags)

当我们加入 CLONE_NEWPID参数时这个进程,就会给这个进程加入一个新的命名空间,在这里面他的pid就是1,上面的容器之所以会出现那种情况就是使用了pid namespace, 当然了还有其他namespace,比如 net user mount等等,再看下面例子

里插入图片描述这个是宿主机的网络命名空间

在这里插入图片描述这是容器的网络命令空间,之所以不同就是系统在经行系统调用的使用给它加上的net namespace

在这里插入图片描述这是容器里面的镜像文件

在这里插入图片描述这是宿主机的镜镜像文件,之所以不同也是系统在进行系统调用的使用加上了mount namespace

在这里插入图片描述而同样的操作在容器内也能执行成功,就是进程调用时加上了user namapace,所以容器的隔离性就是通过各种namespace 实现的,下面是常见的namespace

Pid namespace: 不同用户的进程就是通过Pid namespace 隔离开的,且不同的namespace可以有相同的Pid
net namespace: 网络隔离是通过net namespace 实现的, 每个net namespace 有独立的 network devices, IP address,IP routing tables, /proc/net目录
ipc namespace: Container 中的进程交互还是用linux常见的进程交互方法,包括常见的信号量,消息队列和共享内存
,container 的进程间交互实际上还是 host上 具有相同 Pid namespace 中的进程间交互,因此需要在 IPC
资源申请时加入 namespace 信息 - 每个 IPC 资源有一个唯一的 32 位 ID。
mnt namespace: mnt namespace 允许不同的namespace 的进程看到的文件结构不同,
uts namespace: UTS namespace 允许每个container拥有独立的hostname和domain name 
user namespace: 每个 container 可以有不同的 user 和 group id, 也就是说可以在 container 内部用 container 内部的用户,执行程序而非 Host 上的用户

关于namespace的常见操作
查看当前系统的namespace

lsns -t <type>

查看某进程的namespace

ls -la /proc/<pid>/ns/

进入某namespace

nsenter -t <pid> -n ip addr

namespace小练习

在新 network namespace 执行 sleep 指令:
unshare -fn sleep 60
• 查看进程信息
ps -ef|grep sleep
root 32882 4935 0 10:00 pts/0 00:00:00 unshare -fn sleep 60
root 32883 32882 0 10:00 pts/0 00:00:00 sleep 60
• 查看网络 Namespace
lsns -t net
4026532508 net 2 32882 root unassigned unshare
• 进入改进程所在 Namespace 查看网络配置,与主机不一致
nsenter -t 32882 -n ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

下面我们来看看docker 的常见操作

启动:

docker run 
-it 交互
-d 后台运行
-p 端口映射
-v 磁盘挂载
启动已终止的容器
docker start
停止容器
docker stop
查看容器的进程
查看容器操作
dockers inspect <container id>
进入容器,通过nsenter
PID=$(docker inspect --format "{{.State.Pid}} <container id >")
nsenter --target $PID --mount --uts --ipc --net --pid
拷贝文件到容器内
docker cp file1 <container id>:/file-to-path

dockerfile最佳实践1,我们在主机上编写一个简单的httpserver代码如下

package main

import (
	"fmt"
	"net/http"
)

func HelloHttp(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello Boy")
}

func main() {
	http.HandleFunc("/", HelloHttp)
	http.ListenAndServe(":5656", nil)
}

dockerfile命令如下

FROM golang:1.17-alpine as build

ENV GO111MODULE=on \
    GOPROXY="https://goproxy.io,direct"

WORKDIR /go/src/

ADD go.mod ./

COPY http.go http.go

RUN go mod tidy

RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -ldflags="-s -w" -installsuffix cgo -o http.go .

EXPOSE 5656

ENTRYPOINT ["./http.go"] 

查看构建成功的镜像

docker images | grep http

在这里插入图片描述运行容器

docker run -itd -P --name http my_http:v0.0.1

查看创建的容器

在这里插入图片描述
宿主机上curl一下,我的主机ip是192.168.150.26
在这里插入图片描述基于namespace的隔离手段虽然看起来十分完美但是还是有很多的缺陷,就是隔离的不够彻底,在linux上有很多东西是不能被namespace化的,比如时间,如果你在容器使用settimeofday系统调用修改了时间,那整个操作系统的时间都会被修改,相比在虚拟机中的自由度,在容器内什么能做什么不能做需要我们仔细考虑,尽管在实践中我们确实可以使用 Seccomp 等技术,对容器内部发起的所有系统调用进行过滤和甄别来进行安全加固,但这种方法因为多了一层对系统调用的过滤,必然会拖累容器的性能。何况,默认情况下,谁也不知道到底该开启哪些系统调用,禁止哪些系统调用。还有一个问题是资源的管控,虽然容器进程被隔离了但是它任然可以同其他进程一样使用系统的资源,这样的话就需要一种技术经行限制,那就是cgroups,cgroups是linux系统上用来经行资源限制的重要手段,它还有调整进程优先级的作用,在操作系统中cgroups是通过文件目录暴露给用户使用的
在这里插入图片描述一些工作流程可以用下面的图片经行了解
在这里插入图片描述

下面我们以cpu限制为例子为大家演示一下cgroups的工作方式,
在这里插入图片描述我们在/sys/fs/cgroups/cpu 下新建一个cpu目录,发现里面自动多了很多文件,我们主要使用 cpu.cfs_period_us,cpu.cfs_quota_us,cpu.shares: 可出让的能获得 CPU 使用时间的相对值。cpu.cfs_period_us:cfs_period_us 用来配置时间周期长度,单位为 us(微秒)。cpu.cfs_quota_us:cfs_quota_us 用来配置当前 Cgroup 在 cfs_period_us 时间内最多能使用的 CPU 时间数,单位为 us(微秒)。
我们使用下面命令把系统cpu打满

while : ; do : ; done &

在这里插入图片描述

echo 109772 > /sys/fs/cgroup/cpu/cpu/tasks
echo 10000 > /sys/fs/cgroup/cpu/cpu/cpu.cfs_quota_us 

然后我们再看一下系统cpu使用,发现cpu下来了

在这里插入图片描述当我们使用docker给容器经行资源限制的时候,就会在/sys/fs/cgroups/cpu等相关目录下自动创建一个控制组,把容器pid加入到tasks文件下,根据相关参数在cpu.cfs_quota_us下进行配置如下操作

docker run -itd --cpu-period=10000 --cpu-quota=20000 --name nginx_cpu nginx:latest

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

mkdir /var/lib/lxcfs
lxcfs /var/lib/lxcfs &
docker run -itd -m 256m  --name nginx_lxfs       -v  /var/lib/lxcfs/proc/cpuinfo:/proc/cpuinfo:rw       -v  /var/lib/lxcfs/proc/diskstats:/proc/diskstats:rw       -v  /var/lib/lxcfs/proc/meminfo:/proc/meminfo:rw       -v  /var/lib/lxcfs/proc/stat:/proc/stat:rw       -v  /var/lib/lxcfs/proc/swaps:/proc/swaps:rw       -v  /var/lib/lxcfs/proc/uptime:/proc/uptime:rw     448a08f1d2f9 sh

查看容器top
在这里插入图片描述Cgroup driver
systemd:
• 当操作系统使用 systemd 作为 init system 时,初始化进程生成一个根 cgroup 目录结构并作为 cgroup
管理器。
• systemd 与 cgroup 紧密结合,并且为每个 systemd unit 分配 cgroup。
cgroupfs:
• docker 默认用 cgroupfs 作为 cgroup 驱动。
存在问题:
• 在 systemd 作为 init system 的系统中,默认并存着两套 groupdriver。
• 这会使得系统中 Docker 和 kubelet 管理的进程被 cgroupfs 驱动管,而 systemd 拉起的服务由
systemd 驱动管,让 cgroup 管理混乱且容易在资源紧张时引发问题。
因此 kubelet 会默认–cgroup-driver=systemd,若运行时 cgroup 不一致时,kubelet 会报错。

在前面我们了解到docker 使用namespace作为隔离,使用cgroups进行限制,那他的文件系统呢,为什么跟宿主加的不一样呢,前面我们我们知道mount namespace 会为容器形成一个新的文件系统,但是在系统调用指定使用 mount namaspace 时不会改变容器的试图,只在挂载的时候实现,mount namespace 是由chroot发展起来的,而且mount namespace 也是第一个namespace,我们通过下面的小实验来了解一下chroot


$ mkdir -p $HOME/test
$ mkdir -p $HOME/test/{bin,lib64,lib}
$ cd $T


$ cp -v /bin/{bash,ls} $HOME/test/bin

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

$ chroot $HOME/test /bin/bash

这个时候你执行 ls / 会发现根目录被改变了,chroot是只改变即将运行的 某进程的根目录。pviot_root主要是把整个系统切换到一个新的root目录,然后去掉对之前rootfs的依赖,以便于可以umount 之前的文件系统(pivot_root需要root权限),系统调用会先执行pviot_root,没有再用chroot,docker镜像有层的概念,并且增量改变,这就是union fs
Union FS
• 将不同目录挂载到同一个虚拟文件系统下 (unite several directories into a single virtual filesystem)
的文件系统
• 支持为每一个成员目录(类似Git Branch)设定 readonly、readwrite 和 whiteout-able 权限
• 文件系统分层, 对 readonly 权限的 branch 可以逻辑上进行修改(增量地, 不影响 readonly 部分的)。
• 通常 Union FS 有两个用途, 一方面可以将多个 disk 挂到同一个目录下, 另一个更常用的就是将一个
readonly 的 branch 和一个 writeable 的 branch 联合在一起。
Docker 的文件系统
典型的 Linux 文件系统组成:
• Bootfs(boot file system)
• Bootloader - 引导加载 kernel,
• Kernel - 当 kernel 被加载到内存中后 umount
bootfs。
• rootfs (root file system)
• /dev,/proc,/bin,/etc 等标准目录和文件。
• 对于不同的 linux 发行版, bootfs 基本是一致的,
但 rootfs 会有差别。
Docker 启动
Linux
• 在启动后,首先将 rootfs 设置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite”供用户使用。
Docker 启动
• 初始化时也是将 rootfs 以 readonly 方式加载并检查,然而接下来利用 union mount 的方式将一个
readwrite 文件系统挂载在 readonly 的 rootfs 之上;
• 并且允许再次将下层的 FS(file system) 设定为 readonly 并且向上叠加。
• 这样一组 readonly 和一个 writeable 的结构构成一个 container 的运行时态, 每一个 FS 被称作一个 FS层。

$ mkdir upper lower merged work
$ echo "from lower" > lower/in_lower.txt
$ echo "from upper" > upper/in_upper.txt
$ echo "from lower" > lower/in_both.txt
$ echo "from upper" > upper/in_both.txt
$ sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work 
`pwd`/merged
$ cat merged/in_both.txt
$ delete merged/in_both.txt
$ delete merged/in_lower.txt
$ delete merged/in_upper.txt

我们看看宿主机上的容器的镜像文件
在这里插入图片描述可以用下图来展示
在这里插入图片描述
第一部分,只读层。它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout,至于什么是 whiteout,我下面马上会讲到)。
第二部分,可读写层。它是这个容器的 rootfs 最上面的一层,它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。可是,你有没有想到这样一个问题:如果我现在要做的,是删除只读层里的一个文件呢?为了实现这样的删除操作,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。第三部分,Init 层。它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值