揭祕有狀態服務上 Kubernetes 的核心技術

騰訊雲原生發表於2021-06-04

背景

隨著 Kubernetes 成為雲原生的最熱門的解決方案,越來越多的傳統服務從虛擬機器、物理機遷移到 Kubernetes,各雲廠商如騰訊自研上雲也主推業務通過Kubernetes來部署服務,享受 Kubernetes 帶來的彈性擴縮容、高可用、自動化排程、多平臺支援等益處。然而,目前大部分基於 Kubernetes 的部署的服務都是無狀態的,為什麼有狀態服務容器化比無狀態服務更難呢?它有哪些難點?各自的解決方案又是怎樣的?

本文將結合我對 Kubernetes 理解、豐富的有狀態服務開發、治理、容器化經驗,為你淺析有狀態容器化的疑難點以及相應的解決方案,希望通過本文,能幫助你理解有狀態服務的容器化疑難點,並能基於自己的有狀態服務場景能靈活選擇解決方案,高效、穩定地將有狀態服務容器化後跑在 Kubernetes 上,提高開發運維效率和產品競爭力。

有狀態服務容器化挑戰

為了簡化問題,避免過度抽象,我將以常用的 Redis 叢集為具體案例,詳解如何將一個 Redis 叢集進行容器化,並通過這個案例進一步分析、擴充有狀態服務場景中的共性問題。

下圖是 Redis 叢集解決方案 codis 的整體架構圖(引用自 Codis專案)。

codis 是一個基於 proxy 的分散式 Redis 叢集解決方案,它由以下核心元件組成:

  • zookeeper/etcd, 有狀態後設資料儲存,一般奇數個節點部署
  • codis-proxy, 無狀態元件,通過計算 key 的 crc16 雜湊值,根據儲存在 zookeeper/etcd 內的 preshard 路由表資訊,將key轉發到對應的後端 codis-group
  • codis-group 由一組 Redis 主備節點組成,一主多備,負責資料的讀寫儲存
  • codis-dashboard 叢集控制面API服務,可以通過它增刪節點、遷移資料等
  • redis-sentinel,叢集高可用元件,負責探測、監聽 Redis 主的存活,主故障時發起備切換

那麼我們如何基於 Kubernetes 容器化 codis 叢集,通過 kubectl 操作資源就能一鍵建立、高效管理 codis 叢集呢?

在容器化類似 codis 這種有狀態服務案例中,我們需要解決以下問題:

  • 如何用 Kubernetes 的語言描述你的有狀態服務?
  • 如何為你的有狀態服務選擇合適的 workload 部署?
  • 當 kubernetes 內建的 workload 無法直接描述業務場景時,又該選擇什麼樣的 Kubernetes 擴充套件機制呢?
  • 如何對有狀態服務進行安全變更?
  • 如何確保你的有狀態服務主備例項 Pod 排程到不同故障域?
  • 有狀態服務例項故障如何自愈?
  • 如何滿足有狀態服務的容器化後的高網路效能需求?
  • 如何滿足有狀態服務的容器化後的高儲存效能需求?
  • 如何驗證有狀態服務容器化後的穩定性?

下方是我用思維導圖系統性的梳理了容器化有狀態的服務的技術難點,接下來我分別從以上幾個方面為你闡述容器化的解決方案。

負載型別

有狀態服務的容器化首要問題是如何用 Kubernetes 式的 API、語言來描述你的有狀態服務?

Kubernetes 為複雜軟體世界中的各類業務場景抽象、內建了 Pod、Deployment、StatefulSet 等負載型別(Workload), 那麼各個 Workload 的使用場景分別是什麼呢?

Pod,它是最小的排程、部署單位,由一組容器組成,它們共享網路、資料卷等資源。為什麼 Pod 設計上它是一組容器組成而不是一個呢? 因為在實際複雜業務場景中,往往一個業務容器無法獨立完成某些複雜功能,比如你希望使用一個輔助容器幫助你下載冷備快照檔案、做日誌轉發等,得益於 Pod 的優秀設計,輔助容器可以和你的 Redis、MySQL、etcd、zookeeper 等有狀態容器共享同個網路名稱空間、資料卷,幫助主業務容器完成以上工作。這種輔助容器在 Kubernetes 裡面叫做 sidecar, 廣泛應用於日誌、轉發、service mesh 等輔助場景,已成為一種 Kubernetes 設計模式。Pod 優秀設計來源於 Google 內部 Borg 十多年執行經驗的總結和昇華,可顯著地降低你將複雜的業務容器化的成本。

