Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
iliuyt opened this issue Dec 2, 2020 · 0 comments
Open

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

iliuyt opened this issue Dec 2, 2020 · 0 comments

Comments

@iliuyt
Copy link
Owner

iliuyt commented Dec 2, 2020

问题描述

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(0, 3) + "_" : "") + 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形式启动
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant