乐趣区

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

你为什么要这样做?

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

  • 游戏服务器的扩大很艰难,并且通常是专有软件的工作 – 软件容器和 Kubernetes 应该使它更容易,并且编码更少。
  • 容器为咱们提供了一个可部署的工件,可用于运行游戏服务器。这打消了在部署过程中装置依赖项或配置机器的须要,并且极大地提高了人们对软件在开发和测试中可能像在生产环境中一样运行的信念。
  • 通过将软件容器和 Kubernetes 联合应用,咱们能够建设一个松软的根底,从而基本上能够大规模运行任何类型的软件 – 从部署(deployment),运行状况查看(health checking),日志聚合(log aggregation),扩大(scaling)等等,并应用 API 在简直所有级别上管制这些事件。
  • 从实质上讲,Kubernetes 实际上只是一个集群治理解决方案,简直可用于任何类型的软件。大规模运行专用游戏须要咱们跨机器集群治理游戏服务器过程 – 因而,咱们能够利用在该畛域曾经实现的工作,并依据本人的特定需要对其进行定制。
  • 这两个我的项目都是开源的,并且是踊跃开发的,因而咱们也能够利用将来开发的任何新性能。

Paddle Soccer

为了验证我的实践,我创立了一个非常简单的基于 Unity 的游戏,称为 Paddle Soccer,该游戏本质上与形容的齐全一样。这是一款两人在线游戏,其中每个玩家都是 paddle,他们踢足球,试图相互得分。它具备一个 Unity 客户端以及一个 Unity 专用服务器。它利用 Unity High Level Networking API 来在服务器和客户端之间提供游戏状态同步和 UDP 传输协定。值得注意的是,这是一款 session-based 的游戏; 即:你玩了一段时间,而后游戏完结,你回到大厅再玩,所以咱们将专一于这种扩大,并在决定何时增加或删除服务器实例时应用这种设计。也就是说,实践上这些技巧也实用于 MMO 类型的游戏,只是须要进行一些调整。

Paddle Soccer 架构

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

  1. 玩家连贯到 matchmaker 服务,该服务应用 Redis 将它们配对在一起,以帮忙实现此目标。
  2. 一旦两个玩家退出到一个游戏会话中,matchmaker 会与 game server manager 对话,让它在咱们的机器集群中提供一个游戏服务器。
  3. game server manager 创立一个新的游戏服务器实例,该实例在集群中的一台计算机上运行。
  4. game server manager 还获取游戏服务器运行所在的 IP 地址和端口,并将其传递 matchmaker 服务。
  5. matchmaker 服务将 IP 和端口传递给玩家的客户端。
  6. …最初,玩家间接连贯到游戏服务器,当初能够开始对战了。

因为咱们不想本人构建这种类型的集群治理和游戏服务器编排,因而咱们能够依附容器和 Kubernetes 的弱小性能来解决尽可能多的工作。

容器化游戏服务器

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

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

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 镜像存储库。

运行游戏服务器

对于更传统的零碎,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 相结合,咱们将失去以下后果:

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

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

在本例中,通过利用软件容器和 Kubernetes 的弱小性能,绝对大量的定制代码可能跨大型机器集群部署、创立和治理游戏服务器。诚实说,容器和 Kubernetes 提供给您的性能十分弱小!
图片起源:游戏盒子

退出移动版