通過 Pod 成功將業務程式容器化了,然而 Pod 本身並不具備高可用、自動擴縮容、滾動更新等特性,因此為了解決以上挑戰,Kubernetes 提供了更高階的 Workload Deployment, 通過它你可以實現Pod故障自愈、滾動更新、並結合 HPA 元件可實現按 CPU、記憶體或自定義指標實現自動擴縮容等高階特性,它一般是用來描述無狀態服務場景的,因此特別適合我們上面討論的有狀態叢集中的無狀態元件,比如 codis 叢集的 proxy 元件等。

那麼 Deployment 為什麼不適合有狀態呢?主要原因是 Deployment 生成的 Pod 名稱是變化、無穩定的網路標識身份、無穩定的持久化儲存、滾動更新中過程中也無法控制順序,而這些對於有狀態而言,是非常重要的。一方面有狀態服務彼此通過穩定的網路身份標識進行通訊是其高可用、資料可靠性的基本要求,如在 etcd 中,一個日誌提交必須要經過叢集半數以上節點確認並持久化,在 Redis 中,主備根據穩定的網路身份建立主從同步關係。另一方面,不管是 etcd 還是 Redis 等其他元件,Pod 異常重建後,業務往往希望它對應的持久化資料不能丟失。

為了解決以上有狀態服務場景的痛點,Kubernetes 又設計實現了 StatefulSet 來描述此類場景,它可以為每個 Pod 提供唯一的名稱、固定的網路身份標識、持久化資料儲存、有序的滾動更新發布機制。基於 StatefulSet 你可以比較方便的將 etcd、zookeeper 等元件較單一的有狀態服務進行容器化部署。

通過 Deployment、StatefulSet 我們能將大部分現實業務場景的服務進行快速容器化,但是業務訴求是多樣化的,各自的技術棧、業務場景也是迥異的,有的希望實現 Pod 固定IP的,方便快速對接傳統的負載均衡,有的希望實現釋出過程中,Pod不重建、支援原地更新的,有的希望能指定任意 Statefulset Pod 更新的,那麼 Kubernetes 如何滿足多樣化的訴求呢?

擴充套件機制

Kubernetes 設計上對外提供了一個強大擴充套件體系,如下圖所示(引用自 kubernetes blog),從 kubectl plugin 到 Aggreated API Server、再到 CRD、自定義排程器、再到 operator、網路外掛(CNI)、儲存外掛(CSI)。一切皆可擴充套件,充分賦能業務,讓各個業務可基於Kubernetes擴充套件機制進行定製化開發,滿足大家的特定場景訴求。

CRD 和 Aggreated API Server

當你遇到 Deployment、StatefulSet 無法滿足你訴求的時候,Kubernetes 提供了 CRD 和 Aggreated API Server、Operator 等機制給你擴充套件 API 資源、結合你特定的領域和應用知識,實現自動化的資源管理和運維任務。

CRD 即 CustomResourceDefinition,是 Kubernetes 內建的一種資源擴充套件方式,在 apiserver 內部整合了 kube-apiextension-server, 不需要在 Kubernetes 叢集執行額外的 Apiserver,負責實現 Kubernetes API CRUD、Watch 等常規API操作,支援 kubectl、認證、授權、審計,但是不支援 SubResource log/exec 等定製,不支援自定義儲存,儲存在 Kubernetes 叢集本身的 etcd 上,如果涉及大量 CRD 資源需要儲存則對 Kubernetes 叢集etcd 效能有一定的影響,同時限制了服務從不同叢集間遷移的能力。

Aggreated API Server,即聚合 ApiServer, 像我們常用的 metrics-server 屬於此類,通過此特性 Kubernetes 將巨大的單 apiserver 按資源類別拆分成多個聚合 apiserver, 擴充套件性進一步加強,新增API無需依賴修改 Kubernetes 程式碼,開發人員自己編寫 ApiServer 部署在 Kubernetes 叢集中, 並通過 apiservice 資源將自定義資源的 group name 和 apiserver 的 service name 等資訊註冊到 Kubernetes 叢集上,當 Kubernetes ApiServer 收到自定義資源請求時,根據 apiservice 資源資訊轉發到自定義的 apiserver, 支援 kubectl、支援配置鑑權、授權、審計,支援自定義第三方 etcd 儲存,支援 subResource log/exec 等其他高階特性定製化開發。

總體來說,CRD提供了簡單、無需任何程式設計的擴充套件資源建立、儲存能力,而 Aggreated API Server 提供了一種機制,讓你能對 API 行為有更精細化的控制能力,允許你自定義儲存、使用 Protobuf 協議等。

增強型 Workload

