認識容器,讓我們從它的歷史開始聊起

關於容器的歷史、發展以及技術本質,在互聯網上已經有非常多的文章了。這裏旨在結合自身的工作經驗和理解,通過一系列的文章,講清楚這項技術。

容器的歷史和發展

前世

講到容器,就不得不提 LXC(Linux Container),他是 Docker 的前生,或者說 Docker 是 LXC 的使用者。完整的 LXC 能力在 2008 年合入 Linux 主線,所以容器的概念在 2008 年就基本定型了,並不是後面 Docker 造出來的。關於 LXC 的介紹很多,大體都會說 “LXC 是 Linux 內核提供的容器技術,能提供輕量級的虛擬化能力,能隔離進程和資源”,但總結起來,無外乎就兩大知識點 Cgroups(Linux Control Group)和 Linux Namespace。搞清楚他倆,容器技術就基本掌握了。

**少年期起步艱難
**

2009 年,Cloud Foundry 基於 LXC 實現了對容器的操作,該項目取名爲 Warden。2010 年,dotCloud 公司同樣基於 LXC 技術,使用 Go 語言實現了一款容器引擎,也就是現在的 Docker。那時,dotCloud 公司還是個小公司,出生卑微的 Docker 沒什麼熱度,活得相當艱難。

成長爲巨無霸

2013 年,dotCloud 公司決定將 Docker 開源。開源後,項目突然就火了。從大的說,火的原因就是 Docker 的這句口號 “Build once,Run AnyWhere”。呵呵,是不是似曾相識?對的,和 Java 的 Write Once,Run AnyWhere 一個道理。對於一個程序員來說,程序寫完後打包成鏡像就可以隨處部署和運行,開發、測試和生產環境完全一致,這是多麼大一個誘惑。程序員再也不用去定位因環境差異導致的各種坑爹問題。

Docker 開源項目的異常火爆,直接驅動 dotCloud 公司在 2013 年更名爲 Docker 公司。Docker 也快速成長,幹掉了 CoreOS 公司的 rkt 容器和 Google 的 lmctfy 容器,直接變成了容器的事實標準。也就有了後來人一提到容器就認爲是 Docker。

總結起來,Docker 爲什麼火,靠的就是 Docker 鏡像。他打包了應用程序的所有依賴,徹底解決了環境的一致性問題,重新定義了軟件的交付方式,提高了生產效率。

被列強蠶食

Docker 在容器領域快速成長,野心自然也變大了。2014 年推出了容器雲產品 Swarm(Kubenetes 的同類產品),想擴張事業版圖。同時 Docker 在開源社區擁有絕對話語權,相當強勢。這種走自己的路,讓別人無路可走的行爲,讓容器領域的其他大廠玩家很是不爽,爲了不讓 Docker 一家獨大,決定要幹他。

2015 年 6 月,在 Google、Redhat 等大廠的 “運作” 下,Linux 基金會成立了 OCI(Open Container Initiative)組織,旨在圍繞容器格式和運行時制定一個開放的工業化標準,也就是我們常說的 OCI 標準。同時,Docker 公司將 Libcontainer 模塊捐給 CNCF 社區,作爲 OCI 標準的實現,這就是現在的 RunC 項目。說白了,就是現在這塊兒有個標準了,大家一起玩兒,不被某個特定項目的綁定。

講到 Docker,就得說說 Google 家的 Kubernetes,他作爲容器雲平臺的事實標準,如今已被廣泛使用,儼然已成爲大廠標配。Kubernetes 原生支持 Docker,讓 Docker 的市場佔有率一直居高不下。如圖是 2019 年容器運行時的市場佔有率。

但在 2020 年,Kubernetes 突然宣佈在 1.20 版本以後,也就是 2021 年以後,不再支持 Docker 作爲默認的容器運行時,將在代碼主幹中去除 dockershim。

