UCloud 基於 Kubernetes Operator 的服務化實踐

KUN(Keep UCloud Nimble)是面向 UCloud 內部、基於 Kubernetes 打造的容器服務平臺,旨在提升內部研發效率,幫助改善、規範研發流程。在 KUN 平臺的建設過程中,內部用戶對於一些基礎通用的分佈式軟件如 Redis、Kafka 有強需求,但又不想操心其部署及運維。KUN 團隊在分析這些痛點後,決定利用 Kubernetes Operator 的能力,並彌補了開源 Operator 的一些不足,將 Operator 產品化來幫助用戶部署和管理這些分佈式、帶狀態的應用。通過 Operator 服務化,KUN 平臺擴充了 Kubernetes 交付 Pod、PVC、SVC 的能力,能夠快速交付 Redis 等分佈式、帶狀態的系統,提供了一個平臺之上的平臺。

在這篇文章裏,我們主要來聊一下 Operator 對於 Kubernetes 的價值以及我們團隊基於 Operator 所做的相關工作。

Operator 是什麼,解決了什麼問題

爲什麼需要 Operator

無狀態和有狀態

2014-2015 年容器和微服務的出現,爲軟件開發和基礎架構帶來了巨大的創新和挑戰。容器提供了隔離和限制,同時容器的狀態是易失的,它對自己外部的狀態和數據不關心,專注於單一的服務,比如 Web 應用、日誌服務、業務程序、緩存等。這些服務都能作爲容器交付和運行,而一旦容器數量形成規模,管理的難度也越來越大。

Kubernetes 作爲容器編排框架,可以減輕配置、部署、管理和監控大規模容器應用的負擔。事實上早期的 Kubernetes 非常善於管理無狀態的應用程序,比如 Kubernetes 提供的 Deployment 控制器。它認爲所有的 Pod 都是完全一樣的,Pod 間沒有順序和依賴,擴容的時候就根據模板創建一個一樣的新的應用,也可以任意刪除 Pod。但對於像數據庫這樣的有狀態的應用程序,添加刪除實例可能需要不同的節點做不同的配置,與已有的集羣進行通信協商等,這些操作通常需要我們人工來干預,這就會增加運維的負擔,並且增加出錯的可能性,最重要的是它消除了 Kubernetes 的一個主要賣點:自動化。

這是一個大問題,那麼如何在 Kubernetes 中管理有狀態的應用程序呢?

StatefulSet 的價值和不足 

Kubernetes 的 1.5 版本開始出現了 StatefulSet,StatefulSet 提供了一系列資源來處理有狀態的容器,比如:volume,穩定的網絡標識,從 0 到 N 的順序索引等。通過爲 Pod 編號,再使用 Kubernetes 裏的兩個標準功能:Headless Service 和 PV/PVC,實現了對 Pod 的拓撲狀態和存儲狀態的維護,從而讓用戶可以在 Kubernetes 上運行有狀態的應用。

然而 Statefullset 只能提供受限的管理,通過 StatefulSet 我們還是需要編寫複雜的腳本通過判斷節點編號來區別節點的關係和拓撲,需要關心具體的部署工作,並且一旦你的應用沒辦法通過上述方式進行狀態的管理,那就代表了 StatefulSet 已經不能解決它的部署問題了。

既然 StatefulSet 不能完美的勝任管理有狀態應用的工作,那還有什麼優雅的解決方案呢?答案是 Operator。Operator 在 2016 年由 CoreOS 提出,用來擴充 Kubernetes 管理有狀態應用的能力。

Operator 核心原理

解釋 Operator 不得不提 Kubernetes 中兩個最具價值的理念:“聲明式 API” 和 “控制器模式”。“聲明式 API” 的核心原理就是當用戶向 Kubernetes 提交了一個 API 對象的描述之後,Kubernetes 會負責爲你保證整個集羣裏各項資源的狀態,都與你的 API 對象描述的需求相一致。Kubernetes 通過啓動一種叫做 “控制器模式” 的無限循環,WATCH 這些 API 對象的變化,不斷檢查,然後調諧,最後確保整個集羣的狀態與這個 API 對象的描述一致。

