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

StatefulSet:在 Kubernetes 中輕鬆執行和擴展具狀態應用程式

編輯註記: 這篇文章是關於 Kubernetes 1.5 新功能的 深入文章系列 的一部分

在最新版本 Kubernetes 1.5 中,我們已將以前稱為 PetSet 的功能移至 Beta 版,命名為 StatefulSet。除了社群選定的名稱外,API 物件沒有重大變更,但我們為集合中 Pod 的部署新增了「每個索引最多一個 Pod」的語意。連同有序部署、有序終止、唯一網路名稱和持久穩定儲存,我們認為我們擁有正確的原語來支援許多容器化的具狀態工作負載。我們不聲稱此功能是 100% 完整 (畢竟它是軟體),但我們相信它目前的形式很有用,並且隨著我們朝最終 GA 版本邁進,我們可以以向後相容的方式擴展 API。

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

DeploymentsReplicaSets 是在 Kubernetes 上運行應用程式的無狀態副本的好方法,但它們的語意不太適合部署具狀態應用程式。StatefulSet 的目的是提供一個控制器,該控制器具有正確的語意,用於部署各種具狀態工作負載。但是,將您的儲存應用程式移至 Kubernetes 並非總是正確的選擇。在您全力以赴整合您的儲存層和協調框架之前,您應該問自己幾個問題。

您的應用程式可以使用遠端儲存運行,還是需要本機儲存媒體?

目前,我們建議將 StatefulSets 與遠端儲存一起使用。因此,您必須準備好容忍網路附加儲存的效能影響。即使使用針對儲存優化的實例,您也可能無法實現與本機附加固態儲存媒體相同的效能。您雲端上的網路附加儲存效能是否允許您的儲存應用程式滿足其 SLA?如果是這樣,從自動化的角度來看,在 StatefulSet 中運行您的應用程式提供了引人注目的優勢。如果運行儲存應用程式的節點發生故障,則包含該應用程式的 Pod 可以重新排程到另一個節點,並且由於它使用網路附加儲存媒體,因此它的資料在重新排程後仍然可用。

您是否需要擴展您的儲存應用程式?

透過在 StatefulSet 中運行應用程式,您希望獲得什麼好處?您的整個組織是否只有一個儲存應用程式實例?擴展您的儲存應用程式是您實際遇到的問題嗎?如果您有幾個儲存應用程式實例,並且它們成功滿足了您組織的需求,並且這些需求沒有快速增長,那麼您已經處於局部最佳狀態。

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

可預測的效能有多重要?

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

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

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

如果您在高階硬體或超大型實例大小上運行儲存應用程式,而在商品硬體或較小、較便宜的映像上運行其他工作負載,您可能不想部署異構叢集。如果您可以將所有類型的應用程式標準化為單一實例大小,那麼您可能會從 Kubernetes 提供的彈性資源重新分配和整合中受益。

實務範例 - ZooKeeper

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

建立 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」狀態,然後再建立其後繼者。

$ 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 Headless Service 控制的網路網域。

$  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 完全終止,然後再終止其前身。

$  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 與 PodDisruptionBudgets 結合使用,以隔離您的節點並在維護或停用之前驅逐所有 Pod,則可以確保該程序不會對您的具狀態應用程式造成破壞。

展望未來

隨著 Kubernetes 開發朝著 GA 邁進,我們正在研究使用者提出的長長建議清單。如果您想深入了解我們的待辦事項,請查看具有 stateful 標籤的 GitHub 問題。但是,由於產生的 API 將難以理解,因此我們不希望實作所有這些功能請求。一些功能請求,例如對滾動更新、與節點升級的更好整合以及使用快速本機儲存的支援,將使大多數類型的具狀態應用程式受益,並且我們希望優先考慮這些功能。StatefulSet 的目的是能夠良好地運行大量應用程式,而不是能夠完美地運行所有應用程式。考慮到這一點,我們避免以依賴隱藏機制或無法存取功能的方式實作 StatefulSet。任何人都可以編寫一個與 StatefulSet 類似的控制器。我們稱之為「使其可分叉」。

在接下來的一年中,我們預計許多流行的儲存應用程式都將各自擁有其社群支援的專用控制器或「運算子」。我們已經聽說正在開發 etcd、Redis 和 ZooKeeper 的自訂控制器。我們希望自己編寫更多內容,並支援社群開發其他控制器。

CoreOS 的 etcdPrometheus 的運算子示範了一種在 Kubernetes 上運行具狀態應用程式的方法,該方法提供的自動化和整合程度超出了僅 StatefulSet 可能實現的程度。另一方面,使用像 StatefulSet 或 Deployment 這樣的通用控制器意味著可以透過理解單一組態物件來管理各種應用程式。我們認為 Kubernetes 使用者會欣賞這兩種方法的選擇。