参考资料
《自己动手写docker》
http://www.sel.zju.edu.cn/?p=556
docker原理探索
docker用于开发应用,交付应用,运行应用的一种开源的软件。docker主要是通过操作系统层的虚拟化,来打包运行程序所需的依赖环境的一种容器化的技术, 相对于通过虚拟化硬件的方式而已,docker具有轻量级、快速高效的特点。
随着docker云原生的火爆,再也憋不住来探索一下docker到底是通过什么技术特点来实现的,故简单整理了一下查询的资料。
docker的技术特点
在Linux平台上面,docker实现的技术主要依赖于Namespace和Cgroup,命名空间主要是隔离其他命令空间下的运行环境,将进程运行的环境进行分组管理;Cgroup主要是限制对应的进程的资源的消耗量,从而为运行的容器分配一定的资源而不影响剩余的资源被其他进程使用。
Namespace命名空间
Linux现在提供的命名空间的隔离当前已经有了八个类型的隔离,官网(namespace详细文档),
Namespace Flag Page Isolates
Cgroup CLONE_NEWCGROUP cgroup_namespaces(7) Cgroup root directory
IPC CLONE_NEWIPC ipc_namespaces(7) System V IPC,
POSIX message queues
Network CLONE_NEWNET network_namespaces(7) Network devices,
stacks, ports, etc.
Mount CLONE_NEWNS mount_namespaces(7) Mount points
PID CLONE_NEWPID pid_namespaces(7) Process IDs
Time CLONE_NEWTIME time_namespaces(7) Boot and monotonic
clocks
User CLONE_NEWUSER user_namespaces(7) User and group IDs
UTS CLONE_NEWUTS uts_namespaces(7) Hostname and NIS
domain name
分别都是域名隔离,用户隔离,时间隔离,挂载隔离,网络隔离,IPC隔离和Cgroup隔离。
为了简单的熟悉一下隔离的实际操作是什么样的。
网络隔离
首先需要创建一个network的隔离空间;
# 创建一个网络的隔离空间
[root@node205 ~]# ip netns add test_ns
我们可以通过如下命令来执行隔离网络中的命令
[root@node205 ~]# ip netns exec test_ns ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
接着我们就启动本地环路,从上面的输出可以看出lo是处于DOWN的状态
[root@node205 ~]# ip netns exec test_ns ip link set dev lo up
[root@node205 ~]# ip netns exec test_ns ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.050 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.047 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.040 ms
^C
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.040/0.045/0.050/0.008 ms
启动lo并在隔离网络中执行ping本地的命令可以看出网络是通的;
此时,隔离网络中的lo也已经启动,现在就需要再物理机上启动一个veth0,在隔离网络中启动一个veth1来进行通信,从而完成物理机到隔离网络中的通信。
[root@node205 ~]# ip link add veth0 type veth peer name veth1
[root@node205 ~]# ip link set veth1 netns test_ns
[root@node205 ~]# ip netns exec test_ns ifconfig veth1 10.1.1.1/24 up
[root@node205 ~]# ip netns exec test_ns ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1 (Local Loopback)
RX packets 6 bytes 504 (504.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 6 bytes 504 (504.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth1: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 10.1.1.1 netmask 255.255.255.0 broadcast 10.1.1.255
ether 06:d4:03:63:ba:90 txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
[root@node205 ~]# ifconfig veth0 10.1.1.2/24 up
[root@node205 ~]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.10.205 netmask 255.255.255.0 broadcast 192.168.10.255
inet6 fe80::5c77:44db:eff:7ead prefixlen 64 scopeid 0x20<link>
ether 52:54:00:c6:17:78 txqueuelen 1000 (Ethernet)
RX packets 350951315 bytes 79674104061 (74.2 GiB)
RX errors 0 dropped 3011539 overruns 0 frame 0
TX packets 171821220 bytes 67278483690 (62.6 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1 (Local Loopback)
RX packets 207967871 bytes 62177372000 (57.9 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 207967871 bytes 62177372000 (57.9 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.1.1.2 netmask 255.255.255.0 broadcast 10.1.1.255
inet6 fe80::7cb5:95ff:feda:807b prefixlen 64 scopeid 0x20<link>
ether 7e:b5:95:da:80:7b txqueuelen 1000 (Ethernet)
RX packets 5 bytes 418 (418.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 5 bytes 418 (418.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
从输出可以看出,在物理机上启动了veth0,在隔离网络中启动了veth1,此时我们就可以进行网络测试;
[root@node205 ~]# ping 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.115 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=0.054 ms
^C
--- 10.1.1.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.054/0.084/0.115/0.031 ms
[root@node205 ~]# ip netns exec test_ns ping 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.062 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.048 ms
^C
--- 10.1.1.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.048/0.055/0.062/0.007 ms
分别从物理机上ping 隔离网络,然后再从隔离网络中ping物理机,发现网络环路现在已经完成。
用户隔离
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID ,
}
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
os.Exit(-1)
}
首先先编译go build main.go 得到main
接着通过starce来跟踪一下代码的执行;
[root@node205 docker_t]# go build main.go
[root@node205 docker_t]# strace ./main
execve("./main", ["./main"], [/* 33 vars */]) = 0
....
rt_sigprocmask(SIG_SETMASK, NULL, [], 8) = 0
rt_sigprocmask(SIG_SETMASK, ~[], NULL, 8) = 0
clone(child_stack=0, flags=CLONE_VM|CLONE_VFORK|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWPID|SIGCHLD) = 10914
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
close(4) = 0
read(3, "", 8) = 0
close(3) = 0
waitid(P_PID, 10914,
sh-4.2#
sh-4.2# echo $$
1
sh-4.2# hostname
node205.ceshi.com
sh-4.2# hostname testhost
sh-4.2# hostname
testhost
sh-4.2# exit
exit
{si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=10914, si_uid=0, si_status=0, si_utime=0, si_stime=0}, WEXITED|WNOWAIT, NULL) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=10914, si_uid=0, si_status=0, si_utime=0, si_stime=1} ---
rt_sigreturn({mask=[]}) = 0
futex(0x58de70, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x58dd70, FUTEX_WAKE_PRIVATE, 1) = 1
wait4(10914, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, {ru_utime={0, 15400}, ru_stime={0, 60284}, ...}) = 10914
exit_group(4294967295) = ?
+++ exited with 255 +++
[root@node205 docker_t]# hostname
node205.ceshi.com
[root@node205 docker_t]# echo $$
7725
执行如上的操作可知,修改了sh内的hostname在退出之后并没有影响到物理机上的hostname,并且在sh中输出当前的pid的时候,显示的是1从而完成了对pid的一个环境的隔离。其他的一些特性大家有兴趣可自行验证一下。
Cgroup探索
cgroup官方文档,官网中比较明确的描述了cgroup是组织为分层的功能,然后可以限制其各种类型资源的使用的组并进行监控,对一组进程及其生成的子进程进行资源限制控制与监控,这些资源包括 CPU、内存、存储、网络等 ,可以方便地限制某个进程的资源占用,并且可以实时地监控进程的监控和统计信息 。
有这么高级的功能那我们可以尝试一下;
限制内存
[root@node205 wuzh]# cat c_dead_while.py
while True:
pass
[root@node205 wuzh]# python c_dead_while.py
编写一个死循环的脚本,该脚本就是啥也不敢,一般情况下,该脚本会一直占用一个CPU,并且CPU利用率为100%,
top - 14:47:07 up 69 days, 21:11, 3 users, load average: 0.30, 0.12, 0.08
Tasks: 212 total, 2 running, 208 sleeping, 0 stopped, 2 zombie%Cpu(s): 13.5 us, 0.8 sy, 0.0 ni, 85.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.3 stKiB Mem : 16267272 total, 2505172 free, 3305924 used, 10456176 buff/cacheKiB Swap: 0 total, 0 free, 0 used. 11886264 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND13017 root 20 0 123368 4556 1992 R 100.0 0.0 0:24.62 python
查看CPU利用率确实是100%,接下来我们通过cgroup来限制该进程的CPU使用,
[root@node205 cgroup]# cd /sys/fs/cgroup/cpu
[root@node205 cpu]# ls
cgroup.clone_children cgroup.procs cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat release_agent wucg
cgroup.event_control cgroup.sane_behavior cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release tasks
[root@node205 cpu]# cd wucg/
[root@node205 wucg]# ls
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
[root@node205 wucg]# echo "30000">cpu.cfs_quota_us
[root@node205 wucg]# cat cpu.cfs_quota_us
30000
[root@node205 wucg]# echo "13017">tasks
[root@node205 wucg]#
先在/sys/fs/cgroup/cpu里面创建一个分组wucg,然后再改分组中,输入cpu.cfs_quota_us为30000即运行的CPU的占用时间,此时cpu.rt_period_us的时间为1000000,即会将cpu运行时间限制在30%,此时查看该进程状态;
top - 14:54:35 up 69 days, 21:19, 4 users, load average: 0.99, 0.59, 0.31
Tasks: 213 total, 2 running, 209 sleeping, 0 stopped, 2 zombie%Cpu(s): 4.0 us, 0.1 sy, 0.0 ni, 95.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.1 stKiB Mem : 16267272 total, 2501452 free, 3308644 used, 10457176 buff/cacheKiB Swap: 0 total, 0 free, 0 used. 11883408 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND13017 root 20 0 123368 4556 1992 R 30.2 0.0 4:10.18 python
查看监控可知,该进程的CPU的使用率限制在了30%。
限制内存
[root@node205 wuzh]# cat c_memory_oom.py
c = []
while True:
c.extend([1]*100000)
import time
time.sleep(0.1)
[root@node205 wuzh]# python c_memory_oom.py
该脚本就是不断的申请内存,每个一秒钟就扩充列表的大小,列表大小的扩展会一直去申请内存。
接下来我们就在cgroup中配置一下该运行脚本的进程来达到如果该进程超过了配置的内存使用则oom,杀死该进程。
top - 14:59:00 up 69 days, 21:23, 4 users, load average: 0.02, 0.27, 0.25
Tasks: 212 total, 2 running, 208 sleeping, 0 stopped, 2 zombie%Cpu(s): 0.3 us, 4.2 sy, 0.0 ni, 95.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.2 stKiB Mem : 16267272 total, 2410244 free, 3399452 used, 10457576 buff/cacheKiB Swap: 0 total, 0 free, 0 used. 11792732 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND14339 root 20 0 219404 97496 2072 R 39.5 0.6 0:03.75 python
通过监控可以看出该进程在一直申请内存,内存在飙升,
blkio cpu cpuacct cpu,cpuacct cpuset devices freezer hugetlb memory net_cls net_cls,net_prio net_prio perf_event pids systemd
[root@node205 cgroup]# cd /sys/fs/cgroup/memory/
[root@node205 memory]# ls
cgroup.clone_children memory.kmem.failcnt memory.kmem.tcp.max_usage_in_bytes memory.memsw.limit_in_bytes memory.pressure_level notify_on_release
cgroup.event_control memory.kmem.limit_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.max_usage_in_bytes memory.soft_limit_in_bytes release_agent
cgroup.procs memory.kmem.max_usage_in_bytes memory.kmem.usage_in_bytes memory.memsw.usage_in_bytes memory.stat tasks
cgroup.sane_behavior memory.kmem.slabinfo memory.limit_in_bytes memory.move_charge_at_immigrate memory.swappiness wucg
memory.failcnt memory.kmem.tcp.failcnt memory.max_usage_in_bytes memory.numa_stat memory.usage_in_bytes
memory.force_empty memory.kmem.tcp.limit_in_bytes memory.memsw.failcnt memory.oom_control memory.use_hierarchy
[root@node205 memory]# cd wucg/
[root@node205 wucg]# ls
cgroup.clone_children memory.kmem.limit_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.max_usage_in_bytes memory.soft_limit_in_bytes tasks
cgroup.event_control memory.kmem.max_usage_in_bytes memory.kmem.usage_in_bytes memory.memsw.usage_in_bytes memory.stat
cgroup.procs memory.kmem.slabinfo memory.limit_in_bytes memory.move_charge_at_immigrate memory.swappiness
memory.failcnt memory.kmem.tcp.failcnt memory.max_usage_in_bytes memory.numa_stat memory.usage_in_bytes
memory.force_empty memory.kmem.tcp.limit_in_bytes memory.memsw.failcnt memory.oom_control memory.use_hierarchy
memory.kmem.failcnt memory.kmem.tcp.max_usage_in_bytes memory.memsw.limit_in_bytes memory.pressure_level notify_on_release
[root@node205 wucg]# echo "64k">memory.limit_in_bytes
[root@node205 wucg]# cat memory.limit_in_bytes
65536
[root@node205 wucg]# echo "14339">tasks
[root@node205 wucg]#
当我们将该进程输入到tasks的时候,运行该脚本的进程就退出了
[root@node205 wuzh]# python c_memory_oom.py
已杀死
因为配置了该进程的内存使用量为64k,如果超过了该内存使用量则该进程被杀死。
总结
从流程上来看的话,docker的轻量级主要体现在了通过隔离技术与资源限制的技术达到运行进程的快速的启动,启动过程相比较与传统的硬件虚拟化还是不同的,启动方式更为轻量一些,在程序通通过一些内核提供的参数可以方便的隔离与控制进程,本文只是资料的整理与简单的实践。