1. 程式人生 > >“遷移策略+新容器運行時”應對有狀態應用的冷熱遷移挑戰

“遷移策略+新容器運行時”應對有狀態應用的冷熱遷移挑戰

技術 獲得 容易出錯 角度 text 51cto 耗時 交流 冗余

作者:稻農
阿裏雲智能事業群高級技術專家
參與主導容器運行時及網絡創新工作;目前的工作側重於基於進程虛擬化的研究及增強(網絡及熱遷移方面),在阿裏推行微安全容器及熱遷移等,力圖在保持容器簡單高資源利用率前提下,提供高安全及熱遷移等增強功能

大家好,我的花名是稻農,首先我簡單介紹一下我在這個領域的工作。在阿裏,我們現在主要的側重點是做大規模的運維和新的容器運行時。目前,大家可能已經對 Kubernetes 進行了廣泛地使用,但多數還沒有達到一定規模,有很多痛點以及內部的問題還沒有得到充分暴露。

容器遷移背景及現狀
目前,大多數容器的使用還在百臺到千臺的規模。我先簡單介紹一下阿裏目前內部容器服務。阿裏的淘系應用如天貓、淘寶,目前已經全部實現了容器化,在集團的場景下面是沒有虛擬機的。阿裏用了大概三年到四年的時間,做到了 100% 容器化。

大家都知道使用容器有很多好處,比如它在資源耗費方面有很大優勢。對於“雙十一”大家應該有很明顯地感受,相比之前,現在的“雙十一”會“順滑”很多,這樣地轉變也有容器化的功勞。

技術分享圖片

如果你有存量的業務,那你一定會面臨從虛擬機或物理機遷移到容器的過程。絕大多數開發人員其實認為這是一個負擔,因為他們的應用已經跑起來了,就不太希望因為基礎設施地改變,去做更多工作去進行適配。所以出現了一些我們叫“富容器”或者“豐富復雜應用容器”的特殊容器。

技術分享圖片

簡單說,所謂富容器,就是我們回在容器內放置一些管理組件。阿裏內部組件叫 star agent,它會提供登陸服務,提供各種各樣的包管理,命令行的執行,諸如此類的事情。在真正運維和使用的過程中,整個容器與虛擬機的差別不大。

當然這個東西在業界是存在爭議的,比如我們是不是應該先做微服務化,把所有服務都變成單一、不可改變的鏡像再run 起來,還是我們為了遷就一些技術債務引入富容器這種技術,這個地方是存在爭議的。但是可以告訴大家的是,如果你要完全按照理想化的微服務去執行,基本上很多大的應用(像淘系這些非常復雜的應用,需要改造一下可能要幾個月)可能在第一步就被卡死了。因為我們有富容器,所以這個應用是有狀態的,並不是隨便說我砍掉他,然後異地重啟就可以了,這就是雙刃劍的另一面,上線改造容易,運維變得復雜了。

我們有富容器的一些傳統應用,很難對他們進行微服務無狀態的改造,所以我們看到有很多場景,比如說容器出現故障時,開發或者運維的同學非常希望故障之後的新容器長得跟原來容器一模一樣,比如 ip、名字等任何東西都不變,非常符合他們的理想。

有時候,我們面對一些大規模的容器遷移,比如說在地方開一個很大的機房,我們就會把杭州或者是上海的容器全部遷走。在過程中非常麻煩的是有一些容器是有狀態的,你遷的時候你還不敢動它,因為萬一砍掉,可能紅包就發不了了……

技術分享圖片

大的有狀態的應用會占住物理機,造成沒有辦法去遷移。以上都是容器可攜帶狀態遷移成為規模化運維的典型場景。

容器可攜帶狀態遷移成為規模化運維難點在於 K8s 或者說整個容器與虛擬機的運維。Docker 公司曾給出說法,虛擬機像寵物一樣,需要受到很精心地呵護才能永遠活得很好,只要不好就需要去修它,這個就是寵物式的管理。K8s 認為容器應該是牛群式的放養,死了就直接重啟而不需要對每一頭牛做特別好地呵護,因為成本很高。

