自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

干貨分享:用 Go 從頭實現(xiàn)一個迷你 Docker—Gocker

新聞 前端
在本文中,我們將關(guān)注 Linux 操作系統(tǒng)上的容器,并簡單地說明為什么Windows 上的容器[1]根本不存在。

 容器很受歡迎。容器已成為應(yīng)用程序在服務(wù)器上打包和運行的默認方式,最初是由 Docker 普及的?,F(xiàn)在,Docker 是公司的名稱和一個命令(一組命令),使您可以輕松管理容器(創(chuàng)建,運行,刪除,網(wǎng)絡(luò))。但是,容器本身是從一組操作系統(tǒng)原語創(chuàng)建的。在本文中,我們將關(guān)注 Linux 操作系統(tǒng)上的容器,并簡單地說明為什么Windows 上的容器[1]根本不存在。

Linux 下沒有創(chuàng)建容器的單個系統(tǒng)調(diào)用。它們是利用 Linux 命名空間和控制組或 cgroups 構(gòu)成的松散構(gòu)造。

Gocker 是什么?

Gocker[2] 是一個使用 Go 編程語言從頭開始實現(xiàn) Docker 核心功能的項目。它主要目的是提供對容器在 Linux 系統(tǒng)調(diào)用級別上如何工作的理解。Gocker 允許你創(chuàng)建容器,管理容器鏡像(Image),在容器中執(zhí)行進程等。

干貨分享:用 Go 從頭實現(xiàn)一個迷你 Docker—Gocker

Gocker 的功能

Gocker 可以模擬 Docker 的內(nèi)核,讓你管理 Docker 鏡像(從 Docker Hub 獲取),運行容器,列出正在運行的容器或在已經(jīng)運行的容器中運行進程:

  • 在容器中運行進程
    • gocker run <--cpus=cpus-max> <--mem=mem-max> <--pids=pids-max> <image[:tag]> </path/to/command>
  • 列出正在運行的容器
    • gocker ps
  • 在運行的容器中執(zhí)行進程
    • gocker exec </path/to/command>
  • 列出本地可用的鏡像
    • gocker images
  • 刪除本地可用的鏡像
    • gocker rmi

其他功能

  • Gocker 使用 Overlay 文件系統(tǒng)快速創(chuàng)建容器,而無需復(fù)制整個文件系統(tǒng),同時還可以在多個容器實例之間共享同一容器鏡像。
  • Gocker 容器擁有自己的網(wǎng)絡(luò)命名空間,并且能夠訪問 Internet。請參閱下面的限制。
  • 您可以控制系統(tǒng)資源,例如 CPU 百分比,RAM 數(shù)量和進程數(shù)。Gocker 通過利用 cgroups 實現(xiàn)了這一目標。

Gocker 容器隔離性

用 Gocker 創(chuàng)建的容器擁有自己的以下命名空間(請參見 run.go 和 network.go):

  • 文件系統(tǒng) File system (via chroot)
  • PID
  • IPC
  • UTS (hostname)
  • Mount
  • Network

在創(chuàng)建用于限制以下內(nèi)容的 cgroup 時,除非你在 gocker run 命令中指定了 --mem,--cpus 或 --pids 選項,否則容器將使用無限的資源。這些標志分別限制了容器可以使用的最大 RAM,CPU 內(nèi)核和 PID。

  • CPU 核心數(shù)
  • RAM
  • PID 數(shù)量(限制進程)

命名空間(Namespaces)基礎(chǔ)

所有 Linux 計算機在啟動時都是 “default” 命名空間的一部分。在計算機上創(chuàng)建的進程也繼承默認命名空間。換句話說,因為所有對象也都存在于默認命名空間中,進程可以看到正在運行的其他進程,網(wǎng)絡(luò)接口,掛載點,名為 IPC 的對象或權(quán)限允許的文件。當創(chuàng)建一個進程時,我們可以告訴 Linux 為我們創(chuàng)建一個新的 PID 命名空間,在這種情況下,新進程及其任何后代形成一個新的層次結(jié)構(gòu)或 PID,而新創(chuàng)建的初始進程為 PID 1,就像 Linux 機器上特殊的初始化進程一樣。假設(shè)使用新的 PID 命名空間創(chuàng)建了一個名為 “new_child” 的進程。當該進程或其后代使用諸如 getpid() 或 getppid() 之類的系統(tǒng)調(diào)用時,它們會在新命名空間中看到 PID。例如,對于這兩個系統(tǒng)調(diào)用,在新創(chuàng)建的 PID 命名空間中的 new_child 將獲得 1。而當您從默認命名空間查看 new_child 的 PID 時,當然不會為其分配 1(那是默認命名空間中的 init 了)。

Linux 操作系統(tǒng)提供了在創(chuàng)建進程時或與之關(guān)聯(lián)的正在運行的進程創(chuàng)建新命名空間的方法。所有命名空間,無論其類型如何,都被分配了內(nèi)部 ID。命名空間是一種內(nèi)核對象。一個進程只能屬于一個命名空間。例如,假設(shè)一個進程 new_child 的 PID 命名空間設(shè)置為內(nèi)部 ID 為 0x87654321 的命名空間,它不能屬于另一個 PID 命名空間。但是,可能存在其他屬于同一 PID 命名空間 0x87654321 的其他進程。同樣,new_child 的后代將自動屬于相同的 PID 命名空間。命名空間是繼承的。