為了滿足業務上述的原地更新、指定Pod更新等高階特性需求,騰訊內部及社群都提供了相應的解決方案。騰訊內部有經過大規模生產環境檢驗的 StatefulSetPlus(未開源的)和 tkestack TAPP(已開源),社群也還有有阿里的開源專案 Openkruise,pingcap 為了解決 StatefulSet 指定 Pod 更新問題也推出了一個目前還處於試驗狀態的 advanced-statefulset 專案。

StatefulSetPlus 是為了滿足騰訊內部大量傳統業務上 Kubernetes 而設計的, 它在相容 StatefulSet 全部特性的基礎上,支援容器原地升級,對接了 TKE 的 ipamd 元件,實現了固定IP,支援 HPA,支援 Node 不可用時,Pod 自動漂移實現自愈,支援手動分批升級等特性。

Openkruise 包含一系列 Kubernetes 增強型的控制器元件,包括 CloneSet、Advanced StatefulSet、SideCarSet等,CloneSet 是個專注解決無狀態服務痛點的 Workload,支援原地更新、指定 Pod 刪除、滾動更新過程中支援Partition, Advanced StatefulSet 顧名思義,是個加強版的 StatefulSet, 同時支援原地更新、暫停和最大不可用數。

使用增強版的 workload 元件後,你的有狀態服務就具備了傳統虛擬機器、物理機部署模式下的原地更新、固定IP等優越特性。不過,此時你是直接基於 StatefulSetPlus、TAPP 等 workload 容器化你的服務還是基於 Kubernetes 擴充套件機制定義一個自定義資源, 專門用於描述你的有狀態服務各個元件,並基於 StatefulSetPlus、TAPP 等workload 編寫自定義的 operator 呢?

前者適合於簡單有狀態服務場景,它們元件少、管理方便,同時不需要你懂任何 Kubernetes 程式設計知識,無需開發。後者適用於較複雜場景,要求你懂 Kubernetes 程式設計模式,知道如何自定義擴充套件資源、編寫控制器。 你可以結合你的有狀態服務領域知識,基於 StatefulSetPlus、TAPP 等增強型 workload 編寫一個非常強大的控制器,幫助你一鍵完成一個複雜的、多元件的有狀態服務建立和管理工作,並具備高可用、自動擴縮容等特性。

基於 operator 擴充套件

在我們上文的 codis 叢集案例中,就可以選擇通過 Kubernetes 的 CRD 擴充套件機制,自定義一個 CRD 資源來描述一個完整的 codis 叢集,如下圖所示。

通過 CRD 實現宣告式描述完你的有狀態業務物件後,我們還需要通過 Kubernetes 提供的 operator 機制來實現你的業務邏輯。Kubernetes operator 它的核心原理就是控制器思想,從 API Server 獲取、監聽業務物件的期望狀態、實際狀態,對比期望狀態與實際狀態的差異,執行一致性調諧操作,使實際狀態符合期望狀態。

它的核心工作原理如上圖(引用自社群)所示。

  • 通過 Reflector 元件的 List 操作,從 kube-apiserver 獲取初始狀態資料(CRD等)。
  • 從 List 請求返回結構中獲取資源的 ResourceVersion,通過 Watch 機制指定 ResourceVersion 實時監聽 List之後的資料變化。
  • 收到事件後新增到 Delta FIFO 佇列,由 Informer 元件進行處理。
  • Informer 將 delta FIFO 佇列中的事件轉發給 Indexer 元件,Indexer 元件將事件持久化儲存在本地的快取中。
  • operator開發者可通過 Informer 元件註冊 Add、Update、Delete 事件的回撥函式。Informer 元件收到事件後會回撥業務函式,比如典型的控制器使用場景,一般是將各個事件新增到 WorkQueue 中,operator 的各個協調 goroutine 從佇列取出訊息,解析 key,通過 key 從 Informer 機制維護的本地 Cache 中讀取資料。
  • 比如當收到建立一個 Codis CRD 物件的事件後,發現實際無這個物件相關的 Deployment/TAPP 等元件在執行,這時候你就可以通過的 Deployment API 建立 proxy 服務,TAPP API建立Redis服務等。

排程

在解決完如何基於 Kubernetes 內建的 workload 和其擴充套件機制描述、承載你的有狀態服務後,你面臨的第二個問題就是如何確保有狀態服務中“等價”Pod跨故障域部署,確保有狀態服務的高可用性?

首先如何理解“等價” Pod 呢? 在 codis、TDSQL 叢集中,一組 Redis/MySQL 主備例項,負責處理同一個資料分片的請求,通過主備實現高可用。因主備例項 Pod 負責的是同資料分片,因此我們稱之為等價 Pod,生產環境期望它們應跨故障域部署。