在 K8s 裏面,我們經常看到的就是擴縮容,針對他的假設都是裏面的應用是無狀態。然後在執行層面,大家現在用的一般都是普通容器,或者說是標準容器引擎,就是 runC,雖然 runC
裏面有個 checkpoint 和 restore 的機制,大家用起來就會發現基本上是不可用,坑非常多。

容器可攜帶狀態遷移成為規模化運維有兩個難點,恰好是我們要解決的兩個問題:

首先是管理面,K8s 上支撐 pod 的遷移與伸縮不一樣,我們所認為遷移就是這個容器要原封不動地在異地再重生;另外,我們認為冷遷移就是業務時間中斷比較長,中斷時間短的就是熱遷移。那這個長短的分界嶺在哪呢?每個雲廠商會有一些不同的看法。我們認為大概到毫秒級以下,一百毫秒或者十個毫秒這樣的級別,可以認為它是熱遷移。其實任何遷移基本上都會有業務中斷的時間,任何一種機制去實現都不可能實現零時間切換。我們看一下 K8s 系統對整個容器遷移,2015 年開始,我們就討論過 pod 的遷移要不要放到K8s裏面去,大家可以去翻 K8s 社區 issue,但一直沒有下文。
其次是在執行層面,runC 作為容器運行時主流,雖有 CRIU 的項目輔助,仍然無法提供完善可靠的遷移機制。

管理面支撐 Pod 遷移
接下來,我們看看 K8s 為什麽不能夠做遷移?當前 K8s 系統的 Pod 遷移仍為空白。其中存在以下問題:

技術分享圖片

因為每個 pod 有獨立的標識,還有名字、ID 等。這個東西是要保證唯一性的,不然 K8s 自己也管理不了這些東西。假設有兩個同學,他們的學號、名字長相完全一樣,校長是要糊塗的。K8s 主推了對業務的伸縮,就是靠無狀態伸縮,他不會把某一個容器從這遷到那去。

另外,K8s 骨幹系統不支持 Pod
標識及 IP 沖突。我們認為 API server、schedule 這種必不可少的部分是骨幹系統。幾個骨幹系統是不支持任何標識沖突的。如果有兩個 pod,他們 ID 一樣, API server
就會糊塗,邏輯就會出問題。

K8s 是一套容器的管理系統,阿裏周邊對網盤 ID、對 ip,各種各樣的資源,都有自己的管理系統。這些管理系統在 K8s 的世界裏面,表現為不同的資源 controler,因為這些資源都是錢買來的,要跟底層賬務系統聯動,將來大家都會遇到這些問題,比如說這個部門是否有預算等。在阿裏的 K8s 周邊,我們已經開發了大量的這種 controler 了,他們都是按照這個標識,我們叫 SN 來管理應用的。數據庫裏面記錄,每個容器都有一個的標識。

伸縮跟遷移是沖突。因為遷移的時候你可能需要砍掉舊容器。砍掉容器之後,伸縮控制如果正在生效(RC),它就會自動起一個新容器,而不是把這個容器遷移過去,所以這個地方我們對 RC 這些控制器都要做一定程度地改造。

其他問題,比如說很多遠程盤不支持多 Mount。因為我們在做遷移的時候,這個盤一定要做到至少有兩個 Mount。就是我的舊容器跟新容器,能夠同時把 PV mount 上去,很多遠程盤還是不支持的。

剩下還有一些傳統底層支撐系統,比如 IP 管理系統不允許出現地址沖突。比如我們在分 ip 的時候分兩個一樣的是不可想象的,這個是我們最簡單的遷移過程。遷移過程是這樣的,我們想最小地去改造 K8s,當你的系統真正上了管理系統,復雜了以後,大家都會對 K8s 有一些適應性地改造。

這些改造最好的表現形式可能就是 controler 了,你不要去對骨幹系統做改動,改動之後就很難再回到主線來了。在遷移過程中,我們還是沒有辦法一定要對骨幹系統做一些改造,那麽我們想盡量減少它的改造量。

