记一次NodeJS测试集群全线瘫痪的解决思路

记一次NodeJS测试集群全线瘫痪的解决思路

事情背景

测试服务器集群用docker部署有30~40个用pm2管理的NodeJS web应用。近期遇上升级PM2与NodeJS版本。所有服务器用pm2启动都出现了一个问题,导致凡是发布代码到测试服务器的都陷入了瘫痪,服务502:

错误内容如下:

[PM2][ERROR] Process failed to launch spawn E2BIG

经查询资料,pm2 作者的回答是参数过长,一连懵逼不知何意?经过长达6小时的逐步分析,终于找到了问题和应急解决方案,问题是docker部署方式随着日积月累的项目数增加导致环境变量数据过大,终于今天爆发。解决的整个过程思路清晰,对部署相关的经验对前端人士有积极作用,故记录过程以做分享。

解决思路及过程:

  1. 排除代码问题(最开始由某业务线引爆,并不知所有业务线均此问题)
  2. 线上线下环境一致性(排除测试服务器与开发环境的版本差异)
  3. 版本回退 (排除升级版本导致)
  4. docker部署迁移至ECS环境(排除环境问题)
  5. 回归本质,调试pm2
  6. 锁定问题,解决方案

排除代码问题

由于某业务线抛出测试服务器部署失败,此错误信息从未见过,网上查看资源甚少,一时间无法断定。比较常规的联想到是开发提交代码导致。但查看后只是提交常规代码,迅速排除了此可能性。为了排除是代码运行环境问题,对测试服务器与本地开发环境做了环境的统一来排查

线上线下环境一致性

通过 pm2 report 命令查看对比了线上线下的pm2 与nodejs版本,发现了差异,并是本地开发环境与线上严格一致。确认一致后问题仍存在,排除了是代码环境所致。联想到近期运维做的NodeJS与pm2的升级,因此对此作了版本的回退。

版本回退

沟通运维同学对测试服务器进行pm2、nodejs的版本回退。回退后问题仍然存在。此时各业务线陆续爆出测试服务器故障,无法自动部署。因此排除了各环境问题。怀疑是测试服务器的特殊性所致。

docker部署迁移至ECS环境

此时已排除了各种情况,前端开发者已无力解决了。运维提出一个方案,考虑到测试环境是docker部署,存在特殊性,预发环境与线上环境采用ECS部署,并无此问题,通过服务器的迁移来临时解决测试服务器故障的问题。迁移后,结果有效,但是需要对几十个应用进行迁移,工作量较大,并且不是长久之计。于是考虑到pm2也是nodejs应用,可以通过步步调试追踪问题本质。

回归本质,调试pm2

根据信息反馈,有提示是参数长度过大,并附有堆栈信息,考虑调试pm2追踪问题本源。

pm2是全局安装的,因此调试需要在操作系统的全局pm2位置进行打日志调试。

pm2全局源码位置(注意:因操作系统而异):

C:\Users\pc\AppData\Roaming\npm\node_modules\pm2\lib

pm2调试方法

1、DEBUG=*

NODE_ENV=test PORT=30138 DEBUG=* pm2 reload process.json --only nodejs-xxx-test

2、打console.log

方式一二并用,跟进错误位置与日志记录位置,锁定是环境变量参数的长度异常

//pm2/lib/client.js 498行
Client.prototype.executeRemote = function executeRemote(method, app_conf, fn) {
  var self = this;

  // stop watch on stop | env is the process id
  if (method.indexOf('stop') !== -1) {
    this.stopWatch(method, app_conf);
  }
  // stop watching when process is deleted
  else if (method.indexOf('delete') !== -1) {
    this.stopWatch(method, app_conf);
  }
  // stop everything on kill
  else if (method.indexOf('kill') !== -1) {
    this.stopWatch('deleteAll', app_conf);
  }
  else if (method.indexOf('restartProcessId') !== -1 && process.argv.indexOf('--watch') > -1) {
    delete app_conf.env.current_conf.watch;
    this.toggleWatch(method, app_conf);
  }

  if (!this.client || !this.client.call) {
    this.start(function(error) {
      if (error) {
        if (fn)
          return fn(error);
        console.error(error);
        return process.exit(0);
      }
      if (self.client) {
        return self.client.call(method, app_conf, fn);
      }
    });
    return false;
  }
  console.log('=========长度======', JSON.stringify(app_conf).length);
  debug('Calling daemon method pm2:%s on rpc socket:%s', method, this.rpc_socket_file);
  return this.client.call(method, app_conf, fn);
};

追踪参数app_conf数据来源的根源

//pm2/lib/client.js 199行
/**
   * Do not copy internal pm2 environment variables if acting on process
   * is made from a programmatic script started by PM2
   */
  if (cst.PM2_PROGRAMMATIC)
    Common.safeExtend(env, process.env);
  else
    env = process.env;

  // Change to double check  (dropped , {pm_cwd: cwd})
  app.env = [{}, env, app.env || {}].reduce(function(e1, e2){
    return util._extend(e1, e2);
  });

经过反复调试,锁定环境变量根源,就是我们熟知的process.env。

问题原因命中

process.env会获取操作系统的所有变量。因此docker部署应用多的情况下会导致env过长,

发现docker部署存在缺陷,系统变量很多,存在大量的“TEST_" 等变量数据。造成process.env多达70000以上字节长度。占了99%的process.env长度,并且皆为无用变量。

知道以后可直接通过以下命令查看

node
> process.env

锁定问题,解决方案

知道了是环境变量过多导致,解决就有2种办法。一种是docker部署上改进,避免测试集群的操作系统环境变量过多。第二种是分析环境变量数据是否有用,过滤无用数据。第一种方案显然对于我们前端开发人员来说,一时间难以处理。第二种通过改pm2的代码做个简单过滤,适合前端人员。

修改方法十分简单,过滤env的key值,到达精简env的目的。

方案二不是最佳实践,需要对pm2进行修改并打包,后续需自己维护一个pm2包,并且不是普适性的解决方案,只对我司情况有效。因此也只是临时解决方案。

方案不是很好,但有效的使测试环境悉数全部归于正常。后续解决从docker部署上改进才是专业的解法。

总结与思考

前端需要知道一些linux、docker等部署知识,对于pm2、nodejs的底层不需要非常了解,但是遇到问题时如果能够深入去剖析问题,能达到意想不到的效果。

编辑于 2019-07-17