其次如何理解故障域?故障域表示潛在的故障影響範圍,可按範圍分為主機級、機架級、交換機級、可用區級等。一組 Redis 主備案例,至少應該實現主機級高可用,任意一個分片所在的主例項所在的節點故障,備例項應自動提升為主,整個 Redis 叢集所有分片仍可提供服務。同樣,在 TDSQL 叢集中,一組 MySQL 例項,至少應該實現交換機、可用區級別容災,以確保核心的儲存服務高可用。

那麼如何實現上面所述等價 Pod 跨故障域部署呢?

答案是排程。 Kubernetes 內建的排程器可根據你的Pod所需資源和排程策略,自動化的將 Pod 分配到最佳節點,同時它還提供了強大的排程擴充套件機制,讓你輕鬆實現自定義排程策略。一般情況下,在簡單的有狀態服務場景下,你可以基於 Kubernetes 提供的親和和反親和高階排程策略,實現 Pod 跨故障域部署。

假設希望通過容器化、高可用部署一個含三節點的 etcd 叢集,故障域為可用區,每個etcd節點要求分佈在不同可用區節點上,我們如何基於 Kubernetes 提供的親和 (affinity) 和反親和 (anti affinity) 特性實現跨可用區部署呢?

親和與反親和

很簡單,我們可以通過給部署 etcd 的 workload 新增如下的反親和性配置,宣告目的 etcd 叢集 Pod 的標籤,拓撲域為 node 可用區,同時是硬親和規則,若 Pod不 滿足規則將無法排程。

那麼排程器又遇到被新增了反親和配置的 Pod 後是如何排程的呢?

affinity:
  PodAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchExpressions:
        - key: etcd_cluster
          operator: In
          values: ["etcd-test"]
      topologyKey: failure-domain.beta.Kubernetes.io/zone

首先排程器監聽到 etcd workload 生成的的待排程 Pod 後,通過反親和配置中的標籤查詢出已排程 Pod 的節點、可用區資訊,然後在排程器的篩選階段,若候選節點可用區與已排程 Pod 可用區一致,則淘汰,最後進入評優階段的節點都是滿足 Pod 跨可用區部署等條件限制的節點,根據排程器配置的評優策略,選擇出一個最優節點,將 Pod 繫結到此節點上,最終實現 Pod 跨可用區部署、容災。

然而在 codis 叢集、TDSQL 分散式叢集等複雜場景中,Kubernetes 自帶的排程器可能就無法滿足你的訴求了,但是它提供瞭如下的擴充套件機制幫助你自定義排程策略,實現各種複雜場景的排程訴求。

自定義排程策略、extend scheduler 等

首先你可以修改排程器的篩選/斷言 (predicates) 和評分/優先順序 (priorities) 策略, 配置滿足你業務訴求的排程策略。比如你希望降低成本,用最小的節點數支撐叢集所有服務,那麼我們需要讓 Pod 儘量優先往滿足其資源訴求、已分配資源較多的節點上排程。 此場景,你就可以通過修改 priorities 策略,配置 MostRequestedPriority 策略,調大權重。

然後你可以基於 Kubernetes 排程器實現 extend scheduler, 在排程器的 predicates 和 priorities 階段,回撥你的擴充套件排程服務,已滿足你的排程訴求。比如你希望負責同一個資料分片的一組 MySQL 或 Redis 主備例項實現跨節點容災,那麼你就可以實現自己的predicates 函式,將同組已排程 Pod 的節點從候選節點中刪除,保證進入 priorities 流程的節點都是滿足你業務訴求的。

接著你可以基於 Kubernetes 的排程器實現自己獨立的排程器,部署獨立的排程器到叢集后,你只需要通過將 Pod的 schedulerName 宣告為你獨立的排程器即可。

scheduler framwork

最後 Kubernetes 在1.15版本中推出了一個新的排程器擴充套件框架,它在排程器的核心流程前後都增加了 hook。選擇待排程 Pod,支援自定義排隊演算法,篩選流程提供了 PreFilter 和 Filter 介面,評分流程增加了 PreScore,Score,NormalizeScore 等介面,繫結流程提供 PreBind 和 Bind,PostBind 三個介面。基於新的排程器擴充套件框架,業務可更加精細化、低成本的控制排程策略,自定義排程策略更加簡單、高效。

高可用

解決完排程問題後,我們的有狀態服務已經可以高可用的部署了。然而高可用部署不代表服務能高可用的對外的提供服務,容器化後我們也許會遇到比傳統物理機、虛擬機器模式部署更多的穩定性挑戰。穩定性挑戰可能來自業務編寫的 operator、Kubernetes 元件、docker/containerd 等執行時元件、linux 核心等,那如何應對以上各種因素導致的穩定性問題呢?

