我们把新项目的前端架构换成了React Server Components,上线后一切正常,直到一次大促活动,流量暴增后系统出现了严重的性能问题,页面加载超时,服务内存飙升,差点影响整个活动。本文完整复盘这次故障,从现象到根因,从应急处理到后续优化,以及我们从中学到的经验教训。

一、背景:为什么用React Server Components

先说说我们为什么选择React Server Components(简称RSC)。

我们的项目是一个电商平台,对首屏加载速度和SEO要求都很高。以前用的是客户端渲染(CSR),首屏白屏时间长,SEO效果差。后来试过SSR(Next.js的服务端渲染),效果不错,但服务端压力大,而且每次交互都要重新渲染,体验不够流畅。

RSC出来之后,我们觉得这是一个理想的方案:组件可以在服务端渲染,不需要把JS发送到客户端,首屏快、SEO好,同时客户端组件又能保持交互性,兼顾了性能和体验。于是我们在新项目中全面采用了RSC架构,用的是Next.js的App Router。

上线初期一切正常,首屏速度确实快了,SEO也改善了,我们都觉得这个技术选型很成功。直到大促活动来了。

二、故障发生:大促当天的惊魂时刻

大促当天,流量是平时的五倍。活动开始后十分钟,监控告警就响了:页面加载超时率超过5%,服务端CPU和内存飙升。

我当时正在监控大屏前,看到数据一下子就红了,心里咯噔一下。用户开始投诉页面打不开、加载慢,客服电话被打爆。大促活动每一秒都是钱,这个时候出问题,损失可想而知。

我们立刻开始排查。

现象:

  • 商品详情页加载时间从平时的1秒变成了8秒以上
  • Node.js服务内存使用率从40%飙升到90%,频繁触发GC
  • CPU使用率也很高,接近80%
  • 部分请求超时,返回502错误
  • 数据库连接数飙升,接近上限

最诡异的是:平时流量小的时候一切正常,只有高并发的时候才出问题。而且不是所有页面都慢,主要是商品详情页和活动页慢,首页和列表页还好。

三、应急处理:先止血再排查

线上出问题,第一原则是先止血,再排查根因。

我们做了以下应急处理:

  1. 扩容:立刻把Node.js服务的实例从4台扩容到10台,缓解单台压力。扩容之后超时率有所下降,但还是偏高,内存依然在涨。
  2. 降级:把RSC的部分服务端组件降级成客户端渲染,减少服务端压力。具体做法是把一些非首屏必需的组件改成"use client",放到客户端渲染。降级之后,页面加载速度明显提升。
  3. 限流:对商品详情页做限流,超过阈值的请求排队或返回缓存页面,保护后端服务不被打垮。
  4. 缓存:紧急开启了页面级缓存,对热门商品详情页做CDN缓存,减少回源请求。

做完这些操作,大约花了二十分钟,系统终于稳定下来了,超时率降到了1%以下,内存和CPU也恢复正常。大促活动得以继续,虽然损失了一些流量和销售额,但至少没有完全瘫痪。

止血之后,我们开始认真排查根因。

四、根因排查:一层层剥开

这个故障的根因不是单一的,而是多个问题叠加的结果。我们花了两天时间,一层层排查,终于找到了所有问题。

问题1:服务端组件中做了大量同步数据请求

排查中发现,我们的商品详情页是一个服务端组件,里面调用了五六个数据接口:商品信息、库存、价格、评价、推荐商品、相关文章。这些接口是串行调用的,一个接一个,总耗时加起来有2-3秒。

平时流量小的时候,数据库和接口响应快,感觉不明显。但高并发的时候,数据库连接池被占满,接口响应变慢,串行调用的总耗时就被放大了,页面渲染时间急剧增加。

而且,RSC的服务端组件是在请求时渲染的,每个请求都要重新执行这些数据请求和渲染,没有缓存。高并发下,大量请求同时打过来,服务端根本处理不过来。

问题2:没有合理使用缓存

我们对RSC的理解有偏差,以为服务端渲染就不需要缓存了。实际上,RSC更需要缓存,因为服务端渲染的成本比客户端渲染高得多。

商品信息、库存、价格这些数据,其实变化不是特别频繁,完全可以缓存。但我们没有做任何缓存,每次请求都重新查数据库、调接口,高并发下数据库压力巨大。

而且,我们没有利用React的缓存机制(比如React的cache函数、Next.js的fetch缓存),导致同一个请求中重复的数据也重复查询。

问题3:客户端和服务端组件边界划分不合理

我们把太多组件放到了服务端渲染,包括一些交互复杂、数据变化频繁的组件。这些组件在服务端渲染成本高,而且每次交互都要重新渲染,体验也不好。

正确的做法应该是:首屏必需的、静态的内容用服务端组件;交互复杂的、动态的内容用客户端组件;非首屏内容用懒加载。我们当时为了追求首屏速度,把太多东西塞到了服务端,反而适得其反。

问题4:Node.js服务内存泄漏

排查中还发现了一个内存泄漏问题:我们在服务端组件中用了一个全局的缓存对象,但没有设置过期时间和容量限制,随着请求增多,这个对象越来越大,内存不断上涨,最终导致频繁GC,服务变慢。

这个问题平时流量小的时候不明显,因为内存涨得慢,重启服务就清掉了。但大促高并发下,内存快速上涨,很快就到了警戒线。

问题5:数据库连接池配置不合理

Node.js服务的数据库连接池配置得太小,最大连接数只有20。高并发下,大量请求等待数据库连接,导致排队和超时。扩容之后,连接池的压力更大了,因为更多的实例在抢有限的数据库连接。

