使用 Kubernetes 扩展专用游戏服务器

系列
- 探索使用 Kubernetes 扩展专用游戏服务器:第 1 部分-容器化和部署
- 探索使用 Kubernetes 扩展专用游戏服务器:第 2 部分-管理 CPU 和内存
- 探索使用 Kubernetes 扩展专用游戏服务器:第 3 部分 - 扩展节点
- 使用 Kubernetes 扩展专用游戏服务器:第 4 部分-缩减节点
在前三篇文章中,我们将游戏服务器托管在 Kubernetes 上,测量并限制它们的资源使用,并根据使用情况扩大集群中的节点。现在我们需要解决更困难的问题:当资源不再被使用时,缩小集群中的节点,同时确保正在进行的游戏在节点被删除时不会中断。
从表面上看,按比例缩小集群中的节点似乎特别复杂。 每个游戏服务器具有当前游戏的内存状态,并且多个游戏客户端连接到玩游戏的单个游戏服务器。 删除任意节点可能会断开活动玩家的连接,这会使他们生气! 因此,只有在节点没有专用游戏服务器的情况下,我们才能从集群中删除节点。
这意味着,如果您运行在谷歌 Kubernetes Engine (GKE) 或类似的平台上,就不能使用托管的自动缩放系统。引用 GKE autoscaler 的文档“ Cluster autoscaler 假设所有复制的 pod 都可以在其他节点上重新启动……” — 这在我们的例子中绝对不起作用,因为它可以很容易地删除那些有活跃玩家的节点。
也就是说,当我们更仔细地研究这种情况时,我们会发现我们可以将其分解为三个独立的策略,当这些策略结合在一起时,我们就可以将问题缩小成一个可管理的问题,我们可以自己执行:
- 将游戏服务器组合在一起,以避免整个集群的碎片化
- 当 CPU容量超过配置的缓冲区时,封锁节点
- 一旦节点上的所有游戏退出,就从集群中删除被封锁的节点
让我们看一下每个细节。
在集群中将游戏服务器分组在一起
我们想要避免集群中游戏服务器的碎片化,这样我们就不会在多个节点上运行一个任性的小游戏服务器集,这将防止这些节点被关闭和回收它们的资源。
这意味着我们不希望有一个调度模式在整个集群的随机节点上创建游戏服务器 Pod,如下所示:

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

要将我们的游戏服务器分组在一起,我们可以利用带有 PreferredDuringSchedulingIgnoredDuringExecution 选项的 Kubernetes Pod PodAffinity 配置。
这使我们能够告诉 Pods 我们更喜欢按它们当前所在的节点的主机名对它们进行分组,这实质上意味着 Kubernetes 将更喜欢将专用的游戏服务器 Pod 放置在已经具有专用游戏服务器的节点上(上面已经有 Pod 了)。
在理想情况下,我们希望在拥有最专用游戏服务器 Pod 的节点上调度专用游戏服务器 Pod,只要该节点还有足够的空闲 CPU 资源。如果我们想为 Kubernetes 编写自己的自定义调度程序,我们当然可以这样做,但为了保持演示简单,我们将坚持使用 PodAffinity 解决方案。也就是说,当我们考虑到我们的游戏长度很短,并且我们将很快添加(and explaining)封锁节点时,这种技术组合已经足够满足我们的需求,并且消除了我们编写额外复杂代码的需要。
当我们将 PodAffinity 配置添加到前一篇文章的配置时,我们得到以下内容,它告诉 Kubernetes 在可能的情况下将带有标签 sessions: game 的 pod 放置在彼此相同的节点上。
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
公众号:黑客下午茶
加我微信(互相学习交流),关注公众号(获取更多学习资料~)
使用 Kubernetes 扩展专用游戏服务器的更多相关文章
- GoWorld – 用Golang写一个分布式可扩展、可热更的游戏服务器
		GoWorld代码:https://github.com/xiaonanln/goworld Golang具有运行效率高.内存安全等优良特性,因此是非常适合用来进行服务器开发.使用Golang开发游戏 ... 
