虽然本人不是 Egg 的重度使用者, 但是仍然不得不承认 Egg 是一个真香的框架。 可是炒蛋吃多了也会腻, 今天就在鸡蛋里挑下骨头, 纯属一家之言, 不喜勿喷。
今天就从性能和多进程上着手, 在 Egg 官网介绍里面有这两条特性:
- 内置多进程管理
- 基于 Koa 开发,性能优异
内置多进程管理
观点: 微服务的兴起, cluster 多进程管理的必要性; 内置的必要性;
Node.js 多进程的管理工具从早期的 forever 到后面非常流行的 pm2。 Node.js 多进程的目的无非通过语言本身的能力利用多核心的优势, 然而微服务 + docker + k8s 的流行, 使得 cluster 本身(包括pm2)应用场景越来越少(这里当然可能存在大量的小微企业, 没有使用微服务, 仍然需要一键启动 cluster 的需求)。
记得很久之前 express 进行了一次瘦身活动, 尽量把组件抽离出来。虽然 egg-cluster 是一个单独的模块, 但是想要启动 Egg 还是必须通过 egg-bin, egg-cluster 一系列调用, 这里不够解耦。个人更喜欢 pm2 这种模式, 给用户更多的选择性。
基于 Koa 开发,性能优异
观点: Koa 性能优异不代表 Egg 性能优异; 性能优异不代表没有前置条件;
Node.js 不是一个进程在战斗。 Node.js 在启动的时候会 fork 出来很多线程, 帮助 Node.js 做其他工作, 例如垃圾清理, DNS 查询等等, 尤其是在 Orinoco 引擎出来之后, 引入了并发和并行垃圾清理, 使得 Node.js 更少的发生 stop-all-world
情况, 代价则是消耗更多的 cpu 资源, 所以如果限制了 cpu 资源, 必然存在资源竞争的情况, 从而导致性能下降。
Egg 牺牲了部分性能换取便携性。Egg 对用户的 Controller 进行了包裹, 使得每次请求都会 new 一个 Controller, 假设 一个 Controller 引入了 1 个 Service, 在不考虑对象复杂性的情况下, 每次请求至少产生 2 个垃圾对象: controller, service, 对于普通的 C10K 情况, 那么每秒就会产生万级别的垃圾对象。
Egg 对于 Controller 的处理如下:
function methodToMiddleware(Controller, key) {
return function classControllerMiddleware(...args) {
const controller = new Controller(this); // 每次路由的调用,都会 new
if (!this.app.config.controller || !this.app.config.controller.supportParams) {
args = [ this ];
}
// throw new Error();
return utils.callFn(controller[key], args, controller);
};
}
复制代码
在限制 cpu 情况下, 对比单例和工厂的差异:
service.js
class Service {
constructor() {
this.model1 = {a: 1,b: 1,c: 1,d: 1,e: 1}
...
this.model10 = {a9: 1,b9: 1,c9: 1,d9: 1,e9: 1};
}
async function1() {
return 0;
}
...
async function10() {
return 0;
}
}
module.exports = Service;
复制代码
factory.controller.js
仿照 Egg 每次请求新建 Controller
const Controller = require('./base.controller');
const controller = {};
const keys = Object.getOwnPropertyNames(Controller.prototype);
for (const key of keys) {
if (key === 'constructor') {
continue;
}
controller[key] = (function methodToMiddleware(Controller, key) {
return function wrapMethod(ctx) {
const control = new Controller(ctx);
control[key].call(control, ctx)
}
})(Controller, key); // 仿写 Egg 的 new Controller
}
module.exports = controller;
复制代码
sigleton.controller.js
传统的的单例写法
const Controller = require('./base.controller');
const controller = new Controller();
const keys = Object.getOwnPropertyNames(Controller.prototype);
for(const key of keys) {
if (key === 'constructor') {
continue;
}
const fn = controller[key]; // 简易绑定this
Object.defineProperty(controller, key, {
get() {
return fn.bind(this)
}
})
}
module.exports = controller;
复制代码
index.js
入口文件
const Koa = require('koa');
const Router = require('koa-router');
const sigleton = require('./sigleton.controller');
// const factory = require('./factory.controller');
const app = new Koa();
const router = new Router();
router.get('/sigleton', sigleton.function1);
// router.get('/factory', factory.function1);
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
复制代码
docker 封装限制 cpu 和 ram 资源
sudo docker run -ti -cpu-set="1" -m 2000m -p 3000:3000 o-my-egg
复制代码
wrk -t1 -c10000
压测结果
suntopo@suntopo-ThinkPad-E490:~$ wrk -t1 -c10000 -d30s --latency http://localhost:3000/sigleton
Running 30s test @ http://localhost:3000/sigleton
1 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 47.11ms 28.93ms 1.32s 99.17%
Req/Sec 21.92k 3.16k 24.55k 96.31%
Latency Distribution
50% 43.92ms
75% 48.48ms
90% 51.95ms
99% 69.15ms
650223 requests in 30.04s, 91.15MB read
Socket errors: connect 8980, read 0, write 0, timeout 0
Requests/sec: 21647.45
Transfer/sec: 3.03MB
suntopo@suntopo-ThinkPad-E490:~$ wrk -t1 -c10000 -d30s --latency http://localhost:3000/factory
Running 30s test @ http://localhost:3000/new
1 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 89.36ms 9.50ms 173.66ms 74.02%
Req/Sec 11.45k 1.15k 13.44k 66.56%
Latency Distribution
50% 88.38ms
75% 96.39ms
90% 99.36ms
99% 113.01ms
340706 requests in 30.02s, 47.76MB read
Socket errors: connect 8980, read 0, write 0, timeout 0
Requests/sec: 11348.55
Transfer/sec: 1.59MB
复制代码
从上面压测的情况来看, 性能差别还是蛮大的, 所以 Egg 在 Controller 层的处理做了性能的妥协, 但这仅仅是在极限情况下的性能差异。
其实在另外一个测试中发现只要 cpus 没有被打满的情况下, 两者几乎没有性能差异, 当时的 qps 大约在 1000 级别。
总结
Egg 好比一辆悍马, 强悍, 稳定, 耐cao, 耗油, 你不会轻易开她去赛车, 也不会随便用她玩改装, 她天生就适合在马路上驰骋。
关注我的微信公众号,欢迎留言讨论,我会尽可能回复,感谢您的阅读。