1. 程式人生 > >網易雲容器服務基於Kubernetes的實踐探索

網易雲容器服務基於Kubernetes的實踐探索

Kubernetes的特點

近年來Docker容器作為一種輕量級虛擬化技術革新了整個IT領域軟體開發部署流程,如何高效自動管理容器和相關的計算、儲存等資源,將容器技術真正落地上線,則需要一套強大容器編排服務,當前大紅大紫的Kubernetes已經被公認為這個領域的領導者。Google基於內部Borg十多年大規模叢集管理經驗在2014年親自傾心打造了Kubernetes這個開源專案,欲倚之與AWS在雲端計算2.0時代一爭高下,即便如此,Kubernetes的定位主要是面向私有云市場,它最典型的部署模式是在GCE或AWS平臺上基於雲主機、雲網絡、雲硬碟及負載均衡等技術給使用者單獨部署一整套Kubernetes容器管理叢集,其本質上是賣的IAAS服務,Kubernetes叢集還是需要靠使用者自己維護,大家知道Kubernetes雖然功能強大但使用、管理、運維門檻也高,出問題了大多數使用者會束手無策。

這幾年國內容器雲領域也是群雄割據,但多數還是以私有云為主,提供公有云容器服務的卻很少,主要是公有云要考慮的問題多、挑戰大。雖然如此,網易雲的還是提供了公有云模式的容器服務。網易雲從2015年開始做容器服務時也被Kubernetes強大的功能、外掛化思想和背後強大的技術實力說吸引,至今已經跟隨Kubernetes一起走過兩年多,積累了不少經驗。因為我們特別希望以公有云的方式提供一種更易用容器服務,任何對容器感興趣的使用者都能快速上手,為此也遇到了很多私有云下不會出現的問題。

列舉幾個Kubernetes在公有云容器場景下的需要特別解決的關鍵問題。一是Kubernetes裡沒有使用者(租戶)的概念,只有一個很弱的名稱空間來做邏輯隔離。二是Kubernetes和Docker的安全問題很突出,API訪問控制較弱且沒有使用者流控機制,一些資源全域性可見。而Docker容器與宿主機共享核心的輕量級隔離從根本上沒法做到徹底安全。三是Kubernetes叢集所需要IAAS資源(如Node,PV)都要預先準備足夠,否則容器隨時會建立失敗,公有云這樣的話會造成嚴重的資源浪費,產生巨大的成本問題。四是Kubernetes單個叢集能支撐的節點總數有限,最大安全規模只有5千個Node,公有云下擴充套件性將會有問題。

網易雲容器如何解決Kubernetes在公有云上的問題

先看下網易雲容器服務的架構圖(如圖1),這裡的Kubernetes處於底層IAAS服務和上層容器平臺的中間,因為我們的容器服務不僅僅提供Kubernetes本身容器編排管理功能,更是為提供一整套專業的容器解決方案,還包括容器映象服務,負載均衡服務,通過使用DevOps 工具鏈高效管理微服務架構。考慮到Kubernetes概念較多、普通使用者使用複雜,也為了便於整合其他配套服務,我們並沒有直接暴露Kubernetes的API和所有概念給普通使用者。

圖片描述
公有云租戶概念

網易雲容器服務基於Kubernetes已有的Namespace的邏輯隔離特性,虛擬出一個租戶的概念,並與Namespace進行永久繫結:一個Namespace只能屬於一個租戶,一個租戶則可以有多個Namespace。這樣Kubernetes裡不同租戶之間的Pod、Service、Secret就能自然分割,而且可以直接在原生的Namespace/Resouce級別的認證授權上進行租戶級別的安全改造。

圖片描述
多租戶安全問題