你可以使用 lsns 實用程序列出計算機中的各種命名空間。即使您的計算機上沒有運行任何容器,也很可能會看到與各種命名空間相關(guān)的其他進程。這表明,命名空間并不僅僅是在容器的上下文中使用。它們可以在任何地方使用。它們提供隔離。它們是一項強大的安全功能。在現(xiàn)代 Linux 系統(tǒng)上,您會看到 init,systemd,幾個系統(tǒng)守護程序,Chrome,Slack,當然還有使用各種命名空間的 Docker 容器。讓我們看一看我機器上的 lsns 實用程序的輸出:

  1.    NS TYPE   NPROCS   PID USER             COMMAND 
  2. 4026532281 mnt         1   313 root             /usr/lib/systemd/systemd-udevd 
  3. 4026532282 uts         1   313 root             /usr/lib/systemd/systemd-udevd 
  4. 4026532313 mnt         1   483 systemd-timesync /usr/lib/systemd/systemd-timesyncd 
  5. 4026532332 uts         1   483 systemd-timesync /usr/lib/systemd/systemd-timesyncd 
  6. 4026532334 mnt         1   502 root             /usr/bin/NetworkManager --no-daemon 
  7. 4026532335 mnt         1   503 root             /usr/lib/systemd/systemd-logind 
  8. 4026532336 uts         1   503 root             /usr/lib/systemd/systemd-logind 
  9. 4026532341 pid         1  1943 shuveb           /opt/google/chrome/nacl_helper 
  10. 4026532343 pid         2  1941 shuveb           /opt/google/chrome/chrome --type=zygote 
  11. 4026532345 net        50  1941 shuveb           /opt/google/chrome/chrome --type=zygote 
  12. 4026532449 mnt         1   547 root             /usr/lib/boltd 
  13. 4026532489 mnt         1   580 root             /usr/lib/bluetooth/bluetoothd 
  14. 4026532579 net         1  1943 shuveb           /opt/google/chrome/nacl_helper 
  15. 4026532661 mnt         1   766 root             /usr/lib/upowerd 
  16. 4026532664 user        1   766 root             /usr/lib/upowerd 
  17. 4026532665 pid         1  2521 shuveb           /opt/google/chrome/chrome --type=renderer 
  18. 4026532667 net         1   836 rtkit            /usr/lib/rtkit-daemon 
  19. 4026532753 mnt         1   943 colord           /usr/lib/colord 
  20. 4026532769 user        1  1943 shuveb           /opt/google/chrome/nacl_helper 
  21. 4026532770 user       50  1941 shuveb           /opt/google/chrome/chrome --type=zygote 
  22. 4026532771 pid         1  2010 shuveb           /opt/google/chrome/chrome --type=renderer 
  23. 4026532772 pid         1  2765 shuveb           /opt/google/chrome/chrome --type=renderer 
  24. 4026531835 cgroup    294     1 root             /sbin/init 
  25. 4026531836 pid       237     1 root             /sbin/init 
  26. 4026531837 user      238     1 root             /sbin/init 
  27. 4026531838 uts       289     1 root             /sbin/init 
  28. 4026531839 ipc       292     1 root             /sbin/init 
  29. 4026531840 mnt       283     1 root             /sbin/init 
  30. 4026531992 net       236     1 root             /sbin/init 
  31. 4026532912 pid         2  3249 shuveb           /usr/lib/slack/slack --type=zygote 
  32. 4026532914 net         2  3249 shuveb           /usr/lib/slack/slack --type=zygote 
  33. 4026533003 user        2  3249 shuveb           /usr/lib/slack/slack --type=zygote 

即使您沒有顯式創(chuàng)建命名空間,進程也將成為默認命名空間的一部分。所有命名空間的詳細信息都記錄在 /proc 文件系統(tǒng)中。您可以通過輸入 ls -l /proc/self/ns/來查看您的 Shell 進程所屬的命名空間。這是我電腦的結(jié)果。另外,這些大多是從 init 繼承的:

  1. ➜  ~ ls -l /proc/self/ns 
  2. total 0 
  3. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 cgroup -> 'cgroup:[4026531835]' 
  4. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 ipc -> 'ipc:[4026531839]' 
  5. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 mnt -> 'mnt:[4026531840]' 
  6. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 net -> 'net:[4026531992]' 
  7. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 pid -> 'pid:[4026531836]' 
  8. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 pid_for_children -> 'pid:[4026531836]' 
  9. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 user -> 'user:[4026531837]' 
  10. lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 uts -> 'uts:[4026531838]' 

沒有容器的命名空間

從 lsns 的輸出中,我們看到容器并不是唯一使用命名空間的對象。為此,讓我們創(chuàng)建一個具有自己的 PID 命名空間的 shell 實例。我們將使用 unshare 實用程序來做到這一點。“unshare” 這個名字很明顯。還有一個同名的 Linux 系統(tǒng)調(diào)用[3],可讓您取消共享默認命名空間,從而使調(diào)用進程加入新創(chuàng)建的命名空間。

  1. ➜  ~ sudo unshare --fork --pid --mount-proc /bin/bash 
  2. [root@kodai shuveb]# ps aux 
  3. USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND 
  4. root           1  0.5  0.0   8296  4944 pts/1    S    08:59   0:00 /bin/bash 
  5. root           2  0.0  0.0   8816  3336 pts/1    R+   08:59   0:00 ps aux 
  6. [root@kodai shuveb]#  

在以上調(diào)用中,unshare 實用程序正在派生一個新進程,調(diào)用 unshare() 系統(tǒng)調(diào)用以創(chuàng)建一個新的 PID 命名空間,然后在其中執(zhí)行 /bin/bash。我們還告訴 unshare 實用程序在新進程中掛載 proc 文件系統(tǒng)。這是 ps 實用程序從其獲取信息的地方。從 ps 命令的輸出中,您確實可以看到該 shell 擁有一個新的 PID 命名空間(PID 為 1),并且由于 ps 是由具有新 PID 命名空間的 shell 啟動的,因此它繼承了該 Shell 并獲得 PID 為 2。作為練習,您可以弄清楚在此容器中運行的 Shell 進程在主機上的 PID 是什么。

