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 所作的相關工做。node

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

爲何須要 Operatorgithub

無狀態和有狀態redis

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 管理有狀態應用的能力方便地暴露給用戶。

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

一、針對 Operator 平臺

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

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

・ 能夠管理經過公共的 Operator 和 Namespace 私有的 Operator 建立的實例。用戶能夠用咱們提供的公用 Operator,也能夠把 Operator 部署到本身的 NameSpaces,給本身的項目提供服務,但這兩種 Operator 建立的應用實例均可以經過 Operator 控制檯管理。

・ 能夠無限添加 Operator。

二、針對 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/red...

。提供:

  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/red...

總結

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

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

相關文章
相關標籤/搜索