ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Node后端重构实战:从Express到NestJS的性能优化与稳定性方案

Node后端重构实战:从Express到NestJS的性能优化与稳定性方案

最早接手这个幼教平台项目时,我心里是拒绝的。表面上看它是个能跑的 Node 服务,业务也不复杂:课程内容、班级打卡、成长相册、家长通知。可一旦把接口拆开看,路由里直接写 SQL、一个方法几百行、图片存在服务器本地、日志全靠 console.log,我就知道这系统迟早要出事。后来日活过了万,早高峰打卡一旦集中,反向代理层大量 504,首页接口慢到家长群里全是吐槽,我们才痛下决心做一次系统性的重构和优化。这次改造前后花了两个月,我没有选择推翻重来,而是在保留原有 API 语义的前提下,从架构、性能、稳定性三个维度对整个 Node 后端做了一轮大手术。这篇实战记录会讲清楚每一步为什么这么做、怎么落地、中间踩过哪些坑,想给正在或准备重构 Node 项目的同学一份能直接参考的路线图。

1. 重构前先做“体检”:项目现状诊断与目标拆解

1.1 旧架构的问题清单:压测跑完才承认系统确实不行了

先说改造前的样子。旧系统用的是 Express 4 + Sequelize + MySQL,单机部署,没有 Redis,没有消息队列,也没有任何监控。最重要的问题不是用的技术旧,而是代码的“熵”已经高到离谱:路由层直接处理业务逻辑,一个打卡接口里同时做查询、状态判断、人数统计、消息推送,一个文件超过一千行;多个接口之间数据不复用,首页聚合要串行调 4 次数据库查询,光这些查询加起来就超过 300ms。

我们先用压力测试和慢查询日志给系统做了一次“体检”。压测结果比较难看:100 并发持续跑 30 秒,单机只能扛 180 QPS,p99 响应超过 2.6 秒;数据库慢日志一天上千条,其中最慢的一条班级考勤汇总查询跑了整整 8 秒。问题最集中的三个模块是打卡、相册和首页聚合,基本都死在数据访问层和串行调用上。没人愿意承认自己写的系统烂,但数据不会说谎。我把这些问题归成四类:第一类是架构问题,业务没有分层,改一个功能牵一发动全身;第二类是性能问题,连接池、索引、分页都处于“能用但不知道哪里不行”的状态;第三类是稳定性问题,没有超时控制、没有熔断、没有监控告警;第四类是安全问题,日志打印了完整手机号,接口也没有按角色做权限收敛。这四个问题互相纠缠,如果只做性能优化不做架构调整,很快又会烂回去。

1.2 两个月达成硬指标:重构范围与目标设定

重构最怕两件事:一个是无限发散,把架构重构变成技术镀金;另一个是目标模糊,改完之后不知道算成功还是失败。我们当时立了三条硬性约束。第一条,对外接口的语义不变,App、小程序、后台管理系统都不需要跟着改,保证业务能持续跑;第二条,采用“绞杀者模式”,把系统按模块逐个替换,先动打卡和相册,再动课程和报表,而不是一次性重写;第三条,所有改动都要有可量化的性能目标,不接受“应该快了一点”这种结论。

量化目标我们定得很具体:常规接口 p95 响应时间从 1.2 秒压到 500 毫秒以内,打卡早高峰 p99 控制在 1 秒以内,压测 QPS 从 180 提升到至少 2000,单机进程数从 1 个扩展到集群模式,数据库慢查询数量从每天上千条降到个位数。实践下来,这些目标基本都实现了,后面每一章我都会贴对应的实测数据。这里给个建议:重构之前先把这些指标写进需求文档,验收时就按这个来,不然团队很容易在“改了多少行代码”上自嗨,最后交付的东西看起来漂亮,用起来照样卡。

2. 架构层重构:从 Express 意大利面到分层模块化

2.1 框架迁移选型:为什么最终选择 NestJS

第一个大决策:要不要换框架。当时摆在面前三个选择,继续在 Express 上打补丁、转 Koa、或者迁到 NestJS。继续打补丁最省事,但我们已经有 40 多个路由文件,继续在 Express 的中间件模型里强行搞分层,最后还是得靠自觉;Koa 的洋葱模型写起来舒服,但它依然没有解决“业务往哪里放”的问题;NestJS 的好处是它把分层变成了框架约束:模块、控制器、服务、提供者都是固定套路,新来的同事不太容易写歪。

