Node.js部署方案全解析

前言

部署方案对互联网系统而言至关重要,其直接影响到系统的稳定性、可靠性、可扩展性和用户体验。部署方案涉及的范围包括很多方面,比如硬件方面有服务器性能(CPU主频、颗数、核数,内存大小)、存储方式(本地、分布式)、带宽(网络吞吐量)等,软件方面有操作系统、中间件、数据库、应用实例(进程)数目等等。对于并发量与数据可预测的系统而言,可以通过计算获得大体需求,然后针对性的选择设备或软件。而对于互联网应用这种开放式的软件系统,很容易形成并发量的爆增,从而导致系统瘫痪。除了在程序开发时要考虑高并发的处理外,还需要在部署方案上进行考虑,即要保证系统便于横向扩展,能够实现快速扩容。
在系统具备一定规模以后,部署任务将是一个繁杂的工作,任何一个环节出现问题,都极有可能导致整个系统不可用。因而部署方案应尽量降低服务间的耦合性,同时开发适合自己系统的部署与监控工具,实现自动化的部署和验证工作。部署方案的另一个重要方面就是回退方案,保证系统升级出现无法短时间解决的问题时能及时回退,并采用增量升级的方式,即一个模块一个模块、一个服务一个服务、一个集群一个集群的更新,切不可一次性全面更新,那样一旦发生问题将是灾难性的。

本质

部署方案的本质是在满足用户需求的基础上,尽量保证系统的稳定性与可用性,并最大限度的降低成本,充分利用软硬件资源。

实施

现在的部署方式基本都是使用负载均衡+分布式部署,我们也采用这样的方式。

1.硬件环境

我们目前有测试环境,预发环境,线上环境三个部分。

测试环境只有一台机器,但是上面跑了不少git分支,分别对应不同的业务,也对应不同的端口。大部分分支是“开发分支”,也就是不保证稳定,用来做开发联调的时候用的,但是在测试机器上会有一个稳定的端口(通常是8001),这个端口跑主干分支的代码,一直稳定存在,用来对接其他后台系统或者客户端的稳定分支环境。测试服务器进程用forever管理,工程师在开发的时候需要自己去服务器拉取代码,配置环境。

预发环境,是一个真实的环境,只是没有访问入口而已,在每次发布前,把代码从分支合到主干(联调前,从主干往分支合一次),然后从主干发布到预发环境,用一个指向预发环境的客户端对各个功能进行部署,测试完毕后再发布到线上环境。这个环境连接的是线上的数据库,线上的缓存,线上的依赖服务。

正式环境,考虑到初期的访问量,正式环境将由负载均衡+4台主应用服务器+一台定时任务服务器。前期考虑到成本与运维压力,我们统一采用云服务的方式。负载均衡、服务器、数据库(服务)、缓存等都是云服务。很多云服务的负载均衡已经实现了高可用和可扩展性,操作也很简单方便,解决了后期系统的横向升级需求。云应用服务器在稳定性、弹性、安全性、易用性、可扩展方面也做的很好,后期可以根据实际的并发量随时进行升级,解决了系统纵向升级的问题。

由于云服务本身都有技术团队支持,运维基本上都不需要担心了,从技术与成本的考虑,选择云服务是非常合适的。

2.软件环境

系统的软件环境主要是linux+nodejs+mysql,可以简称LNM,由于其它服务基本都是用的云,在运行时我们需要考虑的软件产品其实不多。这其实是项目前期最明智的选择。技术依赖越多,自己的特色就越少,还大量占用宝贵的人力成本,因而不是产品初期的良好方式。要想做好一个产品,应尽量把所有精力用在产品的核心上,其余的问题能交出去的就要交出去,能不做的就不要做,这一点对产品的成功与否非常重要。

至于开发环境与工具的选择,只要基础是linux+nodejs+mysql,其余的开发、测试工具完全可以按照自己的喜好来,推荐使用你最喜欢、最熟悉的工具,这样可以最大发挥个人生产力。

3.伺服工具

伺服工具主要是用来管理服务进程、监测服务运行状态、记录日志,保证系统的可用性,大体上起到嵌入式中看门狗的作用,即在系统宕掉以后能够自动重启,恢复运行并记录错误日志。

对于伺服工具的选用,简单的说,线上用PM2,测试用Forever,本地用nodemon。简单分别介绍下为什么。

关于PM2和forever,一个重而大,一个小而轻,为什么线上用pm2测试用forever?

