编者按: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采用了Google Chrome浏览器的V8引擎,性能很好的同时,将许多系统级的API进行了傻瓜化的封装。相对于运行于网页页面的Javascript脚本,Node.js则是作为一个全面的后台运行进程,用Javascript提供了其他语言能够实现的许多功能。 Node.js由于其上手快、事件驱动、异步编程的特性,已被广泛的应用在众多企业的生产环境中。
Socket.IO是一个开源的WebSocket库,它通过Node.js实现WebSocket服务端,同时也提供客户端JS库。Socket.IO支持以事件为基础的实时双向通讯,它可以工作在任何平台、浏览器或移动设备,也是H5开发的常用技术。
对于任何一个项目的已有改动都将是费时费力的,无论是模块重构还是框架更迭。但是有以下的一些优点让你花些时间,将手头的项目代码制作成一个容器镜像。
手头的项目可能在设计以及实现时,就已经做了模块化的编写(这是比较好的情况,更可能是一个不可维护的状态)。
但是如果着手将代码容器化,你将不得已的面对各模块的代码分别运行在不同“虚拟机”里,而仅靠通讯协议来进行数据交互的状况。这也可以说是实现了项目整体的微服务化,在容器化完成以后,项目出了问题,可以直接查对应项目里的日志,哪儿除了错会一览无遗!
总的来说用Vagrant做快发是利用了 Vagrant 的一个插件,将平台服务器作为『虚拟机』启动 Droplet ,是很值得肯定的有效玩法。 VagrantFile 基本上就是个 ruby 脚本,需要一定的ruby知识作为基础;相比 Dockerfile 是类似已定义的宏调用 DSL ,学习成本较低(但如果你要较真,说 COPY 和 ADD 的区别的话,其实也是满麻烦的历史遗留问题,话说回来你知道 WORKDIR 能自动创建文件夹么)。
用Vagrant启动出来的是一个个的虚拟机,如果你的项目资金不足以支撑起一台有VMware esxi的物理服务器,用docker的实现-单一进程即容器化的『虚拟机』,会有更轻量级的内存管控,数据粒度操作以及管理界面学习成本。
实际上技术上的主要区别我觉得在于被初始化的脚本( Vagrant 中是 vm.Provision ),你有没有考虑守护进程的配置,服务出错的调试方案,防火墙配置,软件的定期更新,软件的优化,更在于这个项目本身的易读易改性,谁都不想照着本 500 页的手册页只为了编写一个 APP 是吧(向卡西欧图形计算器致敬)。个人以为 docker 这方面做的更好,比如只需要在启动时配置“环境变量”就能决定我开哪个版本的服务器,而无需触及启动脚本本身。
在服务架构设计时多加些考虑,这样容器化后的项目能有更好的规模弹性拓展能力。 相比传统的接入负载均衡+大量后台服务器的设计,用容器化的项目能做到更细粒度的弹性扩容:
你只需要在写Dockerfile的时候在第一行,改个版本号,然后重新构建一个镜像,就算在构建时出了问题,也能在反馈日志中查找到。同时,你可以在一台服务器上快速、方便的同属运行多个版本的项目,对于测试和上线都是非常棒的选择。
在本次容器化实验中,使用了灵雀云CaaS公有云平台。
weplay是一个socket.io库的展示项目,你可以在socket.io官网的展示页面看到它。它是由socket.io库的开创者rauchg,将一个已有的用Javascript实现的GBC模拟器,包装成一个Node.js模块后,再运用socket.io和HTML5的画布API,用Redis作为数据中转以及持续化存储的一个中型项目。
1.项目总览:
项目已经被很明确的分割成了几个模块,由后台服务器链接起来。
2.数据库的加密加固工作:
由于此项目之前的架构大概是一体化,单一服务器的部署思路。Redis数据库端没有做加密访问。但是我们都知道,Redis是很容易被破解的,它的快速是把双刃剑。如果我们在此添加访问受密码保护的Redis服务器的代码,那么我们的服务就算是在某些危机四伏的弹性云平台上也能很好的运作。 所以对于所有模块进行打补丁。
3.与容器化无关的功能增加与版本更新
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很简单,不外乎几个步骤:
FROM
宏,用来指定基础镜像,docker的应用镜像都是有层级的,可以复用造一个轮子不用从砍大叔开始。 MAINTAINER
,用来告诉使用者这个镜像有问题该找谁 RUN
,你希望镜像在被构建(build)时,执行什么命令,在这里我们用git工具下载最新的源代码并用 npm
安装依赖 ADD
,你希望将什么文件复制到容器中 EXPOSE
,你希望容器的哪些端口对外开放 CMD
,有哪些命令(如果是指令,一定主意路径要在PATH环境变量中)是需要在容器初始运行时作为进程PID1存在的(当ENTRYPOINT为空时)。 和几个注意点:
RUN apt-get update && apt-get install -y libcairo2-dev && rm -rf /var/lib/apt/lists/*
,就做到了最小化安装一个软件源里的二进制包。 docker-entrypoint.sh
。方便拓展和日后修改。 对于这个项目的Dockerfile,大家可以参见如下给出的软件仓库链接:
1. weplay-web -> letweplay
2. weplay + weplay-presence -> letweplay-io
3. weplay-emulator -> letweplay-core
docker-entrypoint.sh的一般编写思路
例如letweplay的入口脚本:
```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
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 "redis"
[alauda] Creating and starting service "core"
[alauda] Creating and starting service "io"
[alauda] Creating and starting service "entry"
[alauda] OK
4) 因为redis在启动后需要一定时间才能开始监听端口,而链接到的各容器会不能立即接连到数据库而出错。所以我们需要使用如下指令手动重启容器,由于它们并不存有数据,所以重启是无害的。
$ alauda service stop io && alauda service start io
$ alauda service stop entry && alauda service start entry
$ alauda service stop core && alauda service start core
5) 既然用了有状态的容器,那么就对数据库做一个存档吧
$ alauda backup create redis-$(date +%F) redis '/data'
[alauda] Creating backup "redis-2015-11-30"
[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部分
IO服务器letweplay-io部分
模拟器服务器letweplay-core部分
>前台页面无反应,但看得出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服务器端的故障。 关于问题的判断,可以打开浏览器的控制台,如果有类似如图的问题:
那么就针对环境变量进行修改,改成正确的地址,如图所示:
letweplay-core项目总是启动不了,提示『Bad GAME selection, please try another one!』错误
这个问题出现letweplay-core服务在下载ROM文件时发生超时等不能下载的情况。如果你运行的容器对下载服务器的线路不是特别稳定的话就回发生这种情况,在这种情况下。我们可以用DN_SERVER环境变量来定义第三方的下载服务器。
letweplay
letweplay-io
letweplay-core
演示地址:lwp-entry-imdjh.myalauda.cn
RUN git clone git://github.com/imdjh/weplay
改成 RUN git clone --depth=1 git://....
这样的话,docker构建进程就会认为之前的缓存已经失效(未命中)而执行下载代码的工作。 --no-cache=true
。
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