docker 中运行pm2 日志太大问题解决

·  阅读 2271

问题描述

docker中运行pm2 日志磁盘过大,导致项目无法运行。但是我们并不需要pm2日志,日志是通过容器进行收集的。

问题查找

运行 pm2-runtime --json package.json 和 pm2-runtime server.js 时,发现后者不会打印日志到磁盘上,因为指定的日志目录为 /dev/null, 但我们package.json也没有指定out_file和error_file,但却会打印日志到 $HOME/.pm2/logs下。

源码分析

打开pm2源码进行分析。

首先找到pm2-runtime命令文件,位于/bin目录下,可以看到引用的是Runtime4Docker文件

require('../lib/binaries/Runtime4Docker.js');
复制代码

查看Runtime4Docker文件,分析后可以看到最后执行的是pm2.start命令,根据引用找到 lib/API.js文件

// 这个是命令入口
commander.command('*')
  .action(function(cmd){
    Runtime.instanciate(cmd);
  });


var Runtime = {
  pm2 : null,
  instanciate : function(cmd) {
      ....
      // 执行启动命令
      Runtime.startApp(cmd, function(err) {
        if (err) {
          console.error(err.message || err);
          return Runtime.exit();
        }
      });
    });
  }
}

.....

startApp : function(cmd, cb) {
    function exec() {
      // 执行pm2 start 命令
      this.pm2.start(cmd, commander, function(err, obj) {
        ....
      });
    }
    // via --delay <seconds> option
    setTimeout(exec.bind(this), commander.delay * 1000);
  },
复制代码

在API.js文件中找到start命令,分析可知,根据cmd参数的不同,分别执行了_startJson和_startScript命令

 start(cmd, opts, cb) {
    ....
    if (Common.isConfigFile(cmd) || typeof cmd === "object") {
      // 配置启动 pm2-runtime --json package.json
      that._startJson(cmd, opts, "restartProcessId", cb);
    } else {
      // js脚本启动 pm2-runtime start server.js
      that._startScript(cmd, opts, cb);
    }
  }
复制代码

首先分析 --json 的情况,就是执行 _startJson 方法,各种回调流转过程就跳过了,最终会调用startApps,并在Common.resolveAppAttributes这里 去初始化了参数配置,下一步进入 lib/Common.js

_startJson(file, opts, action, pipe, cb) {
   
    .....

    // app启动
    function startApps(app_name_to_start, cb) {
     
      .....

      eachLimit(
        apps_to_start,
        conf.CONCURRENT_ACTIONS,
        function(app, next) {
      
          .....

          try {
            // 配置路径获取
            resolved_paths = Common.resolveAppAttributes(
              {
                cwd: that.cwd,
                pm2_home: that.pm2_home,
              },
              app
            );
          } catch (e) {
            apps_errored.push(e);
            return next();
          }
          
          ......
        },
       
      );
      return false;
    }
  }
复制代码

resolveAppAttributes 方法中又调用了prepareAppConf进行初始化配置

Common.resolveAppAttributes = function(opts, legacy_app) {
  var appConf = fclone(legacy_app);
  // 初始化配置
  var app = Common.prepareAppConf(opts, appConf);
  if (app instanceof Error) {
    Common.printError(cst.PREFIX_MSG_ERR + app.message);
    throw new Error(app.message);
  }
  return app;
};
复制代码

根据用户是否设置out_file,error_file等参数来初始化

