標籤:

攜程容器雲優化實踐

隨著微服務架構的流行,把容器技術推到了一個至高點上;而隨著Docker,K8S等容器技術的日趨成熟,DevOps的概念也再次熱度上升;面對容器化的大潮趨勢,各家公司都在積極地響應和實踐,攜程也在這方面做了不少工作,形成了自己的容器雲平台。

從容器雲的打造思路上,攜程將其劃分成了水上、水下兩大部分:

  • 水下部分是指容器雲服務的基礎架構
  • 水上部分是指面向容器而產生的一系列工程實踐配套

水下部分對Dev來說相對透明,而水上部分則會對Dev工作有直接影響,也就是DevOps概念里所提到「混亂之牆」所在的地方。所以只要水上、水下同時做好了,容器雲才能真正落地,並符合DevOps理念的設想。

一、基礎架構

容器雲在攜程主要經歷了以下3個階段:

第一階段,模擬虛機,通過OpenStack進行管理

在這個階段,主要的目的是驗證攜程已有的應用是否能夠在Docker容器下正常運行,並提升系統與容器的兼容性。這個過程中系統的主要架構還是使用OpenStack的nova模塊,把Docker模擬成虛機的形式進行管理,除了應用實際運行的環境產生了變化,其他任何流程,工具都不變,從而使影響範圍控制在最小。

第二階段,實現鏡像發布,使用Chronos運行Job應用

在這個階段,主要的目的是通過鏡像的方式實現應用的發布和變更。真正實現 Immutable Delivery,即一旦部署後,不再對容器進行變化。並且在這個過程中,架構從比較繁重的OpenStack體系中解脫出來。使用輕量級的Mesos+Chronos來調度Job應用,在這個過程中我們同時去掉了對Long Running的Service類型應用的支持,以方便測試在極端情況下調度的消耗,和整個系統的穩定性。

此時,整個容器雲的架構如下圖,

實踐證明這個架構在應對大量並發job的調度時,Mesos自身調度消耗過大,因為每啟動一次job都需要拉起一個docker實例,開銷客觀。同時也證明在攜程這樣的應用體量下,直接使用開源Framework是無法滿足我們的需求的,這也促使我們開始走向自研Framework的方向。

第三階段,自研Framework

在這個階段,我們主要要解決的問題有:

  • 同時支持Job與Service兩種類型的應用
  • 為每個docker實例分配獨立的IP
  • 支持stateful的應用
  • 完善容器的監控體系

此時的總體架構如下圖:

與第二階段的架構不同之處:

  • 首先,重新封裝了Mesos的Rest API層,使得對外提供的API更豐富(可以與其他已有系統結合,提供更多的功能),同時基於一些規範統一的考慮,收攏了一些個性化參數的使用。除此之外,獨立抽象API層也是為了將來能夠快速適配其他架構體系,如K8S時,可以做到對上應用透明。
  • 其次,對Mesos做了集群化分布,從而提高Mesos本身的可用性。
  • 最後,為了應對大量Job類應用的調度,採用了與long running一樣的方式,將executor放置於容器內部。做到Job調度時,不重新啟動容器,而是在容器內部調度一個進程。

說完了系統架構後,還有2個比較重要的問題:

網路

攜程對容器實例的要求是,單容器單IP,且可路由,所以網路選項上採用的仍舊是Neutron+OVS+VLan這個模式,這個模式比較穩定,網路管理也比較透明。在實際對每個容器配置網路的過程中,攜程自研了一套初始化hook機制,以通過該機制在容器啟動後從外部獲取對應的網路信息,如網段,或者Neutronport等,在配置到容器內,這樣就完成了網路配置的持久化。大致的機制如下圖所示:

當然利用這個hook機制還能處理其他一些特殊的case,之後也會有提到。

監控

監控分為2個部分,一塊是對Mesos集群的監控。攜程用了很多開源技術,如:Telegraf、influxdb、Grafana等,並做了一些擴展來實現mesos集群的監控,採集mesos-master狀態、task執行數量、executor狀態等等,以便當mesos集群出現問題時能第一時間知道整個集群狀態,進而進行修復。

另一塊是對容器實例的監控,攜程監控團隊開發了一套監控系統hickwall, 實現了對容器的監控支持。 hickwall agent部署在容器物理機上,通過Docker client 、cgroup等採集容器的運行情況,包括 CPU 、Memory、Disk IO等常規監控項;由於容器鏡像發布會非常頻繁的創建、刪除容器,因此我們把容器的監控也做成自動發現,由hickwall agent發現到新的容器,分析容器的label信息(比如: appid、版本等)來實現自動註冊監控;在單個容器監控的基礎上,還可以按照應用集群來聚合顯示整個集群的監控信息;

