使用 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)
}


相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
弹性计算 人工智能 Serverless
阿里云ACK One:注册集群云上节点池(CPU/GPU)自动弹性伸缩,助力企业业务高效扩展
在当今数字化时代,企业业务的快速增长对IT基础设施提出了更高要求。然而,传统IDC数据中心却在业务存在扩容慢、缩容难等问题。为此,阿里云推出ACK One注册集群架构,通过云上节点池(CPU/GPU)自动弹性伸缩等特性,为企业带来全新突破。
|
5月前
|
安全 数据可视化 Linux
在线游戏的地基:VPS和专用服务器性价比大比拼!
游戏服务器是在线游戏的基石,选择合适的服务器类型对玩家体验至关重要。本文对比了VPS(虚拟专用服务器)和专用服务器的优劣势:VPS经济灵活、易于管理,但性能和安全存在局限,适合预算有限或玩家规模适中的游戏;专用服务器性能强大、安全可靠且可控性高,但成本和技术门槛较高,更适合大型MMO或竞技游戏。根据游戏类型、预算、技术能力和扩展需求,合理选择服务器类型是关键。初创阶段可选用中端VPS,成长阶段考虑高端VPS或低端专用服务器,成熟阶段则需高端专用服务器集群。未来,混合架构或将实现性能与成本的平衡。最终,以玩家流畅体验为导向,选择最适合的服务器方案。
181 3
|
8月前
|
Kubernetes API 网络安全
当node节点kubectl 命令无法连接到 Kubernetes API 服务器
当Node节点上的 `kubectl`无法连接到Kubernetes API服务器时,可以通过以上步骤逐步排查和解决问题。首先确保网络连接正常,验证 `kubeconfig`文件配置正确,检查API服务器和Node节点的状态,最后排除防火墙或网络策略的干扰,并通过重启服务恢复正常连接。通过这些措施,可以有效解决与Kubernetes API服务器通信的常见问题,从而保障集群的正常运行。
554 17
|
12月前
|
Prometheus Kubernetes 监控
深入探索Kubernetes中的Pod自动扩展(Horizontal Pod Autoscaler, HPA)
深入探索Kubernetes中的Pod自动扩展(Horizontal Pod Autoscaler, HPA)
|
8月前
|
Kubernetes Shell Windows
【Azure K8S | AKS】在AKS的节点中抓取目标POD的网络包方法分享
在AKS中遇到复杂网络问题时,可通过以下步骤进入特定POD抓取网络包进行分析:1. 使用`kubectl get pods`确认Pod所在Node;2. 通过`kubectl node-shell`登录Node;3. 使用`crictl ps`找到Pod的Container ID;4. 获取PID并使用`nsenter`进入Pod的网络空间;5. 在`/var/tmp`目录下使用`tcpdump`抓包。完成后按Ctrl+C停止抓包。
258 12
|
9月前
|
Kubernetes 监控 Serverless
基于阿里云Serverless Kubernetes(ASK)的无服务器架构设计与实践
无服务器架构(Serverless Architecture)在云原生技术中备受关注,开发者只需专注于业务逻辑,无需管理服务器。阿里云Serverless Kubernetes(ASK)是基于Kubernetes的托管服务,提供极致弹性和按需付费能力。本文深入探讨如何使用ASK设计和实现无服务器架构,涵盖事件驱动、自动扩展、无状态设计、监控与日志及成本优化等方面,并通过图片处理服务案例展示具体实践,帮助构建高效可靠的无服务器应用。
|
Prometheus Kubernetes 监控
k8s部署针对外部服务器的prometheus服务
通过上述步骤,您不仅成功地在Kubernetes集群内部署了Prometheus,还实现了对集群外服务器的有效监控。理解并实施网络配置是关键,确保监控数据的准确无误传输。随着监控需求的增长,您还可以进一步探索Prometheus生态中的其他组件,如Alertmanager、Grafana等,以构建完整的监控与报警体系。
695 62
|
Prometheus Kubernetes 监控
k8s部署针对外部服务器的prometheus服务
通过上述步骤,您不仅成功地在Kubernetes集群内部署了Prometheus,还实现了对集群外服务器的有效监控。理解并实施网络配置是关键,确保监控数据的准确无误传输。随着监控需求的增长,您还可以进一步探索Prometheus生态中的其他组件,如Alertmanager、Grafana等,以构建完整的监控与报警体系。
332 60

热门文章

最新文章

推荐镜像

更多