第一步,我們會生成一個從資源上來講跟原來的 pod 或者容器完全一樣的一個 pod,它需要幾核幾 U,它需要一個什麽遠程盤,它需要一個什麽的多少個 ip,多 ip 的話還要考慮多少個 ip,多少個直通網卡,或者是非直通的網卡,資源完全一樣,這個是完全標準的。我們創建一個資源,一個新 pod,這就像一個占位符一樣。假設我這臺物理機要壞了,那麽我在一個打標之後,我在一個新的好的物理集群上,生產一個這樣的 pod,讓我拿到了資源。

第二個過程就是說我們兩邊的 agent,比如說是 runC 或者是阿裏做的 pouch-container 也好,我們這種 OCI 的 Agent 之間會有一個協商的過程,它的協商過程就是會把舊的 pod 的狀態同步過去,剛才我們新生成的 pod ,實際它是占位符。

我們會把新的鏡像動態地插入 pod 裏,API 對 CRI 的接口是支持的,當前我們沒有辦法在一個已經產生的 pod 裏面去插入新的 container。但實際上 OCI 接口本身是支持的,可以在一個 sandbox 裏面去刪掉已有的 container 和增加新的 container,不需要做什麽新的工作,只要打通管理層的事情就可以了。

另外,喚醒記憶的過程其實就是兩邊狀態同步,狀態同步完畢,我們會做一個切流,切流就是把舊的容器不再讓新的需求過來,一旦我們監控到一個靜默期,它沒有新的需求過來,我們會把舊的 pod 停掉。其實暫時不停掉也沒關系,因為反正沒有客戶來找他進行服務了,已經被隔離到整個系統外面去了,刪除資源是危險操作,一般會放置個一兩天,以備萬一要回滾。

最後一個過程新開發工作量比較大,我們要把前面那個占位作用的 pod 標識改掉,ip 與舊的設置成一樣,然後一切需要同步的東西都在這一步完成。完成之後就上去通知 API
server 說遷移過程完成,最後完成整個過程。所以大家會看到,其實第一步基本上標準的 K8s 就支持。

技術分享圖片

第二步我們是 K8s 不感知的,就是我們在兩個宿主機上做兩個 agent 做狀態同步。對 K8S 的改造也比較小。那麽最後一個會比較多,API server 肯定是要改。RC 控制器可能改,如果你有 CI 的這種就是 IPM 的管理,IPM 的管理,這個地方要改。

接下來,我從 OCI 的運行這個地方來來討論這個過程,因為其實是有兩層面,一個是我們籃框這裏是一個 pod,從它的狀態 Dump 落盤到遠端把它恢復,整個同步過程中,我們會插入對 K8s 系統的調用,涉及對容器管理系統的改造。

技術分享圖片

看外面這兩個白框,上面這個我們叫預處理過程,其實就是前面講的,我們要去創建新的 pod、占位符,然後在那邊把資源申請到最後一個後期建議。我們剛才說的最後一步,我們叫標識的重構重建跟舊的 pod 完全一樣的,大家在我們開發過程中會遇到各種各樣的沖突,比如說 API server 會說,你有兩個標識一樣的,這個代碼就要特殊處理。APM 有時候會跳出來說你有 ip 沖突,這樣也要特殊處理,至少有幾個骨幹系統肯定是要做的。

這部分因為涉及到 K8s 骨幹的改造, patch 我們還沒有提上來。接下來還要跟社區討論他們要不要 follow 我們的做法,因為現在 K8s 的容器就是無狀態的觀點還比較占上風。

剛才我們講到管理面我們認為是事務處理的,路上會有很多障礙,但是這些障礙都是可以搬掉的,就是說無非是這個東西不允許沖突,我改一改讓他允許沖突,或者允許短時間的一個並存,那個東西不允許我再改一改。比較硬核的部分是底層引擎去支撐熱遷移,尤其是熱遷移,冷遷移其實問題不大,冷遷移就是說我只是恢復那些外部可見的狀態(不遷移內存頁表等內部數據),如果對我的業務恢復時間沒有什麽要求的話,就比較容易做。

