带服务端渲染(SSR)的React应用高负载时崩溃求助
看来你在高负载场景下遇到了SSR服务崩溃的问题,结合你用的react-boilerplate + React Router + Sagas + Express技术栈,我之前处理过类似的场景,咱们一步步拆解可能的问题和解决办法:
一、先排查资源耗尽与阻塞问题
SSR基于Express单线程模型,高负载下最容易因为资源占满或事件循环阻塞导致崩溃:
- API调用的连接池耗尽:你提到每个SSR请求最多调用5个API端点,而Node默认的
http.Agent最大连接数是5,如果每个请求都占满连接,后续请求会排队超时甚至直接崩溃。
解决办法:给API请求工具(比如axios)配置自定义Agent,调高最大连接数:import http from 'http'; const customHttpAgent = new http.Agent({ maxSockets: 20 }); // 根据服务器CPU/带宽调整 const customHttpsAgent = new https.Agent({ maxSockets: 20 }); // 在axios请求中使用 axios.get('/api/user', { httpAgent: customHttpAgent, httpsAgent: customHttpsAgent }); - 未做超时限制的慢API:如果某个API响应超时,会导致当前请求的Sagas任务一直挂起,占用资源且阻塞事件循环。
解决办法:给所有API调用添加超时配置:
同时在Sagas里捕获超时错误,避免任务无限挂起。axios.get('/api/data', { timeout: 5000 }); // 5秒超时,超时直接抛出错误
二、内存泄漏排查与修复
高负载下内存泄漏会快速耗尽进程内存,导致崩溃,SSR场景常见泄漏点:
- 未清理的Sagas任务:如果每个SSR请求都创建新的Saga Middleware实例,或者请求结束后没有取消未完成的Sagas任务,残留的任务会持续占用内存。
解决办法:在每个请求的生命周期内管理Sagas任务,请求结束后强制取消:// Express路由处理函数示例 app.get('*', async (req, res) => { const sagaTask = sagaMiddleware.run(rootSaga); try { // 等待Sagas完成数据获取 await sagaTask.toPromise(); // 渲染React组件为HTML字符串 const html = renderToString(<App />); res.send(html); } catch (err) { console.error('SSR数据获取失败:', err); res.status(500).send('服务器错误'); } finally { // 无论成功失败,取消当前请求的Sagas任务 sagaTask.cancel(); } }); - React组件的副作用泄漏:SSR渲染时,部分组件的
useEffect/useLayoutEffect可能会在服务端意外执行(比如订阅、定时器),导致内存泄漏。
解决办法:在组件中判断环境,仅在客户端执行副作用:useEffect(() => { if (typeof window === 'undefined') return; // 服务端直接跳过 // 客户端才执行的副作用逻辑,比如定时器、事件监听 const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); // 清理副作用 }, []);
三、进程崩溃的兜底方案
即使优化了代码,高负载下仍可能出现意外,需要兜底机制:
- 用PM2做进程管理:PM2可以自动重启崩溃的进程,还能开启Cluster模式,把请求分散到多个Node进程,避免单进程崩溃导致整个服务挂掉。
启动命令:pm2 start server.js -i max # 根据CPU核心数自动创建进程数 - 全局错误捕获与日志:在Express和Node进程层面添加错误监听,记录详细日志方便排查:
// Express全局错误中间件 app.use((err, req, res, next) => { console.error(`SSR崩溃请求URL: ${req.url},错误信息:`, err.stack); res.status(500).send('服务器内部错误'); }); // Node进程层面捕获未处理异常 process.on('uncaughtException', (err) => { console.error('未捕获异常导致崩溃:', err.stack); process.exit(1); // 退出进程,PM2会自动重启 }); process.on('unhandledRejection', (reason, promise) => { console.error('未处理Promise拒绝:', reason.stack); process.exit(1); });
四、SSR性能优化缓解负载压力
减少每个请求的处理时间,能有效降低高负载下的崩溃概率:
- 缓存SSR结果:对于静态页面或更新频率低的页面,缓存渲染后的HTML字符串,避免重复渲染和API调用。比如用内存缓存或Redis:
const cache = new Map(); // 简单内存缓存,生产环境建议用Redis app.get('*', async (req, res) => { const cacheKey = `ssr_${req.url}`; if (cache.has(cacheKey)) { return res.send(cache.get(cacheKey)); } // 正常渲染流程... const html = renderToString(<App />); cache.set(cacheKey, html); setTimeout(() => cache.delete(cacheKey), 3600000); // 1小时后过期 res.send(html); }); - 并行发起API请求:确保Sagas中无依赖的API调用是并行执行的,减少数据获取的总时间。用Sagas的
alleffect:import { all, call } from 'redux-saga/effects'; function* fetchPageData() { // 并行发起多个API请求 const [userData, productData, articleData] = yield all([ call(fetchUserApi), call(fetchProductApi), call(fetchArticleApi), ]); // 处理数据并更新state }
内容的提问来源于stack exchange,提问作者Yura Borunov
相关产品推荐
相关产品推荐

