鸡蛋(Egg)里挑骨头

虽然本人不是 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, 耗油, 你不会轻易开她去赛车, 也不会随便用她玩改装, 她天生就适合在马路上驰骋。

关注我的微信公众号,欢迎留言讨论,我会尽可能回复,感谢您的阅读。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享