探索使用 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 提供給您的功能很是強大!
圖片來源:遊戲盒子

相關文章
相關標籤/搜索