使用Go語言編寫一個極簡版的容器Container
前置知識儲備:
- Linux 基礎知識
Docker 是基于 Linux 容器技術(shù)構(gòu)建的,因此了解 Linux 操作系統(tǒng)的基本原理、命令和文件系統(tǒng)等知識對于理解本文乃至于Docker 源碼非常重要。
- 容器技術(shù)基礎
了解容器技術(shù)的基本概念、原理和實現(xiàn)方式對于理解 Docker 源碼非常有幫助??梢詤⒖?Docker 官方文檔[1]中的容器概述部分,以及相關的教程和文章。
- Go 語言基礎
Docker 的源碼主要是用 Go 語言編寫的,具體可以參考Go 語言官方文檔[2]。

[圖片來源:Docker架構(gòu)概覽[3]]
什么是容器化
容器化是作為一種虛擬化技術(shù),允許應用程序和其依賴的資源(如庫、環(huán)境變量等)被封裝在一個獨立的運行環(huán)境中,稱為容器。其核心概念主要包括:
- 隔離性
容器使用操作系統(tǒng)級別的虛擬化技術(shù),如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。這提供了更好的用戶隔離和權(quán)限管理。 通過使用這些命名空間,可以創(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)在運行程序,程序?qū)⒃?UTS、PID 和 MNT 命名空間內(nèi)運行。
在 Docker 中,根文件系統(tǒng)是由 Docker 鏡像提供的,并且在容器啟動時被掛載到容器的根目錄上。Docker 根文件系統(tǒng)一般具有分層結(jié)構(gòu)、只讀性和寫時復制等特性。
現(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 的掛載和層級結(jié)構(gòu)等限制。
所以我們將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。
結(jié)語
編寫一個容器(container)是一個相當復雜的任務,涉及到許多底層的概念和技術(shù)?;仡櫛疚?,使用golang一步步“還原”一個mini版的container所需步驟基本如下:
- 了解容器技術(shù)和相關概念:在開始編寫mini容器之前,強烈建議先了解一些容器技術(shù)的基本原理,如命名空間(namespaces)、控制組(cgroups)、文件系統(tǒng)隔離等。
- 選擇編程語言和庫:之所以選擇使用 Golang 進行容器的編寫,因為它提供了強大的并發(fā)和系統(tǒng)編程能力。同時,還可以使用一些相關的庫,如
os/exec和syscall。 - 創(chuàng)建容器的基本結(jié)構(gòu):首先創(chuàng)建出一個基本的容器結(jié)構(gòu),該結(jié)構(gòu)將包含容器的信息,如 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)建、啟動、停止和銷毀等生命周期事件。這可能涉及到信號處理、資源清理等操作。
除此之外,還需要考慮到安全性、權(quán)限管理、資源限制等多方面因素。
當然,實際的容器實現(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-10
golang數(shù)據(jù)結(jié)構(gòu)之golang稀疏數(shù)組sparsearray詳解
這篇文章主要介紹了golang數(shù)據(jù)結(jié)構(gòu)之golang稀疏數(shù)組sparsearray的相關知識,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09
go編程中g(shù)o-sql-driver的離奇bug解決記錄分析
這篇文章主要為大家介紹了go編程中g(shù)o-sql-driver的離奇bug解決記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05