- 游戏服务器生成全局唯一ID的几种方法
		在服务器系统开发时,为了适应数据大并发的请求,我们往往需要对数据进行异步存储,特别是在做分布式系统时,这个时候就不能等待插入数据库返回了取自动id了,而是需要在插入数据库之前生成一个全局的唯一id,使 ... 
- Scut游戏服务器免费开源框架-3
		Scut游戏服务器免费开源框架--快速开发(3) Scut快速开发(3) 1 开发环境 需要安装的软件 a) 消息队列 b) 数据库,Sql2005以上版本 ... 
- 游戏服务器设计之NPC系统
		游戏服务器设计之NPC系统 简介 NPC系统是游戏中非常重要的系统,设计的好坏很大程度上影响游戏的体验.NPC在游戏中有如下作用: 引导玩家体验游戏内容,一般游戏内有很多主线.支线任务,而任务的介绍. ... 
- 上海C++游戏服务器群活动PPT下载
		下载页面: http://download.csdn.net/download/jq0123/8227519 跨服与跨区的设计PPT 上海C++游戏服务器群 2014.11.9 沙龙讲义. 自我介绍 ... 
- Pomelo分布式游戏服务器框架
		Pomelo介绍&入门 目录 前言&介绍 安装Pomelo 创建项目并启动 创建项目 项目结构说明 启动 测试连接 聊天服务器 新建gate和chat服务器 配置master.json ... 
- 游戏服务器的思考之三:谈谈MVC
		游戏服务器也是基于MVC架构的吗?是的,所有的应用系统都是基于MVC架构的,这是应用系统的天性.不管是客户端还是后台,都包含模型.流程.界面这3个基本要素:不同类型的应用,3要素的“重量”可能各有偏差 ... 
- 游戏服务器和Web服务器的区别
		用Go语言写游戏服务器也有一个多月了,也能够明显的感受到两者的区别.这篇文章就是想具体的聊聊其中的区别.当然,在了解区别之间,我们先简单的了解一下Go语言本身. 1. Go语言的特点 Go语言跟其他的 ... 
- 老出BUG怎么办?游戏服务器常见问题解决方法分享
		在游戏开发中,我们经常会遇到一些技术难题,而其引发的bug则会影响整个游戏的品质.女性向手游<食物语>就曾遇到过一些开发上的难题,腾讯游戏学院专家团Wade.Zc.Jovi等专家为其提供了 ... 
随机推荐
- fibonacci number & fibonacci sequence
			fibonacci number & fibonacci sequence https://www.mathsisfun.com/numbers/fibonacci-sequence.html ... 
- React tutorial
			https://www.algolia.com Build Unique Search ExperiencesHosted Search API that delivers instant and r ... 
- .NET 面试题:   C# override && overloading (C# 覆写 && 重载)
			1 1 1 .NET 面试题, C# ,override , overloading, 覆写, 重载,.NET,ASP.NET, override (覆写/重写): 方法名相同,参数的个数和类型相同, ... 
- SwiftUI & Compose View
			SwiftUI & Compose View OK // // ContentView.swift // Landmarks // // Created by 夏凌晨 on 2020/10/2 ... 
- jest & code testing
			jest jest & code testing https://jestjs.io/zh-Hans/ 24.9 https://jestjs.io/docs/zh-Hans/getting- ... 
- Raspberry Pi & Raspberry Pi 4
			Raspberry Pi & Raspberry Pi 4 pdf https://www.raspberrypi.org/magpi/issues/beginners-guide-2nd-e ... 
- macOS open url from terminal
			macOS open url from terminal open URL && start terminal bash open url in chrome open chrome ... 
- react 遍历 object
			@observable obj = { name: "ajanuw", age: 22, }; @computed get list() { return _.toPairs(th ... 
- NGK:APP一站式挖矿高收益项目
			NGK是10月中旬刚上线的公链项目,采用手机挖矿形式.NGK数字增益平台,200美金即可入场,收益可观,分为静态和动态两种,投资算力收益超高.邀请好友挖矿还有额外的返佣. NGK立志为所有人创造无差别 ... 
- 「NGK每日快讯」12.14日NGK公链第41期官方快讯!
