原创科普整理
在前两篇文章中,我们研究了如何在 Kubernetes
上托管专用游戏服务器,并测量和限制其内存和 CPU
资源。在本期中,我们将探讨如何利用上一篇文章中的 CPU
信息来确定何时需要扩展Kubernetes
集群,因为随着玩家人数的增加,我们已经没有足够的空间来容纳更多的游戏服务器。
在开始编写代码以增加 Kubernetes
集群的大小之前,我们应该做的第一步是将我们的应用程序(例如,match makers
,game server controllers
和即将编写的 node scaler
)分离到不同的应用程序中 一 在集群的不同节点上,而不是游戏服务器运行的地方。
这有几个好处:
matchmaker
由于某种原因而导致 CPU
峰值,那么将存在一个额外的障碍,以确保它不会不适当地影响正在运行的专用游戏服务器。CPU
核和内存的大机器来运行游戏服务器节点,也可以使用带有更少内核和内存的小机器来运行控制器应用程序,因为它们需要的资源更少。我们基本上能够为手头的工作选择合适的机器尺寸。这给了我们很大的灵活性,同时仍然具有成本效益。Kubernetes
使建立异构集群相对简单,并为我们提供了工具,可通过节点上的节点选择器的功能来指定集群中 Pod
的调度位置。
值得注意的是,beta
中还具有更复杂的 Node Affinity
功能,但是在此示例中我们不需要它,因此我们暂时将其忽略。
首先,我们需要将标签(一组键-值对)分配给集群中的节点。这与您使用 Deployments
创建 Pods
并使用 Services
公开它们时所看到的情况完全相同,只是将其应用于节点。我使用谷歌的云平台的容器引擎和它使用节点池标签应用于集群中的节点创建和建立异构集群——但你也可以做类似的事情在其他云提供商,以及直接通过 Kubernetes API
或命令行客户端。
在本例中,我将标签role:apps和role:game-server添加到集群中的适当节点。然后,我们可以在Kubernetes配置中添加一个nodeSelector选项,以控制集群中的 Pods被调度到哪些节点上面。
例如,下面是 matchmaker
应用程序的配置,您可以看到节点选择器设置为 role:apps
,以确保它只在应用程序节点(标记为“apps”
角色的节点)上创建容器实例。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: matchmaker
spec:
replicas: 5
template:
metadata:
labels:
role: matchmaker-server
spec:
nodeSelector:
role: apps # here is the node selector
containers:
- name: matchmaker
image: gcr.io/soccer/matchmaker
ports:
- containerPort: 8080
同样的,我们可以从上一篇文章中调整配置,使所有专用的游戏服务器 pod
调度仅在我们专门为它们指定的机器上,即那些标记为 role: game-server
:
apiVersion: v1
kind: Pod
metadata:
generateName: "game-"
spec:
hostNetwork: true
restartPolicy: Never
nodeSelector:
role: game-server # here is the node selector
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"
请注意,在示例代码中,使用 Kubernetes API
提供了与上面相同的配置,但 yaml
版本更容易理解,而且它是我们在整个系列中一直使用的格式。
云提供商上的 Kubernetes
往往带有自动伸缩功能,比如谷歌云平台集群自动伸缩器,但由于它们通常是为无状态应用程序构建的,而且我们的专用游戏服务器将游戏模拟存储在内存中,所以它们在这种情况下无法工作。然而,使用 Kubernetes
提供的工具,构建我们自己的定制 Kubernetes
集群自动scaler
并不是特别困难!
对于云环境,在 Kubernetes
集群中扩展和缩小节点可能更有意义,因为我们只想为我们需要/使用的资源付费。如果我们在自己的场所中运行,则更改 Kubernetes
集群的大小可能没什么意义,而且我们可以在所有拥有的机器上运行一个大型集群,并将它们保持为静态大小,因为添加 并且删除物理计算机要比在云上花费更多,并且由于我们拥有/租赁计算机的时间更长,因此不一定能节省我们的钱。
有多种潜在策略可用来确定何时要扩展集群中的节点数量,但是在本示例中,我们将使事情变得相对简单:
CPU
资源容量和使用率作为我们跟踪集群中一个节点上可以容纳多少专用游戏服务器的指标(在本例中,我们假设我们总是有足够的内存)。CPU
容量缓冲区。也就是说,如果在任何时刻,你都无法在不耗尽集群 CPU
资源的情况下将 n
个服务器添加到集群中,那么就增加更多的节点。CPU
容量低于缓冲区数量。n
秒,还要计算是否需要将新节点添加到群集,因为所测量的 CPU
容量资源在缓冲区下方。node scaler
本质上是运行一个事件循环来执行上面概述的策略。
结合使用 Go
和原生 Kubernetes Go client library
库可以相对容易地实现这一点,如下面在节点缩放器的 Start()
函数中所见。
注意,为了使事件循环更清晰,我已经删除了大部分错误处理和其他样板文件,但如果您感兴趣,这里是原始代码。
// Start the HTTP server on the given port
func (s *Server) Start() error {
// Access Kubernetes and return a client
s.cs, _ = kube.ClientSet()
// ... there be more code here ...
// Use the K8s client's watcher channels to see game server events
gw, _ := s.newGameWatcher()
gw.start()
// async loop around either the tick, or the event stream
// and then scaleNodes() if either occur.
go func() {
log.Print("[Info][Start] Starting node scaling...")
tick := time.Tick(s.tick)
// ^^^ MAIN EVENT LOOP HERE ^^^
for {
select {
case <-gw.events:
log.Print("[Info][Scaling] Received Event, Scaling...")
s.scaleNodes()
case <-tick:
log.Printf("[Info][Scaling] Tick of %#v, Scaling...", tick)
s.scaleNodes()
}
}
}()
// Start the HTTP server
return errors.Wrap(s.srv.ListenAndServe(), "Error starting server")
}
对于那些不熟悉 Go
的人,让我们分析一下:
kube.ClientSet()
– 我们有一小段实用程序代码,它向我们返回一个 Kubernetes ClientSet
,它使我们能够访问正在运行的集群的 Kubernetes API
。gw, _ := s.newGameWatcher
– Kubernetes
具有 API
,使您可以监视整个集群中的更改。在这种特殊情况下,此处的代码返回一个包含 Go Channel
(本质上是一个阻塞队列)的数据结构,特别是 gw.events
,每当在集群中添加或删除游戏 Pod
时,该数据结构都将返回一个值。tick := time.Tick(s.tick)
– 这将创建另一个 Go Channel
,该 Channel
一直阻塞到给定时间(在这种情况下为10秒),然后返回一个值。“// ^^^ MAIN EVENT LOOP HERE ^^^”
注释下。在此代码块中是一条 select
语句。这实际上声明了系统将阻塞,直到 gw.events channel
或 tick channel
(每 10
秒触发一次)返回一个值,然后执行 s.scaleNodes()
。这意味着,每当添加/删除游戏服务器或每 10
秒触发一次 scaleNodes
命令。s.scaleNodes()
– 运行上面概述的规模节点策略。在 s.scaleNodes()
中,我们通过 Kubernetes API
查询我们在每个 Pod
上设置的 CPU
限制,以及集群中每个 Kubernetes
节点上可用的总 CPU
。我们可以通过 Rest API
和 Go Client
在 Pod specification
中查看已配置的 CPU
限制,这使我们能够跟踪每台游戏服务器占用的 CPU
数量以及任何存在于节点上 Kubernetes
管理的 Pod
。通过 Node specification
,Go Client
还可以跟踪每个节点中可用的 CPU
容量。在这种情况下,需要对 Pods
占用的 CPU
数量求和,然后从每个节点的容量中减去 CPU
的数量,然后确定是否需要将一个或多个节点添加到集群中,这样我们才能保持该缓冲区空间,用于创建新的游戏服务器。
如果您在此示例中深入研究代码,将会看到我们正在使用 Google Cloud Platform
上的 API
向集群添加新节点。为 Google Compute Engine
托管实例组提供的 API
允许我们从Kubernetes
集群的 Nodepool
中添加(和删除)实例。话虽这么说,任何云提供商都将具有类似的 API
,让您做同样的事情,在这里您可以看到我们定义的接口,该接口用于抽象该实现细节,以便可以轻松地对其进行修改以与其他提供商一起使用。
在下面,您可以看到节点缩放器的部署 YAML
。如您所见,环境变量用于设置所有配置选项,包括:
CPU
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nodescaler
spec:
replicas: 1 # only want one, to avoid race conditions
template:
metadata:
labels:
role: nodescaler-server
spec:
nodeSelector:
role: apps
strategy:
type: Recreate
containers:
- name: nodescaler
image: gcr.io/soccer/nodescaler
env:
- name: NODE_SELECTOR # the nodes to be managed
value: "role=game-server"
- name: CPU_REQUEST # how much CPU each server needs
value: "0.1"
- name: BUFFER_COUNT # how many servers do we need buffer for
value: "30"
- name: TICK # how often to tick over and recheck everything
value: "10s"
- name: MIN_NODE # minimum number of nodes for game servers
value: "1"
- name: MAX_NODE # maximum number of nodes for game servers
value: "15"
您可能已经注意到,我们将部署设置为 replicas: 1
。我们这样做的原因是,我们总是希望在Kubernetes
集群中在任何给定的时间点上只有一个活跃的 node scaler
实例。这确保了集群中不会有超过一个进程试图扩大或最终缩小我们的节点,这肯定会导致竞争条件,并可能导致各种奇怪的情况。
同样,如果要更新节点缩放器,要确保在创建节点缩放器之前正确关闭节点缩放器,我们还配置strategy.type: Recreate
,以便 Kubernetes
在重新创建节点缩放器之前销毁当前运行的节点缩放器 Pod
。更新版本,也避免了任何潜在的竞争情况。
部署节点缩放器后,让我们跟踪日志并查看其运行情况。在下面的视频中,通过日志可以看到,当群集中有一个节点分配给游戏服务器时,我们有能力启动 40
个专用游戏服务器,并配置了 30
个专用游戏服务器的缓冲区的需求。当我们通过 matchmaker
通过运行专用游戏服务器来填充可用的CPU容量时,请注意在剩余空间中可创建的游戏服务器数量会如何下降,最终会添加一个新节点来维护缓冲区!
油管视频: