0%

容器实现-namespace

前言

最近一直在看Kubernetes有关的知识,自认为对于k8s已经有了较深的理解,且在处理客户问题中也积累了相当多的排错经验。不过,在处理k8s相关问题时,我感觉到有几个知识点是我比较欠缺的,没有掌握的:

  • 容器:即容器本身的问题,而非容器编排引擎的问题。比如说如下的案例:
    • 使用kubectl delete PODNAMEdocker rm CONTAINERID删除容器后,发现容器依然存在。经研发大佬研究后说是docker的bug,docker与容器之间的同步出现问题。
    • 运行容器服务的云服务器出现重启现象,发现原因是内核crash。经研发大佬研究后发现是cgroup在耗尽内存后申请内存失败引发的内核空指针crash。
  • 微服务:在k8s上运行微服务相关的咨询。这些并非排错问题,但我觉得有必要进行相关的学习,k8s本身就适合作为微服务的载体,如果说光学k8s不学微服务,岂不等于武功练了一半?
  • k8s的编程扩展:出于实用度以及职业的考虑,我在学习k8s的过程中一直没有涉及k8s的编程扩展部分,比如自己实现CRI,CNI,CSI,以及CRD,以及自定义准入控制器。我不打算专门研究这些,但是有必要带着看一点点。

对于容器相关知识的学习,我打算参照《自己动手写docker》一书,亲身实践一下docker的细节;对于微服务相关知识,则计划看Istio有关的知识,该程序有可能像k8s一样成为微服务的标准,有必要学习;对于k8s的编程扩展,则在以后的咨询中带着学习。

于是,这篇文章就是我学习容器实现的开篇。


namespace简介

namespace即命名空间,是linux内核的一个功能,正如各个编程语言中的命名空间用来隔离变量的作用域一样,linux的namespace的功能也是隔离,只是隔离的对象从变量变为进程的环境,如pid,网络等等。

linux将进程的环境纳入namespace总感觉有种想到哪个就纳入哪个的意味。从最开始的Mount Namespace,到UTS Namespace等等,再到最近的Cgroup Namespace,命名空间的考量并非一开始就决定的,而是在版本的迭代中不断地创造新的Namespace。当然这一点并非坏事,不断地创造和迭代能够让Namespace愈发地完善。

以下是目前已实现的Namespace:

  • mount:挂载命名空间,使进程有一个独立的挂载文件系统,始于Linux 2.4.19
  • ipc:ipc命名空间,使进程有一个独立的ipc,包括消息队列,共享内存和信号量,始于Linux 2.6.19
  • uts:uts命名空间,使进程有一个独立的hostname和domainname,始于Linux 2.6.19
  • net:network命令空间,使进程有一个独立的网络栈,始于Linux 2.6.24
  • pid:pid命名空间,使进程有一个独立的pid空间,始于Linux 2.6.24
  • user:user命名空间,是进程有一个独立的user空间,始于Linux 2.6.23,结束于Linux 3.8
  • cgroup:cgroup命名空间,使进程有一个独立的cgroup控制组,始于Linux 4.6

容器使用到的命名空间主要是前六种,最后一个cgroup作为容器实现中的另一种技术,用来限制容器的资源使用,并不需要进行隔离。

linux提供了这些namespace,自然也需要提供相关的系统调用,供开发者们去使用。linux主要提供了如下API:

  • clone():创建新的进程。根据参数可以同时创建新的namespace。
  • unshare():将进程移出某个namespace。
  • setns():将进程加入某个namespace。

简单地说就是创建/加入/移除namespace。

示例

程序示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"log"
"os"
"os/exec"
"syscall"
)

func main() {
// 指定子进程运行的命令为sh
cmd := exec.Command("sh")
// 指定子进程属性
cmd.SysProcAttr = &syscall.SysProcAttr{
// 指定需创建并让子进程加入的命名空间
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWNET | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
// 指定uid的映射:HostID为宿主机的uid;ContainerID为容器的uid
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getuid(),
Size: 1,
},
},
// 指定gid的映射,同uid
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getgid(),
Size: 1,
},
},
}
// 指定子进程的标准输入,输出,错误的文件描述符为系统默认
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

// 运行子进程,如报错则记录。运行过程中父进程阻塞
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
// 子进程运行结束后打印结束信息
log.Println("subprocess done.")
}

实际上,每个进程都拥有其命名空间,可以在/proc/$$/ns目录中看到命名空间的全局id:

1
2
3
4
5
6
7
8
[root@staight chmdocker]# ls -l /proc/1/ns
total 0
lrwxrwxrwx 1 root root 0 Oct 3 00:30 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Oct 3 00:30 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Oct 3 00:30 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Oct 3 00:30 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Oct 3 00:30 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Oct 3 00:30 uts -> uts:[4026531838]

