本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

StatefulSet:在 Kubernetes 中輕鬆執行和擴充套件有狀態應用程式

編者按:這篇博文是關於 Kubernetes 1.5 新功能系列深度文章的一部分。

在最新版本 Kubernetes 1.5 中,我們已將以前稱為 PetSet 的功能以 StatefulSet 的形式推入測試版。除了社群選擇的名稱外,API 物件沒有重大變化,但我們添加了“每個索引最多一個 Pod”的語義,用於部署集合中的 Pod。結合有序部署、有序終止、唯一網路名稱和持久穩定儲存,我們認為我們擁有支援許多容器化有狀態工作負載的正確原語。我們不聲稱該功能 100% 完整(畢竟它是軟體),但我們相信它以目前的形式是有用的,並且我們可以在向最終 GA 版本進展時以向後相容的方式擴充套件 API。

StatefulSet 何時是我的儲存應用程式的正確選擇?

部署(Deployments)副本集(ReplicaSets)是在 Kubernetes 上執行應用程式無狀態副本的絕佳方式,但它們的語義並不完全適用於部署有狀態應用程式。StatefulSet 的目的是提供一個具有正確語義的控制器,用於部署各種有狀態工作負載。然而,將儲存應用程式遷移到 Kubernetes 並不總是正確的選擇。在您全力以赴地整合儲存層和編排框架之前,您應該問自己幾個問題。

您的應用程式可以使用遠端儲存執行還是需要本地儲存介質?

目前,我們建議將 StatefulSet 與遠端儲存一起使用。因此,您必須準備好承受網路附加儲存的效能影響。即使使用儲存最佳化例項,您也不太可能實現與本地連線的固態儲存介質相同的效能。您的雲上的網路附加儲存效能是否允許您的儲存應用程式滿足其 SLA?如果是這樣,從自動化的角度來看,在 StatefulSet 中執行您的應用程式提供了引人注目的好處。如果執行您的儲存應用程式的節點發生故障,包含該應用程式的 Pod 可以重新排程到另一個節點上,並且由於它使用網路附加儲存介質,其資料在重新排程後仍然可用。

您需要擴充套件您的儲存應用程式嗎?

在 StatefulSet 中執行您的應用程式,您希望獲得什麼好處?您的整個組織是否只有一個儲存應用程式例項?擴充套件儲存應用程式是您實際存在的問題嗎?如果您有幾個儲存應用程式例項,並且它們成功滿足了您的組織的需求,並且這些需求沒有迅速增加,那麼您已經處於區域性最優狀態。

但是,如果您擁有微服務生態系統,或者您經常建立包含儲存應用程式的新服務足跡,那麼您可能會從自動化和整合中受益。如果您已經使用 Kubernetes 來管理生態系統的無狀態層,那麼您應該考慮使用相同的基礎設施來管理您的儲存應用程式。

可預測的效能有多重要?

Kubernetes 尚不支援跨容器的網路或儲存 I/O 隔離。將您的儲存應用程式與“吵鬧的鄰居”並置可能會降低您的應用程式可以處理的 QPS。您可以透過將包含您的儲存應用程式的 Pod 排程為節點上的唯一租戶(從而為其提供專用機器)或使用 Pod 反親和性規則來隔離爭奪網路或磁碟的 Pod 來緩解此問題,但這表示您必須積極識別和緩解熱點。

如果從儲存應用程式中榨取絕對最大 QPS 不是您的主要關注點,如果您願意並且能夠緩解熱點以確保您的儲存應用程式滿足其 SLA,並且如果開啟新“足跡”(服務或服務集合)、擴充套件它們以及靈活地重新分配資源的便利性是您的主要關注點,那麼 Kubernetes 和 StatefulSet 可能是解決該問題的正確解決方案。

您的應用程式是否需要專用硬體或例項型別?

如果您在高階硬體或超大例項大小上執行您的儲存應用程式,而您的其他工作負載在商品硬體或更小、更便宜的映象上執行,那麼您可能不想部署異構叢集。如果您可以為所有型別的應用程式標準化為單一例項大小,那麼您可能會受益於從 Kubernetes 獲得的靈活資源重新分配和整合。

一個實際的例子 - ZooKeeper

ZooKeeper 是 StatefulSet 的一個有趣的用例,原因有二。首先,它證明 StatefulSet 可以用於在 Kubernetes 上執行分散式、強一致性儲存應用程式。其次,它是在 Kubernetes 上執行像 Apache HadoopApache Kafka 這樣的工作負載的先決條件。在 Kubernetes 文件中提供了關於在 Kubernetes 上部署 ZooKeeper 叢集的深入教程,我們將在下面概述其中的一些關鍵功能。

建立 ZooKeeper 叢集
建立叢集就像使用 kubectl create 生成清單中儲存的物件一樣簡單。

