在前三篇文章中,咱们将游戏服务器托管在 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)
}
图片起源:http://www.laoshoucun.com/ 页游