如圖所示,Kubenetes 自身定義了標準的容器運行時接口 CRI(Container Runtime Interface),目的是能對接任何實現了 CRI 接口的容器運行時。在初期,Docker 是容器運行時不容置疑的王者,Kubenetes 便內置了對 Docker 的支持,通過 dockershim 來實現標準 CRI 接口到 Docker 接口的適配,以此獲得更多的用戶。隨着開源的容器運行時 Containerd(實現了 CRI 接口,同樣由 Docker 捐給 CNCF)的成熟,Kubenetes 不再維護 dockershim,僅負責維護標準的 CRI,解除與某特定容器運行時的綁定。當然,也不是 Kubenetes 不支持 Docker 了,只是 dockershim 誰維護的問題。隨着 Kubenetes 態度的變化,預計將會有越來越多的開發者選擇直接與開源的 Containerd 對接,Docker 公司和 Docker 開源項目(現已改名爲 Moby)未來將會發生什麼樣的變化,誰也說不好。

講到這裏,不知道大家有沒有注意到,Docker 公司其實是捐獻了 Containerd 和 runC。這倆到底是啥東西。簡單的說,runC 是 OCI 標準的實現,也叫 OCI 運行時,是真正負責操作容器的。Containerd 對外提供接口,管理、控制着 runC。所以上面的圖,真正應該長這樣。

Docker 公司是一個典型的小公司因一個爆款項目火起來的案例,不管是技術層面、公司經營層面以及如何跟大廠纏鬥,不管是好的方面還是壞的方面,都值得我們去學習和了解其背後的故事。

什麼是容器

按國際慣例,在介紹一個新概念的時候,都得從大家熟悉的東西說起。幸好容器這個概念還算好理解,喝水的杯子,洗腳的桶,養魚的缸都是容器。容器技術裏面的 “容器” 也是類似概念,只是裝的東西不同罷了,他裝的是應用軟件本身以及軟件運行起來需要的依賴。用魚缸來類比,魚缸這個容器裏面裝的應用軟件就是魚,裝的依賴就是魚食和水。這樣大家就能理解 Docker 的 Logo 了。大海就是宿主機,Docker 就是那條鯨魚,鯨魚背上的集裝箱就是容器,我們的應用程序就裝在集裝箱裏面。

在講容器的時候一定繞不開容器鏡像,這裏先簡單的把容器鏡像理解爲是一個壓縮包,後續再詳細講解。壓縮包裏包含應用的可執行程序以及程序依賴的文件(例如:配置文件和需要調用的動態庫等),接下來通過實際操作來看看容器到底是個啥。

宿主機視角看容器

1、首先,我們啓動容器。

docker run -d --

這是 Docker 的標準命令。意思是使用 euleros_arm:2.0SP8SPC306 鏡像(鏡像名: 版本號)創建一個新的名字爲 “aimar-1-container” 的容器,並在容器中執行 shell 命令:每秒打印一次“aimar-1-container”。

參數說明:

docker run -d --
207b7c0cbd811791f7006cd56e17033eb430ec656f05b6cd172c77cf45ad093c

從輸出中,我們看到一串長字符 207b7c0cbd811791f7006cd56e17033eb430ec656f05b6cd172c77cf45ad093c。他就是容器 ID,能唯一標識一個容器。當然在使用的時候,不需要使用全 id,直接使用縮寫 id 即可(全 id 的前幾位)。例如下圖中,通過 docker ps 查詢到的容器 id 爲 207b7c0cbd81。

aimar-1-container 容器啓動成功後,我們在宿主機上使用 ps 進行查看。這時可以發現剛纔啓動的容器就是個進程,PID 爲 12280。

我們嘗試着再啓動 2 個容器,並再次在宿主機進行查看,你會發現又新增了 2 個進程,PID 分別爲 20049 和 21097。

所以,我們可以得到一個結論。從宿主機的視角看,容器就是進程。

2、接下來,我們進入這個容器。

docker exec -it 207b7c0cbd81 /bin/bash

