探索使用 Kubernetes 扩展专用游戏服务器:第1部分-容器化和部署

你为何要这样作?

尽管容器(containers)和 Kubernetes 是很酷的技术,但为何咱们要在此平台上运行游戏服务器?算法

  • 游戏服务器的扩展很困难,而且一般是专有软件的工做 - 软件容器和 Kubernetes 应该使它更容易,而且编码更少。
  • 容器为咱们提供了一个可部署的工件,可用于运行游戏服务器。这消除了在部署过程当中安装依赖项或配置机器的须要,而且极大地提升了人们对软件在开发和测试中可以像在生产环境中同样运行的信心。
  • 经过将软件容器和 Kubernetes 结合使用,咱们能够创建一个坚实的基础,从而基本上能够大规模运行任何类型的软件 - 从部署(deployment),运行情况检查(health checking),日志聚合(log aggregation),扩展(scaling)等等,并使用 API 在几乎全部级别上控制这些事情。
  • 从本质上讲,Kubernetes 实际上只是一个集群管理解决方案,几乎可用于任何类型的软件。大规模运行专用游戏须要咱们跨机器集群管理游戏服务器进程 – 所以,咱们能够利用在该领域已经完成的工做,并根据本身的特定需求对其进行定制。
  • 这两个项目都是开源的,而且是积极开发的,所以咱们也能够利用将来开发的任何新功能。

Paddle Soccer

1dJT4rMCLCjrUdtRFMCX0vi02kLcKUo5cdVPFzNI.png

为了验证个人理论,我建立了一个很是简单的基于 Unity 的游戏,称为 Paddle Soccer,该游戏实质上与描述的彻底同样。这是一款两人在线游戏,其中每一个玩家都是 paddle,他们踢足球,试图互相得分。它具备一个 Unity 客户端以及一个 Unity 专用服务器。它利用 Unity High Level Networking API 来在服务器和客户端之间提供游戏状态同步和 UDP 传输协议。 值得注意的是,这是一款 session-based 的游戏; 即:你玩了一段时间,而后游戏结束,你回到大厅再玩,因此咱们将专一于这种扩展,并在决定什么时候添加或删除服务器实例时使用这种设计。 也就是说,理论上这些技巧也适用于 MMO 类型的游戏,只是须要进行一些调整。ubuntu

Paddle Soccer 架构

Paddle Soccer 使用传统的总体体系结构来进行基于会话的多人游戏:segmentfault

archopt.gif

  1. 玩家链接到 matchmaker 服务,该服务使用 Redis 将它们配对在一块儿,以帮助实现此目的。
  2. 一旦两个玩家加入到一个游戏会话中,matchmaker 会与 game server manager 对话,让它在咱们的机器集群中提供一个游戏服务器。
  3. game server manager 建立一个新的游戏服务器实例,该实例在集群中的一台计算机上运行。
  4. game server manager 还获取游戏服务器运行所在的IP地址和端口,并将其传递 matchmaker 服务。
  5. matchmaker 服务将 IP 和端口传递给玩家的客户端。
  6. …最后,玩家直接链接到游戏服务器,如今能够开始对战了。

因为咱们不想本身构建这种类型的集群管理和游戏服务器编排,所以咱们能够依靠容器和 Kubernetes 的强大功能来处理尽量多的工做。api

容器化游戏服务器

此过程的第一步是将游戏服务器放入软件容器中,以便 Kubernetes 能够部署它。 将游戏服务器放置在 Docker 容器中基本上与容器化其余任何软件相同。安全

这是用于将 Unity 专用游戏服务器放置在容器中的 Dockerfilebash

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(标准输出,即显示在前台),由于 DockerKubernetes 将从中收集日志。网络

从这里,我能够构建该镜像并将其推送到 Docker registry,以便我能够共享该镜像并将其部署到个人 Kubernetes 集群。我为此使用 Google Cloud Platform 的私有 Container Registry,所以我有一个私有且安全的 Docker 镜像存储库。session

运行游戏服务器

对于更传统的系统,Kubernetes 提供了几个真正有用的构造,包括可以在一组机器集群上运行一个应用程序的多个实例的能力,以及在它们之间进行负载均衡的强大工具 可是,对于游戏服务器,这与咱们想要的是直接相反的。 游戏服务器一般在内存中维护有关玩家和游戏的状态数据,而且须要很是低的延迟链接以维持该状态与游戏客户端的同步性,以使玩家不会注意到延迟。 所以,咱们须要直接链接到游戏服务器,而无需任何中介,这会增长延迟,由于每一毫秒都很重要。架构

第一步是运行游戏服务器。 每一个实例都是有状态的,所以彼此不相同,所以咱们不能像大多数无状态系统(例如 Web 服务器)那样使用 Deployment。 相反,咱们将依靠在 Kubernetes 上安装软件的最基本的构建模块 – Pod