$ kubectl create -f [http://k8s.io/docs/tutorials/stateful-application/zookeeper.yaml](https://raw.githubusercontent.com/kubernetes/kubernetes.github.io/master/docs/tutorials/stateful-application/zookeeper.yaml)

service "zk-headless" created

configmap "zk-config" created

poddisruptionbudget "zk-budget" created

statefulset "zk" created

建立清單時,StatefulSet 控制器會根據其序號建立每個 Pod,並等待每個 Pod 處於 Running 和 Ready 狀態,然後才建立其後續 Pod。

$ kubectl get -w -l app=zk

NAME      READY     STATUS    RESTARTS   AGE

zk-0      0/1       Pending   0          0s

zk-0      0/1       Pending   0         0s

zk-0      0/1       Pending   0         7s

zk-0      0/1       ContainerCreating   0         7s

zk-0      0/1       Running   0         38s

zk-0      1/1       Running   0         58s

zk-1      0/1       Pending   0         1s

zk-1      0/1       Pending   0         1s

zk-1      0/1       ContainerCreating   0         1s

zk-1      0/1       Running   0         33s

zk-1      1/1       Running   0         51s

zk-2      0/1       Pending   0         0s

zk-2      0/1       Pending   0         0s

zk-2      0/1       ContainerCreating   0         0s

zk-2      0/1       Running   0         25s

zk-2      1/1       Running   0         40s

檢查 StatefulSet 中每個 Pod 的主機名,您會看到 Pod 的主機名也包含 Pod 的序號。

$ for i in 0 1 2; do kubectl exec zk-$i -- hostname; done

zk-0

zk-1

zk-2

ZooKeeper 將每個伺服器的唯一識別符號儲存在一個名為“myid”的檔案中。用於 ZooKeeper 伺服器的識別符號只是自然數。對於叢集中的伺服器,“myid”檔案透過將從 Pod 主機名中提取的序號加一來填充。

$ for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done

myid zk-0

1

myid zk-1

2

myid zk-2

3

每個 Pod 都有一個基於其主機名和由 zk-headless 無頭服務控制的網路域的唯一網路地址。

$  for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done

zk-0.zk-headless.default.svc.cluster.local

zk-1.zk-headless.default.svc.cluster.local

zk-2.zk-headless.default.svc.cluster.local

唯一的 Pod 序號和唯一的網路地址的組合允許您使用一致的叢集成員身份填充 ZooKeeper 伺服器的配置檔案。

$  kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg

clientPort=2181

dataDir=/var/lib/zookeeper/data

dataLogDir=/var/lib/zookeeper/log

tickTime=2000

initLimit=10

syncLimit=2000

maxClientCnxns=60

minSessionTimeout= 4000

maxSessionTimeout= 40000

autopurge.snapRetainCount=3

autopurge.purgeInteval=1

server.1=zk-0.zk-headless.default.svc.cluster.local:2888:3888

server.2=zk-1.zk-headless.default.svc.cluster.local:2888:3888

server.3=zk-2.zk-headless.default.svc.cluster.local:2888:3888

StatefulSet 允許您以一致和可重複的方式部署 ZooKeeper。您不會建立多個具有相同 ID 的伺服器,伺服器可以透過穩定的網路地址相互查詢,並且它們可以執行領導選舉和複製寫入,因為叢集具有一致的成員資格。

驗證叢集是否正常工作的最簡單方法是向一個伺服器寫入值並從另一個伺服器讀取。您可以使用 ZooKeeper 分發包附帶的“zkCli.sh”指令碼來建立一個包含一些資料的 ZNode。

$  kubectl exec zk-0 zkCli.sh create /hello world

...


WATCHER::


WatchedEvent state:SyncConnected type:None path:null

Created /hello

您可以使用相同的指令碼從叢集中的另一個伺服器讀取資料。

$  kubectl exec zk-1 zkCli.sh get /hello

...


WATCHER::


WatchedEvent state:SyncConnected type:None path:null

world

...

您可以透過刪除 zk StatefulSet 來關閉叢集。

$  kubectl delete statefulset zk

statefulset "zk" deleted

級聯刪除會根據 Pod 序號的相反順序銷燬 StatefulSet 中的每個 Pod,並等待每個 Pod 完全終止後才終止其前一個 Pod。

$  kubectl get pods -w -l app=zk

NAME      READY     STATUS    RESTARTS   AGE

zk-0      1/1       Running   0          14m

zk-1      1/1       Running   0          13m

zk-2      1/1       Running   0          12m

NAME      READY     STATUS        RESTARTS   AGE

zk-2      1/1       Terminating   0          12m

zk-1      1/1       Terminating   0         13m

zk-0      1/1       Terminating   0         14m

zk-2      0/1       Terminating   0         13m

zk-2      0/1       Terminating   0         13m

zk-2      0/1       Terminating   0         13m

zk-1      0/1       Terminating   0         14m

zk-1      0/1       Terminating   0         14m

zk-1      0/1       Terminating   0         14m

zk-0      0/1       Terminating   0         15m

zk-0      0/1       Terminating   0         15m

zk-0      0/1       Terminating   0         15m

您可以使用 kubectl apply 重新建立 zk StatefulSet 並重新部署叢集。

$  kubectl apply -f [http://k8s.io/docs/tutorials/stateful-application/zookeeper.yaml](https://raw.githubusercontent.com/kubernetes/kubernetes.github.io/master/docs/tutorials/stateful-application/zookeeper.yaml)

service "zk-headless" configured

configmap "zk-config" configured

statefulset "zk" created

如果您使用“zkCli.sh”指令碼獲取在刪除 StatefulSet 之前輸入的值,您會發現叢集仍然提供資料。

$  kubectl exec zk-2 zkCli.sh get /hello

...


WATCHER::


WatchedEvent state:SyncConnected type:None path:null

world

...

StatefulSet 確保即使 StatefulSet 中的所有 Pod 都被銷燬,當它們被重新排程時,ZooKeeper 叢集也可以選舉新的領導者並繼續提供請求。

容忍節點故障

ZooKeeper 將其狀態機複製到叢集中的不同伺服器,其明確目的是容忍節點故障。預設情況下,Kubernetes 排程器可能會將 zk StatefulSet 中的多個 Pod 部署到同一個節點。如果 zk-0 和 zk-1 Pod 部署在同一個節點上,並且該節點發生故障,ZooKeeper 叢集將無法形成法定人數來提交寫入,並且 ZooKeeper 服務將經歷中斷,直到其中一個 Pod 可以重新排程。

您應該始終為叢集中的關鍵程序預留容量,如果這樣做,在這種情況下,Kubernetes 排程器將重新排程 Pod 到另一個節點,並且中斷將是短暫的。

如果您的服務的 SLA 不允許因單個節點故障而導致的短暫中斷,您應該使用 PodAntiAffinity 註解。用於建立叢集的清單包含這樣的註解,它告訴 Kubernetes 排程器不要將來自 zk StatefulSet 的多個 Pod 放置在同一個節點上。

容忍計劃維護

用於建立 ZooKeeper 叢集的清單還會建立 PodDistruptionBudget (zk-budget)。zk-budget 告知 Kubernetes 服務可以容忍的干擾(不健康的 Pod)的上限。

 {

              "podAntiAffinity": {

                "requiredDuringSchedulingRequiredDuringExecution": [{

                  "labelSelector": {

                    "matchExpressions": [{

                      "key": "app",

                      "operator": "In",

                      "values": ["zk-headless"]

                    }]

                  },

                  "topologyKey": "kubernetes.io/hostname"

                }]

              }

            }

}
$ kubectl get poddisruptionbudget zk-budget

NAME        MIN-AVAILABLE   ALLOWED-DISRUPTIONS   AGE

zk-budget   2               1                     2h

zk-budget 表明,為了使叢集健康,叢集中至少有兩個成員必須始終可用。如果您在節點離線之前嘗試清空節點,並且如果清空會導致終止違反預算的 Pod,則清空操作將失敗。如果您將 kubectl drain 與 PodDisruptionBudget 結合使用,在維護或退役之前隔離節點並驅逐所有 Pod,您可以確保該過程不會對您的有狀態應用程式造成干擾。

展望未來

隨著 Kubernetes 開發轉向 GA,我們正在審查使用者提出的長串建議。如果您想深入瞭解我們的待辦事項列表,請檢視 帶有“stateful”標籤的 GitHub 問題。然而,由於由此產生的 API 將難以理解,我們預計不會實現所有這些功能請求。一些功能請求,例如支援滾動更新、更好地與節點升級整合以及使用快速本地儲存,將使大多數型別的有狀態應用程式受益,我們預計將優先處理這些功能。StatefulSet 的目的是能夠很好地執行大量應用程式,而不是能夠完美地執行所有應用程式。考慮到這一點,我們避免以依賴隱藏機制或不可訪問功能的方式實現 StatefulSet。任何人都可以編寫一個與 StatefulSet 類似工作的控制器。我們稱之為“使其可分叉”。

在接下來的一年裡,我們預計許多流行的儲存應用程式都將擁有自己的社群支援的專用控制器或“操作員”。我們已經聽說過關於 etcd、Redis 和 ZooKeeper 的自定義控制器的工作。我們預計自己會編寫更多,並支援社群開發其他控制器。

來自 CoreOS 的 etcdPrometheus 的操作員展示了一種在 Kubernetes 上執行有狀態應用程式的方法,該方法提供了比單獨使用 StatefulSet 更高的自動化和整合水平。另一方面,使用像 StatefulSet 或 Deployment 這樣的通用控制器意味著可以透過理解單個配置物件來管理各種應用程式。我們認為 Kubernetes 使用者會欣賞這兩種方法的選擇。