如果是运行于不同命名空间的进程,命名空间的全局id也将会不同。

接下来开始验证各个命名空间是否成功隔离:

mount

运行程序,挂载/proc目录:

1
2
3
4
5
6
[root@staight chmdocker]# go run main.go 
sh-4.2# mount -t proc /proc
sh-4.2# ls /proc
1 buddyinfo cmdline crypto dma fb interrupts irq keys kpagecount locks misc mtrr partitions scsi softirqs sys timer_list uptime vmstat
4 bus consoles devices driver filesystems iomem kallsyms key-users kpageflags mdstat modules net sched_debug self stat sysrq-trigger timer_stats version zoneinfo
acpi cgroups cpuinfo diskstats execdomains fs ioports kcore kmsg loadavg meminfo mounts pagetypeinfo schedstat slabinfo swaps sysvipc tty vmallocinfo

切换终端,查看宿主机的命名空间:

1
2
3
4
5
6
7
[root@staight chmdocker]# ls /proc
1 11818 1284 14 20351 23186 2562 28 3035 3191 38 5 5877 693 8930 9648 buddyinfo crypto fb irq kpagecount misc partitions softirqs timer_list vmstat
10 11831 1285 16 20460 23188 2565 29 3041 3193 46 50 645 6939 9 9655 bus devices filesystems kallsyms kpageflags modules sched_debug stat timer_stats zoneinfo
105 11880 13 18 20461 24 2568 2945 3046 3460 4663 51 65 7 9314 9660 cgroups diskstats fs kcore loadavg mounts schedstat swaps tty
11 12 1360 19 21 25 2570 3 3047 35 4776 5767 665 781 9322 9662 cmdline dma interrupts keys locks mtrr scsi sys uptime
1125 12302 1387 2 22 25069 26 3009 30581 36 48 5773 669 8 9365 9972 consoles driver iomem key-users mdstat net self sysrq-trigger version
11546 1277 1393 20 23 2560 27 3027 3074 37 49 5774 680 8708 9411 acpi cpuinfo execdomains ioports kmsg meminfo pagetypeinfo slabinfo sysvipc vmallocinfo

可以看到,sh子进程的/proc目录与宿主机的/proc目录不一致,mount命名空间被隔离。

net

运行程序,查看网络接口:

1
2
3
4
5
6
7
8
9
10
11
[root@staight chmdocker]# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:e2:d7:89 brd ff:ff:ff:ff:ff:ff
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 02:42:0d:fc:79:bb brd ff:ff:ff:ff:ff:ff
[root@staight chmdocker]# go run main.go
sh-4.2# ip link
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

网络接口只剩下环回口,不再是宿主机的网络接口,net命名空间成功被隔离。

ipc

创建消息队列,在sh子进程中查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@staight chmdocker]# ipcmk -Q
Message queue id: 0
[root@staight chmdocker]# ipcs

------ Message Queues --------
key msqid owner perms used-bytes messages
0xf619f303 0 root 644 0 0

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status

------ Semaphore Arrays --------
key semid owner perms nsems

[root@staight chmdocker]# go run main.go
sh-4.2# ipcs

------ Message Queues --------
key msqid owner perms used-bytes messages

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status

------ Semaphore Arrays --------
key semid owner perms nsems

在宿主机上创建一个消息队列,而sh子进程中却无法看到,ipc命名空间被隔离。

pid

运行程序,查看pid:

1
2
3
[root@staight chmdocker]# go run main.go 
sh-4.2# echo $$
1

查看sh子进程的pid为1,pid命名空间被隔离。

uts

运行程序,修改hostname:

1
2
3
4
[root@staight chmdocker]# go run main.go 
sh-4.2# hostname -b test
sh-4.2# hostname
test

切换终端,查看hostname:

1
2
[root@staight chmdocker]# hostname
staight

宿主机的hostname并没有随着sh子进程修改hostname而更改,uts命名空间被隔离。

user

使用普通用户运行程序,查看id:

1
2
3
4
5
[staight@staight chmdocker]$ id
uid=1000(staight) gid=1000(staight) groups=1000(staight)
[staight@staight chmdocker]$ ./chmdocker
sh-4.2# id
uid=0(root) gid=0(root) groups=0(root)

在容器内为root用户,user命名空间被隔离。

小结

这篇文章简单地实现了具备隔离性的进程,所谓隔离性即位于新的uts,pid,ipc,net,mount,user命名空间。

一个真正的容器应当还具备资源的限制,防止资源的过度争用。该功能基于linux的cgroup功能,将在以后尝试实现。