自研Framework的動機

  • 輕量化,專註需求

開源Framework為了普適性,和擴展性考慮,相對都比較重,而攜程實際的使用場景,並不是特別複雜,只需要做好最基礎的調度即可。因此自研的話更可以專註業務本身的需求,也可以更輕量化。

  • 兼容性,適配原有中間件

由於攜程已經形成了比較完整的應用架構體系,以及經過多年打造已經成熟的中間件系列。所以自研Framework可以很好地去適配原有的這些資源,使用開源項目反而適配改造的成本會比較大,比如路由系統,監控系統,服務治理系統等等。

  • 程序員的天性,改不如重寫

最後一點就比較實在了,開源項目使用的語言,框架比較分散,長遠來說維護成本比較大

自研Framework的甜頭

正如前面所說,自研Framework能夠很方便地解決一些實際問題,下面就舉一個我們碰到的實際例子。

我們知道mesos本身調度資源的方式是以offer的模式來處理的,簡單來說就是mesos將剩餘資源的總和以offer的形式發送出來,如果有需求則佔用,沒有需求則回收,待下次發送offer。但是如果碰到下圖這樣的情況,即mesos一直給出2核的資源,並且每次都被佔用,那一個需要4核的實例什麼時候能拿到資源呢?

我們把這種情況叫做offer碎片,也就是一個先到的大資源申請,可能一直無法得到合適的offer的情況。

解決這個問題的辦法其實很簡單,無非2種:

1、將短時間內的offer進行合併,再看資源申請的情況

2、縮短mesosoffer的timeout時間,使其強制回收合併資源,再次offer

攜程目前採用的方案2,實現非常簡單。

以上大致介紹了一下攜程容器雲的水下部分,即基礎架構的情況,以及自研Framework帶來的一些好處。關於k8s,由於我們封裝了容器雲對外的API層,所以其實對於底層架構到底用什麼,已經可以很好的掌控,我們也在逐步嘗試將一些stateful的應用跑在k8s上,做到2套架構的並存,充分發揮各自的優勢。

二、工程實踐

容器化的過程除了架構體系的升級,對原先的工程實踐會帶來比較大的衝擊。也會遇到許多理念與現實相衝突的地方,下面分別介紹攜程遇到的一些實際問題和解決思路。

代碼包到鏡像,交付流程如何適配,如何遷移過渡?

DevOps理念提倡「誰開發,誰運行」,藉助docker正好很方便的落地了這個概念。攜程的CI/CD系統同時支持了基於鏡像與代碼包的發布。這樣做的好處是能夠在容器化遷移的過程中做到無縫和灰度。

能像虛機一樣登陸機器嗎?SSH?

docker本身提倡單容器單進程,所以是否需要sshd是個很尷尬的問題。但是對於docker實例的控制,以及執行一些必須的命令還是很有必要的,至少對於ops而言是一種非常有效的排障手段。所以,攜程採用的方式是,通過web console與宿主機建立連接,然後通過exec的方式進入容器。

Tomcat能否作為容器的主進程?

我們知道主進程掛掉,則容器實例也會被銷毀。而Java開發都知道,tomcat啟動失敗是很正常的case。由此就產生了一個矛盾,tomcat啟動失敗,並不等同於容器實例啟動失敗,我們需要去追查tomcat啟動失敗的原因。由此可見,tomcat不能作為容器的主進程。因此,攜程仍舊使用Supervisord來維護tomcat進程。同時在啟動時會註冊一些自定義hook,以應對一些特殊的應用場景。比如:某些應用需要在tomcat成功啟動,或成功停止後進行一些額外的操作,等等。

JVM配置是誰的鍋?

容器上線後一段時間,團隊一直被一個JVM OOM的問題所困擾,原來在虛機跑的好好的應用,為什麼到容器就OOM了呢?最後定位到問題的原因是,容器採用了cpu quota的模式,但JVM無法準確的獲取到cpu的數量,只能獲取到宿主機cpu的數量;同時由於一些java組件會根據cpu的數量來開啟thread數量,這樣就造成了堆外內存殆盡,最終造成OOM。

雖然,找到了OOM的原因,但是對於容器雲來說,卻面臨了一個棘手的問題。容器實例不像虛機,在虛機上,用戶可以按需定義JVM配置,然後再將代碼進行發布。在容器雲上,發布的是鏡像,JVM的配置則變成了鏡像的包含物,無法在runtime時進行靈活修改。

而且,容器本身並不考慮研發流程上的一些問題。比如,我們有不同的測試環境,不同的測試環境可能有不同的JVM配置,這顯然與docker設想的,一個鏡像走天下的想法矛盾了。