關於Kubernetes的API的安全訪問控制,儘管網易雲容器沒有直接暴露Kubernetes的API給使用者,但使用者容器所在的Node端也都要訪問API,Node本質就是使用者的資源。我們在最早基於Kubernetes 1.0開發的時候就專門增加了一套輕量級擴充套件授權控制外掛:基於規則訪問控制,比如配置各租戶只能Get和Watch屬於自己Namespace下的Pod資源,解決對Kubernetes資源API許可權控制粒度不夠精確且無法動態增減租戶的問題。值得欣慰的是幾個月前官方釋出的1. 6新推出RBAC(基於角色訪問控制)功能,使得授權管理機制得以增強,但服務端對使用者Node端訪問的異常流量控制的缺乏依然是一個隱患,為此,我們也在apiserver端增加請求數來源分類統計和控制模組,避免有不良使用者從容器裡逃逸到Node上進行惡意攻擊。
原生的kube-proxy提供的內部負載必須要List&Watch叢集所有Service和Endpoint,導致就算在多租戶場景下Service和Endpoint也要全部暴露,同時導致iptables規則膨脹轉發效率極低;為此我們對kube-proxy也做了優化改造:每個租戶的Node上只會List&Watch自己的相關Namespace下資源即可,這樣既解決了安全問題又優化效能,一箭雙鵰。

至於Docker的隔離不徹底的問題,我們則選擇了最徹底的做法:在容器外加了一層使用者看不見的虛擬機器,通過IAAS層虛擬機器的OS 核心隔離保證容器的安全。

容器的IAAS資源管理

容器雲作為新一代的基礎設施雲服務,資源管理必然也是非常關鍵的。私有云場景下整個叢集資源都屬於企業自己,預留的所有資源都可以一起直接使用、釋放、重用;而公有云多租戶下的所有資源首先是要進行租戶劃分的,一旦加入kubernetes叢集,Node、PV、Network的屬主租戶便已確定不變,如果給每個租戶都預留資源,海量租戶累計起來就非常恐怖了,沒法接受。當然,可以讓公有云使用者在建立容器前,提前把所有需要的資源都準備好,但這樣又會讓使用者用起來更復雜,與容器平臺易用性的初衷不符,我們更希望能幫使用者把精力花在業務本身。

於是我們需要改造kubernetes,以支援按需動態申請、釋放資源。既然要按需實時申請資源,那就先理下容器的建立流程,簡單起見,我們直接建立Pod來說明這個過程,如圖3所示。

圖片描述

Pod創建出來後,首先控制器會檢查是否有PV(網易雲容器為支援網路隔離還增加租戶Network資源),PV資源是否匹配,不匹配則等待。如果Pod不需要PV或者PV匹配後排程器才能正常排程Pod,然後scheduler從叢集所有Ready的 Node列表找合適Node繫結到Pod上,沒有則排程失敗,並從1秒開始以2的指數倍回退(backoff)等待並重新加入排程佇列,直到排程成功。最後在排程的Node的kubelet上拉映象並把容器建立並執行起來。

通過分析上述流程可以發現,可以在控制器上匹配PV或Network時實時建立資源,然後在排程器因缺少Node而排程失敗時再實時建立Node(虛擬機器VM),再等下次失敗backoff重新排程。但是仔細分析後會發現還有很多問題,首先是IAAS中間層提供的建立資源介面都是非同步的,輪詢等待效率會很多,而且PV,Network,Node都序列申請會非常慢,容器本來就是秒級啟動,不能到雲服務上就變成分鐘級別;其次Node從建立VM,初始化安裝Docker、kubelet、kube-proxy到啟動程序並註冊到Kubernetes上時間漫長,排程器backoff重新排程多次也不一定就緒,最後,基於Kubernetes的修改要考慮少侵入,Kubernetes社群極度活躍一直保持3個月釋出一個大版本的節奏,要跟上社群發展可能需要不斷升級線上版本。

