快速Docker化基于Socket.IO的HTML5游戲

dfd7 8年前發布 | 45K 次閱讀 Docker
 

編者按:Node.js由于其上手快、事件驅動、異步編程的特性,已被廣泛的應用在眾多企業的生產環境中。Socket.IO通過Node.js實現 WebSocket服務端,可以工作在任何平臺、瀏覽器或移動設備,也是H5開發的常用技術。weplay是一個Socket.IO庫的展示項目,用了 HTML5的畫布API,用Redis作為數據中轉以及持續化存儲的一個中型項目。 容器化后可以實現模塊化,更方便地實現版本升級和規模擴展,本文以weplay為例,介紹了H5移動端程序容器化的過程。??

作者簡介:戴佳豪,出生并成長在魔都上海。他在上海電力學院完成應用物理學的本科學位,在期間完成了諸如“在線物理做題系統”網站的開發。作為少數本科在 讀RHCE工程師,在容器化與開源世界有著2年的實戰經驗。他是忠實的軍團要塞2玩家,有必要時會發揮“Cheat Engine”的力量。他始終堅信能用“云”力量改變自己的生活。博客: http://www.djh.im 。微博:@戴佳豪_

什么是Node.js

Node.js采用了Google Chrome瀏覽器的V8引擎,性能很好的同時,將許多系統級的API進行了傻瓜化的封裝。相對于運行于網頁頁面的Javascript腳 本,Node.js則是作為一個全面的后臺運行進程,用Javascript提供了其他語言能夠實現的許多功能。 Node.js由于其上手快、事件驅動、異步編程的特性,已被廣泛的應用在眾多企業的生產環境中。

什么是Socket.IO

Socket.IO是一個開源的WebSocket庫,它通過Node.js實現WebSocket服務端,同時也提供客戶端JS庫。Socket.IO支持以事件為基礎的實時雙向通訊,它可以工作在任何平臺、瀏覽器或移動設備,也是H5開發的常用技術。

為什么要容器化

對于任何一個項目的已有改動都將是費時費力的,無論是模塊重構還是框架更迭。但是有以下的一些優點讓你花些時間,將手頭的項目代碼制作成一個容器鏡像。

將代碼的不同模塊徹底分離

手頭的項目可能在設計以及實現時,就已經做了模塊化的編寫(這是比較好的情況,更可能是一個不可維護的狀態)。

但是如果著手將代碼容器化,你將不得已的面對各模塊的代碼分別運行在不同“虛擬機”里,而僅靠通訊協議來進行數據交互的狀況。這也可以說是實現了項目整體的微服務化,在容器化完成以后,項目出了問題,可以直接查對應項目里的日志,哪兒除了錯會一覽無遺!

為什么不用Vagrant?

總的來說用Vagrant做快發是利用了 Vagrant 的一個插件,將平臺服務器作為『虛擬機』啟動 Droplet ,是很值得肯定的有效玩法。 VagrantFile 基本上就是個 ruby 腳本,需要一定的ruby知識作為基礎;相比 Dockerfile 是類似已定義的宏調用 DSL ,學習成本較低(但如果你要較真,說 COPY 和 ADD 的區別的話,其實也是滿麻煩的歷史遺留問題,話說回來你知道 WORKDIR 能自動創建文件夾么)。

用Vagrant啟動出來的是一個個的虛擬機,如果你的項目資金不足以支撐起一臺有VMware esxi的物理服務器,用docker的實現-單一進程即容器化的『虛擬機』,會有更輕量級的內存管控,數據粒度操作以及管理界面學習成本。

實際上技術上的主要區別我覺得在于被初始化的腳本( Vagrant 中是 vm.Provision ),你有沒有考慮守護進程的配置,服務出錯的調試方案,防火墻配置,軟件的定期更新,軟件的優化,更在于這個項目本身的易讀易改性,誰都不想照著本 500 頁的手冊頁只為了編寫一個 APP 是吧(向卡西歐圖形計算器致敬)。個人以為 docker 這方面做的更好,比如只需要在啟動時配置“環境變量”就能決定我開哪個版本的服務器,而無需觸及啟動腳本本身。

更方便規模擴展