有人担心 NestJS 太“重”,我觉得对于业务系统来说,这种重恰恰是优点。它自带依赖注入、全局异常过滤器、管道校验、守卫和拦截器,这些在 Express 里都需要自己拼装。我们用 TypeScript 重构,NestJS 的装饰器能直接描述接口元数据,OpenAPI 文档都能自动生成。迁移也不是一步到位,NestJS 底层默认跑在 Express 上,旧的中间件还可以继续复用,业务模块一个接一个迁,风险可控。如果你正在犹豫要不要换框架,我的建议是先看团队的代码数量和维护成本:代码越乱、人越多,框架约束的价值就越大;如果只是几个接口的小服务,继续用 Express 完全没问题。

2.2 分层落地的关键细节:Controller、Service、Repository 各司其职

改造后的目录结构长这样:

src/ main.ts app.module.ts modules/ attendance/ # 打卡模块 attendance.controller.ts attendance.service.ts attendance.repository.ts course/ # 课程模块 album/ # 成长相册模块 parent/ # 家长端模块 common/ filters/ # 全局异常过滤器 interceptors/ # 统一响应拦截器 pipes/ # 参数校验管道 guards/ # 登录与权限守卫 shared/ database/ # 数据库连接与模型 redis/ # Redis 客户端封装 queue/ # 任务队列

这个结构的思想很朴素:Controller 只做参数接收和响应包装,不写业务;Service 编排业务规则,比如检查打卡状态、计算统计值、触发通知;Repository 只干数据访问这一件事。以前把业务和 SQL 写在一起,等于把菜谱和切菜板放在一个盒子里,看起来省事,一旦要换数据源或调整查询,牵一发动全身。举个打卡接口的例子。旧代码大概是这样的:

