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