在前三篇文章中,咱們將遊戲服務器託管在 Kubernetes
上,測量並限制它們的資源使用,並根據使用狀況擴大集羣中的節點。如今咱們須要解決更困難的問題:當資源再也不被使用時,縮小集羣中的節點,同時確保正在進行的遊戲在節點被刪除時不會中斷。node
從表面上看,按比例縮小集羣中的節點彷佛特別複雜。 每一個遊戲服務器具備當前遊戲的內存狀態,而且多個遊戲客戶端鏈接到玩遊戲的單個遊戲服務器。 刪除任意節點可能會斷開活動玩家的鏈接,這會使他們生氣! 所以,只有在節點沒有專用遊戲服務器的狀況下,咱們才能從集羣中刪除節點。api
這意味着,若是您運行在谷歌 Kubernetes Engine (GKE)
或相似的平臺上,就不能使用託管的自動縮放系統。引用 GKE autoscaler
的文檔「 Cluster autoscaler
假設全部複製的 pod
均可以在其餘節點上從新啓動……」 — 這在咱們的例子中絕對不起做用,由於它能夠很容易地刪除那些有活躍玩家的節點。安全
也就是說,當咱們更仔細地研究這種狀況時,咱們會發現咱們能夠將其分解爲三個獨立的策略,當這些策略結合在一塊兒時,咱們就能夠將問題縮小成一個可管理的問題,咱們能夠本身執行:服務器
CPU
容量超過配置的緩衝區時,封鎖節點讓咱們看一下每一個細節。session
咱們想要避免集羣中游戲服務器的碎片化,這樣咱們就不會在多個節點上運行一個任性的小遊戲服務器集,這將防止這些節點被關閉和回收它們的資源。app
這意味着咱們不但願有一個調度模式在整個集羣的隨機節點上建立遊戲服務器 Pod
,以下所示:
而是咱們想讓咱們的遊戲服務器pod安排得儘量緊湊,像這樣:
要將咱們的遊戲服務器分組在一塊兒,咱們能夠利用帶有 PreferredDuringSchedulingIgnoredDuringExecution
選項的 Kubernetes Pod PodAffinity
配置。ui
這使咱們可以告訴 Pods
咱們更喜歡按它們當前所在的節點的主機名對它們進行分組,這實質上意味着 Kubernetes
將更喜歡將專用的遊戲服務器 Pod
放置在已經具備專用遊戲服務器的節點上(上面已經有 Pod
了)。this
在理想狀況下,咱們但願在擁有最專用遊戲服務器 Pod
的節點上調度專用遊戲服務器 Pod
,只要該節點還有足夠的空閒 CPU
資源。若是咱們想爲 Kubernetes
編寫本身的自定義調度程序,咱們固然能夠這樣作,但爲了保持演示簡單,咱們將堅持使用 PodAffinity
解決方案。也就是說,當咱們考慮到咱們的遊戲長度很短,而且咱們將很快添加(and explaining
)封鎖節點時,這種技術組合已經足夠知足咱們的需求,而且消除了咱們編寫額外複雜代碼的須要。spa
當咱們將 PodAffinity
配置添加到前一篇文章的配置時,咱們獲得如下內容,它告訴 Kubernetes
在可能的狀況下將帶有標籤 sessions: game
的 pod
放置在彼此相同的節點上。rest
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) }
圖片來源:http://www.laoshoucun.com/ 頁遊