PM2更稳定一些,forever偶尔经常会莫名其妙进程就没了,出现几率不大,但是有几率,具体原因不明。
PM2会占用端口,即使你delete,stop了一个进程,它的端口还是占着,除非你把所有的list都kill,如果你的服务器上跑了好几个服务,那就很悲剧了。线上环境还好,不会经常重启,也不会调整端口。但是测试环境就不一样了,一台机器上跑10来个node服务,端口经常还要变一变(不同分支)。用pm2简直就是个大悲剧,这时候forever反而派上了用场,一个服务对应一个进程,更灵活易控制。
nodemon 这个东西,可能很多人也都知道,用来本地开发自动重启的,这里只是提一下,以后不需问”如何不重启进程让node代码生效“之类的问题额,重启还是要重启的,用个工具就方便多了。(一个原则,不要在node进程里保存状态,进程是可以随时被重启的)。

forever和pm2的基本用法就不详述了,文档里都有,pm2有很多强大功能,大家可以多研究下,虽然不一定用得到。

4.自动化部署

部署过程一般伴随这整个系统的始终,从开发、测试,到正式发布以及以后的运维工作都免不了了发布程序。对于一个集群式、服务式的系统而言,如果采取手动发布的方式是不可想象的。建立自己的自动部署工具基本成为必然需求。前文说过能拿来就用的尽量不要自行开发,但是一般的工具未必会完全适合你,为了满足系统自身的需求,有些工具还是自己做的比较好。

自动化部署工具其实是建立自己开发工作的机器人,对于一个开发人员而言,不运用程序为自己做些自动化的事情,实在是对不起自己的身份。个人以为,在向别人推荐自己的产品有多好之前,应该先让自己受益,那样你才能真正说服别人,让别人相信产品带来的好处。建立自己的机器人,就是要把自己从繁琐的事务中解放出来,把主要精力放在产品的提升上,放到核心工作上,让我们自己受益于我们的工作。

机器人的建立可以包含很多方面,凡是重复性、无特殊性的工作基本都可以解决。原来我工作的时候,要采购一套第三方软件,是关于图像识别的。由于要测试识别效果,经常要对几万幅图进行验证。厂家是个能偷懒的,他们把验证结果输出到一个文本文件了事,人工对照正确率真是个考验人的活。而且这件事没有任何技术含量,安排给谁都是浪费我们的宝贵人力,每次安排给一个初级测试工程师,他都要吭哧吭哧干上两天,还容易出错。后来我就写了一段代码,把测试结果统计一下,把验证图片按照结果分别复制到不同的文件夹,测试工程师只需要个文件夹一个文件夹的浏览图片,由于图片较小又归了类,一眼就可以看出很多图片的正确与否,选择一个图片,如果结果不正确按ESC,正确按Enter,一个小时基本就完成了。其实这个工作很简单,只要肯花一点时间去写点代码,可以节省几倍甚至几十倍的工作,可惜很多人宁可不断重复劳动,也不愿意去“偷懒”。

这里我们就自行实现一个自动化部署的例子。自动化部署也是属于开发机器人的一部分,怎么实现呢?我们从实际的需要分析。部署的基本过程是把服务器上的代码做好备份,本地代码(有些可能需要先编译)上传到服务器,配置成生产环境的参数,对数据库进行修改(必要时),然后重启服务。为了不影响在线用户,还要先从负载均衡设备上把要部署的服务摘下,部署完成并经过测试后,再重新挂上,然后继续下一个服务。

我们手动的操作基本就是这个流程,我们现在就把这个人工过程变成自动过程。

第一步就是打包要发布的代码,当然是你已经提交并经过测试的代码,linux打包文件就是用tar命令,nodejs调用本地命令一般是用spawn,代码如下:

  1. const spawn = require('child_process').spawn;
  2. function archive(opt, cb) {
  3. var src = opt.src;
  4. if(src.endsWith('/'))src=src.substring(0, src.length-1);
  5. var tmp = src.split('/');
  6. var dir = src.substring(0, src.lastIndexOf('/'));
  7. var filename = tmp[tmp.length-1];
  8. var archname = filename+'.tar';
  9. var opts = ['-cf', archname, '-C', dir, filename];
  10. var exclude = config.exclude
  11. if(exclude.length >0){
  12. exclude.forEach(function(exd){
  13. opts.push('--exclude='+exd);
  14. });
  15. }
  16. opts.push('--totals');
  17. var tar = spawn('tar', opts, { stdio: [0, 'pipe','pipe'] });
  18. tar.stderr.on('data', (data) => {
  19. console.log('error:'.red, ` ${data}`);
  20. });
  21. tar.on('close', (code) => {
  22. console.log('info:'.green, `package is complete.`);
  23. if(cb)cb();
  24. });
  25. }