比如 Kubernetes 自帶的控制器:Deployment,如果我們想在 Kubernetes 中部署雙副本的 Nginx 服務,那麼我們就定義一個 repicas 爲 2 的 Deployment 對象,Deployment 控制器 WATCH 到我們的對象後,通過控制循環,最終會幫我們在 Kubernetes 啓動兩個 Pod。

Operator 是同樣的道理,以我們的 Redis Operator 爲例,爲了實現 Operator,我們首先需要將自定義對象的說明註冊到 Kubernetes 中,這個對象的說明就叫 CustomResourceDefinition(CRD),它用於描述我們 Operator 控制的應用:redis 集羣,這一步是爲了讓 Kubernetes 能夠認識我們應用。然後需要實現自定義控制器去 WATCH 用戶提交的 redis 集羣實例,這樣當用戶告訴 Kubernetes 我想要一個 redis 集羣實例後,Redis Operator 就能夠通過控制循環執行調諧邏輯達到用戶定義狀態。

所以 Operator 本質上是一個個特殊應用的控制器,其提供了一種在 Kubernetes API 之上構建應用程序並在 Kubernetes 上部署程序的方法,它允許開發者擴展 Kubernetes API,增加新功能,像管理 Kubernetes 原生組件一樣管理自定義的資源。如果你想運行一個 Redis 哨兵模式的主從集羣或者 TiDB 集羣,那麼你只需要提交一個聲明就可以了,而不需要關心部署這些分佈式的應用需要的相關領域的知識,Operator 本身可以做到創建應用、監控應用狀態、擴縮容、升級、故障恢復,以及資源清理等,從而將分佈式應用的使用門檻降到最低。

Operator 核心價值

在這裏我們總結一下 Operator 的價值:

・ Operator 擴展了 Kubernetes 的能力;

・ Operator 將人類的運維知識系統化爲代碼;

・ Operator 以可擴展、可重複、標準化的方式實現目標;

・ Operator 減輕開發人員的負擔。

Operator 服務化目標

聊完 Operator 的能力和價值我們把目光轉向 KUN 上的 Operator 平臺。前面說過,用戶想在 Kubernetes 中快速的運行一些分佈式帶狀態的應用,但是他們本身不想關心部署、運維,既然 Operator 可以靈活和優雅的管理有狀態應用,我們的解決方案就是基於 Operator 將 Kubernetes 管理有狀態應用的能力方便地暴露給用戶。

核心的的目標主要有兩方面:

1、針對 Operator 平臺

・ 提供一個簡單易用的控制檯供用戶使用,用戶只需要點點鼠標就能快速拉起有狀態應用。並且能在控制檯上實時看到應用部署的進度和事件,查看資源,更新資源等。

・ 通過模板提交聲明,參數可配置化,創建應用的參數通用化,將應用名稱等通用配置和應用參數(如:redis 的 maxclients、timeout 等參數)解耦。這樣帶來的好處就是不同的 Operator 可以共用創建頁面,而不需要爲每種 Operator 定製創建頁面,同時 Operator 暴露出更多的應用配置參數時,前端開發也不需關心,由後端通過 API 返回給前端參數,前端渲染參數,用戶修改參數後,通過 API 傳遞到後端,後端將參數與模板渲染成最終的實例聲明提交到 Kubernetes 中,節省了前端開發時間。

・ 可以管理通過公共的 Operator 和 Namespace 私有的 Operator 創建的實例。用戶可以用我們提供的公用 Operator,也可以把 Operator 部署到自己的 NameSpaces,給自己的項目提供服務,但這兩種 Operator 創建的應用實例都可以通過 Operator 控制檯管理。

・ 可以無限添加 Operator。

2、針對 Operator 控制器

