问题描述
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形式启动