1、构建
从源码到真正运行,一般会拆分为构建和部署两个步骤,做到一次构建多次部署。
js语言构建的过程主要是下载依赖,一般安装依赖会指定node_env=production或者只安装dependencies的依赖,构建完成之后打包成tgz文件,部署时解压启动即可。
2、部署
部署服务器需要预装Node.js,框架支持的版本为>=8.0.0
框架内置了egg-cluster来启动Master进程,Master有足够的稳定性,不再需要使用pm2等进程守护模块。同时框架也提供了egg-scripts来支持线上环境的运行和停止。
3、监控
对服务进行性能监控,内存泄露分析以及鼓掌排除等。
${appInfo.root}/logs/${appInfo.name}
路径下,例如 /home/admin/logs/example-app。
/path/to/example-app/logs/example-app。
// config/config.${env}.js
exports.logger = {
dir: '/path/to/your/custom/log/dir',
};
日志分类:
${appInfo.name}-web.log,例如 example-app-web.log
,应用相关日志,供应用开发者使用的日志。egg-web.log
框架内核、插件日志。common-error.log
实际一般不会直接使用它,任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。egg-agent.log agent
进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。日志打印:
//处理请求时的日志
ctx.logger.debug('debug info');
ctx.logger.info('some request data: %j', ctx.request.body);
ctx.logger.warn('WARNNING!!!!');
// 错误日志记录,直接会将错误日志完整堆栈信息记录下来,并且输出到 errorLog 中
// 为了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。
ctx.logger.error(new Error('whoops'));
//应用级别日志
// app.js
module.exports = app => {
app.logger.debug('debug info');
app.logger.info('启动耗时 %d ms', Date.now() - start);
app.logger.warn('warning!');
app.logger.error(someErrorObj);
};
// agent Logger
// agent.js
module.exports = agent => {
agent.logger.debug('debug info');
agent.logger.info('启动耗时 %d ms', Date.now() - start);
agent.logger.warn('warning!');
agent.logger.error(someErrorObj);
};
日志级别:
NONE、DEBUG、INFO、WARN、ERROR五个级别。
默认只会输出INFO及以上级别的日志到文件中,可以修改输出到文件日志的级别
为避免一些插件的调试日志在生产环境打印导致性能问题,生产环境默认禁止打印DEBUG级别的日志,可以打开allowDebugAtProd
配置项在生产环境答应debug日志进行调试。
修改输出到终端日志的级别:
//打印所有级别日志到终端
// config/config.${env}.js
exports.logger = {
consoleLevel: 'DEBUG',
};
//关闭所有打印到终端的日志
// config/config.${env}.js
exports.logger = {
consoleLevel: 'NONE',
};
//开启正式环境下终端日志的输出
// config/config.${env}.js
exports.logger = {
disableConsoleAfterReady: false,
};
日志切割:
1、框架默认按照天进行切割,在每日凌晨按照.log.YYYY-MM-DD
文件名进行切割
2、按照文件大小进行切割
//当文件超过2G时进行切割
// config/config.${env}.js
const path = require('path');
module.exports = appInfo => {
return {
logrotator: {
filesRotateBySize: [
path.join(appInfo.root, 'logs', appInfo.name, 'egg-web.log'),
],
maxFileSize: 2 * 1024 * 1024 * 1024,
},
};
};
3、按小时进行切割
// config/config.${env}.js
const path = require('path');
module.exports = appInfo => {
return {
logrotator: {
filesRotateByHour: [
path.join(appInfo.root, 'logs', appInfo.name, 'common-error.log'),
],
},
};
};
频繁的磁盘IO会影响性能,所以一般采用的文件日志写入的策略是:日志同步写入内存,异步每隔一段时间刷盘。
1、通过app使用HTTPClient
框架在应用初始化的时候,会自动将HTTPClient初始化到app.httpclient,同时增加了app.curl(url,options)
方法,
// app.js
module.exports = app => {
app.beforeStart(async () => {
// 示例:启动的时候去读取 https://registry.npm.taobao.org/egg/latest 的版本信息
const result = await app.curl('https://registry.npm.taobao.org/egg/latest', {
dataType: 'json',
});
app.logger.info('Egg latest version: %s', result.data.version);
});
};
2、通过ctx使用HTTPClient
框架提供了ctx.curl(url,options)
和ctx.httpClient
保持跟在app下使用的体验一致,这样既可方便的使用ctx.curl()方法完成一次http请求。
当用户请求没有导致Session被修改时,框架不会延长session的有效期,为了不让用户退出登录态,框架提供了一个renew配置项用于实现此功能,会在发现用户session的有效期仅剩下最大有效期的一半的时候,重置session的有效期。
// config/config.default.js
module.exports = {
session: {
renew: true,
},
};
针对特定用户的session有效时间设置,可以通过ctx.session.maxAge=
来实现
js代码是单线程的,即Node.js进程只能运行在一个cpu上,Node.js官方提供的解决方案是cluster模块,
单个 Node.js 实例在单线程环境下运行。为了更好地利用多核环境,用户有时希望启动一批 Node.js 进程用于加载。
集群化模块使得你很方便地创建子进程,以便于在服务端口之间共享。
在服务器上同时启动多个进程,每个进程里跑的都是同一份源码,这些进程可以同时监听一个端口。
其中,负责启动其他进程的叫做Master 进程,只用来负责启动其他进程
被启动的进程叫做Worker进程,接收请求,对外提供服务。Worker进程一般根据服务器的cpu来定。
进程守护:
当一个worker遇到未捕获的异常的时候,此时它处于一个不确定状态,应该让改进程优雅退出:
OOM、系统异常:
Agent机制:
Agent worker是一个单独的进程,专门用来处理一些公共事务。
综上:
框架的启动时序如下
Agent的用法
agent.js 的代码会执行在 agent 进程上,app.js 的代码会执行在 Worker 进程上,他们通过框架封装的 messenger 对象进行进程间通讯(IPC)
// agent.js
module.exports = agent => {
// 在这里写你的初始化逻辑
// 也可以通过 messenger 对象发送消息给 App Worker
// 但需要等待 App Worker 启动成功后才能发送,不然很可能丢失
agent.messenger.on('egg-ready', () => {
const data = { ... };
agent.messenger.sendToApp('xxx_action', data);
});
};
// app.js
module.exports = app => {
app.messenger.on('xxx_action', data => {
// ...
});
};
进程间通信
cluster 的 IPC 通道只存在于 Master 和 Worker/Agent 之间,Worker 与 Agent 进程互相间是没有的。那么 Worker 之间想通讯通过 Master 来转发。
发送:
app.messenger.broadcast(action, data):
发送给所有的 agent / app 进程(包括自己)app.messenger.sendToApp(action, data):
发送给所有的 app 进程app.messenger.sendToAgent(action, data):
发送给 agent 进程agent.messenger.sendRandom(action, data):
app.messenger.sendTo(pid, action, data)
: 发送给指定进程// app.js
module.exports = app => {
// 注意,只有在 egg-ready 事件拿到之后才能发送消息
app.messenger.once('egg-ready', () => {
app.messenger.sendToAgent('agent-event', { foo: 'bar' });
app.messenger.sendToApp('app-event', { foo: 'bar' });
});
}
接收:
app.messenger.on(action, data => {
// process data
});
app.messenger.once(action, data => {
// process data
});