・ 拉起分佈式集羣,自動運配置、運維;・ 可以動態更改所控制應用參數;

・ 控制器本身需要無狀態,不能依賴外部數據庫等;

・ 實時更新狀態,維護狀態,推送事件;

・ 可以運行在集羣範圍,也能運行在單 NameSpace,並且可以共存,不能衝突;

針對這些設計目標最終我們的 Operator 控制檯如下:

同時我們爲 Operator 控制檯定製了第一個 Operator:Redis Operator,未來會推出更多的 Operator,接下來我們就來看下 Redis Operator 的實現。

Redis Operator

Redis 集羣模式選型

我們知道 Redis 集羣模式主要有主從模式、哨兵模式、Redis 官方 Cluster 模式及社區的代理分區模式。

分析以上幾種模式,主從模式的 Redis 集羣不具備自動容錯和恢復功能,主節點和從節點的宕機都會導致讀寫請求失敗,需要等待節點修復才能恢復正常;而 Redis 官方 Cluster 模式及社區的代理分區模式只有在數據量及併發數大的業務中才有使用需求。哨兵模式基於主從模式,但是因爲增加了哨兵節點,使得 Redis 集羣擁有了主從切換,故障轉移的能力,系統可用性更好,而且客戶端也只需要通過哨兵節點拿到 Master 和 Slave 地址就能直接使用。因此我們決定爲 Kun Operator 平臺提供一個快速創建哨兵模式的 Redis 集羣的 Redis Operator。

開源 Operator 的不足

目前已經有一些開源的 Redis Operator,通過對這些 Operator 分析下來,我們發現都不能滿足我們的需求,這些開源的 Operator:

・ 不能設置 Redis 密碼。

・ 不能動態響應更改參數。

・ 沒有維護狀態,推送事件。

・ 不能在開啓了 istio 自動注入的 Namespace 中啓動實例。

・ 只能運行在集羣或者單 Namespace 模式。

改進工作

當前我們定製開發的 Redis Operator 已經在 Github 上開源

https://github.com/ucloud/redis-operator

。提供:

1. 動態響應更改 Redis 配置參數。

2. 實時監控集羣狀態,並且推送事件,更新狀態。

3. 誤刪除節點故障恢復。

4. 設置密碼。

5. 打開關閉持久化快捷配置。

6. 暴露 Prometheus Metrics。

使用 Redis Operator 我們可以很方便的起一個哨兵模式的集羣,集羣只有一個 Master 節點,多個 Slave 節點,假如指定 Redis 集羣的 size 爲 3,那麼 Redis Operator 就會幫我們啓動一個 Master 節點,兩個 Salve 節點,同時啓動三個 Sentinel 節點來管理 Redis 集羣:

Redis Operator 通過 Statefulset 管理 Redis 節點,通過 Deployment 來管理 Sentinel 節點,這比管理裸 Pod 要容易,節省實現成本。同時創建一個 Service 指向所有的哨兵節點,通過 Service 對客戶端提供查詢 Master、Slave 節點的服務。最終,Redis Operator 控制循環會調諧集羣的狀態,設置集羣的拓撲,讓所有的 Sentinel 監控同一個 Master 節點,監控相同的 Salve 節點,Redis Operator 除了會 WATCH 實例的創建、更新、刪除事件,還會定時檢測已有的集羣的健康狀態,實時把集羣的狀態記錄到 spec.status.conditions 中:

status: conditions: - lastTransitionTime: "2019-09-06T11:10:15Z" lastUpdateTime: "2019-09-09T10:50:36Z" message: Cluster ok reason: Cluster available status: "True" type: Healthy - lastTransitionTime: "2019-09-06T11:12:15Z" lastUpdateTime: "2019-09-06T11:12:15Z" message: redis server or sentinel server be removed by user, restart reason: Creating status: "True" type: Creating

爲了讓用戶通過 kubectl 快速查看 redis 集羣的狀態,我們在 CRD 中定義瞭如下的 additionalPrinterColumns:

