
系列
- 探索使用 Kubernetes 擴展專用游戲服務器:第 1 部分-容器化和部署
- 探索使用 Kubernetes 擴展專用游戲服務器:第 2 部分-管理 CPU 和內存
- 探索使用 Kubernetes 擴展專用游戲服務器:第 3 部分 - 擴展節點
- 使用 Kubernetes 擴展專用游戲服務器:第 4 部分-縮減節點
在前三篇文章中,我們將游戲服務器托管在 Kubernetes 上,測量並限制它們的資源使用,並根據使用情況擴大集群中的節點。現在我們需要解決更困難的問題:當資源不再被使用時,縮小集群中的節點,同時確保正在進行的游戲在節點被刪除時不會中斷。
從表面上看,按比例縮小集群中的節點似乎特別復雜。 每個游戲服務器具有當前游戲的內存狀態,並且多個游戲客戶端連接到玩游戲的單個游戲服務器。 刪除任意節點可能會斷開活動玩家的連接,這會使他們生氣! 因此,只有在節點沒有專用游戲服務器的情況下,我們才能從集群中刪除節點。
這意味着,如果您運行在谷歌 Kubernetes Engine (GKE) 或類似的平台上,就不能使用托管的自動縮放系統。引用 GKE autoscaler 的文檔“ Cluster autoscaler 假設所有復制的 pod 都可以在其他節點上重新啟動……” — 這在我們的例子中絕對不起作用,因為它可以很容易地刪除那些有活躍玩家的節點。
也就是說,當我們更仔細地研究這種情況時,我們會發現我們可以將其分解為三個獨立的策略,當這些策略結合在一起時,我們就可以將問題縮小成一個可管理的問題,我們可以自己執行:
- 將游戲服務器組合在一起,以避免整個集群的碎片化
- 當
CPU容量超過配置的緩沖區時,封鎖節點 - 一旦節點上的所有游戲退出,就從集群中刪除被封鎖的節點
讓我們看一下每個細節。
在集群中將游戲服務器分組在一起
我們想要避免集群中游戲服務器的碎片化,這樣我們就不會在多個節點上運行一個任性的小游戲服務器集,這將防止這些節點被關閉和回收它們的資源。
這意味着我們不希望有一個調度模式在整個集群的隨機節點上創建游戲服務器 Pod,如下所示:

而是我們想讓我們的游戲服務器pod安排得盡可能緊湊,像這樣:

要將我們的游戲服務器分組在一起,我們可以利用帶有 PreferredDuringSchedulingIgnoredDuringExecution 選項的 Kubernetes Pod PodAffinity 配置。
這使我們能夠告訴 Pods 我們更喜歡按它們當前所在的節點的主機名對它們進行分組,這實質上意味着 Kubernetes 將更喜歡將專用的游戲服務器 Pod 放置在已經具有專用游戲服務器的節點上(上面已經有 Pod 了)。
在理想情況下,我們希望在擁有最專用游戲服務器 Pod 的節點上調度專用游戲服務器 Pod,只要該節點還有足夠的空閑 CPU 資源。如果我們想為 Kubernetes 編寫自己的自定義調度程序,我們當然可以這樣做,但為了保持演示簡單,我們將堅持使用 PodAffinity 解決方案。也就是說,當我們考慮到我們的游戲長度很短,並且我們將很快添加(and explaining)封鎖節點時,這種技術組合已經足夠滿足我們的需求,並且消除了我們編寫額外復雜代碼的需要。
當我們將 PodAffinity 配置添加到前一篇文章的配置時,我們得到以下內容,它告訴 Kubernetes 在可能的情況下將帶有標簽 sessions: game 的 pod 放置在彼此相同的節點上。
apiVersion: v1
kind: Pod
metadata:
generateName: "game-"
spec:
hostNetwork: true
restartPolicy: Never
nodeSelector:
role: game-server
containers:
- name: soccer-server
image: gcr.io/soccer/soccer-server:0.1
env:
- name: SESSION_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
limits:
cpu: "0.1"
affinity:
podAffinity: # group game server Pods
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
sessions: game
topologyKey: kubernetes.io/hostname
封鎖節點
現在我們已經把我們的游戲服務器很好地打包在一起了,我們可以討論“封鎖節點”了。“封鎖節點”到底是什么意思?很簡單,Kubernetes 讓我們能夠告訴調度器:“嘿,調度器,不要在這個節點上調度任何新東西”。這將確保該節點上不會調度新的 pod。事實上,在 Kubernetes 文檔的某些地方,這被簡單地稱為標記節點不可調度。