在服務架構設計時多加些考慮,這樣容器化后的項目能有更好的規模彈性拓展能力。 相比傳統的接入負載均衡+大量后臺服務器的設計,用容器化的項目能做到更細粒度的彈性擴容:

  • 一個項目如果遇到CPU密集的情況,那就多點中間處理容器;
  • 數據庫壓力大了,我們可以考慮設計一個數據庫群集,實時插入新的容器節點;
  • 相比之前的實現,能做到出錯定位準確,開支大大減小等優點。

版本升級更方便

你只需要在寫Dockerfile的時候在第一行,改個版本號,然后重新構建一個鏡像,就算在構建時出了問題,也能在反饋日志中查找到。同時,你可以在一臺服務器上快速、方便的同屬運行多個版本的項目,對于測試和上線都是非常棒的選擇。

靈雀云之美

在本次容器化實驗中,使用了靈雀云CaaS公有云平臺。

  • 持續部署:我欣喜的發現在部署新版本時,網站依然能提供舊版本的服務,而當后臺部署完成以后,更新的容器能自動為后續的請求提供服務;
  • 靈雀云的鏡像加速器:只需要在docker的啟動項中加入靈雀的鏡像加速地址。再重啟docker守護進程,以后pull鏡像速度都是杠杠的;
  • 靈雀的鏡像中心:中國本土的『docker-hub』。你能在這里對鏡像內容進行留言提問;收藏到自己的賬號中,方便日后部署服務;查看構建過程;查看鏡像的版本和源代碼倉庫。但是也是有不足的地方:如不能展示鏡像的層數或是鏡像總體大小;
  • 靈雀提供了免費的域名綁定和動態負載:你可以很方便的使用網頁或CLI工具配置容器,比如對一個容器開啟多個實例,這樣能方便的做到項目的橫向拓展。

項目實戰

原項目簡介

weplay是一個socket.io庫的展示項目,你可以在socket.io官網的展示頁面看到它。它是由socket.io庫的開創者 rauchg,將一個已有的用Javascript實現的GBC模擬器,包裝成一個Node.js模塊后,再運用socket.io和HTML5的畫布 API,用Redis作為數據中轉以及持續化存儲的一個中型項目。

快速Docker化基于Socket.IO的HTML5游戲

準備工作

1.項目總覽:

  • weplay作為IO服務器,提供評論數據的持續化儲存和實時GBC界面數據的輸出服務。
  • weplay-web提供網頁服務。
  • weplay-emulator運行js模擬器,將數據渲染出的每個畫面輸出到Redis中。
  • weplay-presence用socket.io接口統計實時在線總人數。

項目已經被很明確的分割成了幾個模塊,由后臺服務器鏈接起來。

2.數據庫的加密加固工作:

由于此項目之前的架構大概是一體化,單一服務器的部署思路。Redis數據庫端沒有做加密訪問。但是我們都知道,Redis是很容易被破解的,它 的快速是把雙刃劍。如果我們在此添加訪問受密碼保護的Redis服務器的代碼,那么我們的服務就算是在某些危機四伏的彈性云平臺上也能很好的運作。 所以對于所有模塊進行打補丁。

3.與容器化無關的功能增加與版本更新

  • 如添加彈幕模式
  • 優化模擬器運行效率和內存占用
  • 可由環境變量方便的選擇加載的游戲ROM

容器化核心:Dockerfile編寫

1.首先給個具體的實例,是weplay-web,項目網頁服務器的容器化Dockerfile

```FROM node:0.10

MAINTAINER Jiahao Dai < dyejarhoo@gmail.com >

RUN git clone https://github.com/imdjh/weplay-web /srv/weplay-web && \

cd /srv/weplay-web && \

npm install && \

npm install forever -g

ADD docker-entrypoint.sh /entrypoint.sh

RUN chmod +x /entrypoint.sh

EXPOSE 3000

CMD ["/entrypoint.sh"]

```

實際上編寫Dockerfile很簡單,不外乎幾個步驟:

  1. FROM宏,用來指定基礎鏡像,docker的應用鏡像都是有層級的,可以復用造一個輪子不用從砍大叔開始。
  2. MAINTAINER,用來告訴使用者這個鏡像有問題該找誰
  3. RUN,你希望鏡像在被構建(build)時,執行什么命令,在這里我們用git工具下載最新的源代碼并用npm安裝依賴
  4. ADD,你希望將什么文件復制到容器中
  5. EXPOSE,你希望容器的哪些端口對外開放
  6. CMD,有哪些命令(如果是指令,一定主意路徑要在PATH環境變量中)是需要在容器初始運行時作為進程PID1存在的(當ENTRYPOINT為空時)。