RunC 引擎的可遷移性
接下來,我們講 RunC,RunC 應·該是大家用的最多的,它就是標準的 container 去進行的遷移改造。如果大家去看過 checkpoint 開放的這部分代碼,可以發現 RunC 依靠的機制就是一個叫 CRIU 的東西。他在優勢技術已經出現比較長的時間了,他的整個的想法就是,用戶態把一個進程或者一個進程數完全落盤在把它存到磁盤上,然後在異地從磁盤把進程恢復,包括它的 PC 指針,它的棧,它的各種各樣的資源,經過一段時間地摸索,基本上可以認為內存狀態是沒有問題的,就是頁表,頁表是可以做到精確恢復的。

技術分享圖片

不管你這邊涉及多少物理頁是臟的,還是幹凈的,這個都是百分之百可以還原出來的。進程執行的上下文,比如各種寄存器,調用 stack 等,這些都沒問題。跟純進程執行態相關的問題都已經完全解決了,這個不用擔心。然後大家比較擔心的就是一個網絡狀態,比如說大家都知道 TCP 是帶狀態的,它是已連接?等待連接還是斷開?其實這個網絡 Socket 遷移的工作也基本完成了。

我可以簡單講一下,它的實現方法是這樣,就是說它在 Linux set 裏面加了一個修復模式,修復模式一旦啟動,就不再向外發送真正的數據包,而是只進行狀態及內部 buffer 的同步,比如你下達的這種 close,不會向外發包,只是體現為對狀態信息的導出。

比如說你要進入修復模式,那在原端就要關閉 Socket,它並不會真正的去發 close 的 TCP 包,它只是把信息 Dump 出來,在新的目的地端去 connect。它也不會真正去包,最後的結果就是除了 mac 地址不一樣,TCP 裏面的狀態也恢復到遠端了,裏面的內存狀態都轉過去了,經過實際驗證其實也是比較可靠。還有打開的文件句柄恢復,你打開的文件,你現在文件比如說讀寫指針到了 0xFF,文件的 off
set 恢復都是沒有問題的。

其實我們在冷熱遷移中最擔心的就是耗時問題,我一個容器究竟花多長時間?一個 pod 多長時間可以遷移到新目的地的宿主機上去,耗時的就是內存。也就是說像很多 Java 應用,假設你的內存用得越多,你的遷移時間就是準備時間就越長。可以看到我們剛才其實是有一個協商過程。在協商過程中舊的 pod 還在繼續提供服務,但是它會不停的把它的狀態 Sync 到遠端去。這個時間其實並不是業務中斷的時間,如果耗時特別長,也會因為業務時在不停轉的,如果你的內存總是在不停地做大量改動,你的準備時間和最後的完成時間就會非常長,有可能會超時。

我們去評估一個業務能不能做熱遷移,或者這個的時候或者一些情況,它所使用的內存大小是一個比較大的考量。

然後剩下就是我們踩到的坑。現在還有很多東西它支持的不大好。這個地方大家可以理解一個進程,一個進程其實就是一個自己頁表,有自己的堆棧,一個可執行的活體。那麽它支持不好的部分都是外部的,如果它依賴一些主機設備,就很難把一個設備遷移走。

然後剩下就是文件鎖,如果這個文件是的多個進程共用就會加鎖。因為這個鎖的狀態還涉及到別的進程,所以你只遷移這一個進程的時候會出問題。這個地方邏輯上會有問題,其實大家可以籠統地這樣去判斷,如果我依賴的東西是跟別用戶、進程有一些共享,甚至這個東西就是內核的一個什麽設備,這種就比較難遷移走。所以簡單來說就是自包含程度越高越容易遷移。

這個是一個比較詳細的一個圖,跟我剛才講的過程其實是差不多,我們還是會在原端發起熱遷移的請求,請求之後,會發起兩端兩邊 Agent 的sick,然後最後中間會切流。

技術分享圖片

等到 Sync 狀態完成,我們會通知 K8s 說我這邊可以了,那麽 K8s 會完成一個,從舊的容器,就是把流從舊的 pod 切到新 pod 來,然後最後把所有的標識,這個是由底向上的,我的標識跟我的 SN 或者說我的 ip 都改造完了,最後通知一下 K8s 就結束了。