五、根因总结

综合起来,这次故障的根本原因是:我们对RSC的架构特点理解不够,没有针对服务端渲染做合理的架构设计和性能优化,导致高并发下服务端压力过大。

具体来说:

  1. 服务端组件中串行调用大量接口,渲染耗时长
  2. 没有合理使用缓存,重复查询和渲染
  3. 服务端和客户端组件边界划分不合理
  4. 存在内存泄漏问题
  5. 数据库连接池配置不合理

这些问题在低流量下被掩盖了,高并发下集中爆发,导致了这次故障。

六、后续优化

找到根因之后,我们做了一系列优化。

1. 数据请求并行化和缓存

  • 把串行的数据请求改成并行,用Promise.all同时调用,总耗时从2-3秒降到了500毫秒
  • 对商品信息、推荐商品等不常变化的数据做Redis缓存,缓存时间5-30分钟
  • 用React的cache函数包装数据请求,同一个请求中重复的数据只查一次
  • 用Next.js的fetch缓存,对相同的fetch请求自动去重

优化之后,服务端渲染的耗时大幅下降,数据库查询量减少了70%。

2. 合理划分组件边界

重新梳理了组件边界:

  • 首屏必需的静态内容(商品标题、价格、图片)保留在服务端组件
  • 交互复杂的组件(规格选择、加入购物车、评论区)改成客户端组件
  • 非首屏内容(推荐商品、相关文章)用动态导入,客户端懒加载
  • 库存、价格等实时性要求高的数据,用客户端组件异步获取,不阻塞首屏渲染

这样,服务端渲染的内容少了,速度更快了;客户端组件保证了交互体验,首屏和交互都兼顾了。

3. 页面级缓存和CDN

  • 对商品详情页做ISR(增量静态再生),预渲染热门商品页面,每5分钟重新生成
  • 把静态页面推到CDN,用户访问直接命中CDN,不回源
  • 对活动页做完全静态化,构建时生成,活动期间不变化

这样,大部分请求都命中了CDN或静态缓存,只有少数请求打到服务端,服务端压力大大降低。

4. 修复内存泄漏

  • 全局缓存对象改成了LRU缓存,设置最大容量和过期时间
  • 审查了所有服务端组件,确保没有在全局作用域累积数据
  • 增加了内存监控和告警,内存超过阈值自动告警

5. 数据库和服务优化

  • 数据库连接池最大连接数从20调到了100
  • 对慢查询做了优化,增加了索引
  • Node.js服务增加了内存限制和自动重启机制
  • 完善了压测流程,上线前必须做高并发压测

七、优化效果

优化完成后,我们做了一次全链路压测,模拟大促的五倍流量:

  • 页面加载时间:从8秒降到了800毫秒
  • 服务端CPU使用率:从80%降到了40%
  • 内存使用率:稳定在50%左右,不再持续上涨
  • 超时率:0.1%以下
  • 数据库连接数:稳定在50左右,不再飙升

第二次大促的时候,系统稳稳地扛住了流量,没有再出问题。

八、经验和教训

这次故障给了我们深刻的教训,总结一下。

1. 新技术要充分理解再用

RSC是个好技术,但我们在没有充分理解它的架构特点和性能特征的情况下就全面采用了,导致了问题。用新技术之前,一定要深入理解它的原理、优势和局限,做好充分的调研和测试。

2. 高并发场景必须压测

平时测试通过不算数,必须模拟真实的高并发场景做压测。很多问题只有在高并发下才会暴露,比如连接池耗尽、内存泄漏、缓存击穿等。上线前一定要压测,而且要压到峰值流量的两倍以上。

3. 服务端渲染更需要缓存

服务端渲染的成本比客户端渲染高得多,因为每个请求都要在服务端执行渲染。所以,RSC/SSR架构下,缓存比CSR更重要。页面级缓存、数据级缓存、请求级缓存,能加的都要加。

4. 合理划分服务端和客户端边界

不是所有组件都适合服务端渲染。要根据组件的特点合理划分:静态的、首屏必需的用服务端;动态的、交互复杂的用客户端;非首屏的用懒加载。不要为了追求首屏速度把所有东西都塞到服务端。

5. 监控和告警要完善

这次故障能及时发现,靠的是完善的监控和告警。线上系统一定要有完善的监控:响应时间、错误率、CPU、内存、数据库连接数、GC频率等,都要监控起来,设置合理的告警阈值。

6. 应急预案要提前准备

大促这种重要活动,一定要提前准备应急预案:扩容方案、降级方案、限流方案、回滚方案。出了问题能快速止血,把损失降到最低。这次如果没有提前准备扩容和降级的脚本,损失会大得多。

九、写在最后

React Server Components是一个很有前景的技术,它结合了服务端渲染的性能和客户端渲染的交互性。但它不是银弹,用不好反而会带来性能问题。

这次故障虽然惊心动魄,但也让我们对RSC有了更深刻的理解,完善了架构和流程。技术的成长,往往就是在一次次踩坑和排错中实现的。

希望我们的复盘能给正在用或打算用RSC的朋友一些参考,让你们少踩一些坑。技术选型要谨慎,架构设计要合理,性能优化要持续,监控告警要完善,应急预案要准备。做到这些,才能在高并发下稳如泰山。

最后想说,线上故障不可怕,可怕的是出了问题不复盘、不改进。每一次故障都是一次学习的机会,认真复盘、彻底改进,系统就会越来越稳定。