簡單說說容器/沙盒(Sandbox)以及Linux seccomp
如果應用程式邏輯有誤,會造成作業系統崩潰…
這句話其實不對。如果一個應用程式都能讓一個作業系統崩潰了,那這一定是這個系統在設計上或者實現上的BUG!再次重申,我不知道譚浩強的C語言教材現在是怎麼講的,但是至少在15年前,很多老師都會說訪問空指標會造成作業系統崩潰,這在32位虛擬記憶體的系統中是錯誤的。
雖然一個應用程式不能讓一個正常的現代作業系統崩潰,但是它卻可以對作業系統的執行環境造成巨大的人為破壞,比如觸發一個作業系統潛在的漏洞…即便是基於虛擬地址空間的作業系統,也是不安全的。更退一步,拋開安全漏洞,從資源利用的角度看,如何限制一個程序或者一組程序可以使用的資源也是一個亟待解決的問題。換句話說,需求就是隔離
於是人們就想出了沙盒這個概念,即Sandbox。將一個應用程式或者一組應用程式隔離在一個受限的環境中,使其無法逃逸。
概念很OK,實現起來就五花八門了,各說各的理。何謂受限,如何確保…
很早之前就玩過Java Applet,這種依託本地JVM執行遠端Java位元組碼的環境就是一個沙盒,通過一個特殊的類載入器從一個URL載入位元組碼並執行。後面自從不做Java了以後我就再也沒有關注過沙盒這個概念,直到前些時間偶然touch了一下Docker。不禁感嘆,時間過了10年,技術變化的太多。
和大概六七個朋友聊Docker,大家水平絕對是第一梯隊,都比我強太多,結果從他們那裡獲得的結論和我預期的結論相去甚遠。Docker不是紅了好幾年嗎?然而在他們眼裡,無一例外,都在唱衰,他們都是用一種鄙視甚至哀其不幸的眼光來看待Docker這個現象級的玩具
我比較懵的是,我不知道該站哪隊了,其實我是想本著學習的態度向他們討教些乾貨的,可沒想到上來就是如此形而上的東西,令我悚然。我記得上週末午夜正在看恐怖懸疑電影放鬆一下,結果收到來自不同人的三封郵件(其中一封不是討論形而上學的,而是討論那個macvlan虛擬網絡卡和宿主網絡卡之間通訊的),看了以後,便關上了電視,打開了一個SecureCRT終端…我想實地考察一下Docker,firejail這些在他們眼裡為什麼是如此不堪。
我是並不懂什麼容器,沙盒這些的,我只是在工作中碰到了一個關於容器內網絡卡無法釋放的BUG,需要定位,所以才稍微窺了一眼Docker,後來又順藤摸瓜瞭解了firejail,而已…最後突然發現,好一片廣闊的天地,感謝這些人的介紹,我又瞭解了seccomp以及gVisor這些。
現在,表一下我的觀點,形而上的觀點。
Docker或者說類似的容器到底好不好,我覺得這裡面牽扯到兩個問題:
- 核心態實現還是使用者態實現的問題;
- 核心態實現的話,它能不能做好的問題。
同樣的問題在網路協議棧領域也存在,於是就誕生了各類一路高歌猛進的使用者態協議棧,伴隨著的就是各種對核心協議棧的唱衰。
誠然,核心作為一個通用的基礎設施,很多人都傾向於別什麼東西都往核心裡塞,當然,我也一直都這麼想的。那麼只要是依託核心機制的一切東西,看起來總是有那麼一點點彆扭,總是想把它拽出來看看能不能在使用者態實現,這就跟效能優化領域中大多數人看見數值引數就想調大一點是一樣的。所以說,對於大多數持此想法的人而言,即便是seccomp也同樣是不堪,不行,畢竟seccomp也有一部分程式碼在核心態支撐著。
我的看法稍微不同,我更傾向於解決問題而不是設計方案,所以並不是很在意方式。我並不認為Namespace,Cgroup這種核心機制和gVisor沙盒之類說的是一回事。不過從分類上講,比較讓人疑惑和費解的時,一旦承認我上面說的,即它們不是一回事,你就很難解釋為什麼基於Namespace和Cgroup的Docker叫做容器,而基於同樣機制的firejail卻叫做Sandbox(其manual上就是這麼說的)。不過我還是選擇忽略這種措辭上的不同,不再咬文嚼字。
如果說Namespace隔離地不夠,有洩漏,那是BUG,我的第一想法是如何讓它隔離地更徹底,而不是徹底放棄它。如果說Cgroup不夠徹底,那就想辦法讓它徹底,管它使用者態還是核心態呢,管它有沒有汙染核心框架呢。這是容器的範疇。
要說限制程序的行為影響到同一核心上的其它程序,我覺得seccomp就非常不錯。你想想,應用程式如果從不進行IO,那麼在執行期間,作業系統的存在就是一個累贅,比如我就一個CPU密集型的計算任務,根本就不需要作業系統,當然,站在作業系統的角度,為了公平性,還是需要進行強制排程的,除此之外,它便不需要為應用程式提供任何服務。這時應用程式和核心之間的唯一主動互動手段系統呼叫是不需要的(所需的記憶體可以在程序實際啟動之前從庫裡早已準備好的記憶體池裡申請),為了讓這種不需要系統呼叫得到一種保證,用seccomp限制它不是很好嗎?而這個是沙盒的範疇。
從概念上講,沙盒真的就不該依託共享的核心來構建,然後再把共享的核心用某種機制比如Namespace,Cgroup隔離成至少看起來不那麼共享的區域,而這種複雜的策略註定在核心態是做不好的。但在我看來,核心的問題仍然不過是一個bugfix的問題而不是一個refactor問題。
是的,沙盒是要在使用者態做,然而,容器必須是核心支撐,換句話說,兩者並不是一回事,容器裡裝的是沙盒而不是一個或者一組程序,沒人會把罐頭直接扔進集裝箱的,高檔西裝在扔進集裝箱前也要在外面包裹幾層箱子…即便是gVisor也有介紹如何將其裝進Docker。
如果你非要擡槓說不依靠核心容器就能做好一切,那麼就一個問題,如果我把沙盒內的一個程序的一段核心程式碼汙染了,比如汙染成了:
while(1);
怎麼辦?怎麼限制其CPU利用率?不要依靠任何核心的隔離機制。
UNIX/Linux核心本身就是大核心,因此它本身就是揉在一起的一大坨東西,不管是靜態程式碼還是執行時邏輯,它不像理想中的微核心那樣僅僅通過訊息傳遞來溝通,而是依賴了很多共享的東西。
舉一個最簡單的例子,你啟動一個容器:
[email protected]:/home/zhaoya# firejail --net=enp0s17 --ip=192.168.44.55/24
Reading profile /etc/firejail/server.profile
Reading profile /etc/firejail/disable-common.inc
Reading profile /etc/firejail/disable-programs.inc
Reading profile /etc/firejail/disable-passwdmgr.inc
** Note: you can use --noprofile to disable server.profile **
Parent pid 40456, child pid 40457
The new log directory is /proc/40457/root/var/log
Interface MAC IP Mask Status
lo 127.0.0.1 255.0.0.0 UP
eth0-40456 72:63:2f:a3:60:b3 192.168.44.55 255.255.255.0 UP
Default gateway 192.168.44.2
Child process initialized
[email protected]:~#
然後在容器外部執行一個消耗CPU的程式:
int main()
{
while(1);
}
在容器內部的top顯示中,你會發現:
[email protected]:~# top
top - 19:40:41 up 1 day, 11:09, 0 users, load average: 0.39, 0.10, 0.03
Tasks: 3 total, 1 running, 2 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.7 us, 1.0 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.3 us, 0.0 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 2033804 total, 1368180 free, 267104 used, 398520 buff/cache
KiB Swap: 1046524 total, 1046524 free, 0 used. 1614320 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 18264 2068 1816 S 0.0 0.1 0:00.02 firejail
3 root 20 0 21132 4868 3156 S 0.0 0.2 0:00.08 bash
8 root 20 0 44800 3620 3112 R 0.0 0.2 0:00.03 top
很費解吧,一共就三個程序,卻有一個CPU的利用率達到100%,這如何解釋?容器內的觀察者無法觀察到容器外的程序行為,無法分析是誰吃掉了CPU…
確實,這就是一個問題,然而,能解決嗎?能啊。用排程組和Cgroup隔離一下,然後我們改變一下統計資料的解讀方式,按照Cgroup內部來統計百分比,而不是全域性統計,這就解決了問題。
對於沙盒而言,最典型最簡單的作業系統級沙盒就是32位保護模式下的程序本身了吧,一個程序崩潰不至於造成整個作業系統崩潰。而在沒有作業系統沙盒的時代,比如16位真實模式Dos,真的就是一個程序崩潰整個作業系統就連帶著崩潰。
32位虛擬記憶體隔離的代價,就是IPC代替了直接訪問記憶體,消除這種程式碼的方式就是執行緒,所以說,執行緒就是隔離和效率之間一個權衡的產物,沙盒依然是程序。
對於Linux而言,它的風格是一貫的。沙盒是程序而不是執行緒,這點非常明確,然而Linux預設排程的卻是執行緒而不是先排程程序再排程執行緒,在核心裡,它只認task_struct這個schedule entry!也就是說,程序沙盒之間的CPU資源本來就是共享的而不是隔離的,然而記憶體卻是隔離的。雖然我們可以把一個程序的多個執行緒放入同一個排程組,但是一般情況下沒人去那麼做,並且,排程組這個概念本身也是後來才引入的。
我的觀點是,服務放進沙盒,沙盒在使用者態做,然後將沙盒放入一個核心支撐的容器,配置好容器的規格,然後釋出。不然,如果你要把所有的東西整成一大坨,那麼就考慮類似JVM或者別的VM那樣的大傢伙吧….
不過,可以期待,肯定也有人看不慣JVM。總之,什麼都是錯。
咬文嚼字。
我覺得Docker和集裝箱的隱喻為人們帶來了一個新詞,即容器,否則,就都喊沙盒了。這是Docker火爆了之後帶來的禮物…