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

本文根據稻農在「Kubernetes & Cloud Native Meetup-廣州站」現場演講內容整理。

大家好,我的花名是稻農,首先我簡單介紹一下我在這個領域的工作。在阿里,我們現在主要的側重點是做大規模的運維和新的容器運行時。目前,大家可能已經對 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 應該是大家用的最多的,它就是標準的 container 去進行的遷移改造。如果大家去看過 checkpoint 開放的這部分代碼,可以發現 RunC 依靠的機制就是一個叫 CRIU 的東西。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,它也是用現代語言重寫了內核。微軟的雲也在做一個事情。大家的思路是比較一致的。因爲硅谷技術交流是很頻繁的,他們的技術人員之間都是比較知根知底的,谷歌做了 gVisor。

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

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

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

所以我們現在想法就是只需要把 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,這個時候它就可以突破一些限制。

在 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%。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章