使用 Kubernetes 擴充套件專用遊戲伺服器

為少發表於2021-03-29

系列

  1. 探索使用 Kubernetes 擴充套件專用遊戲伺服器:第 1 部分-容器化和部署
  2. 探索使用 Kubernetes 擴充套件專用遊戲伺服器:第 2 部分-管理 CPU 和記憶體
  3. 探索使用 Kubernetes 擴充套件專用遊戲伺服器:第 3 部分 - 擴充套件節點
  4. 使用 Kubernetes 擴充套件專用遊戲伺服器:第 4 部分-縮減節點

在前三篇文章中,我們將遊戲伺服器託管在 Kubernetes 上,測量並限制它們的資源使用,並根據使用情況擴大叢集中的節點。現在我們需要解決更困難的問題:當資源不再被使用時,縮小叢集中的節點,同時確保正在進行的遊戲在節點被刪除時不會中斷。

從表面上看,按比例縮小叢集中的節點似乎特別複雜。 每個遊戲伺服器具有當前遊戲的記憶體狀態,並且多個遊戲客戶端連線到玩遊戲的單個遊戲伺服器。 刪除任意節點可能會斷開活動玩家的連線,這會使他們生氣! 因此,只有在節點沒有專用遊戲伺服器的情況下,我們才能從叢集中刪除節點。

這意味著,如果您執行在谷歌 Kubernetes Engine (GKE) 或類似的平臺上,就不能使用託管的自動縮放系統。引用 GKE autoscaler 的文件“ Cluster autoscaler 假設所有複製的 pod 都可以在其他節點上重新啟動……” — 這在我們的例子中絕對不起作用,因為它可以很容易地刪除那些有活躍玩家的節點。

也就是說,當我們更仔細地研究這種情況時,我們會發現我們可以將其分解為三個獨立的策略,當這些策略結合在一起時,我們就可以將問題縮小成一個可管理的問題,我們可以自己執行:

  1. 將遊戲伺服器組合在一起,以避免整個叢集的碎片化
  2. CPU 容量超過配置的緩衝區時,封鎖節點
  3. 一旦節點上的所有遊戲退出,就從叢集中刪除被封鎖的節點

讓我們看一下每個細節。

在叢集中將遊戲伺服器分組在一起

我們想要避免叢集中游戲伺服器的碎片化,這樣我們就不會在多個節點上執行一個任性的小遊戲伺服器集,這將防止這些節點被關閉和回收它們的資源。

這意味著我們不希望有一個排程模式在整個叢集的隨機節點上建立遊戲伺服器 Pod,如下所示:

而是我們想讓我們的遊戲伺服器pod安排得儘可能緊湊,像這樣:

要將我們的遊戲伺服器分組在一起,我們可以利用帶有 PreferredDuringSchedulingIgnoredDuringExecution 選項的 Kubernetes Pod PodAffinity 配置。

這使我們能夠告訴 Pods 我們更喜歡按它們當前所在的節點的主機名對它們進行分組,這實質上意味著 Kubernetes 將更喜歡將專用的遊戲伺服器 Pod 放置在已經具有專用遊戲伺服器的節點上(上面已經有 Pod 了)。

在理想情況下,我們希望在擁有最專用遊戲伺服器 Pod 的節點上排程專用遊戲伺服器 Pod,只要該節點還有足夠的空閒 CPU 資源。如果我們想為 Kubernetes 編寫自己的自定義排程程式,我們當然可以這樣做,但為了保持演示簡單,我們將堅持使用 PodAffinity 解決方案。也就是說,當我們考慮到我們的遊戲長度很短,並且我們將很快新增(and explaining)封鎖節點時,這種技術組合已經足夠滿足我們的需求,並且消除了我們編寫額外複雜程式碼的需要。

當我們將 PodAffinity 配置新增到前一篇文章的配置時,我們得到以下內容,它告訴 Kubernetes 在可能的情況下將帶有標籤 sessions: gamepod 放置在彼此相同的節點上。

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
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)

相關文章