Redis Cluster Operator 容器化方案

對於 Redis Cluster 的容器化,我行已在 Redis Paas 中做了先期的探索和使用。在 Redis Paas 中實現了 Redis 哨兵模式、集羣模式的容器化部署,結合 IPAAS 使得項目可依據自身需要自助申請並實時交付。但是,在 Redis Paas 使用過程中,出現了 Redis Cluster 主從 Pod 被調度在同一節點,Redis Master 節點所在的容器發生重啓導致數據丟失的問題,而且在功能上難以支持 Redis 集羣擴縮容、主從節點差異化配置、定時持久化等需求。本文針對 Redis Cluster 容器化遇到的問題進行分析,並討論使用 Operator 的解決方案。

Redis Cluster 容器化問題

Redis Cluster 主從分佈

Redis Cluster 從節點除了分擔主節點的讀壓力,也是 Redis 高可用性重要的一部分。Redis 作爲一個內存數據庫,爲了保證高性能不會實時將數據刷盤進行持久化存儲,所以 Redis 節點宕機可能會造成數據丟失。在集羣模式中,分片中的從節點不僅可以分擔主節點的讀壓力,而且也具有保證數據完整性的重要作用。所以在容器化部署時,同一分片的主從節點 Pod 不能調度在同一 K8S 節點,防止由於 K8S 節點宕機分片整體下線導致數據丟失。

Kubernets 中可以使用 Pod 反親和性配置,將同一類型的 Pod 分散在不同的節點。下面 yaml 配置定義所有符合 Redis 標籤的 Pod 以軟反親和性的方式分佈部署在不同的節點上。

spec:  
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - podAffinityTerm:
          labelSelector:
            matchLabels:
              REDIS_POD_LABEL: REDIS_POD_VALUE
          topologyKey: kubernetes.io/hostname

先期 Redis Paas 中,同一項目的所有 Redis Pod 由一個 Statefulset 管理,對同一 Statefulset 中所有 Pod 配置反親和性。在實際使用中發現,這種配置方法在項目 Redis Pod 比較多或者容器平臺節點比較少的情景下,會出現同一分片的主從 Pod 被調度在同一主機節點的情況。在 Redis Cluster Operator 的設計中,將每一個 Redis Cluster 每個分片部署成一個 Statefulset 並在 Statefulset 內配置 Pod 反親和性,這種部署方法能很好的保證 Redis Cluster 分片主從 Pod 處於不同節點。

Redis Master 容器快重啓

在容器環境中部署的 Redis Cluster,數據丟失除了可能發生在主從節點同時下線的情況外,還存在另外一種情景。

Redis Cluster 中,Master 節點下線必須經過 cluster-node-timeout 時間纔會啓動 failover 機制。在容器環境中,如果主節點所在的容器由於某種原因被殺死,再被 Kubernetes 自動拉起,這個時間非常短暫以至於集羣不會啓動 failover 流程。但在這種情況下,Redis 進程下線又上線原先內存中的數據會被全部清空,Slave 節點不執行 failover 流程繼續同步 Master 會導致整個分片數據丟失。該種情況某種意義上可以看作是 Kubernetes 的高可用機制和 Redis Cluster 的發生了衝突。在設計 Operator 方案時,爲避免類似情況的發生,我們需要明確 Kubernetes、Operator 和 Redis 在整個方案中的作用,劃定 Kuberentes、Operator 和 Redis Cluster 的功能邊界:

  1. Kubernetes 和 Operator 不去介入 Redis Cluster 的高可用機制,若有衝突以 Redis Cluster 的爲準

  2. Redis 容器啓動和 Redis 進程啓動相分離,Kubernetes 只負責爲 Redis 分配資源,Operator 監聽 Redis 容器啓動情況並在合適的時機啓動 Redis 進程,將 Redis 加入集羣

在先前 Redis Paas 中,Redis 進程隨容器啓動而啓動,Redis 進程自動讀取本地 nodes.conf 文件重新回到集羣。在 Operator 中,需要監聽 Redis 容器啓動,對節點加入集羣的時機做把控:

前兩種情況可以保證 Redis 的數據安全,第三種情況由於分片全部下線,若沒有在退出時對數據進行持久化保存則無法保證數據完整。除此之外,還有一種極壞的情況——Redis Cluster 中存活的 Master 不足一半,此時 Redis Cluster 的 failover 機制已無法正常運行。此種情況可參照上述第三種情況處理,在該種情況下可能需要執行 cluster failover takeover 命令才能使集羣恢復正常,Operator 不做高危險操作,此時由運維人員手工介入處理。