router.post('/api/attendance/check-in', async (req, res) => { const { kidId } = req.body; const today = formatDate(new Date()); const rows = await db.query( 'SELECT id FROM attendance WHERE kid_id = ? AND date = ?', [kidId, today] ); if (rows.length) return res.json({ code: 0, data: { repeat: true } }); await db.query( 'INSERT INTO attendance (kid_id, date, clock_in_time) VALUES (?, ?, ?)', [kidId, today, Date.now()] ); // 中间还夹杂着给家长发通知、更新班级统计等逻辑 res.json({ code: 0, data: { repeat: false } }); });

新架构把同样逻辑拆成多个文件。Controller 只负责校验参数:

@Controller('attendance') export class AttendanceController { constructor(private readonly attendanceService: AttendanceService) {} @Post('check-in') @HttpCode(200) checkIn(@Body() dto: CheckInDto) { return this.attendanceService.checkIn(dto.kidId); } }

Service 层管好业务规则和事务边界,Repository 层专心访问表结构。这样做的直接收益是:单测好写了,出问题能定位到具体层,接口文档也自动带上了类型信息。分层不是做给架构师看的,是给三个月后回来改代码的自己留的活路。

提示:分层的粒度要克制,不要为了分层而分层。如果项目只有两个接口,Controller 里直接写查询反而更合适;当模块数量超过十几个时,这套约束的价值才会真正体现。

2.3 无状态化改造与 PM2 多进程部署

分层解决的是代码可维护性,接下来要解决的是扩容问题。旧系统所有状态都绑在进程里:session 放内存、上传图片存本地磁盘、日志直接落到服务器文件。这种架构下就算你开 4 个进程,反向代理也得配 ip_hash 把用户固定到一台机器,根本没法横向扩展。无状态化改造其实就三件事:第一,登录态从 session 换成 JWT,或者把 session 挪到 Redis,我选择了 JWT 加短过期,配合刷新令牌;第二,上传的文件全部改到对象存储,本地磁盘只留代码和日志;第三,日志改成标准输出,由进程管理器统一处理,不依赖本地文件系统。

进程管理我们用 PM2 的 cluster 模式,配置如下:

module.exports = { apps: [{ name: 'edu-api', script: 'dist/main.js', instances: 'max', exec_mode: 'cluster', max_memory_restart: '512M', env: { NODE_ENV: 'production' } }] };

instances 设为 max,PM2 会按 CPU 核数拉起对应数量的 worker,Node 的单线程瓶颈在业务量上来后会被多进程摊掉。需要注意两点:cluster 模式下每个 worker 仍然共享同一份代码,千万不要在进程内维护“全局缓存副本”,否则数据不一致;另外,max_memory_restart 一定要设,很多内存泄漏要靠它先兜底撑到我们发现问题。我用一个简单的公式来估算进程数与资源:每个实例内存按 512MB 算,一台 8 核 8G 的服务器可以开 6 到 8 个 worker,剩下的内存留给系统和反向代理。数据库连接池也要跟着乘实例数,否则 8 个进程各建 20 个连接,瞬间就能把 MySQL 的连接数打满。具体怎么算,下一章细说。

3. 性能优化三板斧:数据库、缓存、接口层

3.1 数据库层优化:连接池参数、慢查询、索引与深分页

先说连接池。之前的代码用 Sequelize,连接池配置一直是默认值,重启一次数据库服务的瞬间,大量请求同时建连,很容易把资源打满。我们最终给连接池设定了明确参数:max 20、min 5、idle 10000、acquire 3000。这里有个经验公式:pool.max 乘上 Node 进程数,要小于数据库 max_connections 的 80%。比如数据库 max_connections 是 200,Node 起了 4 个实例,每个 pool.max 就不能超过 40。如果超过了,就提高数据库上限或者降低每实例连接数,这一步必须提前算好,否则上线高峰真的会连环夯。

慢查询是最容易见效的部分。之前提到班级考勤统计查询跑了 8 秒,原因就是带着 JOIN 去大表里做 GROUP BY,没有合适索引。我们用 EXPLAIN 看执行计划,发现 type 是 ALL,直接全表扫描。优化分两步:第一步建了联合索引,让查询先按班级筛选再按日期分组:

ALTER TABLE attendance ADD INDEX idx_class_date_status (class_id, date, status);

第二步把原本三层嵌套子查询的统计 SQL 改成一次 JOIN 加 GROUP BY,并按日期倒序只取最近 30 条。改完这个接口从 8 秒降到了 300 毫秒以内。实操建议是:上线前把所有慢查询日志导出来,按执行次数降序排序,Top 20 基本就能覆盖 80% 的数据库压力。分页也要注意。幼教平台后台经常要导全班打卡记录,早期的分页是 LIMIT 100000, 20 这种写法,页码越深越慢。我们统一改成游标分页:记住上一页最后一条数据的日期和 ID,下一页用 WHERE 条件往前取:

SELECT * FROM attendance WHERE (date, id) < ('2024-01-20 08:00:00', 1024) ORDER BY date DESC, id DESC LIMIT 20;

这种方式在深分页场景能稳定在几十毫秒,代价是前端要传 cursor 而不是 pageNo,对数据持续增长的查询来说,这个改动非常值得。数据库层优化做完,慢查询数量从每天上千条降到了个位数,接口响应时间整体下来了。但数据库是最后防线,不要把它当缓存用,接下来的缓存层才是扛高峰的主力。

3.2 缓存层实战:Redis 防穿透、击穿、雪崩的写法

业务里最明显的热点是课程详情、首页配置和“今天是否已打卡”状态。这类数据读多写少,非常适合用 Redis 挡在数据库前面。缓存策略我推荐 Cache Aside:读的时候先查缓存,未命中再查库并回填;写的时候先更新数据库,再删缓存。这个模式简单可靠,但有几个经典坑必须处理。

第一个坑是缓存穿透:大量请求查询根本不存在的 ID,每次都穿透到数据库。我们对这类场景做了空值缓存,查不到也往 Redis 写一个短过期空标记,再配合接口入口的参数校验,把非法 ID 直接拦掉。第二个坑是缓存击穿:某个热点 key 过期的一瞬间,大量并发同时回源。我们用 SETNX 做互斥锁,只让一个请求去查库:

async getCourse(id: string) { const cached = await this.redis.get(`course:${id}`); if (cached) return JSON.parse(cached); const lockOk = await this.redis.set( `course:lock:${id}`, '1', 'EX', 5, 'NX' ); if (!lockOk) { await sleep(30); return this.getCourse(id); // 锁竞争时短暂等待后重试 } const course = await this.repository.findById(id); if (course) { await this.redis.set( `course:${id}`, JSON.stringify(course), 'EX', 300 + Math.floor(Math.random() * 60) ); } else { await this.redis.set(`course:empty:${id}`, '', 'EX', 60); } return course; }

第三个坑是雪崩:大量 key 同时过期导致数据库瞬间被压垮。做法是在过期时间上加上随机抖动,上面代码里 300 秒再加 0 到 60 秒随机值就是干这个的。注意,上述代码里的递归重试有锁等待时间上限问题,生产环境中更稳妥的方式是用逻辑过期把过期时间藏进 value,或者在锁内加上最大等待时间并返回旧数据兜底。缓存一致性是另一个容易忽视的点。我们坚持“先更新数据库,再删除缓存”,同时删缓存失败的话会有重试任务补偿。最理想的是通过数据库的变更日志异步更新缓存,但对于幼教平台这种业务复杂度,上面的方案配合监控已经足够稳定,不要为了上高端架构把自己玩死。

3.3 接口层优化:统一响应结构、压缩与聚合

接口层优化的目标很简单:让客户端拿最少的数据、最快地完成任务。我们做了三层调整。第一层是统一响应结构,写一个全局拦截器把成功响应包装成 { code: 0, message: 'ok', data },再把所有异常统一交给全局异常过滤器处理。不要小看这件事,它让前端开发者从几百个不同格式的返回里解脱出来,联调效率高了非常多。第二层是压缩。我们上线了 compression 中间件,给响应加了 gzip 压缩,并设了 1KB 阈值避免小响应白白增加 CPU 开销。一个 268KB 的课程列表 JSON 压到了 56KB,对弱网环境下的家长端体验提升明显。愿意折腾的话可以再上 brotli,同样的内容体积还能再小三分之一,但需要确认客户端的支持情况。

第三层是接口聚合。原来的首页要调课程列表、班级公告、今日打卡状态、相册动态 4 个接口,串行等完一圈平均 1.5 秒。我们新增了首页聚合接口,内部用 Promise.all 并发请求同一个进程里的 Service 方法,一次返回所有数据。因为走了内存调用,耗时稳定在 300 毫秒以内。这里给个原则:并发能解决的延迟问题,就不要用串行逻辑硬扛。接口层优化容易过度设计。我们刻意保留了一些“看起来多余”的字段没删,因为删除字段属于破坏性改动,需要先和客户端约定版本再处理。凡是涉及接口语义变更的优化,都应该走版本化发布,否则优化完线上就开始报错。

4. 幼教业务场景专项:打卡高峰、相册上传与数据保护

4.1 早高峰“打卡风暴”:限流、削峰与幂等设计

幼教平台有一个非常典型的流量特征:早高峰。大部分家长集中在 7:30 到 9:00 之间送孩子入园、顺手打卡,这段时间的请求量大约是平峰的 20 倍。如果不做保护,峰值一到,数据库写库和消息推送都会跟着抖。我们做了三道防线。第一道是入口限流,反向代理层按 IP 做了令牌桶限流,配置了每秒允许的请求数和突发桶大小,超过的请求直接返回稍后重试的提示,避免把后端打死。第二道是削峰填谷,打卡这个动作对时效性要求不高,我们把它改成“先写 Redis 标记,立刻返回成功,再通过任务队列异步落库和推送通知”。家长在界面上的体感是秒完成,而真正的数据库压力和推送压力被摊平到一分钟内处理。

第三道是幂等,防止家长手滑连续点两次,或者客户端自动重试导致重复打卡。我们在 Redis 里用 SETNX 做一个当日打卡锁,再在数据库表上加唯一索引 kid_id + date 作为兜底。两道防线一起上,重复数据处理起来非常干净。这里也要泼一盆冷水:不是所有操作都适合异步削峰。打卡这种高频写操作很适合,但涉及名额抢占、库存扣减、支付类的操作,异步化会引入大量对账和补偿逻辑,复杂度可能比收益还高。削峰前先问一句:这个动作真的允许延迟处理吗?

4.2 成长相册海量图片:直传对象存储与 CDN 加速

另一个数据大头是成长相册。家长平均一天要上传大量的照片,旧实现是先传到服务器本地,再同步处理缩略图,带宽和磁盘都被打爆。我们改成前端直传对象存储:客户端先向后端申请一个带签名和过期时间的上传凭证,拿到凭证后直接把文件传到对象存储服务,再由对象存储回调后端做校验和入库。这套流程把服务器的带宽压力完全卸掉了,对象存储和 CDN 才是适合扛这种流量角色的地方。缩略图也不需要自己算,对象存储服务基本都自带图片处理能力,我们只需在 URL 后面附加缩略图参数,CDN 冷节点第一次回源时生成并缓存即可。

安全上有几个容易被忽略的细节:上传回调一定要验签,防止有人伪造回调通知直接入库;文件类型必须做白名单,图片只允许 jpg、png、webp 这些;文件大小在签名阶段就要限制,避免超大文件浪费存储。还有,带家长和孩子信息的相册属于私有数据,下载地址必须用带签名的临时 URL,不能直接暴露永久公开地址。这些细节如果漏掉一个,后面都可能变成安全事故。相册迁移完成后,我们观察了一周,服务器入口带宽从持续打满降到了不到 10%,这个数字也说明之前把静态流量压在应用层有多离谱。

4.3 儿童敏感数据保护:脱敏、权限隔离与审计

做幼教类平台,数据保护天然是红线。家长手机号、家庭住址、孩子接送记录、健康档案,哪一个泄露都是事故。我们在这轮重构里做了几件实际的事:响应给到前端之前,手机号统一脱敏,只保留前三位和后四位;日志框架里加了过滤规则,包含身份证号、手机号等字段的日志直接打码,不让完整信息进日志系统。权限上,我们把接口按角色收敛:家长端只能读取自己孩子的数据,教师端只能读取本班孩子的数据,管理员也不能随意查看跨班数据。数据访问层统一做校验,而不是在 Controller 里临时判断。每一条敏感数据的导出和查看都记录审计日志,出了问题可以追溯到人和时间。

有些团队会把数据保护看成“合规部门的事”,等到出了问题才补。我的经验是,数据保护做在架构层成本最低:写一个脱敏注解、加一个权限守卫,之后再想改,付出的代价是指数级的。这一块不该是后置项,功能和权限必须一起设计一起上线。

5. 上线后守江山:可观测性与 CI/CD 保障

5.1 日志、指标与告警:在线问题提前 15 分钟发现

架构和性能改完,紧接着是监控。没有监控的重构等于闭眼开车。我们给项目接了三类基础监控:指标、日志和告警。日志从 console.log 换成了结构化的 JSON,每条日志自带请求 ID、模块名、耗时和关键参数,链路追踪时能拼回完整请求路径。线上要盯的核心指标不多:QPS、p50/p95/p99 耗时、错误率、事件循环延迟、内存和 GC 情况。事件循环延迟非常容易被人忽略,但它是 Node 服务健康的晴雨表,一旦持续超过 100ms,说明有同步任务卡住了主线程,用户端会大面积超时。

告警阈值不要拍脑袋,结合压测和线上数据来定。我们用的基准是:错误率超过 1% 且持续 5 分钟触发紧急告警;p95 超过 1 秒持续 10 分钟触发告警;事件循环延迟持续超过 100ms 立即告警;内存只增不减也要告警。告警到人、到群,响应时间目标定在 15 分钟内。监控体系建好之后,团队安全感提升很明显,大家终于敢在晚上合代码了,因为线上一旦有问题,手机先响而不是用户先骂。

5.2 自动化测试与 CI/CD 流水线:不靠人工手气

工程质量的另一条腿是自动化。以前发布全靠人工:本地 build,再把 dist 传到服务器,手改配置文件,出一回错基本靠心理素质。这轮重构我们补齐了流水线,流程是:代码提交后自动安装依赖、跑 lint、跑类型检查、跑单元测试和集成测试,全部通过后构建镜像,再滚动发布到服务器。测试策略分三层:Service 层写单元测试,用模拟数据覆盖业务分支,重点测打卡的幂等逻辑和报表统计;Controller 层写接口集成测试,用 HTTP 客户端直接调接口,断言统一响应格式和状态码;前端契约先用 OpenAPI 文档兜底,等有精力再补契约测试。测试不是为了覆盖率数字好看,而是让重构的人在改代码时敢下手。

这里也给个务实建议:如果团队只有一两个人维护,不用一上来就上容器编排那套复杂体系。一个能自动构建、自动测试、自动部署的轻量流水线,已经能避免 80% 的人为事故。复杂度永远跟着团队规模走,先解决最痛的手工发布问题,再谈更高阶的灰度发布和全链路压测。

6. 真实踩坑记录:常见问题与排查技巧

6.1 事件循环被同步任务卡死:全站接口集体超时

有一次线上反馈说下午四点多全站接口突然变慢,持续了十几分钟。我们查监控发现事件循环延迟飙升到 3 秒,CPU 也到了 100%。最后定位到是后台导出打卡报表的功能,直接用同步方法生成电子表格文件,二十几万行数据在内存里排序、合并、写文件,整个事件循环被堵死。这个问题的本质是:Node 的单线程事件循环不允许任何耗时任务长期占用主线程。解决方案是把 CPU 密集型任务卸到任务队列里,由单独的 worker 进程处理,导出完成后异步通知下载。如果你的任务没那么重,也可以用 worker_threads,但是要注意线程数和内存控制。从那之后我们把“凡是可能超过几百毫秒的操作一律不进主线程”写进了团队规范。

6.2 内存泄漏排查:一个 setInterval 引发的血案

另一个印象深刻的坑是内存缓慢上涨,好好跑几天后 RSS 居高不下。第一次遇到时我们用堆快照对比法,在内存正常和异常时分别抓了两次堆快照,对比发现大量没有被清理的定时器对象。查代码发现有人写了一个 setInterval 定期拉取课程配置,但每次模块重新初始化时没有调用 clearInterval 清理旧定时器,引用一直挂着,久而久之内存就被吃掉了。排查这类问题的心得:先看监控里的内存曲线,是缓慢上涨还是直线突增;再抓堆快照,对比两个时间点;最后对照代码里的定时器、全局变量、事件监听器。Node 自带的检查器支持直接用浏览器开发者工具分析堆快照,上手成本比想象中低。内存类问题,七成最后都能在“全局缓存没人清理、定时器没销毁、事件监听器重复注册”这三类原因里找到答案。

6.3 数据库连接池被打满:不一定是连接数不够

有一段时间数据库连接数一直逼近上限,我们把 pool.max 调小反而恢复了。这个反直觉案例的根因是慢查询太多,每条慢查询占住连接迟迟不释放,新请求排队等连接,连接数越垒越高。当时通过 show processlist 看到大量 Copying to tmp table 和 Sending data 的状态,一查就知道是哪几条业务查询拖了后腿。处理思路是先杀慢查询救急,再回头优化索引、减少扫描行数,最后才调整连接池参数。很多人第一反应是把连接池调大,这会让数据库线程数爆炸,反而更慢。连接池调优的顺序永远是:先确认查询快不快,再决定连接够不够。

6.4 压测打出来的缓存穿透事故

还有一次我们上线新版本,压测环境直接拿一堆不存在的孩子 ID 打接口,数据库 CPU 马上飙到 90%。因为之前缓存只做了存在数据的回填,不存在的数据每次都打到数据库。修复就是前面讲的空值缓存加布隆过滤器,同时把接口入参的 ID 格式校验加严。这里多说一句,凡是用户可控的 ID 入参,都要考虑到非法值也能触发全链路查询,不能假设“正常客户端不会传这种参数”。压测场景里有一类专门测异常输入的用例,以后凡是涉及热点 key 的接口,我都会把这个场景写进回归用例里。

下面把常见的几类问题汇总一下,方便排查时对照:

现象定位方法根本原因解决方案
全站接口超时、CPU 100%看事件循环延迟和进程 CPU同步任务阻塞主线程异步化、worker_threads、任务队列
内存只增不减堆快照对比定时器、全局缓存、监听器未清理对照代码清理引用,加 max_memory_restart 兜底
数据库连接数打满show processlist慢查询占住连接先杀慢查询,再优化索引,最后调连接池
数据库 CPU 突然飙高看 Redis 命中率和 DB 慢日志缓存穿透空值缓存、布隆过滤器、参数校验

这次重构加优化的经历,给我最深的体会是:技术和业务之间是相互成就的。如果没有幼教平台早高峰的真实场景,限流、削峰、缓存这些方案很难落到具体;同样,如果没有压测数据兜底,团队很容易在“感觉变快了”里迷失方向。最后分享一个小技巧。线上问题排查永远先找证据再改代码:慢查询捞出来看执行计划,内存问题抓堆快照,接口变慢先看事件循环延迟。证据在手,修复方案往往很清晰;直接猜原因动手改,大概率会引入新的问题。希望这份实战记录能帮你少踩几个我踩过的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进