最終,我們通過增加獨立的ResourceController,藉助watch機制採用全非同步非阻塞、全事件驅動模式。資源不足就發起資源非同步申請,並接著處理後面流程,而資源一旦就緒立馬觸發再排程,申請Node時中間層也提前準備虛擬機器資源池,並將Node初始化、安裝步驟預先在虛擬機器映象中準備好。於是,我們詳細的建立流程演變為圖4所示。(注:最新Kubernetes 已經通過StorageClass型別支援PV dynamic provisioning)
圖片描述
與原生的Kubernetes相比,我們增加了一個獨立的 ResourceController管理所有IAAS資源相關的事情,具體的Pod建立步驟如下:

  • 1、上層client請求apiserver建立一個Pod。
  • 2、ResourceController watch到有新增Pod,檢查PV和Network是否已經建立;
    同時,另一邊的scheduler也發現有新Pod尚未排程,也嘗試對Pod進行排程。
  • 3、因為資源都沒有提前準備,最初ResourceController檢查時發現沒有與Pod匹配的PV和Network,會向IAAS中間層請求建立雲盤和網路資源;
    scheduler 則也因為找不到可排程的Node也同時向IAAS中間層請求建立對應規格的VM資源(Node),這時Pod也不再重入排程佇列,後面一切準備就緒才會重排程。
  • 4、因為IAAS中間層建立資源相對較慢,也只提供非同步介面,待底層資源準備完畢,便立即通過apiserver註冊PV、Network、Node資源
  • 5~6、ResourceController當發現PV和Network都滿足了,就將他們與Pod繫結;當發現Pod申請的Node註冊上來,且PV和Network均繫結,會把Pod設定為ResourceReady就緒狀態
  • 7、Scheduler再次watch到Pod處於ResourceReady狀態,則重新觸發排程過程,
  • 8、Pod排程成功與新動態建立Node進行繫結
  • 9~10、對應Node的kubelet watch到新排程的Pod還沒有啟動,則會先拉取映象再啟動容器。

叢集最大規模問題

從正式釋出1.0版本至今最新的1.7,Kubernetes共經歷了2次大規模的效能優化,從1.0的200個node主要通過增加apiserver cache提升到1000個node,再到1.6通過升級etcdv3和json改protobuf最終提升到5000 node。但是官方稱後續不會再考慮繼續優化單叢集規模了,已有的叢集聯邦功能又太過簡陋。如果公有云場景下隨著已有使用者規模不斷增大,一旦快接近叢集最大規模時,就只能將其中一些大使用者一批批遷移出去來騰空間給剩餘使用者。

於是我們自己在社群版本基礎上又做了大量定製化的效能優化,目前單叢集效能測試最大安全規模已經超過3萬,驗收測試包括叢集高水位下,大併發建立速度deployment和快速重啟master端服務和所有node端kubelet等在內的多種極端異常操作,保證建立時間均值<5s,99值<15s,叢集中心管控服務最差在3分鐘內快速恢復正常。

具體的優化措施包括:

  • 1、 scheduler優化
    根據租戶之間資源完全隔離互補影響的特性,我們將原有的序列排程流程,改造為租戶間完全並行的排程模式,再配合協程池來爭奪可並行的排程任務。在排程演算法上,還採用預先排除資源不足的node、優化過濾函式順序等策略進行區域性優化。
  • 2、 Controller優化
    熟悉Kubernetes的人都知道,Kubernetes有個核心特點就是事件驅動,實時性很好,但是有個Sync事件卻干擾了FIFO的順序,我們通過將Add、Update、Delete、Sync事件排序並增加多優先順序佇列的方式解決這種異常干擾。
    增加Secret本地快取
  • 3、 apiserver優化
    apiserver的核心是提供類似CRUD的restful介面,優化方向無外乎降低響應時間,減少cpu、記憶體消耗以提高吞吐量,我們最主要的一個優化是增加以租戶ID為過濾條件的查詢索引,這樣就能實現在租戶內跨Namespace聚合查詢的效果。另外apiserver的客戶端原生的流控策略太暴力,客戶端預設在流控被限制後會反覆重試,進一步加劇apiserver的壓力,我們增加了一種基於反饋的智慧重試的策略抹平這種突發流量。
  • 4、Node端優化
    kube-proxy本來需要控制整個叢集負載轉發的,Apiserver有了租戶查詢索引後,我們就能只watch自己租戶內的Service/Endpoint,急劇縮小iptables規則數量,提高查詢轉發效率。而且我們還精簡kubelet和kube-proxy記憶體佔用和連線數。