和幾個注意點:

  1. 每個Dockerfile中的宏指令會生成一個鏡像層(image layer),所以最好在執行命令的時候,最后要回收垃圾,這樣執行完這條宏,生成的鏡像層會比較輕巧。例如這條語句:RUN apt-get update &amp;&amp; apt-get install -y libcairo2-dev &amp;&amp; rm -rf /var/lib/apt/lists/*,就做到了最小化安裝一個軟件源里的二進制包。
  2. 將守護進程寫在單一的文件中,例如本項目中的docker-entrypoint.sh。方便拓展和日后修改。

對于這個項目的Dockerfile,大家可以參見如下給出的軟件倉庫鏈接:

1. weplay-web -> letweplay

2. weplay + weplay-presence -> letweplay-io

3. weplay-emulator -> letweplay-core

docker-entrypoint.sh的一般編寫思路

  1. 設置默認環境變量的值
  2. 當某些環境變量未被設置時,進入出錯模式,自動退出執行。
  3. 執行守護進程

例如letweplay的入口腳本:

```bash

!/bin/bash

設置環境變量

export WEPLAY_WEB_PORT=3000

export WEPLAY_IO_URL="${IO_URL_PORT:-BAD}"

export NODE_ENV='production'

if [[ -n "${REDIS_PORT}" ]];then

export WEPLAY_REDIS_URI=${REDIS_PORT_6379_TCP_ADDR}:${REDIS_PORT_6379_TCP_PORT}

# 當某些環境變量未被設置時,進入出錯模式,自動退出執行

export WEPLAY_REDIS_AUTH=${REDIS_PASSWORD}

else

echo "Redis setting not found, can't start server." >&2 && exit 1

fi

if ( $(echo ${WEPLAY_IO_URL} | grep -q BAD ) );then

echo "IO_URL_PORT is missing, can't start entry server." >&2 && exit 1

fi

執行守護進程

forever /srv/weplay-web/index.js

```

容器化實現

1.Docker Compose方案

感謝compose工具,否則部署一個多模塊的項目會是讓人頭疼的一件事。 并且靈雀云的開源命令行工具alauda,支持compose部署,可以做到多模塊的項目一鍵部署在靈雀云平臺。對于本項目來說我們可以按照下面的步驟迅速布置起來一個平臺環境。

1) 首先使用python2的pip工具,下載安裝alauda工具。注意python3環境下,此工具會報錯不能正常使用。

```bash

$ python --version

Python 2.7.10

$ pip install alauda

```

2) 其次讓我們登陸進入自己的賬號,進行日后API調用的Oauth審核。

bash
$ alauda login
Username: imdjh
Password:
[alauda] Successfully logged in as imdjh.
[alauda] OK

3) 此時我們可以看一眼賬號中 服務 的狀態

```bash

$ alauda service ps

Name Command State Ports Instance Count IaaS Region

lwp-db Running lwp-db-imdjh.myalauda.cn:10024->6379/tcp 1 AZURE BEIJING2

lwp-entry Running lwp-entry-imdjh.myalauda.cn:80->3000/tcp 1 AZURE BEIJING2

lwp-core Running 1 AZURE BEIJING2

lwp-io Running lwp-io-imdjh.myalauda.cn:80->3001/tcp 1 AZURE BEIJING2

[alauda] OK

$ # 對于不需要的服務,我們可以使用命令輕松的刪除它

$ alauda service rm lwp-core

```

3) 將如下yml格式的內容保存成my.yml
yml
core:
image: 'index.alauda.cn/imdjh/letweplay-core:fallback'
environment:
- GAME=pokemon/yellow
- SAVEDELAY=120000
- WEPLAY_REDIS_AUTH=rosebud
links:
- redis
size: 'XS'
entry:
image: 'index.alauda.cn/imdjh/letweplay:latest'
environment:
- 'IO_URL_PORT=http://io-imdjh.myalauda.cn'
- 'WEPLAY_REDIS_AUTH=rosebud'
- 'THIS_URL_PORT=http://entry-imdjh.myalauda.cn'
- WEPLAY_WEB_PORT=80
links:
- redis
ports:
- 80/http
restart: on-failure
size: 'XXS'
io:
image: 'index.alauda.cn/imdjh/letweplay-io:latest'
environment:
- 'THIS_URL_PORT=http://io-imdjh.myalauda.cn'
- 'WEPLAY_REDIS_AUTH=rosebud'
links:
- redis
ports:
- 3001/http
size: 'XXS'
redis:
image: 'index.alauda.cn/library/redis:latest'
size: 'XXS'
ports:
- 6379/tcp
volumes:
- /data:10

再運行alauda compose up -f my.yml,等待出現諸如:

$ alauda compose up -f my.yml
[alauda] Creating and starting service &quot;redis&quot;
[alauda] Creating and starting service &quot;core&quot;
[alauda] Creating and starting service &quot;io&quot;
[alauda] Creating and starting service &quot;entry&quot;
[alauda] OK

4) 因為redis在啟動后需要一定時間才能開始監聽端口,而鏈接到的各容器會不能立即接連到數據庫而出錯。所以我們需要使用如下指令手動重啟容器,由于它們并不存有數據,所以重啟是無害的。

$ alauda service stop io &amp;&amp; alauda service start io
$ alauda service stop entry &amp;&amp; alauda service start entry
$ alauda service stop core &amp;&amp; alauda service start core

5) 既然用了有狀態的容器,那么就對數據庫做一個存檔吧

$ alauda backup create redis-$(date +%F) redis '/data'
[alauda] Creating backup &quot;redis-2015-11-30&quot;
[alauda] OK

2.單個擊破-單個部署方案:

如果處于種種原因沒辦法安裝compose工具,那么我們也能一條條的docker run起來:

```bash

$ #跑一個Redis容器,命令為some-redis

$ sudo docker run --name some-redis -d redis

$ #跑個GBC模擬器,鏈接到some-redis

$ sudo docker run -d -e 'GAME=pokemon/yellow' --link some-redis:redis imdjh/letweplay-core

$ #Socket.io服務器開起來,鏈接到some-redis

$ sudo docker run -d --link some-redis:redis -e 'THIS_URL_PORT= http://localhost:300 1' -p 3001:3001 imdjh/letweplay-io

$ #開啟萬維網服務器,鏈接到some-redis,服務已經全部開啟!

$ sudo docker run -d -e 'THIS_URL_PORT=1.2.3.4:3000' -e 'IO_URL_PORT=1.2.3.4:3001' --link some-redis:redis -p 0.0.0.0:3000:3000 imdjh/letweplay

```

3.圖形化操作方案:

如果你更習慣圖形化界面的操作,可以在靈雀云控制臺中,由redis, letweplay-core, letweplay-io, letweplay的順序進行服務的創建。

本實例一共使用了4個容器。配置如下:

3臺XXS(256 MB)的容器分別運行:letweplay, letweplay-io, redis

1臺XS(512MB)的容器運行:letweplay-core

部署的配置參數可以用如下截圖作為參考:

數據庫redis部分

快速Docker化基于Socket.IO的HTML5游戲

IO服務器letweplay-io部分

快速Docker化基于Socket.IO的HTML5游戲

模擬器服務器letweplay-core部分

快速Docker化基于Socket.IO的HTML5游戲

問題&解決

>前臺頁面無反應,但看得出IO服務器還是能用的

我們能看出IO服務器是能用的,但是也最好檢查一下letweplay-io項目的日志和監控狀態。如果一切良好,基本可以排除IO端的問題。

所以接著我們以此類推,查看letweplay-core項目的日志和監控狀態。如果letweplay-core項目的CPU占用率平均小于 5%我們基本可以推斷出是js模擬器發生了問題。我們需要重啟它。 在于靈雀云平臺的實戰中,我發現每當letweplay-core出現問題時,單單重啟core部分也不能解決問題,控制臺中顯示error: failed to connected to redis,可以簡單判斷出是core部鏈接到后端數據庫的部分出了問題(原因尚不明確)。所以我們要按照:redis -> letweplay-io -> letweplay-core -> letweplay的順序依次重啟,因為一旦重啟redis而不重啟其余被redis項目鏈接的項目,會造成鏈接到空項目的問題。

注意:本項目的及時存檔功能,數據是存儲于redis容器中的, 如果redis服務器被重啟了以后,那么游戲的進度數據將會蕩然無存。 對此,我推薦使用Volume掛載或是靈雀云的有狀態容器服務。

游戲畫面不動,在線人數總是為0

當遇到這種情況,肯定是socket.io服務器就有問題了,對此我們首先要處理socket.io服務器端的故障。 關于問題的判斷,可以打開瀏覽器的控制臺,如果有類似如圖的問題:

快速Docker化基于Socket.IO的HTML5游戲

那么就針對環境變量進行修改,改成正確的地址,如圖所示:

快速Docker化基于Socket.IO的HTML5游戲

letweplay-core項目總是啟動不了,提示『Bad GAME selection, please try another one!』錯誤

這個問題出現letweplay-core服務在下載ROM文件時發生超時等不能下載的情況。如果你運行的容器對下載服務器的線路不是特別穩定的話就回發生這種情況,在這種情況下。我們可以用DN_SERVER環境變量來定義第三方的下載服務器。

本項目系列鏡像可用環境變量參考表(cheat sheet)

letweplay

  • WEPLAY_REDIS_AUTH,字符串,空(未指定默認值),指定連接的Redis服務器的連接密碼;
  • THIS_URL_PORT,字符串,空(未指定默認值),指定當前服務器的地址、不能留空;
  • IO_URL_PORT,字符串,空(未指定默認值),指定socket.io服務器的地址、不能留空;
  • WEPLAY_WEB_PORT,整數,3000,接口服務器的端口。

letweplay-io

  • WEPLAY_REDIS_AUTH,字符串,空(未指定默認值),指定連接的Redis服務器的連接密碼;
  • WEPLAY_SERVER_UID,整數,1337,用于指定socket.io服務器的頻道;
  • CONTROLDELAY,整數,50,以毫秒為單位指定每個IP發出指令的最小間隔;
  • COUNTERDELAY,整數,12000,以毫秒為單位人數計數服務器查人頭的間隔;
  • THIS_URL_PORT,字符串,空(未指定默認值),指定當前服務器的地址、不能留空

letweplay-core

  • WEPLAY_SAVE_INTERVAL,整數,120000,單位是毫秒的自動保存延遲;
  • DN_SERVER,字符串,’ http://imdjh-dn.daoapp.io/ ‘,指定第三方ROM下載服務器;
  • GAME,字符串,空(未指定默認值),指定游戲名;
  • WEPLAY_REDIS_AUTH,字符串,空(未指定默認值),指定連接的Redis服務器的連接密碼。

項目展示

演示地址:lwp-entry-imdjh.myalauda.cn

『奇巧淫技』分享

  1. 如果你發現更新了項目的代碼,而在鏡像構建時沒有得到預期的結果時。可能是構建緩存在作怪,這時候一個解決問題的 笨方案 可以是在Dockerfile中下載代碼的宏,諸如RUN git clone git://github.com/imdjh/weplay改成RUN git clone --depth=1 git://....這樣的話,docker構建進程就會認為之前的緩存已經失效(未命中)而執行下載代碼的工作。
  2. 靈雀的構建可以在國內或國外,通常來說,如果你的項目鏡像在構建時(build)就會下載或更新很多文件(如yum update或npm install)就應該選在國外構建,如果是會在構建時下載只有在國內才快的,就選在國內。值得一提的是:國內國外構建對于同一個項目來說會共用緩存層, 這也就給在國內構建多了一個理由。
  3. hub.docker.com在容器構建時是永遠不會復用上一次構建過的層的,即不打開構建緩存功能:--no-cache=true。
  4. 靈雀的博客中會有神秘的禮品活動,混在干貨文章中隨機出現,這么重要的事,我只在最后悄悄告訴你。

快速Docker化基于Socket.IO的HTML5游戲

12月5日,白鷺時代主辦的HTML5移動生態大會上,靈雀云將帶來演講『Docker&CaaS如何加速HTML5游戲產品研發周期及降低運營成本』,歡迎對Docker和游戲感興趣的小伙伴提前入群,交流討論!

拓展閱讀 & 參考鏈接

鏡像倉庫(靈雀)- letweplay letweplay-io letweplay-core

容器源碼倉庫- letweplay letweplay-core letweplay-io

Node.js源碼倉庫- imdjh/gameboy imdjh/weplay-emulator imdjh/weplay-web imdjh/weplay-presence imdjh/weplay

知乎- 使用 Node.js 的優勢和劣勢都有哪些?

微軟文檔- 在 Azure App Service 中使用 Socket.IO 建立 Node.js 聊天應用程式

docker官方文檔- Dockerizing a Node.js web app

基于docker的數據庫群集設計- Getting started Galera with Docker

 本文由用戶 dfd7 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!