3.4 层序控制与性能影响


3.4 层序控制与性能影响

前面两节造好了层,这一节回答装配台上最后的问题:这些层谁先谁后。层序不是代码风格问题——同样的层换个位置,行为直接改变,性能也随之浮动。本节用几组可复现的实验把"顺序改变行为"钉死,再给出一张装配排序表和性能开销的估算方法。

实验一:解析层放到路由后面,body 永远是 undefined

把 bodyparser 挪到路由之后装配,然后向接口 POST 一段 JSON:

// 错误装配:解析层在路由层之后 app.use(router.routes()); app.use(bodyParser()); // 太晚了 router.post('/echo', async (ctx) => { ctx.body = { got: ctx.request.body }; // 输出 { got: undefined } });

结果稳定复现:ctx.request.body 恒为 undefined。原因不需要玄学解释——请求按注册序进层,到达路由层时解析层还没执行过,body 自然还没被解析。修复只需把解析层挪到路由前面。这类错序的特征很典型:不报错,只是静默地拿不到数据,因此排障时第一反应该是检查装配顺序,而不是怀疑解析库。

实验二:兜底层放内层,异常直接打穿到浏览器

第 2.4 节讲过兜底层必须最外。反着装配试试:

// 错误装配:兜底层在业务层内侧 app.use(bodyParser()); app.use(errorHandler()); // 应该放在 bodyParser 外面 app.use(businessRouter()); // 当业务层抛出异常,异常向外冒泡,先经过 errorHandler——没问题; // 但 bodyParser 自身抛出的 413 大包异常发生在 errorHandler 之前(更内侧), // 冒泡时已经没有兜底层可经过,只能落到 app.on('error'),客户端拿到裸 500。

规则由此清晰:你的层想接住谁,就必须装配在谁的外面。兜底层要接住解析层、路由层、业务层的一切异常,所以它是装配序列的第一个;压缩层要压住所有人的最终输出,所以它也在外侧;安全头层要最后说话,所以它贴着最外。外侧到内侧的装配序,就是"关心的范围从全局收窄到局部"的序。

实验三:截停层的位置就是省钱的位置

静态资源层(koa-static)命中即截停,这一特性叠加层序就是性能手段。同一个请求 GET /static/logo.png

  • serve 在前:命中文件,截停返回,鉴权、解析、路由层全部不执行——每个静态请求只花文件 IO 的钱;
  • serve 在后:请求先白跑鉴权(可能还要查会话存储)、解析、路由匹配,全 404 之后才轮到文件层——每个静态请求都要先交一遍"过路费"。

同理,健康检查层、限流层这类"越早拦截越省钱"的层都应该尽量靠外。反过来,必须等数据准备好的层必须靠内——鉴权层依赖 body 里的凭证,就得排在解析层之后。层序设计的核心矛盾就这两句话的拉扯:拦截型要靠外省开销,依赖型要靠内拿数据。

装配排序表

把本章出现过的层按"该在的位置"排一遍,这张表可以直接当 code review 的对照清单:

装配位 理由
最外 错误兜底 接住所有内侧层的异常
次外 请求 ID 最早生成,之后所有层与日志都要用
外侧 安全头 helmet 最后说话,防止被覆盖
外侧 压缩 compress 压最终响应
中外 静态资源 serve 命中即截停,省内侧开销
中段 限流 rateLimit 尽早拦截,但要在静态层之后(静态不占配额)
中段 请求体解析 bodyParser 依赖它的层都在其内
中内 鉴权 auth 依赖 body 或头,产出 ctx.state.user
内侧 业务路由 router 洋葱的内核

有一处细节值得点破:请求 ID 层的位置几乎决定全册日志的可用性——它必须早于任何可能抛异常的层,否则异常发生时 ctx.state.reqId 还不存在,日志里查无此请求。

层数与开销:每个请求花了多少过路费

层序错了是行为问题,层数多了是性能问题。每个中间件给每请求增加的开销由两部分构成:固定开销(一次函数调用、一个 Promise 的创建与恢复,纳秒到微秒量级)与逻辑开销(该层自己的 IO 或计算,微秒到毫秒不等)。前者决定了"几十层洋葱"在现代硬件上也就增加零点几毫秒——层数本身几乎从来不是瓶颈,层里的逻辑才是

// 瓶颈层长这样:在进层阶段做同步阻塞或每请求重活 app.use(async (ctx, next) => { const rule = JSON.parse(fs.readFileSync('./rules.json', 'utf8')); // 每请求同步读盘 const heavy = expensiveTransform(ctx.headers); // 每请求重计算 await next(); });

优化手法与之对应:读配置改成启动时加载加监听变更;重计算改成预计算加缓存;同步调用(fs 的 Sync 系列、复杂正则、大 JSON 序列化)逐个清掉——同步代码在 Node.js 里阻塞的是整个事件循环,不是当前请求,这是第 8.1 节性能剖析的重点对象。层内只留"必须每请求做的事",其他都往工厂层(启动时)或缓存里搬。

⚠️ 常见坑:为了"优化"把多个层的逻辑合并成一个大层。合并省下的微秒级调用开销,换来的是不可截停、不可测试、不可复用的巨型函数——这笔账怎么算都是亏的。层要小而专,开销问题用剖析工具找到真凶再动手。

本节要点回顾

  • 顺序改变行为:解析层错位则 body 恒空,兜底层错位则异常打穿——错序故障的共同特征是静默;
  • 外侧接内侧:想接住谁的异常、想压住谁的输出,就装配在谁的外面;
  • 拦截靠外、依赖靠内:截停层靠外省开销,数据依赖层靠内拿输入;
  • 装配排序表:从兜底到业务路由的九格顺序,可直接当评审清单;
  • 层数不是瓶颈:固定开销微不足道,层内的同步调用与每请求重活才是真凶。

工坊三课到此齐备:会写层、会选层、会排层。洋葱装配完毕,下一章跟着请求正式进层——路由怎么匹配、RESTful 怎么落、参数怎么验,主战场见。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U