我們在設計上應把 Pod 異常當作常態化案例處理,任一 Pod 異常後,在容器化場景中,我們應當具備自愈的機制。若是無狀態服務,我們只需為業務Pod新增合理的存活和就緒檢查即可,Pod 異常後自動重建,節點故障 Pod 自動漂移到其他節點。然而在有狀態服務場景中,即便承載你有狀態服務的 workload,支援節點故障後 Pod 自動漂移功能,卻也可能會因 Pod 自愈時間過長和資料安全性等無法滿足業務訴求,為什麼呢?

假設在 codis 叢集中,一個 Redis 主節點所在node突然”失聯“了,此時若等待5分鐘才進入自愈流程,那麼對外將造成5分鐘的不可用性, 顯然對重要的有狀態服務場景是無法接受的。即便你縮小節點失聯自愈時間,你也無法保證其資料安全性,萬一此時叢集網路出現了腦裂,失聯節點也在對外提供服務,那麼將出現多個 master 雙寫,最終可能導致資料丟失。

那麼有狀態的服務安全的高可用解決方案是什麼呢? 這取決於有狀態服務本身高可用實現機制,Kubernetes 容器平臺層是無法提供安全的解決方案。常用的有狀態服務高可用解決方案有主備複製、去中心化複製、raft/paxos 等共識演算法,下面我分別簡易闡述三者的區別和優劣勢,以及介紹在容器化過程中的注意事項。

主備複製

像我們上面討論的 codis 叢集案例、TDSQL 叢集案例都是基於主備複製實現的高可用,實現上相比去中心化複製、共識演算法較簡單。主備複製又可分為主備全同步複製、非同步複製、半同步複製。

全同步複製是指主收到一個寫請求後,必須等待全部從節點確認返回後,才能返回給客戶端成功,因此若一個從節點故障,整個系統就會不可用,這種方案為了保證多副本集的一致性,而犧牲了可用性,一般使用不多。

非同步複製是指主收到一個寫請求後,可及時返回給 client,非同步將請求轉發給各個副本, 但是若還未將請求轉發到副本前就故障了,則可能導致資料丟失,但可用性是最高的。

半同步複製介於全同步複製、非同步複製之間,它是指主收到一個寫請求後,至少有一個副本接收資料後,就可以返回給客戶端成功,在資料一致性、可用性上實現了平衡和取捨。

基於主備複製模式實現的有狀態服務,業務需要實現、部署主備切換的 HA 服務,HA服務按實現架構,可分為主動上報型和分散式探測型。主動上報型以 TDSQL 中 MySQL 主備切換為例,各個 MySQL 節點上部署有 agent, 向後設資料儲存叢集 (zookeeper/etcd) 上報心跳,若 master 心跳丟失, HA 服務將快速發起主備切換。分散式探測型以 Redis sentinel 為例,部署奇數個哨兵節點,各個哨兵節點定時探測Redis主備例項的可用性,彼此之間通過 gossip 協議互動探測結果,若對一個主 Redis 節點故障達到多數派認可,那麼就由其中一個哨兵發起主備切換流程。

總體來說,基於主備複製的有狀態服務,在傳統的部署模式,節點故障後,依賴運維、人工替換節點。容器化後的有狀態服務,可通過 operator 實現故障節點自動替換、快速垂直擴容等特性,顯著降低運維複雜度,但是 Pod 可能會發生重建等,應部署負責主備切換的HA服務,負責主備 Pod 的切換,以提高可用性。若業務對資料一致性非常敏感,較頻繁的切換的可能會導致增大丟失資料的概率,可通過使用 dedicated 節點、穩定及較新的執行時和Kubernetes 版本等減少不穩定因素。

去中心化複製

跟主從複製相反的就是去中心化複製,它是指在一個n副本節點叢集中,任意節點都可接受寫請求,但一個成功的寫入需要w個節點確認,讀取也必須查詢至少r個節點。你可以根據實際業務場景對資料一致性的敏感度,設定合適w/r引數。比如你希望每次寫入後,任意client都能讀取到新值,若n是3個副本,你可以將w和r設定為2,這樣當你讀兩個節點時候,必有一個節點含有最近寫入的新值,這種讀我們稱之為法定票數讀 (quorum read)。

AWS 的 dynamo 系統就是基於無中心化的複製演算法實現的,它的優點是節點角色都是平等的,降低運維複雜度,可用性更高,容器化難度更低,無需部署HA元件等,但缺陷是去中心化複製,務必會導致各種寫入衝突,業務需要關注衝突處理等。

共識演算法

基於複製演算法實現的資料庫,為了保證服務可用性,大多數提供的是最終一致性,不管是主從複製還是去中心化複製,都存在一定的缺陷,無法滿足資料強一致、高可用的訴求。