網易雲容器服務的其他實踐及總結

容器的網路是非常複雜一塊,容器雲服務至少要提供穩定、靈活、高效的跨主機網路,雖然開源網路實現很多,但是它們要麼不支援多租戶、要麼效能不好,且直接拿沒有經過大規模線上考驗的開源軟體問題總會很多。幸運的時網易雲有自己專業的IAAS雲網絡團隊,他們能提供專業級的VPC網路解決方案,天生就支援多租戶、安全策略控制和高效能擴充套件,已經做到容器與虛擬主機的網路是完全互通且地位對等的。

網易雲容器服務還在Kubernetes社群版本基礎上結合產品需求新增了很多功能,包括支援特有的有狀態容器,及Node故障時容器系統目錄也能自動遷移以保持資料不變,多副本Pod可按Node的AvailableZone分佈強制均衡排程(社群只盡力均衡)、容器垂直擴容、有狀態容器動態掛解除安裝外網IP等。

相比容器的輕量級虛擬化,虛擬機器雖然安全級別更高,但是在cpu、磁碟、網路等方面都存在一定的效能損耗,而有些業務卻又對效能要求非常高。針對這些特殊需求,最近我們也在開發基於Kubernetes的高效能裸機容器,繞過虛擬機器將網路、儲存等虛擬化技術直接對接到Docker容器裡,在結合SR-IOV網路技術、網易高效能雲盤NBS(netease block storage)等技術將虛擬化的效能損耗降到最低。

最後,分享一些網易雲容器服務上線近兩年來的遇到的比較典型的坑。

  • 1、Apiserver作為叢集hub中心本身是無狀態的可水平擴充套件,但是多apiserver讀寫會在Apiserver切換時可能會出現寫入的資料不能立馬讀到的問題,原因是etcd的raft協議不是所有節點強一致寫的。

  • 2、haproxy連線的問題,多Apiserver前用haproxy做負載均衡,haproxy很容易出現客戶端埠不夠用和連線數過多的問題,可以通過擴大埠範圍、增加源ip地址等方式解決埠問題,通過增加client/service的心跳探活解決異常連線GC的問題。

  • 3、使用者覆蓋更新已有tag的私有容器映象問題,強烈建議大家不要覆蓋已有tag的映象,也不要使用latest這樣模糊的映象標籤,否則RS多Pod副本或者同一個Node上同映象容器很容易出現版本不一致的詭異問題。

  • 4、有些容器小檔案非常多,很容易把inode用光而磁碟空間卻剩餘很多的問題,建議把這種型別應用排程到inode配置多的node上,另外原生kubelet也存在不會檢查inode過多觸發映象回收的問題。

  • 5、有些Pod刪除時銷燬過慢的問題,Pod支援graceful刪除,但是如果容器映象啟動命令寫得不好,可能會導致訊號丟失不光沒法graceful刪除還會導致延遲30s的問題

總之,在公有云場景下,使用者來源廣泛,使用習慣千變萬化沒法控制,我們已經碰到過很多純私有云場景下很難出現的問題,如使用者映象跑起不來,Pod多容器埠衝突,日誌直接輸出到標準輸出,或者日誌寫太快沒有切割,甚至把容器磁碟100%寫滿等,因為篇幅有限,所以只能挑選幾個有代表性的專門說明。因為雲上要考慮的問題太多,特別是這種基礎設施服務類的,使用場景又非常靈活,線上出現的一些問題之前完全想不到,包括很多還是使用者自己使用的問題,但為了要讓使用者有更好的體驗,也只能盡力而為,優先選擇一些通用的問題去解決。

作者:婁超,網易雲容器編排技術負責人。曾經參與淘寶分散式檔案系統tfs和阿里雲快取服務研發,2015年加入網易參與網易雲容器服務研發,經歷網易雲基礎服務(蜂巢)v1.0,v2.0的容器編排相關的設計和研發工作,並推動網易雲內部Kubernetes版本不斷升級。