新運行時帶來的機會
最後,我想分享的是新的運行時。我們在社區裏面會看到容器,現在來說在私有雲上,它的主要的形態還時 RunC,就是普通的 Linux 標準容器。那麽我們在公有雲上為了能混布,或者說跟內外客戶/在線離線都在一起,他對安全的要求比較高,我們一般會選虛擬機類型的容器引擎,比如像 kata 這樣的東西。

早先來說你的選擇就只有兩種,要麽你講效率,不講安全,就是純容器;要麽你講安全,不講效率,就跑一個虛擬機式的容器。自從從去年開始,谷歌、亞馬遜等頭部玩家開始做一些新的事情,叫做進程及虛擬化。就像我們在中學物理講過,比如說光的波粒二象性,它在一些維度上看起來是波,一些維度上是粒子。其實這個與程級虛擬化是很相像的,也就是說從資源管理這個角度看,它是一個普通進程。 但是在內部為了加強隔離性,會做一個自己的內核,我們認為從這個角度看,它是一個虛機;但是從外部資源角度來看,因為這個內核是隱形生效的(並沒有動用其他虛擬機工具去啟動容器),也不會去實現完整的設備級模擬,管理系統感覺它就是一個普通進程。

這是一種新的潮流,也就是說我們判斷這個東西(當然我們還需要在裏面做很多工作),像這種比較顛覆性的事情,有可能會將來成為容器運行時的一個有益補充或者是主流。簡單的說,這種新的運行時會有自己的私有內核,而且這個內核一般現在都不會再用 C 語言去再寫一遍,因為底層語言比較繁瑣,也很容易出錯。

用過 C語言的人都知道帶指針管理很危險,Linux 社區 bug 比比皆是,現代的做法都會用 Go 語言或其他一些高級語言重寫,有自己的垃圾回收的機制,指針就不要自己去管理了。一般不會提供很豐富的虛擬設備管理。因為這部分對一個應用來說是冗余的,普通應用它跑起來,其實很少去關心我要用什麽設備需要什麽特殊 proc 配置,簡單的說就是把虛擬機的冗余部分全部砍掉,只留下我跟普通應用 Linux 的 APP 跑起來。

技術分享圖片

這個是我們對運行史的一個簡單的比較,是從自包含的角度來講,因為包含的程度越高,它的熱遷移越容易實現,或一般來說安全性也越高。

亞馬遜現在在做 Firecracker,它也是用現代語言重寫了內核。微軟的雲也在做一個事情。大家的思路是比較一致的。因為矽谷技術交流是很頻繁的,他們的技術人員之間都是比較知根知底的,Google 做了 gVisor。華為用的是 Kata。

大家可能聽說過谷歌的 gVisor,gVisor 是這樣一個機制,就是說我會在一個 APP,就是普通的未經任何修改的 ,跑在容器裏的 Linux 應用,那麽我們怎麽去讓他用我們的內核而不用 Linux 內核?核心的事情,就是要捕獲他的系統調用,或者說劫持都可以。

系統調用的劫持有軟硬兩種方法,軟件來說,我們在 Linux 內核裏面利用 pTrace 的機制,強迫就設完之後你設置的進程的所有系統調用,他不會讓內核去,而是先到你進程來。這個叫做軟件實現。

其次我們叫硬件實現的,就是說我們會強迫這個 APP 跑在虛擬機的狀態。我們知道在虛擬機裏面,虛擬機會有自己中斷向量表,他是通過這種方式來獲取執行時的。然後我們的 Guest kernel 是這樣的,我們會看到現在的類似內核是無比龐大的,應該截止到現在有 2000 萬行代碼,這裏面絕大部分其實跟容器運行時沒有太大關系。

所以像 Google 和亞馬遜包括我們現在想法就是,我只需要把 Syscall 服務作好,也就是說 APP 它看到的無非就是這 300 多個 Syscall。這 300 多個系統調用你能夠服務好,就是不管你的 Syscall 服務用 Go 寫,還是用 Python 寫的(不講究效率的話)你都可以認為你有自己的內核,然後跟主機的內核是隔離的,因為我沒有讓 APP 直接接觸主機內核的東西。