命名空間的類型

了解 PID 命名空間后,讓我們嘗試了解其他命名空間以及它們的含義。命名空間手冊頁[4]討論了 8 種不同的命名空間。以下是帶有簡短說明的各種類型,以及指向相關(guān)手冊頁的鏈接:

NamespaceFlagIsolatesCgroup[5]CLONE_NEWCGROUPCgroup root directoryIPC[6]CLONE_NEWIPCSystem V IPC, POSIX message queuesNetwork[7]CLONE_NEWNETNetwork devices,stacks, ports, etc.Mount[8]CLONE_NEWNSMount pointsPID[9]CLONE_NEWPIDProcess IDsTime[10]CLONE_NEWTIMEBoot and monotonic clocksUser[11]CLONE_NEWUSERUser and group IDsUTS[12]CLONE_NEWUTSHostname and NIS domain name

您可以想象使用這些命名空間為新的或現(xiàn)有的流程做什么。當它們在同一臺計算機上運行時,您幾乎可以將它們隔離在一個虛擬機上運行。您可以將多個進程隔離在各自的命名空間中,并在同一主機內(nèi)核上運行。這比運行多個虛擬機要有效得多。

創(chuàng)建新的命名空間或加入現(xiàn)有的命名空間

默認情況下,當您使用 fork() 創(chuàng)建進程時,子進程將繼承調(diào)用 fork() 的進程的命名空間。如果您希望創(chuàng)建的新進程成為新命名空間的一部分,該怎么辦?但 fork() 沒有參數(shù),不允許我們在創(chuàng)建子進程之前對其進行控制。然而,您可以使用 clone() 系統(tǒng)調(diào)用來施加這種控制,從而可以非常精細地控制它創(chuàng)建的新進程。

有關(guān) clone() 的說明

在 Linux 下,雖然有不同的系統(tǒng)調(diào)用,例如 fork(),vfork() 和 clone() 來創(chuàng)建新進程。但是在內(nèi)部,內(nèi)核中的 fork() 和 vfork() 只是使用不同的參數(shù)調(diào)用 clone()。圍繞內(nèi)核源代碼(為了更好的說明,我進行了一些編輯)非常容易理解。在文件kernel/fork.c[13] 中,您可以看到以下內(nèi)容:

  1. SYSCALL_DEFINE0(fork) 
  2.   struct kernel_clone_args args = { 
  3.     .exit_signal = SIGCHLD, 
  4.   }; 
  5.  
  6.   return _do_fork(&args); 
  7.  
  8. SYSCALL_DEFINE0(vfork) 
  9.   struct kernel_clone_args args = { 
  10.     .flags    = CLONE_VFORK | CLONE_VM, 
  11.     .exit_signal  = SIGCHLD, 
  12.   }; 
  13.  
  14.   return _do_fork(&args); 
  15.  
  16.  
  17. SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, 
  18.      int __user *, parent_tidptr, 
  19.      int __user *, child_tidptr, 
  20.      unsigned long, tls) 
  21.   struct kernel_clone_args args = { 
  22.     .flags    = (lower_32_bits(clone_flags) & ~CSIGNAL), 
  23.     .pidfd    = parent_tidptr, 
  24.     .child_tid  = child_tidptr, 
  25.     .parent_tid  = parent_tidptr, 
  26.     .exit_signal  = (lower_32_bits(clone_flags) & CSIGNAL), 
  27.     .stack    = newsp, 
  28.     .tls    = tls, 
  29.   }; 
  30.  
  31.   if (!legacy_clone_args_valid(&args)) 
  32.     return -EINVAL; 
  33.  
  34.   return _do_fork(&args); 

如您所見,這三個系統(tǒng)調(diào)用僅使用不同的參數(shù)調(diào)用 _do_fork()。_do_fork() 實現(xiàn)創(chuàng)建新進程的邏輯。

使用 clone() 創(chuàng)建具有新命名空間的進程

