使用Go語言編寫一個極簡版的容器Container
前置知識儲備:
- Linux 基礎知識
Docker 是基于 Linux 容器技術構建的,因此了解 Linux 操作系統(tǒng)的基本原理、命令和文件系統(tǒng)等知識對于理解本文乃至于Docker 源碼非常重要。
- 容器技術基礎
了解容器技術的基本概念、原理和實現(xiàn)方式對于理解 Docker 源碼非常有幫助??梢詤⒖?Docker 官方文檔[1]中的容器概述部分,以及相關的教程和文章。
- Go 語言基礎
Docker 的源碼主要是用 Go 語言編寫的,具體可以參考Go 語言官方文檔[2]。
[圖片來源:Docker架構概覽[3]]
什么是容器化
容器化是作為一種虛擬化技術,允許應用程序和其依賴的資源(如庫、環(huán)境變量等)被封裝在一個獨立的運行環(huán)境中,稱為容器。其核心概念主要包括:
- 隔離性
容器使用操作系統(tǒng)級別的虛擬化技術,如Linux的命名空間和控制組(cgroup),實現(xiàn)隔離。每個容器都有自己的進程空間、文件系統(tǒng)、網(wǎng)絡和用戶空間,使得容器之間相互隔離,不會相互干擾。
- 輕量性
相比傳統(tǒng)的虛擬機(VM),容器更加輕量級。容器共享主機操作系統(tǒng)的內(nèi)核,因此啟動更快、占用更少的資源。
- 可移植性
容器可以在不同的環(huán)境中運行,包括開發(fā)、測試和生產(chǎn)環(huán)境。容器以相同的方式運行,不受底層基礎設施的影響,提供了更好的可移植性。
- 可擴展性
容器可以根據(jù)需求進行擴展和縮減。容器編排工具(如Kubernetes)可以自動管理容器的部署、伸縮和負載均衡,提供彈性和可擴展性。
"如果創(chuàng)建一個容器就像系統(tǒng)調(diào)用 create_container 一樣簡單就好了"[4]
Guideline
這里我們粗略的估算一下可能涉及到的步驟會有:導入必要的包、main函數(shù)、子進程及其命名空間、掛載文件系統(tǒng)、運行子進程命令等。
我們知道真正的容器實現(xiàn)要復雜得多。它可能會涉及更多的命名空間設置、資源限制、文件系統(tǒng)掛載、網(wǎng)絡配置等方面的工作。
但是本文,“刪繁就簡”,主要是為了了解容器的基本原理。
按照這種實現(xiàn)的思路,我們開始一步步用代碼實現(xiàn):
package?main import?( ?"fmt" ?"os" ?"os/exec" ?"syscall" ) func?main()?{ ?//?根據(jù)命令行參數(shù)選擇執(zhí)行不同的操作 ?switch?os.Args[1]?{ ?case?"run": ??parent()?//?執(zhí)行parent函數(shù) ?case?"child": ??child()?//?執(zhí)行child函數(shù) ?default: ??panic("wat?should?I?do")?//?拋出異常,程序無法繼續(xù)執(zhí)行 ?} } func?parent()?{ ?cmd?:=?exec.Command("/proc/self/exe",?append([]string{"child"},?os.Args[2:]...)...) ?cmd.Stdin?=?os.Stdin ?cmd.Stdout?=?os.Stdout ?cmd.Stderr?=?os.Stderr ?//?運行命令并檢查錯誤 ?if?err?:=?cmd.Run();?err?!=?nil?{ ??fmt.Println("ERROR",?err) ??os.Exit(1) ?} } func?child()?{ ?cmd?:=?exec.Command(os.Args[2],?os.Args[3:]...) ?cmd.Stdin?=?os.Stdin ?cmd.Stdout?=?os.Stdout ?cmd.Stderr?=?os.Stderr ?//?運行命令并檢查錯誤 ?if?err?:=?cmd.Run();?err?!=?nil?{ ??fmt.Println("ERROR",?err) ??os.Exit(1) ?} } func?must(err?error)?{ ?//?如果錯誤不為空,拋出panic異常 ?if?err?!=?nil?{ ??panic(err) ?} }
我們從 main.go 開始,讀取第一個參數(shù)。如果是 "run",我們就運行Parent函數(shù),如果是 "child",我們就運行子方法。父方法運行"/proc/self/exe",這是一個包含當前可執(zhí)行文件內(nèi)存映像的特殊文件。
換句話說,我們重新運行自己,但將 child 作為第一個參數(shù)傳遞。
我們可以借此執(zhí)行另外一個執(zhí)行用戶請求的程序(在 os.Args[2:]
中提供)。有了這個簡單的腳手架,我們就可以創(chuàng)建一個容器了。
命名空間
在 Linux 中,命名空間(Namespace)[5]是一種內(nèi)核功能,用于隔離進程的資源視圖。它允許在同一系統(tǒng)上運行的進程具有獨立的資源副本,如進程 ID、網(wǎng)絡接口、文件系統(tǒng)掛載點等。這種隔離性可以提供更好的安全性和資源管理。 以下是一些常見的 Linux 命名空間類型:
- PID命名空間:每個進程在 PID 命名空間中都有一個唯一的進程 ID。不同的 PID 命名空間中的進程 ID 可以重復,因此進程在其所屬的命名空間中可以認為是唯一的。
- 網(wǎng)絡命名空間:每個網(wǎng)絡命名空間都有自己的網(wǎng)絡設備、IP 地址、路由表和防火墻規(guī)則。這使得在不同的網(wǎng)絡命名空間中可以進行網(wǎng)絡隔離和配置。
- 文件系統(tǒng)命名空間:文件系統(tǒng)命名空間允許在不同的命名空間中使用不同的文件系統(tǒng)視圖。這意味著一個進程可以在一個命名空間中看到的文件和目錄,在另一個命名空間中可能是不可見的。
- UTS 命名空間:UTS 命名空間用于隔離主機名和域名。每個 UTS 命名空間可以有自己獨立的主機名,這在容器化環(huán)境中非常有用。
- IPC 命名空間:IPC 命名空間用于隔離不同進程之間的進程間通信(IPC)機制,如信號量、消息隊列和共享內(nèi)存等。
- 用戶命名空間:用戶命名空間允許在不同命名空間中重新映射用戶和組 ID。這提供了更好的用戶隔離和權限管理。 通過使用這些命名空間,可以創(chuàng)建獨立的容器環(huán)境,每個容器都有自己的資源副本,從而實現(xiàn)更好的隔離和資源管理。
UTS命名空間
Linux UTS Namespace[6]。在 UTS 命名空間中,每個命名空間都有自己的主機名和域名。UTS 命名空間的使用場景包括:容器化和網(wǎng)絡隔離等。
要在程序中添加命名空間,我們只需在 parent() 方法的第二行,添加下面的這幾行代碼,以便于在Go運行子進程時傳遞給其一些額外的標識。
cmd.SysProcAttr?=?&syscall.SysProcAttr{ ?Cloneflags:?syscall.CLONE_NEWUTS?|?syscall.CLONE_NEWPID?|?syscall.CLONE_NEWNS、 }
如果現(xiàn)在運行程序,程序將在 UTS、PID 和 MNT 命名空間內(nèi)運行。
在 Docker 中,根文件系統(tǒng)是由 Docker 鏡像提供的,并且在容器啟動時被掛載到容器的根目錄上。Docker 根文件系統(tǒng)一般具有分層結構、只讀性和寫時復制等特性。
現(xiàn)在,雖然我們的進程處于一組孤立的命名空間中,但文件系統(tǒng)看起來與主機相同。為了解決這個問題,我們需要以下四行代碼來實現(xiàn)根文件系統(tǒng):
must(syscall.Mount("rootfs",?"rootfs",?"",?syscall.MS_BIND,?"")) ?must(os.MkdirAll("rootfs/oldrootfs",?0700)) ????//?將當前目錄?`/`?移到?`rootfs/oldrootfs`?并將新的?rootfs?目錄交換到?`/` ?must(syscall.PivotRoot("rootfs",?"rootfs/oldrootfs")) ?must(os.Chdir("/"))
所以完整代碼如下:
package?main import?( ?"fmt" ?"os" ?"os/exec" ?"syscall" ) func?main()?{ ?//?根據(jù)命令行參數(shù)選擇執(zhí)行不同的操作 ?switch?os.Args[1]?{ ?case?"run": ??parent()?//?執(zhí)行parent函數(shù) ?case?"child": ??child()?//?執(zhí)行child函數(shù) ?default: ??panic("wat?should?I?do")?//?拋出異常,程序無法繼續(xù)執(zhí)行 ?} } func?parent()?{ ?cmd?:=?exec.Command("/proc/self/exe",?append([]string{"child"},?os.Args[2:]...)...) ?//?設置子進程的命名空間 ?cmd.SysProcAttr?=?&syscall.SysProcAttr{ ??Cloneflags:?syscall.CLONE_NEWUTS?|?syscall.CLONE_NEWPID?|?syscall.CLONE_NEWNS, ?} ?cmd.Stdin?=?os.Stdin ?cmd.Stdout?=?os.Stdout ?cmd.Stderr?=?os.Stderr ?//?運行命令并檢查錯誤 ?if?err?:=?cmd.Run();?err?!=?nil?{ ??fmt.Println("ERROR",?err) ??os.Exit(1) ?} } func?child()?{ ?//?掛載文件系統(tǒng) ?must(syscall.Mount("rootfs",?"rootfs",?"",?syscall.MS_BIND,?"")) ?must(os.MkdirAll("rootfs/oldrootfs",?0700)) ?must(syscall.PivotRoot("rootfs",?"rootfs/oldrootfs")) ?must(os.Chdir("/")) ?cmd?:=?exec.Command(os.Args[2],?os.Args[3:]...) ?cmd.Stdin?=?os.Stdin ?cmd.Stdout?=?os.Stdout ?cmd.Stderr?=?os.Stderr ?//?運行命令并檢查錯誤 ?if?err?:=?cmd.Run();?err?!=?nil?{ ??fmt.Println("ERROR",?err) ??os.Exit(1) ?} } func?must(err?error)?{ ?//?如果錯誤不為空,拋出panic異常 ?if?err?!=?nil?{ ??panic(err) ?} }
是的,至此,基于golang實現(xiàn)的極簡版的容器代碼已經(jīng)有了基本骨架。
Cgroups
Linux Cgroups[7] 在 Docker 容器化中起著重要的作用,它提供了對容器的資源限制和隔離,使得容器可以在共享的宿主機上運行而不會相互干擾:
- 資源限制
通過 Cgroups,Docker 可以對容器的資源使用進行限制,如 CPU、內(nèi)存、磁盤和網(wǎng)絡等。這樣可以避免容器過度占用宿主機資源,保證系統(tǒng)的穩(wěn)定性和公平性。
- 隔離性
Cgroups 提供了容器級別的資源隔離,每個容器都可以被分配和限制其使用的資源。這樣,容器之間的資源使用不會互相干擾,一個容器的問題也不會影響其他容器或宿主機。
- 容器管理
Docker 使用 Cgroups 對容器進行管理和監(jiān)控。通過讀取和設置 Cgroups 的屬性,Docker 可以實時了解容器的資源使用情況,并可以調(diào)整資源限制以滿足需求。
在cgroup(控制組)這部分,需要注意Cgroup 的掛載和層級結構等限制。
所以我們將Cgrous這一部分加入到代碼實現(xiàn)中來如下:
package?main import?( ????"fmt" ????"io/ioutil" ????"os" ????"os/exec" ????"strconv" ????"syscall" ) func?main()?{ ????//?創(chuàng)建?cgroup ????err?:=?createCgroup("mycontainer") ????if?err?!=?nil?{ ????????fmt.Println("Failed?to?create?cgroup:",?err) ????????return ????} ????defer?func()?{ ????????//?退出時刪除?cgroup ????????err?:=?deleteCgroup("mycontainer") ????????if?err?!=?nil?{ ????????????fmt.Println("Failed?to?delete?cgroup:",?err) ????????} ????}() ????//?限制?CPU?使用率為?50% ????err?=?setCPULimit("mycontainer",?50) ????if?err?!=?nil?{ ????????fmt.Println("Failed?to?set?CPU?limit:",?err) ????????return ????} ????//?在容器中運行命令 ????cmd?:=?exec.Command("/bin/bash") ????cmd.Stdin?=?os.Stdin ????cmd.Stdout?=?os.Stdout ????cmd.Stderr?=?os.Stderr ????cmd.SysProcAttr?=?&syscall.SysProcAttr{ ????????Cloneflags:?syscall.CLONE_NEWNS?|?syscall.CLONE_NEWPID?|?syscall.CLONE_NEWUTS?|?syscall.CLONE_NEWIPC?|?syscall.CLONE_NEWNET, ????????Cgroup:?????"mycontainer", ????} ????err?=?cmd.Run() ????if?err?!=?nil?{ ????????fmt.Println("Failed?to?run?command?in?container:",?err) ????} } func?createCgroup(name?string)?error?{ ????cgroupPath?:=?"/sys/fs/cgroup/cpu/"?+?name ????err?:=?os.Mkdir(cgroupPath,?0755) ????if?err?!=?nil?{ ????????return?err ????} ????//?將當前進程加入到?cgroup?中 ????err?=?ioutil.WriteFile(cgroupPath+"/tasks",?[]byte(strconv.Itoa(os.Getpid())),?0644) ????if?err?!=?nil?{ ????????return?err ????} ????return?nil } func?deleteCgroup(name?string)?error?{ ????cgroupPath?:=?"/sys/fs/cgroup/cpu/"?+?name ????err?:=?os.Remove(cgroupPath) ????if?err?!=?nil?{ ????????return?err ????} ????return?nil } func?setCPULimit(name?string,?limit?int)?error?{ ????cgroupPath?:=?"/sys/fs/cgroup/cpu/"?+?name ????err?:=?ioutil.WriteFile(cgroupPath+"/cpu.cfs_quota_us",?[]byte(strconv.Itoa(limit*1000)),?0644) ????if?err?!=?nil?{ ????????return?err ????} ????return?nil }
在上面,我們將當前進程加入到新創(chuàng)建的"mycontainer" 的 cgroup,然后,設置該 cgroup 的 CPU 使用率限制為 50%。繼而實現(xiàn)在容器中運行一個交互式的 shell。
結語
編寫一個容器(container)是一個相當復雜的任務,涉及到許多底層的概念和技術?;仡櫛疚?,使用golang一步步“還原”一個mini版的container所需步驟基本如下:
- 了解容器技術和相關概念:在開始編寫mini容器之前,強烈建議先了解一些容器技術的基本原理,如命名空間(namespaces)、控制組(cgroups)、文件系統(tǒng)隔離等。
- 選擇編程語言和庫:之所以選擇使用 Golang 進行容器的編寫,因為它提供了強大的并發(fā)和系統(tǒng)編程能力。同時,還可以使用一些相關的庫,如
os/exec
和syscall
。 - 創(chuàng)建容器的基本結構:首先創(chuàng)建出一個基本的容器結構,該結構將包含容器的信息,如 ID、進程 ID、文件系統(tǒng)等。
- 設置容器的命名空間:使用 Golang 的
syscall
包,設置容器的命名空間,如 PID 命名空間、網(wǎng)絡命名空間等。這樣可以將容器中的進程與主機系統(tǒng)的進程隔離開來。 - 設置容器的文件系統(tǒng):創(chuàng)建一個文件系統(tǒng),可以是一個文件夾或鏡像文件,用于存儲容器內(nèi)的文件和目錄。這里我們可以借助于 Golang 的
os
和io/ioutil
包來操作文件系統(tǒng)。 - 啟動容器中的進程:使用
os/exec
包,在容器的命名空間中啟動一個新的進程, 并指定要運行的可執(zhí)行文件和參數(shù)。 - 設置容器的網(wǎng)絡:如果想讓容器具有網(wǎng)絡連接能力,我們還需要設置容器的網(wǎng)絡命名空間,并進行相關網(wǎng)絡配置。這可能涉及到創(chuàng)建虛擬網(wǎng)絡設備、配置 IP 地址等。
- 處理容器的生命周期:需要考慮到容器的創(chuàng)建、啟動、停止和銷毀等生命周期事件。這可能涉及到信號處理、資源清理等操作。
除此之外,還需要考慮到安全性、權限管理、資源限制等多方面因素。
當然,實際的容器實現(xiàn)要更加復雜和完善。在實際項目應用中,我們可能還需要考慮到如文件系統(tǒng)隔離、網(wǎng)絡隔離等遠比這些復雜的場景。
以上就是使用Go語言編寫一個極簡版的容器Container的詳細內(nèi)容,更多關于Go編寫容器的資料請關注腳本之家其它相關文章!
相關文章
VScode下配置Go語言開發(fā)環(huán)境(2023最新)
在VSCode中配置Golang開發(fā)環(huán)境是非常簡單的,本文主要記錄了Go的安裝,以及給vscode配置Go的環(huán)境,具有一定的參考價值,感興趣的可以了解一下2023-10-10golang數(shù)據(jù)結構之golang稀疏數(shù)組sparsearray詳解
這篇文章主要介紹了golang數(shù)據(jù)結構之golang稀疏數(shù)組sparsearray的相關知識,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09go編程中go-sql-driver的離奇bug解決記錄分析
這篇文章主要為大家介紹了go編程中go-sql-driver的離奇bug解決記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05