additionalPrinterColumns: - JSONPath: .spec.size description: The number of Redis node in the ensemble name: Size type: integer - JSONPath: .status.conditions[].type description: The status of Redis Cluster name: Status type: string - JSONPath: .metadata.creationTimestamp name: Age type: date

由於 CRD 的 additionalPrinterColumns 對數組類型支持不完善,只能顯示數組的第一個元數據,所以需要將 spec.status.conditions 中的狀態按時間倒序,最新的狀態顯示在上方,方便用戶查看最新的狀態。同時用戶也可以通過 kubectl 命令直接查看集羣的健康狀況:

$ kubectl get redisclusterNAME SIZE STATUS AGEtest 3 Healthy d

cluster-scoped 和 namespace-scoped

我們在 WATCH Redis 集羣實例的新建、更新、刪除事件時,添加了過濾規則,shoudManage 方法會檢測實例是否含有 redis.kun/scope: cluster-scoped 這條 annotation,如果含有這條 annotation 並且 Redis Operator 工作在全局模式下(WATCH 了所有的 Namespace),那麼這個實例的所有事件纔會被 Operator 所接管。

Pred := predicate.Funcs{UpdateFunc: func(e event.UpdateEvent) bool {// returns false if redisCluster is ignored (not managed) by this operator.if !shoudManage(e.MetaNew) {return false}log.WithValues("namespace", e.MetaNew.GetNamespace(), "name", e.MetaNew.GetName()).Info("Call UpdateFunc")// Ignore updates to CR status in which case metadata.Generation does not changeif e.MetaOld.GetGeneration() != e.MetaNew.GetGeneration() {log.WithValues("namespace", e.MetaNew.GetNamespace(), "name", e.MetaNew.GetName()).Info("Generation change return true")return true}return false},DeleteFunc: func(e event.DeleteEvent) bool {// returns false if redisCluster is ignored (not managed) by this operator.if !shoudManage(e.Meta) {return false}log.WithValues("namespace", e.Meta.GetNamespace(), "name", e.Meta.GetName()).Info("Call DeleteFunc")metrics.ClusterMetrics.DeleteCluster(e.Meta.GetNamespace(), e.Meta.GetName())// Evaluates to false if the object has been confirmed deleted.return !e.DeleteStateUnknown},CreateFunc: func(e event.CreateEvent) bool {// returns false if redisCluster is ignored (not managed) by this operator.if !shoudManage(e.Meta) {return false}log.WithValues("namespace", e.Meta.GetNamespace(), "name", e.Meta.GetName()).Info("Call CreateFunc")return true},}// Watch for changes to primary resource RedisClustererr = c.Watch(&source.Kind{Type: &redisv1beta1.RedisCluster{}}, &handler.EnqueueRequestForObject{}, Pred)if err != nil {return err}

通過識別 annotation,Redis Operator 可以運行在單個 Namespace 下,也可以運行在集羣範圍,並且單 Namespace 和集羣範圍的 Operator 不會互相干擾,各司其職。

快速持久化

我們還瞭解到用戶使用 Redis 時,有一些使用場景是直接將 Redis 當做數據庫來用,需要持久化配置,而有些只是當做緩存,允許數據丟失。爲此我們特意在 Redis 集羣的 CRD 中添加了快速持久化配置的開關,默認爲啓用,這會爲用戶自動開啓和配置 RDB 和 AOF 持久化,同時結合 PVC 可以將用戶的數據持久化起來。當節點故障,被誤刪除時數據也不會丟失,並且 PVC 默認不會跟隨 Redis 集羣的刪除而刪除,當用戶在相同 Namespace 下啓動同名的 Redis 集羣時,又可以使用上次的 PVC,從而恢復數據。

podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchLabels: app.kubernetes.io/component: redis app.kubernetes.io/managed-by: redis-operator app.kubernetes.io/name: test app.kubernetes.io/part-of: redis-cluster redis.kun/v1beta1: prj-shu_test topologyKey: kubernetes.io/hostname weight: 100

爲了讓 Redis 擁有更高的可用性,我們爲 Redis 節點提供了設置 node affinity, pod anti affinity 的能力,可以靈活的控制 Reids 數據節點跑在不同 Node 或者不同的數據中心,做到跨機房容災。如上所示,Redis Operator 缺省情況下會爲每個 Pod 注入 podAntiAffinity,讓每個 redis 服務儘量不會運行在同一個 node 節點。

監控

生產級別的應用離不開監控,Operator 中還內置了 Prometheus Exporter,不光會將 Operator 自身的一些 Metrics 暴露出來,還會將 Operator 創建的每一個 Reids 集羣實例的狀態通過 Metrics 暴露出來。

# HELP redis_operator_controller_cluster_healthy Status of redis clusters managed by the operator.# TYPE redis_operator_controller_cluster_healthy gaugeredis_operator_controller_cluster_healthy{name="config",namespace="xxxx"} 1redis_operator_controller_cluster_healthy{name="flows-redis",namespace="yyyy"} 1# HELP rest_client_requests_total Number of HTTP requests, partitioned by status code, method, and host.# TYPE rest_client_requests_total counterrest_client_requests_total{code="200",host="[2002:xxxx:xxxx:1::1]:443",method="GET"} 665310rest_client_requests_total{code="200",host="[2002:xxxx:xxxx:1::1]:443",method="PATCH"} 82415rest_client_requests_total{code="200",host="[2002:xxxx:xxxx:1::1]:443",method="PUT"} 4.302288e+06rest_client_requests_total{code="201",host="[2002:xxxx:xxxx:1::1]:443",method="POST"} 454rest_client_requests_total{code="404",host="[2002:xxxx:xxxx:1::1]:443",method="GET"} 1rest_client_requests_total{code="404",host="[2002:xxxx:xxxx:1::1]:443",method="PATCH"} 235rest_client_requests_total{code="409",host="[2002:xxxx:xxxx:1::1]:443",method="POST"} 2rest_client_requests_total{code="409",host="[2002:xxxx:xxxx:1::1]:443",method="PUT"} 184# HELP workqueue_adds_total Total number of adds handled by workqueue# TYPE workqueue_adds_total counterworkqueue_adds_total{name="rediscluster-controller"} 614738# HELP workqueue_depth Current depth of workqueue# TYPE workqueue_depth gaugeworkqueue_depth{name="rediscluster-controller"} 0# HELP workqueue_longest_running_processor_microseconds How many microseconds has the longest running processor for workqueue been running.# TYPE workqueue_longest_running_processor_microseconds gaugeworkqueue_longest_running_processor_microseconds{name="rediscluster-controller"} 0

這還不夠,我們還爲每個 Redis 節點提供了單獨暴露 Metrics 的能力,用戶可以在啓動 redis 集羣的時候爲每個 redis 節點注入單獨的 Exporter,這樣每個集羣的每個 Redis 數據節點都能被我們單獨監控起來,結合 Prometheus 和 Alter Manger 可以很方便將 Operator 以及 Operator 創建的實例監控起來。

結合 Operator 的運維、Statefulset 的能力加上 Sentinel 的能力,等於說爲 Redis 集羣加了三重保險,可以確保集羣的高可用。

UCloud 自研的 Redis Operator 目前已正式開源,詳細實現請參考

https://github.com/ucloud/redis-operator

總結

通過 Operator 服務化,KUN 平臺可以向用戶交付更多複雜的分佈式應用,真正做到開箱即用。開發人員可以專心業務實現,而不需要學習關係大量的運維部署調優知識,推進了 Dev、Ops、DevOps 的深度一體化。運維經驗、方案和功能通過代碼的方式進行固化和傳承,減少人爲故障的概率,降低了使用有狀態應用的門檻,極大了提升了開發人員的效率。

關注 “UCloud 技術”,後臺回覆 “粉絲” 進粉絲交流羣

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