Gocker 通過 Go 的 “exec” 包使用 clone() 系統(tǒng)調(diào)用執(zhí)行以下操作。在處理與運行容器有關(guān)的內(nèi)容的 run.go[14] 中,您可以看到以下內(nèi)容:

  1. cmd = exec.Command("/proc/self/exe", args...) 
  2. cmd.Stdin = os.Stdin 
  3. cmd.Stdout = os.Stdout 
  4. cmd.Stderr = os.Stderr 
  5. cmd.SysProcAttr = &syscall.SysProcAttr{ 
  6.   Cloneflags: syscall.CLONE_NEWPID | 
  7.     syscall.CLONE_NEWNS | 
  8.     syscall.CLONE_NEWUTS | 
  9.     syscall.CLONE_NEWIPC, 
  10. doOrDie(cmd.Run()) 

在 syscall.SysProcAttr 中,我們可以傳入 Cloneflags,然后將其傳遞給對 clone() 系統(tǒng)調(diào)用。細心的讀者會注意到,我們不在這里設(shè)置單獨的網(wǎng)絡(luò)命名空間。在 Gocker 中,我們設(shè)置了一個虛擬以太網(wǎng)接口,將其添加到新的網(wǎng)絡(luò)命名空間,并使用另一個 Linux 系統(tǒng)調(diào)用使容器加入該命名空間。我們將在后面討論。

使用 unshare() 創(chuàng)建和加入新的命名空間

如果要為現(xiàn)有進程創(chuàng)建新的命名空間,則不必使用 clone() 創(chuàng)建新的子進程,Linux 提供了 unshare()[15] 系統(tǒng)調(diào)用。

加入其他進程所屬的命名空間

為了加入文件引用的命名空間或加入其他進程所屬的命名空間,Linux 提供了setns()[16] 系統(tǒng)調(diào)用。我們將很快看到,這非常有用。

Gocker 如何創(chuàng)建容器

由于 Gocker 的主要目的是幫助理解 Linux 容器,因此保留了一些來自 Gocker 的日志消息。從這個意義上講,它比運行 Docker 更為冗長。讓我們看一下日志,以指導(dǎo)我們執(zhí)行程序。然后,我們可以進行深入分析,看看實際情況如何:

  1. ➜  sudo ./gocker run alpine /bin/sh 
  2. 2020/06/13 12:37:53 Cmd args: [./gocker run alpine /bin/sh] 
  3. 2020/06/13 12:37:53 New container ID: 33c20f9ee600 
  4. 2020/06/13 12:37:53 Image already exists. Not downloading. 
  5. 2020/06/13 12:37:53 Image to overlay mount: a24bb4013296 
  6. 2020/06/13 12:37:53 Cmd args: [/proc/self/exe setup-netns 33c20f9ee600] 
  7. 2020/06/13 12:37:53 Cmd args: [/proc/self/exe setup-veth 33c20f9ee600] 
  8. 2020/06/13 12:37:53 Cmd args: [/proc/self/exe child-mode --img=a24bb4013296 33c20f9ee600 /bin/sh] 
  9. / #  

在這里,我們要求 Gocker 從 Alpine Linux 鏡像運行 shell。稍后我們將了解如何管理鏡像(Image)?,F(xiàn)在,請注意以 “ Cmd args:” 開頭的日志行。此行表示產(chǎn)生了一個新進程。第一行日志向我們顯示了由于運行 Gocker 命令而使 shell 程序啟動的過程。但是,到最后,我們看到了另外三個進程。最后一個是帶有參數(shù) “child-mode” 的 /bin/sh,我們在 Alpine Linux 鏡像中使用它。在此之前,我們看到其他兩個進程分別帶有參數(shù) “setup-netns” 和 “setup-veth”。這些命令設(shè)置了一個新的網(wǎng)絡(luò)命名空間,并設(shè)置了一個虛擬以太網(wǎng)設(shè)備對的容器端,使容器分別與外界通信。

由于各種原因,Go 語言不直接支持 fork() 系統(tǒng)調(diào)用。我們通過創(chuàng)建一個新進程來解決此限制,但是要在其中再次執(zhí)行當前程序。/proc/self/exe 指向當前正在運行的可執(zhí)行文件的路徑。我們根據(jù)命令行傳遞不同的命令行參數(shù)來調(diào)用適當?shù)暮瘮?shù)(當在子進程中 fork() 返回時將調(diào)用該函數(shù))。

源代碼的組織

Gocker 源代碼通過命令(如參數(shù))組織在文件中。例如,主要服務(wù)于 gocker run 命令行參數(shù)的函數(shù)位于 run.go 文件中。類似地,gocker exec 主要需要的功能在 exec.go 文件中。這并不意味著這些文件是獨立的。它們從其他文件中自由調(diào)用函數(shù)。還有一些文件可以實現(xiàn)通用功能,例如 cgroups.go 和 utils.go。

運行容器

