使用 Kubernetes 扩展专用游戏服务器:第4部分-缩减节点

简介: 使用 Kubernetes 扩展专用游戏服务器:第4部分-缩减节点

在前三篇文章中,我们将游戏服务器托管在 Kubernetes 上,测量并限制它们的资源使用,并根据使用情况扩大集群中的节点。现在我们需要解决更困难的问题:当资源不再被使用时,缩小集群中的节点,同时确保正在进行的游戏在节点被删除时不会中断。

从表面上看,按比例缩小集群中的节点似乎特别复杂。每个游戏服务器具有当前游戏的内存状态,并且多个游戏客户端连接到玩游戏的单个游戏服务器。删除任意节点可能会断开活动玩家的连接,这会使他们生气!因此,只有在节点没有专用游戏服务器的情况下,我们才能从集群中删除节点。


这意味着,如果您运行在谷歌 Kubernetes Engine (GKE) 或类似的平台上,就不能使用托管的自动缩放系统。引用 GKE autoscaler 的文档“ Cluster autoscaler 假设所有复制的 pod 都可以在其他节点上重新启动……” — 这在我们的例子中绝对不起作用,因为它可以很容易地删除那些有活跃玩家的节点。


也就是说,当我们更仔细地研究这种情况时,我们会发现我们可以将其分解为三个独立的策略,当这些策略结合在一起时,我们就可以将问题缩小成一个可管理的问题,我们可以自己执行:


  1. 将游戏服务器组合在一起,以避免整个集群的碎片化
  2. CPU 容量超过配置的缓冲区时,封锁节点
  3. 一旦节点上的所有游戏退出,就从集群中删除被封锁的节点

让我们看一下每个细节。


系列



  1. 探索使用 Kubernetes 扩展专用游戏服务器:第1部分-容器化和部署
  2. 探索使用Kubernetes扩展专用游戏服务器:第2部分-管理CPU和内存
  3. 探索使用Kubernetes扩展专用游戏服务器:第3部分 - 扩展节点


在集群中将游戏服务器分组在一起



我们想要避免集群中游戏服务器的碎片化,这样我们就不会在多个节点上运行一个任性的小游戏服务器集,这将防止这些节点被关闭和回收它们的资源。


这意味着我们不希望有一个调度模式在整个集群的随机节点上创建游戏服务器 Pod,如下所示:


微信图片_20220611154912.png

而是我们想让我们的游戏服务器pod安排得尽可能紧凑,像这样:


微信图片_20220611154914.png

要将我们的游戏服务器分组在一起,我们可以利用带有 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 文档的某些地方,这被简单地称为标记节点不可调度。


微信图片_20220611154940.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)
}


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
2月前
|
弹性计算 调度 数据中心
阿里云 ACK One 注册集群云上弹性:扩展业务新利器
随着企业数字化转型深入,传统IDC数据中心因物理容量限制,难以实现动态扩容,缺乏弹性能力。阿里云ACK One注册集群凭借其高度灵活性和丰富资源选择,成为解决此问题的最佳方案。通过与阿里云资源的整合,ACK One不仅实现了计算资源的按需扩展,提高了资源利用率,还通过按需付费模式降低了成本,使企业能够更高效地应对业务增长和高峰需求。
|
2月前
|
机器学习/深度学习 JavaScript Cloud Native
Node.js作为一种快速、可扩展的服务器端运行时环境
Node.js作为一种快速、可扩展的服务器端运行时环境
62 8
|
3月前
|
Prometheus Kubernetes 监控
深入探索Kubernetes中的Pod自动扩展(Horizontal Pod Autoscaler, HPA)
深入探索Kubernetes中的Pod自动扩展(Horizontal Pod Autoscaler, HPA)
|
3月前
|
弹性计算 人工智能 Serverless
阿里云ACK One:注册集群云上节点池(CPU/GPU)自动弹性伸缩,助力企业业务高效扩展
在当今数字化时代,企业业务的快速增长对IT基础设施提出了更高要求。然而,传统IDC数据中心却在业务存在扩容慢、缩容难等问题。为此,阿里云推出ACK One注册集群架构,通过云上节点池(CPU/GPU)自动弹性伸缩等特性,为企业带来全新突破。
|
4月前
|
Kubernetes 应用服务中间件 nginx
搭建Kubernetes v1.31.1服务器集群,采用Calico网络技术
在阿里云服务器上部署k8s集群,一、3台k8s服务器,1个Master节点,2个工作节点,采用Calico网络技术。二、部署nginx服务到k8s集群,并验证nginx服务运行状态。
1443 1
|
4月前
|
Kubernetes API 调度
k8s中节点无法启动Pod
【10月更文挑战第3天】
180 6
|
4月前
|
Kubernetes 应用服务中间件 Linux
多Master节点的k8s集群部署
多Master节点的k8s集群部署
|
4月前
|
应用服务中间件 PHP Apache
PbootCMS提示错误信息“未检测到您服务器环境的sqlite3数据库扩展...”
PbootCMS提示错误信息“未检测到您服务器环境的sqlite3数据库扩展...”
|
5月前
|
存储 关系型数据库 API
深入理解后端技术:构建高效、可扩展的服务器端应用
本文将探讨后端开发的核心概念和技术,包括服务器端编程、数据库管理、API设计和安全性等方面。通过深入浅出的方式,让读者了解如何构建高效、可扩展的后端系统。我们将从基本的后端框架开始,逐步深入到高级主题,如微服务架构和容器化部署。无论您是初学者还是有经验的开发人员,都能在本文中找到有价值的信息和实用的建议。
|
6月前
|
缓存 安全 Java
Java服务器端技术:Servlet与JSP的集成与扩展
Java服务器端技术:Servlet与JSP的集成与扩展
62 3

热门文章

最新文章