Common.prepareAppConf = function(opts, app) {
    ......
    ["log""out""error""pid"].forEach(function(f) {
      // 获取out_file error_file等参数是否存在
      var af = app[f + "_file"],
        ps,
        ext = f == "pid" ? "pid" : "log",
        isStd = !~["log""pid"].indexOf(f);

      if (af) af = resolveHome(af);
  
      // 如果没有设置文件路径参数,就配置默认参数
      if ((f == "log" && typeof af == "boolean" && af) || (f != "log" && !af)) {
        // cst是默认配置文件,默认目录配置(DEFAULT_LOG_PATH、DEFAULT_PID_PATH)在 paths.js 中 constants.js 引用了 paths.js
        ps = [
          // 默认文件目录
          cst["DEFAULT_" + ext.toUpperCase() + "_PATH"],
          // 这里是默认文件日志文件名称
          formated_app_name + (isStd ? "-" + f : "") + "." + ext,
        ];
      } 
      // 如果设置了文件路径参数,就检测目录是否存在,不存在就创建目录
      else if (f != "log" || (f == "log" && af)) {
        ps = [cwd, af];
  
        var dir = path.dirname(path.resolve(cwd, af));
        if (!fs.existsSync(dir)) {
          Common.printError(
            cst.PREFIX_MSG_WARNING + "Folder does not exist: " + dir
          );
          Common.printOut(cst.PREFIX_MSG + "Creating folder: " + dir);
          require("mkdirp")(dir, function(err) {
            if (!err) return;
            Common.printError(
              cst.PREFIX_MSG_ERR + "Could not create folder: " + path.dirname(af)
            );
            throw new Error("Could not create folder");
          });
        }
      }
      // PM2 paths
      
      // 设置默认参数,删除用户设置参数,然后返回
      ps &&
        (app[
          "pm_" + (isStd ? f.substr(03) + "_" : "") + ext + "_path"
        ] = path.resolve.apply(null, ps));
      delete app[f + "_file"];
    });
  
    return app;
  };
复制代码

看到这里我们就知道通过 --json 的方式启动会设置默认日志目录,我们再看下js脚本的方式启动

首先命令后启动时,如果没有配置output和error默认为/dev/null,该参数会传到_startScript方法中,opts的output和error都为/dev/null

commander.version(pkg.version)
  .....
  .option('--error <path>''error log file destination (default disabled)''/dev/null')
  .option('--output <path>''output log file destination (default disabled)''/dev/null')
  ....
复制代码

_startScript 方法中通过 Config.filterOptions 进行了配置过滤和转换 ,Config位于 lib/tools/Config.js

_startScript(script, opts, cb) {
    if (typeof opts == "function") {
      cb = opts;
      opts = {};
    }
    var that = this;

    /**
     * Commander.js tricks
     */
    // 参数过滤
    var app_conf = Config.filterOptions(opts);
   
    ......
  }
复制代码

查看 filterOptions 方法,通过schema进行过滤和转换,将output、error参数转换成了 out_file和error_file

Config.filterOptions = function(cmd) {
  var conf = {};
  var schema = this.schema;

  for (var key in schema) {
    var aliases = schema[key].alias;
    aliases && aliases.forEach(function(alias){
      if (typeof(cmd[alias]) !== 'undefined') {
        conf[key] || (conf[key] = cmd[alias]);
      }
    });
  }

  return conf;
};
复制代码

然后再看 _startScript 的 restartExistingProcessPathOrStartNew方法,该方法启动和重启时都会调用 restartExistingProcessPathOrStartNew 可以看到 Common.resolveAppAttributes 方法,后续执行逻辑与 --json模式一样 这里app_conf就是之前过滤的参数,因为已经设置了out_file和error_file为 /dev/null,所以最终日志目录为 /dev/null

    function restartExistingProcessPathOrStartNew(cb) {
      that.Client.executeRemote("getMonitorData", {}, function(err, procs) {
       
        ......

        var resolved_paths = null;

        try {
          resolved_paths = Common.resolveAppAttributes(
            {
              cwd: that.cwd,
              pm2_home: that.pm2_home,
            },
            app_conf
          );
        } catch (e) {
          Common.printError(e);
          return cb(Common.retErr(e));
        }
        .....

    }
复制代码

解决方案

通过以上解析,可以使用以下方案

  • package.json 中设置out_file 和 error_file 为 /dev/null
  • 使用pm2-runtime start server.js形式启动
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改