在 main.go[17] 中,您可以看到是否運行了 Gocker 命令,我們檢查以確保 gocker0 橋接器已啟動并正在運行。否則,我們通過調(diào)用完成工作的 setupGockerBridge() 來啟動它。最后,我們調(diào)用函數(shù) initContainer(),該函數(shù)在 run.go 中實現(xiàn)。讓我們仔細看看該函數(shù):

  1. func initContainer(mem int, swap int, pids int, cpus float64,  
  2.                                 src string, args []string) { 
  3.   containerID := createContainerID() 
  4.   log.Printf("New container ID: %s\n", containerID) 
  5.   imageShaHex := downloadImageIfRequired(src) 
  6.   log.Printf("Image to overlay mount: %s\n", imageShaHex) 
  7.   createContainerDirectories(containerID) 
  8.   mountOverlayFileSystem(containerID, imageShaHex) 
  9.   if err := setupVirtualEthOnHost(containerID); err != nil { 
  10.     log.Fatalf("Unable to setup Veth0 on host: %v", err) 
  11.   } 
  12.   prepareAndExecuteContainer(mem, swap, pids, cpus, containerID,  
  13.                                 imageShaHex, args) 
  14.   log.Printf("Container done.\n"
  15.   unmountNetworkNamespace(containerID) 
  16.   unmountContainerFs(containerID) 
  17.   removeCGroups(containerID) 
  18.   os.RemoveAll(getGockerContainersPath() + "/" + containerID) 

首先,我們通過調(diào)用 createContainerID() 創(chuàng)建唯一的容器 ID。然后,我們調(diào)用 downloadImageIfRequired(),以便可以從Docker Hub 下載容器鏡像(如果本地尚不可用)。Gocker 使用 /var/run/gocker/containers 中的子目錄來掛載容器根文件系統(tǒng)。createContainerDirectories() 會解決這個問題。mountOverlayFileSystem() 知道如何處理多層 Docker 鏡像,并在 /var/run/gocker/containers/<container-id>/fs/mnt 上為可用鏡像安裝合并的文件系統(tǒng)。盡管這看起來令人生畏,但如果您閱讀源代碼,這并不難理解。覆蓋(Overlay)文件系統(tǒng)允許您創(chuàng)建一個堆疊的文件系統(tǒng),其中較低的層(在這種情況下是 Docker 根文件系統(tǒng))是只讀的,而任何更改都將保存到 “upperdir”,而無需更改較低層中的任何文件。這允許許多容器共享一個 Docker 鏡像。當我們在虛擬機上下文中說“鏡像”時,它通常是指磁盤鏡像。但是在這里,它只是一個目錄或一組目錄(奇特的名字:layers),帶有構(gòu)成 Docker “鏡像”根文件系統(tǒng)的文件,可以使用 Overlay 文件系統(tǒng)掛載該文件來創(chuàng)建根文件系統(tǒng)一個新的容器。

接下來,我們創(chuàng)建一個虛擬的以太網(wǎng)配對設(shè)備,它非常類似于調(diào)用 setupVirtualEthOnHost() 的管道。它們采用名稱 veth0_ <container-id> 和 veth1_ <container-id> 的形式。我們將一對中的 veth0 部分連接到主機上的網(wǎng)橋 gocker0。稍后,我們將在容器內(nèi)部使用該對的 veth1 部分。它們就像管道一樣,是從具有自己的網(wǎng)絡(luò)命名空間的容器內(nèi)部進行網(wǎng)絡(luò)通信的秘鑰。隨后,我們將介紹如何在容器內(nèi)設(shè)置 veth1 部件。

最后,調(diào)用 prepareAndExecuteContainer(),它實際上在容器中執(zhí)行該過程。當此函數(shù)返回時,容器已完成執(zhí)行。最后,我們進行一些清理并退出。讓我們看看 prepareAndExecuteContainer() 的作用。它實際上創(chuàng)建了我們看到的日志的 3 個進程,并使用 setup-netns,setup-veth 和 child-mode 參數(shù)運行相同的 gocker 二進制文件。

設(shè)置可在容器內(nèi)工作的網(wǎng)絡(luò)

設(shè)置新的網(wǎng)絡(luò)命名空間非常容易。您只需將 CLONE_NEWNET 包含在傳遞給 clone() 系統(tǒng)調(diào)用的標志位掩碼中即可。棘手的是確保容器內(nèi)部可以具有網(wǎng)絡(luò)接口,通過該接口可以與外部進行通信。在 Gocker 中,我們創(chuàng)建的第一個新命名空間是網(wǎng)絡(luò)的命名空間。當使用 setup-ns 和 setup-veth 參數(shù)調(diào)用 gocker 時會發(fā)生這種情況。首先,我們設(shè)置一個新的網(wǎng)絡(luò)命名空間。setns() 系統(tǒng)調(diào)用可以將調(diào)用進程的命名空間設(shè)置為由文件描述符所引用的命名空間,該文件描述符指向 /proc/<pid>/ns 中的文件,該文件列出了進程所屬的所有命名空間。讓我們看一下 setupNewNetworkNamespace() 函數(shù),該函數(shù)是通過使用 setup-netns 作為參數(shù)調(diào)用 gocker 而被調(diào)用的。(譯注:即上文提到的 Cmd args: [/proc/self/exe setup-netns 33c20f9ee600] )

  1. func setupNewNetworkNamespace(containerID string) { 
  2.   _ = createDirsIfDontExist([]string{getGockerNetNsPath()}) 
  3.   nsMount := getGockerNetNsPath() + "/" + containerID 
  4.   if _, err := syscall.Open(nsMount,  
  5.                 syscall.O_RDONLY|syscall.O_CREAT|syscall.O_EXCL, 
  6.                 0644); err != nil { 
  7.     log.Fatalf("Unable to open bind mount file: :%v\n", err) 
  8.   } 
  9.  
  10.   fd, err := syscall.Open("/proc/self/ns/net", syscall.O_RDONLY, 0
  11.   defer syscall.Close(fd) 
  12.   if err != nil { 
  13.     log.Fatalf("Unable to open: %v\n", err) 
  14.   } 
  15.  
  16.   if err := syscall.Unshare(syscall.CLONE_NEWNET); err != nil { 
  17.     log.Fatalf("Unshare system call failed: %v\n", err) 
  18.   } 
  19.   if err := syscall.Mount("/proc/self/ns/net", nsMount,  
  20.                                 "bind", syscall.MS_BIND, ""); err != nil { 
  21.     log.Fatalf("Mount system call failed: %v\n", err) 
  22.   } 
  23.   if err := unix.Setns(fd, syscall.CLONE_NEWNET); err != nil { 
  24.     log.Fatalf("Setns system call failed: %v\n", err) 
  25.   } 

每當 Linux 內(nèi)核中的最后一個進程終止時,它都會自動刪除該命名空間。但是,有一種技術(shù)可以通過綁定來保留命名空間,即使其中沒有任何進程。在 setupNewNetworkNamespace() 函數(shù)中,我們使用此技術(shù)。我們首先打開進程的網(wǎng)絡(luò)命名空間文件,該文件位于 /proc/self/ns/net 中。然后,我們使用 CLONE_NEWNET 參數(shù)調(diào)用 unshare() 系統(tǒng)調(diào)用。這會將與其所屬的命名空間解除關(guān)聯(lián),并創(chuàng)建一個新的新網(wǎng)絡(luò)命名空間,同時將其設(shè)置為該進程的網(wǎng)絡(luò)命名空間。然后,我們將此進程的網(wǎng)絡(luò)命名空間專用文件的綁定到一個已知的文件名,即 /var/run/gocker/net-ns/<container-id>。該文件可隨時用于引用該網(wǎng)絡(luò)命名空間?,F(xiàn)在,我們可以退出此進程,但是由于此進程的新網(wǎng)絡(luò)命名空間已綁定到新文件上,因此內(nèi)核將保留此命名空間。

接下來,使用 setup-veth 參數(shù)調(diào)用 gocker。這將調(diào)用函數(shù) setupContainerNetworkInterfaceStep1() 和 setupContainerNetworkInterfaceStep2()。在第一個函數(shù)中,我們查找 veth1_<container-id> 接口,并將其命名空間設(shè)置為在上一步中創(chuàng)建的新網(wǎng)絡(luò)命名空間。原本該接口將在主機上不可見。但問題是:由于它與 veth0_<container-id> 接口配對,該接口在主機上仍然可見,因此加入此網(wǎng)絡(luò)命名空間的任何進程都可以與主機進行通信。第二個函數(shù)將 IP 地址添加到網(wǎng)絡(luò)接口,并將 gocker0 網(wǎng)橋設(shè)置為其默認網(wǎng)關(guān)設(shè)備。

現(xiàn)在,主機上有一個網(wǎng)絡(luò)接口,而新的網(wǎng)絡(luò)命名空間上有一個可以相互通信的接口。而且由于該網(wǎng)絡(luò)命名空間可以由文件引用,因此我們可以隨時使用 setns() 系統(tǒng)調(diào)用打開該文件并加入該網(wǎng)絡(luò)命名空間。這正是我們要做的。

此后,prepareAndExecuteContainer() 調(diào)用將設(shè)置一個新進程,該進程使用 child-mode 參數(shù)運行 gocker。這是最后一個進程,將產(chǎn)生我們要在容器中運行的命令。讓我們看一下運行 child-mode 的進程的新命名空間。我們之前已經(jīng)看過了這段代碼:

  1. cmd = exec.Command("/proc/self/exe", args...) 
  2. cmd.Stdin = os.Stdin 
  3. cmd.Stdout = os.Stdout 
  4. cmd.Stderr = os.Stderr 
  5. cmd.SysProcAttr = &syscall.SysProcAttr{ 
  6.   Cloneflags: syscall.CLONE_NEWPID | 
  7.     syscall.CLONE_NEWNS | 
  8.     syscall.CLONE_NEWUTS | 
  9.     syscall.CLONE_NEWIPC, 
  10. doOrDie(cmd.Run()) 

在這里,我們設(shè)置新的 PID,mount,UTS 和 IPC 命名空間。請記住,我們有一個通過文件可以引用的新網(wǎng)絡(luò)命名空間。我們只需要加入它。我們將很快完成。child-mode 進程將調(diào)用函數(shù) execContainerCommand()。這里代碼:

  1. func execContainerCommand(mem int, swap int, pids int, cpus float64, 
  2.   containerID string, imageShaHex string, args []string) { 
  3.   mntPath := getContainerFSHome(containerID) + "/mnt" 
  4.   cmd := exec.Command(args[0], args[1:]...) 
  5.   cmd.Stdin = os.Stdin 
  6.   cmd.Stdout = os.Stdout 
  7.   cmd.Stderr = os.Stderr 
  8.  
  9.   imgConfig := parseContainerConfig(imageShaHex) 
  10.   doOrDieWithMsg(syscall.Sethostname([]byte(containerID)), "Unable to set hostname"
  11.   doOrDieWithMsg(joinContainerNetworkNamespace(containerID), "Unable to join container network namespace"
  12.   createCGroups(containerID, true
  13.   configureCGroups(containerID, mem, swap, pids, cpus) 
  14.   doOrDieWithMsg(copyNameserverConfig(containerID), "Unable to copy resolve.conf"
  15.   doOrDieWithMsg(syscall.Chroot(mntPath), "Unable to chroot"
  16.   doOrDieWithMsg(os.Chdir("/"), "Unable to change directory"
  17.   createDirsIfDontExist([]string{"/proc""/sys"}) 
  18.   doOrDieWithMsg(syscall.Mount("proc""/proc""proc"0""), "Unable to mount proc"
  19.   doOrDieWithMsg(syscall.Mount("tmpfs""/tmp""tmpfs"0""), "Unable to mount tmpfs"
  20.   doOrDieWithMsg(syscall.Mount("tmpfs""/dev""tmpfs"0""), "Unable to mount tmpfs on /dev"
  21.   createDirsIfDontExist([]string{"/dev/pts"}) 
  22.   doOrDieWithMsg(syscall.Mount("devpts""/dev/pts""devpts"0""), "Unable to mount devpts"
  23.   doOrDieWithMsg(syscall.Mount("sysfs""/sys""sysfs"0""), "Unable to mount sysfs"
  24.   setupLocalInterface() 
  25.   cmd.Env = imgConfig.Config.Env 
  26.   cmd.Run() 
  27.   doOrDie(syscall.Unmount("/dev/pts"0)) 
  28.   doOrDie(syscall.Unmount("/dev"0)) 
  29.   doOrDie(syscall.Unmount("/sys"0)) 
  30.   doOrDie(syscall.Unmount("/proc"0)) 
  31.   doOrDie(syscall.Unmount("/tmp"0)) 

在這里,我們將容器的主機名設(shè)置為容器 ID,加入之前創(chuàng)建的新網(wǎng)絡(luò)命名空間,創(chuàng)建允許我們控制 CPU,PID 和 RAM 使用率的 Linux 控制組,并加入這些 Cgroup,然后復(fù)制主機的 DNS 解析文件進入容器的文件系統(tǒng),對已安裝的 Overlay 文件系統(tǒng)執(zhí)行 chroot(),掛載所需的文件系統(tǒng),以使容器能夠平穩(wěn)運行,設(shè)置本地網(wǎng)絡(luò)接口,根據(jù)容器鏡像的建議設(shè)置環(huán)境變量并最終運行用戶希望我們運行的命令?,F(xiàn)在,此命令將在一組新的命名空間中運行,從而使它幾乎完全與主機隔離。

限制容器資源

除了使用命名空間實現(xiàn)隔離之外,容器的另一個重要特征:限制容器可以消耗的資源量的能力。Linux 下的 Cgroup 很簡單,通過它我們能夠做到這一點。雖然命名空間是通過諸如 unshare(),setns() 和 clone() 之類的系統(tǒng)調(diào)用來實現(xiàn)的,但 Cgroup 是通過創(chuàng)建目錄并將文件寫入虛擬文件系統(tǒng)(位于 /sys/fs/cgroup 下)來管理的。在 Cgroups 虛擬文件系統(tǒng)層次結(jié)構(gòu)中,每個容器創(chuàng)建了 3 個目錄:

  • /sys/fs/cgroup/pids/gocker/<container-id>
  • /sys/fs/cgroup/cpu/gocker/<container-id>
  • /sys/fs/cgroup/mem/gocker/<container-id>

對于每個創(chuàng)建的目錄,內(nèi)核都會添加各種文件,從而可以自動配置該 cgroup。

這是我們配置容器的方式:

  • 當容器啟動時,我們將創(chuàng)建 3 個目錄,每個目錄用于我們關(guān)心的三個 cgroup:CPU,PID 和 Memory。
  • 然后,我們通過寫入該目錄內(nèi)的文件來設(shè)置 cgroup 的限制。例如,要設(shè)置容器中允許的最大 PID 數(shù)量,我們將該最大數(shù)量寫入 /sys/fs/cgroup/pids/gocker/<cont-id>/pids.max。這將配置此 Cgroup。
  • 現(xiàn)在,我們可以通過將其 PID 添加到 /sys/fs/cgroup/pids/gocker/<cont-id>/cgroup.procs 中來添加需要由該 Cgroup 控制的進程。

這就是全部。一旦添加了要由 Cgroup 控制的進程,內(nèi)核將自動將所有進程后代的 PID 添加到適當?shù)?Cgroup 的 cgroup.procs 文件中。我們在容器(添加到了上面的 3 個 Cgroups 中)中啟動一個進程,并且該進程是容器啟動其他進程的祖先進程,所以所有限制也都會被繼承。

限制 CPU

讓我們嘗試將容器可以使用的 CPU 限制為主機系統(tǒng) 1 個 CPU 內(nèi)核的 20%。讓我們開始一個受此限制的容器,安裝 Python 并運行一個 while 循環(huán)。我們通過向 gocker 傳遞 --cpu = 0.2 標志來實現(xiàn):

  1. sudo ./gocker run --cpus=0.2 alpine /bin/sh 
  2. 2020/06/13 18:14:09 Cmd args: [./gocker run --cpus=0.2 alpine /bin/sh] 
  3. 2020/06/13 18:14:09 New container ID: d87d44b4d823 
  4. 2020/06/13 18:14:09 Image already exists. Not downloading. 
  5. 2020/06/13 18:14:09 Image to overlay mount: a24bb4013296 
  6. 2020/06/13 18:14:09 Cmd args: [/proc/self/exe setup-netns d87d44b4d823] 
  7. 2020/06/13 18:14:09 Cmd args: [/proc/self/exe setup-veth d87d44b4d823] 
  8. 2020/06/13 18:14:09 Cmd args: [/proc/self/exe child-mode --cpus=0.2 --img=a24bb4013296 d87d44b4d823 /bin/sh] 
  9. / # apk add python3 
  10. fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz 
  11. fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz 
  12. (1/10) Installing libbz2 (1.0.8-r1) 
  13. (2/10) Installing expat (2.2.9-r1) 
  14. (3/10) Installing libffi (3.3-r2) 
  15. (4/10) Installing gdbm (1.13-r1) 
  16. (5/10) Installing xz-libs (5.2.5-r0) 
  17. (6/10) Installing ncurses-terminfo-base (6.2_p20200523-r0) 
  18. (7/10) Installing ncurses-libs (6.2_p20200523-r0) 
  19. (8/10) Installing readline (8.0.4-r0) 
  20. (9/10) Installing sqlite-libs (3.32.1-r0) 
  21. (10/10) Installing python3 (3.8.3-r0) 
  22. Executing busybox-1.31.1-r16.trigger 
  23. OK: 53 MiB in 24 packages 
  24. / # python3 
  25. Python 3.8.3 (default, May 15 202001:53:50)  
  26. [GCC 9.3.0] on linux 
  27. Type "help""copyright""credits" or "license" for more information. 
  28. >>> while True: 
  29. ...     pass 
  30. ...  

在宿主機器運行 top,查看在容器內(nèi)部運行的 python 進程占用了多少 CPU。

干貨分享:用 Go 從頭實現(xiàn)一個迷你 Docker—Gocker

Cgroup將CPU限制為20%

從另一個終端,讓我們使用 gocker exec 命令在同一容器內(nèi)啟動另一個 python 進程,并在其中運行 while 循環(huán)。

  1. ➜  sudo ./gocker ps                        
  2. 2020/06/13 18:21:10 Cmd args: [./gocker ps] 
  3. CONTAINER ID  IMAGE    COMMAND 
  4. d87d44b4d823  alpine:latest  /usr/bin/python3.8 
  5. ➜  sudo ./gocker exec d87d44b4d823 /bin/sh 
  6. 2020/06/13 18:21:24 Cmd args: [./gocker exec d87d44b4d823 /bin/sh] 
  7. / # python3 
  8. Python 3.8.3 (default, May 15 202001:53:50)  
  9. [GCC 9.3.0] on linux 
  10. Type "help""copyright""credits" or "license" for more information. 
  11. >>> while True: 
  12. ...     pass 
  13. ...  

現(xiàn)在有 2 個 python 進程,在不受 Cgroup 限制的情況下,不出意外的話,將消耗 2 個完整的 CPU 內(nèi)核?,F(xiàn)在,讓我們看一下主機上 top 命令的輸出:

干貨分享:用 Go 從頭實現(xiàn)一個迷你 Docker—Gocker

Cgroup通過2個進程將CPU限制為20%

從主機 top 命令的輸出中可以看到,兩個 python 進程(都運行循環(huán))都限制為每個 CPU 占用 10%。容器的 20% CPU 配額由調(diào)度程序公平分配給容器中的 2 個進程。請注意,也可以指定一個以上 CPU 內(nèi)核的余量。例如,如果要允許一個容器最大使用 2 個半核心,請在標志中將其指定為 --cpu = 2.5。

限制 PID

在新的 PID 命名空間中運行 Shell 程序的容器似乎消耗 7 個 PID。這意味著,如果您啟動一個 PID 上限為 7 的新容器,則將無法在 Shell 上啟動其他進程。讓我們對此進行測試。(盡管容器中只有 2 個處于運行狀態(tài)的進程,但我不確定為什么要消耗 7 個 PID。這需要進一步研究。)

  1. ➜  sudo ./gocker run --pids=7 alpine /bin/sh 
  2. [sudo] password for shuveb:  
  3. 2020/06/13 18:28:00 Cmd args: [./gocker run --pids=7 alpine /bin/sh] 
  4. 2020/06/13 18:28:00 New container ID: 920a577165ef 
  5. 2020/06/13 18:28:00 Image already exists. Not downloading. 
  6. 2020/06/13 18:28:00 Image to overlay mount: a24bb4013296 
  7. 2020/06/13 18:28:00 Cmd args: [/proc/self/exe setup-netns 920a577165ef] 
  8. 2020/06/13 18:28:00 Cmd args: [/proc/self/exe setup-veth 920a577165ef] 
  9. 2020/06/13 18:28:00 Cmd args: [/proc/self/exe child-mode --pids=7 --img=a24bb4013296 920a577165ef /bin/sh] 
  10. / # ls -l 
  11. /bin/sh: can't fork: Resource temporarily unavailable 
  12. / #  

限制 RAM

開啟一個新容器,將最大允許內(nèi)存設(shè)置為 128M?,F(xiàn)在,我們將在其中安裝 python,并分配大量 RAM。這應(yīng)該會觸發(fā)內(nèi)核的內(nèi)存不足(OOM),使其殺死我們的 python 進程。讓我們看看實際情況:

  1. ➜ sudo ./gocker run --mem=128 --swap=0 alpine /bin/sh 
  2. 2020/06/13 18:30:30 Cmd args: [./gocker run --mem=128 --swap=0 alpine /bin/sh] 
  3. 2020/06/13 18:30:30 New container ID: b22bbc6ee478 
  4. 2020/06/13 18:30:30 Image already exists. Not downloading. 
  5. 2020/06/13 18:30:30 Image to overlay mount: a24bb4013296 
  6. 2020/06/13 18:30:30 Cmd args: [/proc/self/exe setup-netns b22bbc6ee478] 
  7. 2020/06/13 18:30:30 Cmd args: [/proc/self/exe setup-veth b22bbc6ee478] 
  8. 2020/06/13 18:30:30 Cmd args: [/proc/self/exe child-mode --mem=128 --swap=0 --img=a24bb4013296 b22bbc6ee478 /bin/sh] 
  9. / # apk add python3 
  10. fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz 
  11. fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz 
  12. (1/10) Installing libbz2 (1.0.8-r1) 
  13. (2/10) Installing expat (2.2.9-r1) 
  14. (3/10) Installing libffi (3.3-r2) 
  15. (4/10) Installing gdbm (1.13-r1) 
  16. (5/10) Installing xz-libs (5.2.5-r0) 
  17. (6/10) Installing ncurses-terminfo-base (6.2_p20200523-r0) 
  18. (7/10) Installing ncurses-libs (6.2_p20200523-r0) 
  19. (8/10) Installing readline (8.0.4-r0) 
  20. (9/10) Installing sqlite-libs (3.32.1-r0) 
  21. (10/10) Installing python3 (3.8.3-r0) 
  22. Executing busybox-1.31.1-r16.trigger 
  23. OK: 53 MiB in 24 packages 
  24. / # python3 
  25. Python 3.8.3 (default, May 15 202001:53:50)  
  26. [GCC 9.3.0] on linux 
  27. Type "help""copyright""credits" or "license" for more information. 
  28. >>> a1 = bytearray(100 * 1024 * 1024
  29. Killed 
  30. / #  

需要注意的一件事是,我們使用 --swap = 0 將分配給該容器的 swap 設(shè)置為零。否則,Cgroup 雖然限制 RAM 使用,但它將允許容器使用無限的交換空間。當 swap 設(shè)置為零時,容器將被完全限制為所允許的 RAM 值。

 

 

責任編輯:張燕妮 來源: Go語言中文網(wǎng)
相關(guān)推薦

2022-04-01 15:18:42

Web 框架網(wǎng)絡(luò)通信

2020-08-14 10:01:25

編程神經(jīng)網(wǎng)絡(luò)C語言

2022-11-14 08:01:48

2023-05-10 08:05:41

GoWeb應(yīng)用

2024-01-08 08:36:29

HTTPGo代理服務(wù)器

2022-10-08 00:00:00

AdminUser數(shù)據(jù)庫鑒權(quán)

2023-02-01 08:04:07

測試flask網(wǎng)頁

2023-02-26 01:37:57

goORM代碼

2022-03-06 19:57:50

狀態(tài)機easyfsm項目

2019-03-29 15:34:39

Go框架Web

2019-07-05 08:39:39

GoSQL解析器

2024-01-02 13:58:04

GoREST API語言

2021-04-15 08:55:51

Go struc代碼

2021-04-25 08:58:00

Go拍照云盤

2024-06-13 08:36:11

2014-04-14 15:54:00

print()Web服務(wù)器

2014-07-08 09:27:24

SQLSERVER腳本

2021-06-25 10:38:05

JavaScript編譯器前端開發(fā)

2021-07-02 07:18:19

Goresults通道類型

2015-10-19 17:38:01

AnsibleDocker應(yīng)用部署
點贊
收藏

51CTO技術(shù)棧公眾號