為了安全,我們也不允許用戶直接去操作主機文件。 大家看到,RunC 上面像這樣的你去操作的文件,事實上在主機上或者在宿主機上都是有一個代表,不管你 Overlay 出來,還是快照 DeviceMapper,你可以在磁盤上還能找到這個真實的存儲。

其實這個是一個很大的威脅,就是說用戶可以直接去操作文件系統。他們操作文件系統之後,其實我們可以相信文件系統是有很多 bug 的。它代碼量那麽大,總是有可能突破的。所以我們加了防護層叫 Gofer。Gofer 是一個文件的代理進程,只有就是用戶所發出的所有 file 的 read 和 right 都會被我們截獲,截獲完會經過 Gofer 的一個審查。如果你確實有權限去碰這個文件,他才會去給你這個操作,這個是大概的一個架構。

然後簡單講一下 gVisor 裏面是怎麽跑的,APP 在 RunC 裏面,它直接 call 到的就是主機的內核。就是這條紅線,Call 這個內核,他該獲得哪些 syscall 就會獲得 syscall,如果假設內核是有什麽故障或者 bug,這個時候它就可以突破一些限制,應該是上星期吧, RunC 就報了一個很大的逃逸漏洞,需要整改!

在 Gvisor 的裏面,他的執行方是這樣的,我的 APP 第一步會被 PTrace 或者我的 KVM-Guest 內核捕獲,捕獲之後在我們叫 Sentry,為什麽這個紅線畫的劃到 kernel 上面,因為捕獲的過程,要麽是經過 KVM-Guest-ring0 的,要麽是經過PTrace系統調用,所以我認為還是內核要幫忙。然後sEntry拿到這個系統調用之後,他會去做力所能及的事情,比如說你要去讀一些PROC文件,你要去申請文件句柄,本地就可以完成服務返回,這個事非常高效的。

技術分享圖片

然後有一些事情,比如說你要去,假設說你要去讀寫主機上的一個網卡這樣的事情,sEentry 自己確實做不了,他就會把這個需求轉發到主機內核上去,等到得到服務之後再原路返回。文件操作就是這樣的,如果你它讀寫任何的主機文件,都會去Call 到 Gofer的進程(審查請求),然後代理訪問服務,去讀寫真正的文件把結果返回。這個是大家可以看到,APP 就是被關在兩重牢籠裏面就叫 Guest Kernel(sentry),一個是 Host Kernel,因為他本身又是一個進程,所以從安全性上來講,因為 APP 和 sEntry 不共頁表,其實可以說比虛擬機還要安全。

因為虛擬機裏面 Guest Kernel 就是跟 APP 共頁表,Guest Kernel 躲在這個列表的上端。而在 gVisor 裏面 Guest Kernel 跟 APP 是完全不同的兩套頁表,諸如此類有很多方面,大家會發現 gVisor 比虛擬機更加的安全。

當然我們做了這麽多隔離,也會有副作用,就是運行效率會有問題,尤其是網絡,包括阿裏、谷歌我們都會持續改進,還有很多同事在裏面做很多工作,會把虛擬機已有的一些經驗用到 Go 的內核上去,我們的理想是虛擬損耗到 5% 以下。

最後這個議題就是說我們現在有很多新的運行時,大家在選型的時候,其實除了 RunC 除了 Kata 等等。

技術分享圖片

未來,大家可以去比較各種運行時。當我們選型一個容器的引擎,會去綜合地看它的運行效率,它的安全性,尤其是代碼復雜度,代碼越多,基本上你可以認為這個東西出 bug 的幾率就越高,代碼越少其實越好,大概是這樣的考量。

我們跟業界還有一個合作,還再有一個我們還在想做對容器運行時做一個平,最後綜合打一個分。完成後會開源給大家使用。怎麽去評價一個 runtime 是好的?高效的,它的安全性到多少分?就跟汽車的這種評分一樣的。我今天介紹大概就是這些。

“遷移策略+新容器運行時”應對有狀態應用的冷熱遷移挑戰