opt是配置选项,里面把源码目录,排除文件等参数设置好,即可调用tar命令进行打包。

第二步,文件上传,这里需要一个nodejs包ssh2,这个包实现了用js执行sftp等远程传输命令。我们也可以自己实现这部分功能,比如还用spawn调用scp、sftp等命令,只不过没有在服务端添加密钥的话,需要手动输入密码。ssh2可以通过程序输入密码,

  1. const Client = require('ssh2');
  2. //连接服务器选项
  3. var options ={
  4. host: '192.168.*.*',//服务器地址
  5. port: 22,
  6. username: 'user',
  7. password: 'pass'
  8. }
  9. /**
  10. *dest:目的路径,/home/web/testprj/
  11. *arch:发布的包,/test.tar
  12. ****************************/
  13. function upload(dest, arch){
  14. var conn = new Client();
  15. conn.on('ready', function() {
  16. conn.sftp(function(err, sftp) {
  17. if (err) throw err;
  18. sftp.fastPut(arch, dest+arch, function(err, result) {
  19. if (err) throw err;
  20. console.log('info:'.green, 'transport complete.');
  21. shutdown(conn);
  22. });
  23. });
  24. }).connect(options);
  25. }

这样程序包即可上传服务器,接着就可以执行部署步骤了,这里还是用ssh2插件,通过ssh远程执行命令,首先进行解压:

  1. function extract(conntar, path) {
  2. conn.exec('tar -xf '+tar +' -C '+path, function(err, stream) {
  3. if (err) throw err;
  4. stream.on('close', function(code, signal) {
  5. console.log('info: '.green, 'archive is extract successfully.');
  6. startup(conn);
  7. }).on('data', function(data) {
  8. console.log('info: '.green, `${data}`);
  9. }).stderr.on('data', function(data) {
  10. console.log('error: '.red, `${data}`);
  11. });
  12. });
  13. }

这里conn是上面压缩时创建的ssh2的客户端连接对象,tar是上传后的包名,path是要解压到的目的路径。

之后就是具体的部署过程,比如备份,替换文件,修改配置,执行sql语句等等,都和解压命令一样,通过ssh远程调用命令执行。不同的项目具体的部署细节千差万别,可以很简单,也可以很复杂,但实现方式都没有什么本质区别,都是把手动的工作按照顺序翻译成程序执行,这里就不一一详述了。这项工作完成以后,对我们工作效率的提升是非常巨大的,也可以让枯燥的开发工作变的有趣一点。

由于部署命令大部分在控制台完成,我们有必要加入丰富的提示信息,把每一步完成的情况反馈给我们,同时为使信息清晰、整洁,我们引入colors这个npm包,它可以让控制台的字符按照我们想要的颜色显示,只需要在字符串后面加上.red等表示色彩的值就可以了。

我们还可以把这个工具写成控制台命令,类似npm这样的。这样我们可以通过命令重做部署过程的任意一步,以免部署失败后重头来过。命令行程序的开发基本就是做两件事,一是解析命令,就是获取到控制台输入的命令、选项、参数等信息,二是执行对应功能。当我们运行node xx.js时,即启动了一个node进程,这时控制台输入的所有信息都保存在progress.argv这个数组里,前两个参数就是node和 xx.js。

控制台命令的开发可以使用commander这个npm包,它提供了一个框架,你只要简单的注册命令、选项等信息,然后实现对应的功能就可以了,具体可以参考相关文档。

当然,如果你是一个系统高手,完全可以用shell命令实现相同的功能。某种程度上说,nodejs与shell有些像,他们本身都是个壳,具体做事的都是操作系统以及系统上的各个服务。

仅仅完成这些只是自动化部署的第一步,后面还要考虑怎么进行服务监测,怎么分阶段自动化部署集群等等,不过这些问题在于细节的完善,需要的是时间和精力而已。

我们可以参考一下PM2自动部署的方式,有兴趣的可以去研究下,支持多机自动部署,写个配置文件,然后执行一个命令即可,这里就不展开啦。详见文档:https://github.com/Unitech/PM2/blob/master/ADVANCED_README.md#deployment

案例

亿书官网部署方案

http://www.lxway.net/15605021.html
https://certsimple.com/blog/deploy-node-on-linux
Node.js as a background service