如何解決以上覆制演算法的困境呢?

答案就是 raft/paxos 共識演算法,它最早是基於複製狀態機背景下提出來的,由共識模組、日誌模組、狀態機組成, 如下圖(引用自 Raft 論文)。通過共識模組保證各個節點日誌的一致性,然後各個節點基於同樣的日誌、順序執行指令,最終各個複製狀態機的結果是一致性的。這裡我以 raft 演算法為例,它由 leader 選舉、日誌複製、安全性組成,leader 節點故障後,follower 節點可快速發起新的 leader 選舉,並確保資料安全性,follwer 節點故障後,只要多數節點存活,就不影響叢集整體可用性。

基於共識演算法實現的有狀態服務,典型案例是 etcd/zookeeper/tikv 等,在此架構中,服務本身整合了 leader 選舉演算法、資料同步機制,使得運維和容器化複雜度相比主備複製的服務要顯著降低,容器化更加安全。即便容器化過程中遇上 Bug 導致 leader 節點故障,得益於共識演算法,資料安全和服務可用性幾乎不受任何影響,因此優先推薦將使用共識演算法的有狀態服務進行容器化。

高效能

實現完有狀態服務在 Kubernetes 中更穩的執行的目標後,下一步目標則是追求高效能、更快,而有狀態服務的高能又依託底層容器化網路方案、磁碟 IO 方案。在傳統的物理機、虛擬機器部署模式中,有狀態服務擁有固定的IP、高效能的 underlay 網路、高效能的本地 SSD 磁碟,那麼在容器化後,如何達到傳統模式的效能呢? 我將分別從網路和儲存分別簡易闡述 Kubernetes 的解決方案。

可擴充套件的網路解決方案

首先是可擴充套件、外掛化的網路解決方案。得益於 Google 多年的 Borg 容器化執行經驗和教訓,在 Kubernetes 的網路模型中,每個 Pod 擁有獨立的IP,各個 Pod 可以跨主機通訊而需NAT, 同時 Pod 也可以與 Node 節點實現網路互通。Kubernetes 優秀的網路模型良好的相容了傳統的物理機、虛擬機器業務的網路方案,讓傳統業務上Kubernetes 更加簡單。最重要的是,Kubernetes 提供了開放的 CNI 網路外掛標準,它描述瞭如何為 Pod 分配 IP和實現 Pod 跨節點容器互通,各個開源、雲廠商可以基於自己業務業務場景、底層網路,實現高效能、低延遲的CNI外掛,最終達到跨節點容器互通。

在基於 CNI 實現的各種 Kubernetes 的網路解決方案中,按資料包的收發模式實現可分為 underlay 和 overlay 兩類。前者是直接基於底層網路,實現互聯互通,擁有良好的效能,後者是基於隧道轉發,它是在底層網路的基礎上,加上隧道技術,構建一個虛擬的網路,因此存在一定的效能損失。

這裡我分別以開源的 flannel 和 tke 叢集網路方案為例,闡述各自的解決方案、優缺點。

在 flannel 中,它設計上後端支援 udp、vxlan、host-gw 等多種轉發模式。udp 和 vxlan 轉發模式是基於 overlay隧道轉發模式實現,它支援將原始請求封裝在 udp、vxlan 資料包內,然後基於 underlay 網路轉發給目的容器。udp 是在使用者態進行資料的封解包操作,效能較差,一般用於debug和不支援 vxlan 協議的低版本核心。vxlan 是在核心態完成了資料的封解包操作,效能損失較小。host-gw 模式則是直接通過下發每個子網的IP路由資訊到各個節點上,實現跨主機的 Pod 網路通訊,無需資料包的封解包操作,相比 udp/vxlan,效能最佳,但要求各主機節點的二層網路是連通的。

在tke叢集網路方案中,我們也支援多種網路通訊方案,經歷了從 global route、VPC-CNI 到 Pod 獨立網路卡的三種模式的演進。global route 即全域性路由,每個節點加入叢集時,會分配一個唯一的 Pod cidr, tke 會通過 VPC 的介面下發全域性路由到使用者 VPC 的子機所在的母機上。當使用者 VPC 的容器、節點訪問的ip屬於此 Pod cir 時,就會匹配到此全域性路由規則,轉發到目標節點上。此方案中 Pod CIDR 並不屬於VPC資源,因此它不是 VPC 的一等公民,無法使用 VPC 的安全組等特性,但是其簡單、同時在使用者VPC層不需要任何的資料解封包操作,效能無較大的損失。