Redis 服務對外暴露

容器化環境不同於物理環境或者虛擬機環境,容器環境的容器 IP 不固定,而且容器平臺使用 overlay 網絡,網絡 IP 對集羣外不可見。在這種情景下,若應用與 Redis 同集羣部署,可以使用爲 Redis Cluster 建立的 Service 來訪問,但是如果有集羣外訪問的需求,該種方式則不能滿足。對此,需要爲每個單獨的 Redis Pod 建立 NodePort 型的 Service,並且在 Redis 中增加如下配置:

cluster-announce-port REDIS_SERVICE-NODEPORT
cluster-announce-bus-port GOSSIP-NODEPORT
cluster-announce-ip HOSTIP

Redis Cluster Operator

Operator 的核心是 CRD(CustomResourceDefinition) 和自定義 Controller。在項目中,我們使用 Kubebuilder 進行 Redis Cluster Operator 開發,項目整體結構如下所示:

RedisCluster CRD

建立 RedisCluster CRD 資源,首先將 Redis Cluster 的需求進行抽象,如下所示:

spec:
  masterSize: 3
  clusterReplicas: 2
  exposeService: true
  image: redis:6.0.4
  dbSize: 4Gi

該 spec 描述了部署一個 Redis Cluster 的基本需求,包含分片數、每個分片從副本數量、是否需要集羣外訪問、使用的 Redis 鏡像版本和每個 Redis 節點的最大內存大小。

然後,我們使用 CRD 將該需求定義擴展到 Kubernetes,成爲 Kubernetes 支持的資源:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: redisclusters.redis.cmbc.com.cn
spec:
  group: redis.cmbc.com.cn
  version: v1alpha1
  names:
    kind: RedisCluster
    listKind: RedisClusterList
    plural: redisclusters
    singular: rediscluster
  scope: Namespaced

該 yaml 文件定義了 Kubernetes 支持的一個資源,資源的 Group 是 redis.cmbc.com.cn,Version 是 v1alpha1,Kind 是 RedisCluster。

接下來就可以使用如下 yaml 在 Kubernetes 中創建 RedisCluster 資源的實例:

apiVersion: redis.cmbc.com.cn/v1alpha1
kind: RedisCluster
metadata:
  name: demorediscluster
  namespace: redispaas
spec:
  ...

RedisCluster Controller

在 Kubernetes 中註冊了種類爲 RedisCluster 的資源後,可以使用 kubectl apply/create 命令在容器集羣中創建 RedisCluster 的實例。此時,容器集羣僅僅做到了 “認識” 該資源,而對該資源的 spec 內容的解析和實際的管理需要開發 Controller。

Operator 的理念是將專業領域知識注入 Kubernetes。在 Redis Cluster Operator 中,Controller 通過協調操作 Redis 和 Kubernetes 完成 Redis 集羣的創建和維持。如上圖所示,Controller 部分包含兩層:

  1. 組件管理層。該部分使用 Redis 和 Kubernetes 客戶端,分別完成對 Redis 集羣的管理和對 Kubernetes 原生資源的操縱,並形成調用接口

  2. 在組件管理層上,根據不同的需求和資源狀態,組合調用 Redis 和 Kubernetes 管理接口,實現功能需求

Controller 操作本質上是一個狀態機操作,整體操作流程如下所示:

對於一個三分片、每個分片一個 Slave 的 Redis Cluster 需求,Controller 按照下圖部署集羣並保持集羣狀態:

  1. 每個分片部署一個 Statefulset,並配置 Pod 反親和性,保證分片主從節點不被調度在同一主機上

  2. 使用 Configmap 爲所有 Redis Pod 注入配置文件

  3. 使用 Secret 保存 Redis 密碼

  4. 爲某個 Redis Cluster 建立一個 ClusterIP 型的 Service,集羣內通過 Service 訪問

  5. 若有集羣外訪問需求,則爲每個 Pod 建立 NodePort 型的 Service,集羣外通過 NodePort 進行訪問

作者簡介

孟玉立,中國民生銀行信息科技部開源軟件支持組工程師, 目前主要負責 Kubernetes、Redis 的源碼研究和工具開發等相關工作。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UzrLSrjDJV1qpqgAVItoHg