docker exec 也是 Docker 的標準命令,用於進入某個容器。意思是進入容器 id 爲 207b7c0cbd81 的容器,進入後執行 / bin/bash 命令,開啓命令交互。

參數說明:

-it 其實是 - i 和 - t 兩個參數,意思是容器啓動後,要分配一個輸入 / 輸出終端,方便我們跟容器進行交互,實現跟容器的 “對話” 能力。

從 hostname 從 kwephispra09909 變化爲 207b7c0cbd81,說明我們已經進入到容器裏面了。在容器中,我們嘗試着啓動一個新的進程。

[root@207b7c0cbd81 /]# /bin/sh -c "while true; do echo aimar-1-container-embed; sleep 1; done" &

再次回到宿主機進行 ps 查看,你會發現不管是直接啓動容器,還是在容器中啓動新的進程,從宿主機的角度看,他們都是進程。

容器視角看容器

前面我們已經進入容器裏面,並啓動了新的進程。但是我們並沒有在容器裏查看進程的情況。在容器中執行 ps,會發現得到的結果和宿主機上執行 ps 的結果完全不一樣。下圖是容器中的執行結果。

在 Container1 容器中只能看見剛起啓動的 shell 進程(container1 和 container1-embed),看不到宿主機上的其他進程,也看不到 Container2 和 Container3 裏面的進程。這些進程像被關進了一個盒子裏面,完全感知不到外界,甚至認爲我們執行的 container1 是 1 號進程(1 號進程也叫 init 進程,是系統中所有其他用戶進程的祖先進程)。所以,從容器的視角,容器覺得 “我就是天,我就是地,歡迎來到我的世界”。

但尷尬的是,在宿主機上,他們卻是普通得不能再普通的進程。注意,相同的進程,在容器裏看到的進程 ID 和在宿主機上看到的進程 ID 是不一樣的。容器中的進程 ID 分別是 1 和 1859,宿主機上對應的進程 ID 分別是 12280 和 9775(見上圖)。

總結

通過上面的實驗,對容器的定義就需要再加上一個定語。容器就是進程 => 容器是與系統其他部分隔離開的進程。這個時候我們再看下圖就更容易理解,容器是跑在宿主機 OS(虛機容器的宿主機 OS 就是 Guest OS)上的進程,容器間以及容器和宿主機間存在隔離性,例如:進程號的隔離。

在容器內和宿主機上,同一個進程的進程 ID 不同。例如:Container1 在容器內 PID 是 1,在宿主機上是 12280。那麼該進程真正的 PID 是什麼呢?當然是 12280!那爲什麼會造成在容器內看到的 PID 是 1 呢,造成這種幻象的,正是 Linux Namespace。

Linux Namespace 是 Linux 內核用來隔離資源的方式。每個 Namespace 下的資源對於其他 Namespace 都是不透明,不可見的。

Namespace 按隔離的資源進行分類:

前面提到的容器內外,看到的進程 ID 不同,正是使用了 PID Namespace。那麼這個 Namespace 在哪呢?在 Linux 上一切皆文件。是的,這個 Namespace 就在文件裏。在宿主機上的 proc 文件中(/proc / 進程號 / ns)變記錄了某個進程對應的 Namespace 信息。如下圖,其中的數字(例如:pid:[4026534312])則表示一個 Namespace。

對於 Container1、Container2、Container3 這 3 個容器,我們可以看到,他們的 PID Namespace 是不一樣的。說明他們 3 個容器中的 PID 相互隔離,也就是說,這 3 個容器裏面可以同時擁有 PID 號相同的進程,例如:都有 PID=1 的進程。

在一個命名空間中,那這倆進程就相互可見,只是 PID 與宿主機上看到的不同而已。

至此,我們可以對容器的定義再細化一層。容器是與系統其他部分隔離開的進程 =》容器是使用 Linux Namespace 實現與系統其他部分隔離開的進程。

原文鏈接:https://bbs.huaweicloud.com/blogs/285728

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/F1Egt-7wbj5s77y13s2Vcg