為了解決容器 Pod IP 不是 VPC 一等公民而導致一系列 VPC 特性無法使用的問題,tke 叢集實現了 VPC-CNI 網路模式,Pod IP 來自使用者 VPC 的子網,跨節點容器網路通訊、節點與容器通訊與 VPC 內的 CVM 節點通訊原理一致,底層都是基於 VPC 的 GRE 隧道路由轉發實現,資料包在節點內通過策略路由轉發到目標容器。基於此方案,容器 Pod IP 可享受 VPC 的特性,實現CLB直連Pod,固定IP等一系列高階特性。

近期為了滿足遊戲、儲存等業務對容器網路效能更加極致的要求,TKE 團隊又推出了下一代網路方案,Pod 獨佔彈性網路卡的 VPC-CNI 模式,不再經過節點的網路協議棧,極大縮短容器訪問鏈路和延時,並使 PPS 可以達到整機上限。基於此方案我們實現了 Pod 繫結 EIP/NAT,不再依賴節點的外網訪問能力,支援 Pod 繫結安全組,實現Pod級別的安全隔離,詳細可閱讀文章末尾的相關文章。

基於 Kubernetes 的可擴充套件網路模型,業務可以實現特定場景的高效能網路外掛。比如騰訊內部的 tenc 平臺,基於 SR-IOV 技術的實現了 sriov-cni CNI 外掛,它可以給 Kubernetes 提供高效能的二層VLAN網路解決方案。特別是對網路效能要求高的場景,比如分散式機器學習訓練,遊戲後端服務等。

可擴充套件的儲存解決方案

介紹完可擴充套件的網路解決方案後,有狀態服務的另一大核心瓶頸則是高效能儲存IO訴求。 在傳統的部署模式中,有狀態服務一般使用的是本地硬碟,並根據服務的型別、規格、對外的 SLA,選擇 HDD、SSD 等不同型別的磁碟。 那麼在 Kubernetes 中如何滿足不同場景下的儲存訴求呢?

在 Kubernetes 儲存體系中,此問題被拆分成若干個子問題來優雅解決,並具備良好的可擴充套件性、可維護性,無論是本地盤、還是雲盤、NFS等檔案系統都可基於其擴充套件實現相應的外掛, 並實現了開發、運維職責分離。

那麼 Kubernetes 的儲存體系是如何構建的呢?

我通過如何給你的有狀態Pod應用掛載一個資料儲存盤為案例,來介紹 Kubernetes 的可擴充套件儲存體系,它可以分為以下步驟:

  • 應用如何申請一個儲存盤呢?(消費者)

  • Kubernetes 儲存體系是如何描述一個儲存盤的呢?人工建立儲存盤呢還是自動化按需建立儲存盤?(生產者)

  • 如何將儲存資源池的盤與儲存盤申請者的訴求進行匹配?(控制器)

  • 如何描述儲存盤的型別、資料刪除策略、以及此型別盤的服務提供者資訊呢?(storageClass)

  • 如何實現對應的儲存資料卷外掛?(FlexVolume、CSI)

首先 Kubernetes 中提供了一個名為PVC的資源,描述應用申請的儲存盤的型別、訪問模式、容量規格,比如你想給etcd服務申請一個儲存類為cbs, 大小100G的雲盤,你可以建立一個如下的PVC。

apiVersion: v1
kind: PersistentVolumeClaim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
  storageClassName: cbs

其次 Kubernetes 中提供了一個名為 PV 的資源,描述儲存盤的型別、訪問模式、容量規格,它對應一塊真實的磁碟,支援通過人工和自動建立兩種模式。下圖描述的是一個 100G 的 cbs 硬碟。

apiVersion: v1
kind: PersistentVolume
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 100Gi
  persistentVolumeReclaimPolicy: Delete
  qcloudCbs:
    cbsDiskId: disk-r1z73a3x
  storageClassName: cbs
  volumeMode: Filesystem

接著,當應用建立一個 PVC 資源時,Kubernetes 的控制器就會嘗試將其與PV進行匹配,儲存盤的型別是否一致、PV的容量大小是否滿足 PVC 的訴求,若匹配成功,此 PV 的狀態會變成繫結, 控制器會進一步的將此PV對應的儲存資源attach到應用 Pod 所在節點上,attach 成功後,節點上的 kubelet 元件會將對應的資料目錄掛載到儲存盤上,進而實現讀寫。

以上就是應用申請一個盤的流程,那麼在容器中如何通過 PV/PVC 這套體系實現支援多種型別的塊儲存和網路檔案系統呢?比如塊儲存服務支援普通 HHD 雲盤,SSD 高效能雲盤,SSD 雲盤,本地盤等,遠端網路檔案系統支援NFS等。其次是Kubernetes控制器如何按需動態的去建立PV呢?