最後,對於終端用戶而言,在選擇容器時,往往挑選的是flavor,因此我們需要對應不同的flavor定義一套標準的JVM配置,利用之前提到的容器啟動時的hook機制,從外部獲取該容器匹配的標準JVM配置。

我們也總結了一些對於對外內存的最佳實踐,如下:

? Xmx = Xms = Flavor * 80%

? Xss = 256K

? 堆外最小800,最大2G,符合這個規則之內,以20%計

問題又來了,用戶需要自定義JVM?

最終,我們將JVM配置劃分成了3個部分:

1、系統默認推薦部分

2、用戶自定義override部分

3、系統強制覆蓋部分

允許用戶通過代碼或外部配置系統,對應用的JVM參數進行配置,這些配置會覆蓋掉系統默認推薦的配置,但是有一些配置是公司標準,不允許覆蓋的,比如統一的jmx服務地址等,這些內容則會在最終被按標準替換成公司統一的值。

Dockerfile的原罪

Dockerfile有很多好處,但同時也存在很多壞處:

  • 無法執行條件運算
  • 不支持繼承
  • 維護難度大
  • 可能成為一個後面,破壞環境標準

因此,如果允許PD對每個應用都自定義dockerfile的話,很有可能破壞已有的很多標準,產生各種各樣的個性化行為,使得統一運維變成不可能,這種情況在攜程這樣的運維體諒下,是無法接受的。

打造「plugin」服務平台

所以,攜程決定通過 「plugin」服務的方式,把dockerfile的使用管控起來,將一些常規的通過dockerfile實現的功能形成為「plugin」,在Image build的過程中進行執行。這樣做的好處是,所提供的服務可標準化,並且可復用,還可以任意組裝。比如:我們分別提供「安裝FTP」,「安裝Jacoco」等插件服務。用戶在完成自己的代碼後,進行image build時就可以單選或多選這些服務,那最後形成的image中就會附帶這些插件。並且針對不同的測試環境可以選擇不同的插件,形成不同的鏡像。

對於一個「plugin」而言,甚至可以定義一些hook(註冊supervisord hook),以及一些可exec執行的腳本,從而進一步擴展了「plugin」的能力。比如可以插入一個tomcat的啟停腳本,從而獲取從外部控制容器內tomcat的能力。

公司內的每個PD都可以申請註冊「plugin」,審核通過後,就可以在平台上被其他應用所使用。註冊步驟:

1、為服務定義名稱和說明

2、選擇服務可支持的環境(如:測試,生產)

3、上傳自定義的dockerfile

4、上傳自定義的可運行腳本

「Jacoco Plugin」的實例

Jacoco是一個在服務端收集代碼覆蓋率的工具,以幫助測試人員確認測試覆蓋率。這個工具的使用有以下幾個需求:

1、需要在代碼允許環境中安裝Jacoco agent

2、只需要在特定的測試環境進行安裝,生產環境不能安裝

3、被測應用啟動後,需要往Jacoco後端服務進行註冊

4、測試過程中可以方便控制Jacoco的啟停(通過tomcat啟動參數控制)

針對以上的需求,定製一個「JacocoPlugin」的工作,如下圖:

1、通過dockerfile安裝 jacoco agent

2、註冊一個supervisord hook,在tomcat啟動成功後向Jacoco service進行註冊

3、利用一個自定義tomcat重啟腳本,並在平台的web server上暴露api來控制jacoco的啟停

這樣,所有容器雲上的應用在image build時就都可以按需選擇是否需要開通 jacoco 服務了。

利用這樣的平台機制,還提供了一系列其他類型的「plugin」服務,以解決環境個性化配置的問題。

三、總結

1、devops或者容器化是理念的變化,更需要接地氣的實施方案

2、基礎架構,工程實踐和配套服務,需要並進,才能落地

3、適合自己的方案才是最好的方案

攜程的容器雲進程還在不斷的進化之中,很多新鮮的事務和問題等待著我們去發現和探索。

【作者簡介】王瀟俊,多年來致力於雲平台及持續交付的實踐。2015年加入攜程,參與攜程部署架構的全面改造,主導設計和打造新一代的適用於微服務的發布系統。同時負責基於攜程私有雲的兼容虛機與容器的持續交付平台。ROR狂熱粉絲,敏捷文化的忠實擁躉。

更多來自攜程技術人的一手乾貨,歡迎搜索關注「攜程技術中心」微信公號。


推薦閱讀:

從 MVC 到微服務,技術演變的必經之路 | 架構師實踐日
Kubernetes 在華為全球 IT 系統中的實踐 | 架構師實踐日
攜程容器雲實踐

TAG:容器云 |