儘管容器(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
提供給您的功能很是強大!
圖片來源:遊戲盒子