在下面的代碼中,如果您專注於 s.bufferCount < available,您將看到,如果當前擁有的 CPU 緩沖區的數量大於我們所需要的數量,我們將向警戒節點發出請求。
// scale scales nodes up and down, depending on CPU constraints
// this includes adding nodes, cordoning them as well as deleting them
func (s Server) scaleNodes() error {
nl, err := s.newNodeList()
if err != nil {
return err
}
available := nl.cpuRequestsAvailable()
if available < s.bufferCount {
finished, err := s.uncordonNodes(nl, s.bufferCount-available)
// short circuit if uncordoning means we have enough buffer now
if err != nil || finished {
return err
}
nl, err := s.newNodeList()
if err != nil {
return err
}
// recalculate
available = nl.cpuRequestsAvailable()
err = s.increaseNodes(nl, s.bufferCount-available)
if err != nil {
return err
}
} else if s.bufferCount < available {
err := s.cordonNodes(nl, available-s.bufferCount)
if err != nil {
return err
}
}
return s.deleteCordonedNodes()
}
從上面的代碼中還可以看到,如果我們降到配置的 CPU 緩沖區以下,則可以取消集群中任何可用的封閉節點的約束。 這比添加一個全新的節點要快,因此在從頭開始添加全新的節點之前,請先檢查受約束的節點,這一點很重要。由於這個原因,我們還配置了刪除隔離節點的時間延遲,以限制不必要地在集群中創建和刪除節點時的抖動。
這是一個很好的開始。 但是,當我們要封鎖節點時,我們只希望封鎖其上具有最少數量的游戲服務器 Pod 的節點,因為在這種情況下,隨着游戲會話的結束,它們最有可能先清空。
得益於 Kubernetes API,計算每個節點上的游戲服務器 Pod 的數量並按升序對其進行排序相對容易。 從那里,我們可以算術確定如果我們封鎖每個可用節點,是否仍保持在所需的 CPU 緩沖區上方。 如果是這樣,我們可以安全地封鎖這些節點。
// cordonNodes decrease the number of available nodes by the given number of cpu blocks (but not over),
// but cordoning those nodes that have the least number of games currently on them
func (s Server) cordonNodes(nl *nodeList, gameNumber int64) error {
// … removed some input validation ...
// how many nodes (n) do we have to delete such that we are cordoning no more
// than the gameNumber
capacity := nl.nodes.Items[0].Status.Capacity[v1.ResourceCPU] //assuming all nodes are the same
cpuRequest := gameNumber * s.cpuRequest
diff := int64(math.Floor(float64(cpuRequest) / float64(capacity.MilliValue())))
if diff <= 0 {
log.Print("[Info][CordonNodes] No nodes to be cordoned.")
return nil
}
log.Printf("[Info][CordonNodes] Cordoning %v nodes", diff)
// sort the nodes, such that the one with the least number of games are first
nodes := nl.nodes.Items
sort.Slice(nodes, func(i, j int) bool {
return len(nl.nodePods(nodes[i]).Items) < len(nl.nodePods(nodes[j]).Items)
})
// grab the first n number of them
cNodes := nodes[0:diff]
// cordon them all
for _, n := range cNodes {
log.Printf("[Info][CordonNodes] Cordoning node: %v", n.Name)
err := s.cordon(&n, true)
if err != nil {
return err
}
}
return nil
}
從集群中刪除節點
現在我們的集群中的節點已經被封鎖,這只是一個等待,直到被封鎖的節點上沒有游戲服務器 Pod 為止,然后再刪除它。下面的代碼還確保節點數永遠不會低於配置的最小值,這是集群容量的良好基線。
您可以在下面的代碼中看到這一點:
// deleteCordonedNodes will delete a cordoned node if it
// the time since it was cordoned has expired
func (s Server) deleteCordonedNodes() error {
nl, err := s.newNodeList()
if err != nil {
return err
}
l := int64(len(nl.nodes.Items))
if l <= s.minNodeNumber {
log.Print("[Info][deleteCordonedNodes] Already at minimum node count. exiting")
return nil
}
var dn []v1.Node
for _, n := range nl.cordonedNodes() {
ct, err := cordonTimestamp(n)
if err != nil {
return err
}
pl := nl.nodePods(n)
// if no game session pods && if they have passed expiry, then delete them
if len(filterGameSessionPods(pl.Items)) == 0 && ct.Add(s.shutdown).Before(s.clock.Now()) {
err := s.cs.CoreV1().Nodes().Delete(n.Name, nil)
if err != nil {
return errors.Wrapf(err, "Error deleting cordoned node: %v", n.Name)
}
dn = append(dn, n)
// don't delete more nodes than the minimum number set
if l--; l <= s.minNodeNumber {
break
}
}
}
return s.nodePool.DeleteNodes(dn)
}
我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)