Pod 只是一个或多个与某些共享资源(例如 IP 地址和端口空间)一块儿运行的容器。在这种特定状况下,每一个 Pod 仅具备一个容器,所以,若是使事情更容易理解,只需在本文中将 Pod 视为软件容器的同义词便可。

直接链接到容器

一般,容器在本身的网络名称空间中运行,若是不作一些工做将运行容器中的开放端口转发给主机,则容器不能经过主机直接链接。在 Kubernetes 上运行容器也没有什么不一样 —— 一般使用 Kubernetes 服务做为负载平衡器来公开一个或多个支持容器。然而,对于游戏服务器来讲,这是行不通的,由于对网络流量的低延迟要求。

幸运的是,经过在配置 Pod 时将 hostNetwork 设置为 trueKubernetes 容许 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

让咱们来分析一下:

  1. kind:告诉 Kubernetes 咱们想要一个 Pod
  2. metadata > generateName:告诉 Kubernetes 在集群中为此 Pod 生成一个惟一的名称,其前缀为 “game-”
  3. spec > hostNetwork:因为将其设置为 true,所以 Pod 将在与主机相同的网络名称空间中运行。
  4. spec > restartPolicy:默认状况下,Kubernetes 将在容器崩溃时从新启动它。在这种状况下,咱们不但愿这种状况发生,由于咱们在内存中有游戏状态,若是服务器崩溃了,咱们就很难从新开始游戏。
  5. spec > containers > image:告诉 Kubernetes 将哪一个容器镜像部署到 Pod。 在这里,咱们使用先前为专用游戏服务器建立的容器镜像。
  6. spec > containers > env > SESSION_NAME:咱们将把 Pod 的集群惟一名称做为环境变量 SESSION_NAME 传递到容器中,稍后咱们将使用它。这由 Kubernetes Downward API 提供支持。

若是咱们使用 kubectl 命令行工具将该 YAML 文件部署到 Kubernetes,而且知道它将打开哪一个端口,则可使用命令行工具和/或 Kubernetes APIKubernetes 集群中查找它正在运行节点的 IP,并将其发送到游戏客户端,以便它能够直接链接!

因为咱们也能够经过 Kubernetes API 建立 Pod,所以 Paddle Soccer 具备一个称为会话的游戏服务器管理系统,该系统具备/ create 处理程序,能够在 Kubernetes 上建立游戏服务器的新实例。 调用时,它将使用上面的详细信息将游戏服务器建立为 Pod。 而后,只要须要启动新的游戏服务器以容许两个玩家玩游戏,就能够经过配对服务调用该服务!

经过从生成的 Pod 名称中查找新 Pod,咱们还可使用内置的 Kubernetes API 来肯定新 Pod 在集群中的哪一个节点上。反过来,咱们能够查找该节点的外部 IP,如今咱们知道了要发送给游戏客户端的 IP 地址。

这已经为咱们解决了一些问题:

  • 咱们有一个预先构建的解决方案,用于经过容器镜像和 Kubernetes 将服务器部署到咱们的机器集群中。
  • Kubernetes 管理整个群集中的游戏服务器的调度,而无需咱们编写本身的 bin-packing 算法来优化资源使用。
  • 能够经过标准的 Docker / Kubernetes 机制部署新版本的游戏服务器;咱们不须要本身编写。
  • 咱们能够免费得到各类好处——从日志聚合到性能监视等等。
  • 咱们没必要编写太多代码来协调跨计算机集群的游戏服务器。

Port 管理

因为咱们可能会在 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 相结合,咱们将获得如下结果:

arch2opt.gif

  1. 一名玩家的客户端链接到 matchmaker 服务,但它什么也不作,由于它须要两名玩家来玩。
  2. 第二个玩家的客户端链接到 matchmaker 服务,matchmaker 服务决定它须要一个游戏服务器来链接这两个玩家,因此它向游戏服务器管理器发送一个请求。
  3. 游戏服务器管理器调用 Kubernetes API,以告知它在其中包含专用游戏服务器的集群中启动Pod
  4. 专用游戏服务器启动。
  5. 专用游戏服务器向游戏服务器管理器进行注册,并告知其开始在哪一个端口上。
  6. 游戏服务器管理器从 Kubernetes 获取上述端口信息和 PodIP 信息,并将其传递回Matchmaker
  7. matchmaker 将端口和 IP 信息传回给两个玩家客户端。
  8. 客户端如今直接链接到专用游戏服务器,并玩游戏。

EtVoilà!(瞧)咱们的集群中正在运行一个多人专用游戏!

在本例中,经过利用软件容器和 Kubernetes 的强大功能,相对少许的定制代码可以跨大型机器集群部署、建立和管理游戏服务器。老实说,容器和 Kubernetes 提供给您的功能很是强大!
图片来源:游戏盒子

相关文章
相关标签/搜索