尽管容器(containers
)和 Kubernetes
是很酷的技术,但为何咱们要在此平台上运行游戏服务器?算法
Kubernetes
应该使它更容易,而且编码更少。Kubernetes
结合使用,咱们能够创建一个坚实的基础,从而基本上能够大规模运行任何类型的软件 - 从部署(deployment
),运行情况检查(health checking
),日志聚合(log aggregation
),扩展(scaling
)等等,并使用 API
在几乎全部级别上控制这些事情。Kubernetes
实际上只是一个集群管理解决方案,几乎可用于任何类型的软件。大规模运行专用游戏须要咱们跨机器集群管理游戏服务器进程 – 所以,咱们能够利用在该领域已经完成的工做,并根据本身的特定需求对其进行定制。为了验证个人理论,我建立了一个很是简单的基于 Unity
的游戏,称为 Paddle Soccer
,该游戏实质上与描述的彻底同样。这是一款两人在线游戏,其中每一个玩家都是 paddle
,他们踢足球,试图互相得分。它具备一个 Unity
客户端以及一个 Unity
专用服务器。它利用 Unity High Level Networking API
来在服务器和客户端之间提供游戏状态同步和 UDP
传输协议。 值得注意的是,这是一款 session-based
的游戏; 即:你玩了一段时间,而后游戏结束,你回到大厅再玩,因此咱们将专一于这种扩展,并在决定什么时候添加或删除服务器实例时使用这种设计。 也就是说,理论上这些技巧也适用于 MMO
类型的游戏,只是须要进行一些调整。ubuntu
Paddle Soccer
使用传统的总体体系结构来进行基于会话的多人游戏:segmentfault
matchmaker
服务,该服务使用 Redis
将它们配对在一块儿,以帮助实现此目的。matchmaker
会与 game server manager
对话,让它在咱们的机器集群中提供一个游戏服务器。game server manager
建立一个新的游戏服务器实例,该实例在集群中的一台计算机上运行。game server manager
还获取游戏服务器运行所在的IP地址和端口,并将其传递 matchmaker 服务。matchmaker
服务将 IP
和端口传递给玩家的客户端。因为咱们不想本身构建这种类型的集群管理和游戏服务器编排,所以咱们能够依靠容器和 Kubernetes
的强大功能来处理尽量多的工做。api
此过程的第一步是将游戏服务器放入软件容器中,以便 Kubernetes
能够部署它。 将游戏服务器放置在 Docker
容器中基本上与容器化其余任何软件相同。安全
这是用于将 Unity
专用游戏服务器放置在容器中的 Dockerfile
:bash
FROM ubuntu:16.04 RUN useradd -ms /bin/bash unity WORKDIR /home/unity COPY Server.tar.gz . RUN chown unity:unity Server.tar.gz USER unity RUN tar --no-same-owner -xf Server.tar.gz && rm Server.tar.gz ENTRYPOINT ["./Server.x86_64", "-logFile", "/dev/stdout"]
因为 Docker
默认状况下以 root
用户身份运行,所以我想建立一个新用户并在该账户下的容器内运行全部进程。 所以,我为游戏服务器建立了一个 “unity”
用户,并将游戏服务器复制到其主目录中。 在构建过程当中,我建立了专用游戏服务器的压缩包,而且将其构建为能够在 Linux
操做系统上运行。服务器
我惟一要作的另外一件有趣的事是,当我设置 ENTRYPOINT
(容器启动时运行)时,我告诉 Unity
将日志输出到 /dev/stdout
(标准输出,即显示在前台),由于 Docker
和 Kubernetes
将从中收集日志。网络
从这里,我能够构建该镜像并将其推送到 Docker registry
,以便我能够共享该镜像并将其部署到个人 Kubernetes
集群。我为此使用 Google Cloud Platform
的私有 Container Registry
,所以我有一个私有且安全的 Docker
镜像存储库。session
对于更传统的系统,Kubernetes
提供了几个真正有用的构造,包括可以在一组机器集群上运行一个应用程序的多个实例的能力,以及在它们之间进行负载均衡的强大工具 可是,对于游戏服务器,这与咱们想要的是直接相反的。 游戏服务器一般在内存中维护有关玩家和游戏的状态数据,而且须要很是低的延迟链接以维持该状态与游戏客户端的同步性,以使玩家不会注意到延迟。 所以,咱们须要直接链接到游戏服务器,而无需任何中介,这会增长延迟,由于每一毫秒都很重要。架构
第一步是运行游戏服务器。 每一个实例都是有状态的,所以彼此不相同,所以咱们不能像大多数无状态系统(例如 Web
服务器)那样使用 Deployment
。 相反,咱们将依靠在 Kubernetes
上安装软件的最基本的构建模块 – Pod
。
Pod
只是一个或多个与某些共享资源(例如 IP
地址和端口空间)一块儿运行的容器。在这种特定状况下,每一个 Pod
仅具备一个容器,所以,若是使事情更容易理解,只需在本文中将 Pod
视为软件容器的同义词便可。
一般,容器在本身的网络名称空间中运行,若是不作一些工做将运行容器中的开放端口转发给主机,则容器不能经过主机直接链接。在 Kubernetes
上运行容器也没有什么不一样 —— 一般使用 Kubernetes
服务做为负载平衡器来公开一个或多个支持容器。然而,对于游戏服务器来讲,这是行不通的,由于对网络流量的低延迟要求。
幸运的是,经过在配置 Pod
时将 hostNetwork
设置为 true
,Kubernetes
容许 Pod
直接使用主机网络名称空间。因为容器与主机在同一内核上运行,所以能够直接进行网络链接,而无需额外的延迟,这意味着咱们能够直接链接到 Pod
所运行的机器的 IP
,也能够直接链接到正在运行的容器。
虽然个人示例代码对 Kubernetes
进行了直接的 API
调用来建立 Pod
,但一般的作法是将Pod
定义保存在 YAML
文件中,这些文件经过命令行工具 kubectl
发送到 Kubernetes
集群。下面是一个 YAML
文件的例子,它告诉 Kubernetes
为专用游戏服务器建立一个 Pod
,这样咱们就能够讨论更详细的细节了:
apiVersion: v1 kind: Pod metadata: generateName: "game-" spec: hostNetwork: true restartPolicy: Never containers: - name: soccer-server image: gcr.io/soccer/soccer-server:0.1 env: - name: SESSION_NAME valueFrom: fieldRef: fieldPath: metadata.name
让咱们来分析一下:
kind
:告诉 Kubernetes
咱们想要一个 Pod
!metadata > generateName
:告诉 Kubernetes
在集群中为此 Pod
生成一个惟一的名称,其前缀为 “game-”
spec > hostNetwork
:因为将其设置为 true
,所以 Pod
将在与主机相同的网络名称空间中运行。spec > restartPolicy
:默认状况下,Kubernetes
将在容器崩溃时从新启动它。在这种状况下,咱们不但愿这种状况发生,由于咱们在内存中有游戏状态,若是服务器崩溃了,咱们就很难从新开始游戏。spec > containers > image
:告诉 Kubernetes
将哪一个容器镜像部署到 Pod
。 在这里,咱们使用先前为专用游戏服务器建立的容器镜像。spec > containers > env > SESSION_NAME
:咱们将把 Pod
的集群惟一名称做为环境变量 SESSION_NAME
传递到容器中,稍后咱们将使用它。这由 Kubernetes Downward API
提供支持。若是咱们使用 kubectl
命令行工具将该 YAML
文件部署到 Kubernetes
,而且知道它将打开哪一个端口,则可使用命令行工具和/或 Kubernetes API
在 Kubernetes
集群中查找它正在运行节点的 IP
,并将其发送到游戏客户端,以便它能够直接链接!
因为咱们也能够经过 Kubernetes API
建立 Pod
,所以 Paddle Soccer
具备一个称为会话的游戏服务器管理系统,该系统具备/ create
处理程序,能够在 Kubernetes
上建立游戏服务器的新实例。 调用时,它将使用上面的详细信息将游戏服务器建立为 Pod
。 而后,只要须要启动新的游戏服务器以容许两个玩家玩游戏,就能够经过配对服务调用该服务!
经过从生成的 Pod
名称中查找新 Pod
,咱们还可使用内置的 Kubernetes API
来肯定新 Pod
在集群中的哪一个节点上。反过来,咱们能够查找该节点的外部 IP
,如今咱们知道了要发送给游戏客户端的 IP
地址。
这已经为咱们解决了一些问题:
Kubernetes
将服务器部署到咱们的机器集群中。Kubernetes
管理整个群集中的游戏服务器的调度,而无需咱们编写本身的 bin-packing
算法来优化资源使用。Docker
/ Kubernetes
机制部署新版本的游戏服务器;咱们不须要本身编写。因为咱们可能会在 Kubernetes
集群中的每一个节点上运行多个专用游戏服务器,所以它们每一个都须要本身的端口才能运行。 不幸的是,Kubernetes
不能为咱们提供帮助,可是解决这个问题并非特别困难。
第一步是肯定要让流量经过的端口范围。 这使您的群集的网络规则变得更轻松(若是您不想即时添加/删除网络规则),但若是你的玩家须要在本身的网络上设置端口转发或相似的东西,这也会让事情变得更容易。
为了解决这个问题,我尽可能让事情简单化:在建立个人 pod
时,我传递能够用做两个环境变量的端口范围,并让 Unity
专用服务器在该范围中随机选择一个值,直到它成功打开一个套接字。
您能够看到 Paddle Soccer Unity
游戏服务器正是这样作的:
public static void Start(IUnityServer server) { instance = new GameServer(server); for (var i = 0; i < maxStartRetries; i++) { // select a random port in a range, and set it instance.SelectPort(); if (instance.server.StartServer()) { instance.Register(); return; } } throw new Exception(string.Format("Could not find port")); }
每次对 SelectPort
的调用都会选择一个范围内的随机端口,该端口将在 StartServer
调用时打开。 若是没法打开端口并启动服务器,则 StartServer
将返回 false
。
您可能还注意到对 instance.Register
的调用。这是由于 Kubernetes
并无提供任何方法来检查该容器从哪一个端口开始,因此咱们须要编写本身的端口。 为此,Paddle Soccer
游戏服务器管理器具备一个简单的/ register REST
端点,该端点由 Redis
支持用于存储,该端点具备Kubernetes
提供的 Pod
名称(咱们经过环境变量进行传递),并存储服务器启动时使用的端口。 它还提供了/ get
端点,用于查找游戏服务器在哪一个端口上启动。 它已与建立游戏服务器的 REST
端点打包在一块儿,所以咱们在 Kubernetes
中提供了一项用于管理游戏服务器的单一服务。
这是专用的游戏服务器注册代码:
private void Register() { var session = new Session { id = Environment.GetEnvironmentVariable("SESSION_NAME"), port = this.port }; var host = "http://sessions/register"; server.PostHTTP(host, JsonUtility.ToJson(session)); }
您能够看到游戏服务器在何处将环境变量 SESSION_NAME
与集群惟一的 Pod
名称一块儿使用,并将其与端口组合。而后,此组合做为 JSON
数据包发送到游戏服务器管理器的/ register
处理程序,即会话的/ register
处理程序。
若是咱们将其与 Paddle Soccer
游戏客户端以及一个很是简单的 matchmaker
相结合,咱们将获得如下结果:
matchmaker
服务,但它什么也不作,由于它须要两名玩家来玩。matchmaker
服务,matchmaker
服务决定它须要一个游戏服务器来链接这两个玩家,因此它向游戏服务器管理器发送一个请求。Kubernetes API
,以告知它在其中包含专用游戏服务器的集群中启动Pod
。Kubernetes
获取上述端口信息和 Pod
的 IP
信息,并将其传递回Matchmaker
。matchmaker
将端口和 IP
信息传回给两个玩家客户端。EtVoilà!(瞧)咱们的集群中正在运行一个多人专用游戏!
在本例中,经过利用软件容器和 Kubernetes
的强大功能,相对少许的定制代码可以跨大型机器集群部署、建立和管理游戏服务器。老实说,容器和 Kubernetes
提供给您的功能很是强大!
图片来源:游戏盒子