使用go实现docker简易版

写在前面

本blog是基于《自己动手写docker》一书的实验笔记,与原书本的生产环境和golang版本都不同,所以会导致各种bug,本文针对这些进行了修复。

系统环境

测试环境

[root@iz2zegp1t778mbekgw2rdrz fs]# lsb_release -a
LSB Version:    :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description:    CentOS Linux release 7.3.1611 (Core) 
Release:        7.3.1611
Codename:       Core
[root@iz2zegp1t778mbekgw2rdrz fs]# uname -r
3.10.0-514.26.2.el7.x86_64
[root@iz2zegp1t778mbekgw2rdrz fs]# go version
go version go1.13.5 linux/amd64
[root@iz2zegp1t778mbekgw2rdrz fs]# docker -v
Docker version 19.03.12, build 48a66213fe

Namespace

概念

是Kernel的一个功能,主要用于资源隔离,当前Linux支持六种Namespace

Namespace版本系统调用参数内核版本
UTS NamespaceCLONE_NEWUTS2.6.19
IPC NamespaceCLONE_NEWIPC2.6.19
Mount NamespaceCLONE_NEWNS2.4.19
User NamespaceCLONE_NEWUTS3.8
PID NamespaceCLONE_NEWPID2.6.24
Network NamespaceCLONE_NEWUTS2.6.29

Namespace 的API主要由下列三个调用

  • clone():创建新process,由参数来确定被创建的n、Namespace的类型,并且其子进程也会被包含到Namespace中。
  • unshare():将进程移出某个Namespace。
  • setns():将进程加入某个Namespace中。

6个Namespace

UTS Namespace

UTS Namespace(UNIX Time-sharing System Namespace)主要用来隔离nodename和domainname这两个系统标识。在每个UTS Namespace中,每个Namespace允许拥有自己的hostname。

代码实现:

func UTSNS() {
    cmd := exec.Command("sh")
    
    // 笔者使用的是MACOS,服务器是阿里云。如果使用goland等IDE,IDE自动识别当前系统为Darwin,下面这个struct会报错,但是不用管他。
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

运行代码,进入交互式终端,使用pstree -pl查看当前的进程树

[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go 
sh-4.2# pstree -pl
├─sshd(17408)───bash(17410)───go(17546)─┬─main(17571)─┬─sh(17574)───pstree(17575)

可以看到,父进程为main(17571),子进程为sh(17574)
查看它们是否在同一个UTS Namespace中

sh-4.2# readlink /proc/17571/ns/uts 
uts:[4026531838]
sh-4.2# readlink /proc/17574/ns/uts 
uts:[4026532220]

的确不在同一个UTS Namespace中,接下来验证hostname是否可以隔离。

sh-4.2# hostname -b hu5ky
sh-4.2# hostname
hu5ky

可见当前shell中的hostname为hu5ky。重新开启一个shell

[root@iz2zegp1t778mbekgw2rdrz home]# hostname
iz2zegp1t778mbekgw2rdrz

可见当前主机的hostname为iz2zegp1t778mbekgw2rdrz,因此hostname隔离成立。

IPC Namespace

IPC Namespace是用来隔离system V mq和POSIX mq的。每一个IPC Namespace都有自己的system V mq和POSIX mq。

代码实现:

func IPCNS() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

开启两个shell来验证IPC的资源隔离
首先在宿主机打开一个shell:

[root@iz2zegp1t778mbekgw2rdrz ~]# ipcs -q

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

可以看到目前没有ipc,接下来创建一个新的ipc:

[root@iz2zegp1t778mbekgw2rdrz ~]# ipcmk -Q
Message queue id: 0
[root@iz2zegp1t778mbekgw2rdrz ~]# ipcs -q

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

打开另一个shell:

[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go 
sh-4.2# ipcs -q

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

可以看到,在运行了main.go进入sh后,使用ipcs -q查询的ipc为空,因此宿主机和开启的新终端ipc资源隔离,并且IPC Namespace创建成功。

Mount Namespace

Mount Namespace用来隔离各个进程的挂载点视图。也就是说,在Mount Namespace中使用mount或者umount不会影响其他进程的挂载。因为Mount Namespace是第一个支持的Namespace,所以其系统调用参数为NEWNS(New Namespace)。

代码实现:

func MountNS() {
    cmd := exec.Command("sh")
    syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

/proc作用是实现进程和内核之间通信。首先运行代码,查看/proc中的资源:

sh-4.2# ls /proc/
1      17729  18481  238  328   460  765        cmdline      interrupts  locks         scsi           uptime
10     18     18493  239  338   462  766        consoles     iomem       mdstat        self           version
10423  18292  18515  241  356   464  795        cpuinfo      ioports     meminfo       slabinfo       vmallocinfo
106    18298  18518  25   37    465  797        crypto       irq         misc          softirqs       vmstat
12     18300  18520  253  370   489  798        devices      kallsyms    modules       stat           zoneinfo
13     18317  19     258  39    490  8          diskstats    kcore       mounts        swaps
14     18319  1928   259  40    5    9          dma          keys        mtrr          sys
15     18394  2      26   4031  6    908        driver       key-users   net           sysrq-trigger
15494  18396  215    27   41    60   acpi       execdomains  kmsg        pagetypeinfo  sysvipc
15526  18431  216    28   430   7    buddyinfo  fb           kpagecount  partitions    timer_list
16     18433  236    29   451   700  bus        filesystems  kpageflags  sched_debug   timer_stats
17     18479  237    3    454   763  cgroups    fs           loadavg     schedstat     tty

可以看见目前由大量的资源信息,因为目前/proc中还是宿主机的资源信息,我们将其挂载到当前进程:

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

可以看到,现在少了很多的资源信息,我们查看一下当前的进程信息:

sh-4.2# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 11:44 pts/1    00:00:00 sh
root         5     1  0 11:47 pts/1    00:00:00 ps -ef

可以看到当前的ps命令查看的信息中,sh进程的PID为1,实现了挂载隔离,docker volume就是借助这个特性实现的。

User Namespace

User Namespace主要作用是用来隔离当前用户的用户组ID,一个进程在User Namespace内外的User ID和Group ID可以是不同的,比较常用的作用是使一个非root用户在一个User Namespace中以root权限去调度资源。Linux Kernel >=3.8支持非root用户创建一个User Namespace。

代码实现:

func USERNS() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
            syscall.CLONE_NEWUSER,
        UidMappings: []syscall.SysProcIDMap{
            {
                ContainerID: 1234,
                HostID:      syscall.Getuid(),
                Size:        1,
            },
        },
        GidMappings: []syscall.SysProcIDMap{
            {
                ContainerID: 1234,
                HostID:      syscall.Getgid(),
                Size:        1,
            },
        },

    }
    //cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(1)} // 和之前的版本不一样
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
    os.Exit(-1)
}

首先在运行前看一下当前用户在宿主机的id信息。

[root@iz2zegp1t778mbekgw2rdrz docker]# id
uid=0(root) gid=0(root) groups=0(root)

运行代码查看id信息:

sh-4.2$ id
uid=1234 gid=1234 groups=1234

uid,gid,groups不同,User Namespace生效。

PID Namespace

PID Namespace用来隔离进程的ID,在不同的PID Namespace中同一个进程拥有不同的PID。当进入一个docker container时,使用ps -ef查看到当前的进程PID为1,但是在宿主机查看时,PID并不为1,这就是PID Namespace的作用。

代码实现:

func PIDNS() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

我们需要打开两个shell终端来验证。
首先打开宿主机的shell:

[root@iz2zegp1t778mbekgw2rdrz ~]# pstree -pl
├─sshd(18317)───bash(18319)───go(18440)─┬─main(18462)─┬─sh(18465)

可以看到当前的sh的PID为18465,接下来运行main.go的终端来查看PID:

[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go 
sh-4.2# echo $$
1

可以看到当前的PID为1,也就是说宿主机的PID映射到当前的进程是PID为1,PID Namespace创建成功。

**NOTICE:**在sh终端内不能使用ps命令,因为ps,top等命令会用到/proc资源。因为当前的sh中/proc资源还是宿主机的资源,需要使用mount挂在到当前Namespace中才能看到当前的PID。

Network Namespace

Network Namespace是用来隔离网络设备,IP,端口等网络栈的Namespace。Network Namespace可以使当前命名空间拥有自己独立的(虚拟的)网络设备,各个Namespace之间的端口都不会冲突。

代码实现:

func NETNS() {
    cmd := exec.Command("sh")
    syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
            syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
        UidMappings: []syscall.SysProcIDMap{
            {
                ContainerID: 1234,
                HostID:      syscall.Getuid(),
                Size:        1,
            },
        },
        GidMappings: []syscall.SysProcIDMap{
            {
                ContainerID: 1234,
                HostID:      syscall.Getgid(),
                Size:        1,
            },
        },
    }
    //cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(1)} // 和之前的版本不一样
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
    os.Exit(-1)
}

首先查看一下宿主机的网卡信息:

[root@iz2zegp1t778mbekgw2rdrz docker]# ifconfig
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.18.0.1  netmask 255.255.0.0  broadcast 172.18.255.255
        ether 02:42:ed:46:5e:70  txqueuelen 0  (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

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.203.111  netmask 255.255.240.0  broadcast 172.17.207.255
        ether 00:16:3e:34:01:d3  txqueuelen 1000  (Ethernet)
        RX packets 5105  bytes 2556002 (2.4 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3632  bytes 5609285 (5.3 MiB)
        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
        loop  txqueuelen 1  (Local Loopback)
        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

可以看到有三个网络设备,然后运行main.go查看网络设备:

[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go 
sh-4.2$ ifconfig
sh-4.2$ 

可以看到当前是没有任何的网络设备的,因此Network Namespace生效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值