為了支援多種型別的儲存訴求,Kubernetes 提供了一個 StorageClass 的資源來描述一個儲存類。它描述了儲存盤的類別、繫結和刪除策略、由什麼服務元件提供資源建立。比如高效能版和基礎版的 MySQL 服務依賴不同型別的儲存磁碟,你只需要建立 PVC 的時候填寫相應的 storageclass 名字即可。

最後,Kubernetes 為了支援開源社群、雲廠商眾多的儲存資料卷,提供了儲存資料卷擴充套件機制,從早期的 in-tree 的內建資料卷、到 FlexVolume 外掛、再到現在已經 GA 的的容器化儲存 CSI 外掛機制, 儲存服務提供商可將任意的儲存系統整合到Kubernetes儲存體系中。比如 storage cbs 的 provisioner 是騰訊雲的 TKE 團隊,我們會基於 Kubernetes 的 flexvolume/CSI 擴充套件機制,通過騰訊雲 CBS 的 API 實現建立、刪除cbs硬碟。

apiVersion: storage.Kubernetes.io/v1
kind: StorageClass
parameters:
  type: cbs
provisioner: cloud.tencent.com/qcloud-cbs
reclaimPolicy: Delete
volumeBindingMode: Immediate

為了滿足有狀態等服務對磁碟IO效能的極致追求,Kubernetes 基於以上介紹的 PV/PVC 儲存體系,實現了 local pv 機制,它可避免網路 IO 開銷,讓你的服務擁有更高的IO讀寫效能。local pv 核心是通過將本地盤、lvm 分割槽抽象成 PV,使用 local pv 的 Pod,依賴延遲繫結特性實現準確排程到目標節點。

local pv的關鍵核心技術點是容量隔離(lvm、xfs quota)、IO隔離(cgroup v1一般要定製核心,cgroup v2支援buffer io等)、動態provision等問題,為了解決以上或部分痛點,社群也誕生了一系列的開源專案,如TopoLVM(支援動態provision、lvm),sig-storage-local-static-provisioner等專案,各雲廠商如騰訊內部也有相應的local pv解決方案。總體而言,local pv適用於磁碟io敏感型的etcd、MySQL、tidb等儲存服務,如pingcap的tidb專案就推薦在生產環境使用local pv。

local pv 的缺點是節點故障後,資料無法訪問、可能丟失、無法垂直擴容(受限於節點磁碟容量等)。 因此這對有狀態服務本身和其 operator 提出了更高要求,服務本身需要通過主備複製協議和共識演算法,保證資料安全性。任一節點故障後,operator 能及時擴容新節點,從冷備、leader 快照進行資料恢復。如 tidb 的 tikv 服務,當檢測到例項有異常後,會自動擴容新例項,通過 raft 協議完成資料的複製等。

混沌工程

通過以上技術方案,解決了負載型別選型、自定義資源擴充套件、排程、高可用、高效能網路、高效能儲存、穩定性測試等一系列痛點後,我們可基於 Kubernetes 的構建穩定、高可用、彈性伸縮的有狀態服務。

那麼如何驗證容器化後的有狀態服務穩定性呢?

社群提供了多個基於 Kubernetes 實現的混沌工程開源專案,比如 pingcap 的 chaos-mesh, 提供了 Pod chaos/Network chaos/IO chaos 等多種故障注入。基於 chaos mesh,你可以快速注入 Pod 故障、磁碟IO、網路IO等異常到叢集中任意 Pod,幫助你快速發現有狀態服務本身和 operator Bug、檢驗叢集的穩定性。 在 TKE 團隊中,我們基於 chaos mesh 排查、復現 etcd Bug, 壓測 etcd 叢集的穩定性,極大的降低了我們復現複雜 Bug 的難度,幫助我們提升 etcd 核心的穩定性。

總結

本文通過從有狀態叢集中的各個元件 workload 選型、擴充套件機制的選擇,介紹瞭如何使用 Kubernetes 的描述、部署你的有狀態服務。有狀態服務出於其特殊性,資料安全、高可用、高效能是其核心目標,為了保證服務的高可用,可通過排程和HA服務來實現。通過 Kubernetes 的多種排程器擴充套件機制,你可以將你的有狀態服務的等價 Pod 完成跨故障域部署。通過主備切換服務和共識演算法,你可以完成主節點故障後,備節點自動提升為主,以保證服務的高可用性。高效能主要取決於網路和儲存效能,Kubernetes 提供了 CNI 網路模型和 PV/PVC 儲存體系、CSI 擴充套件機制來滿足各種業務場景下的定製需求。最後介紹了混沌工程在有狀態服務中的應用,通過混沌工程你可以模擬各類異常場景下,你的有狀態服務容錯性,幫助你檢驗和提升系統的穩定性。

參考資料

相關文章