使用 Kubernetes 擴展專用遊戲服務器:第4部分-縮減節點

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

從表面上看,按比例縮小集羣中的節點彷佛特別複雜。 每一個遊戲服務器具備當前遊戲的內存狀態,而且多個遊戲客戶端鏈接到玩遊戲的單個遊戲服務器。 刪除任意節點可能會斷開活動玩家的鏈接,這會使他們生氣! 所以,只有在節點沒有專用遊戲服務器的狀況下,咱們才能從集羣中刪除節點。api

這意味着,若是您運行在谷歌 Kubernetes Engine (GKE) 或相似的平臺上,就不能使用託管的自動縮放系統。引用 GKE autoscaler 的文檔「 Cluster autoscaler 假設全部複製的 pod 均可以在其餘節點上從新啓動……」 — 這在咱們的例子中絕對不起做用,由於它能夠很容易地刪除那些有活躍玩家的節點。安全

也就是說,當咱們更仔細地研究這種狀況時,咱們會發現咱們能夠將其分解爲三個獨立的策略,當這些策略結合在一塊兒時,咱們就能夠將問題縮小成一個可管理的問題,咱們能夠本身執行:服務器

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

讓咱們看一下每一個細節。session

在集羣中將遊戲服務器分組在一塊兒

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

這意味着咱們不但願有一個調度模式在整個集羣的隨機節點上建立遊戲服務器 Pod,以下所示:
image.png
而是咱們想讓咱們的遊戲服務器pod安排得儘量緊湊,像這樣:
image.png
要將咱們的遊戲服務器分組在一塊兒,咱們能夠利用帶有 PreferredDuringSchedulingIgnoredDuringExecution 選項的 Kubernetes Pod PodAffinity 配置。ui

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

在理想狀況下,咱們但願在擁有最專用遊戲服務器 Pod 的節點上調度專用遊戲服務器 Pod,只要該節點還有足夠的空閒 CPU 資源。若是咱們想爲 Kubernetes 編寫本身的自定義調度程序,咱們固然能夠這樣作,但爲了保持演示簡單,咱們將堅持使用 PodAffinity 解決方案。也就是說,當咱們考慮到咱們的遊戲長度很短,而且咱們將很快添加(and explaining)封鎖節點時,這種技術組合已經足夠知足咱們的需求,而且消除了咱們編寫額外複雜代碼的須要。spa

當咱們將 PodAffinity 配置添加到前一篇文章的配置時,咱們獲得如下內容,它告訴 Kubernetes 在可能的狀況下將帶有標籤 sessions: gamepod 放置在彼此相同的節點上。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 文檔的某些地方,這被簡單地稱爲標記節點不可調度。
image.png
在下面的代碼中,若是您專一於 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/ 頁遊

相關文章
相關標籤/搜索