Node.js高并发服务踩坑记录
Node.js做单服高并发有不少坑,这里整理下实际项目中踩过的雷。
性能分析
做性能优化前,得先知道瓶颈在哪。常用两种方法:Node自带Profile和AliNode。
| 方法 |
特点 |
场景 |
| Node自带Profile |
列出函数执行占比 |
快速定位 |
| AliNode CPU Profile |
完整调用栈 |
深度分析 |
Node自带Profile
1 2 3 4 5 6 7 8 9
| node --prof index.js
npm install -g loadtest loadtest http://127.0.0.1:6001 --rps 100
node --prof-process isolate-0x123456789-v8.log > profile.txt
|
生成 profile.txt 里能看到JS和C++代码的执行时间占比。
AliNode深度分析
AliNode是阿里出的Node.js增强版,分析能力更强。
安装:
1 2 3 4
| wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash source ~/.bashrc tnvm install alinode-v3.8.0 tnvm use alinode-v3.8.0
|
使用:
1 2 3 4
| node --perf-basic-prof-only-functions index.js loadtest http://127.0.0.1:6001 --rps 100 sh take_cpu_profile.sh 6989
|
实际案例:
发现最耗性能的是个 get 方法,里面用了深拷贝:
1 2 3 4 5
| get(propName) { if (!this.state[propName]) return null; return JSON.parse(JSON.stringify(this.state[propName])); }
|
改成lodash的cloneDeep:
1 2 3 4 5
| const _ = require('lodash'); get(propName) { if (!this.state[propName]) return null; return _.cloneDeep(this.state[propName]); }
|
| 方法 |
平均响应 |
备注 |
| JSON序列化 |
12.3ms |
基准 |
| lodash深拷贝 |
12.8ms |
稳定性更好 |
日志管理
log4js配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const log4js = require('log4js');
log4js.configure({ appenders: { console: {type: 'console'}, debugFile: { type: 'dateFile', filename: 'logs/debug-', pattern: 'yyyy-MM-dd.log', maxLogSize: 10 * 1000 * 1000, numBackups: 3, alwaysIncludePattern: true }, errorFile: { type: 'dateFile', filename: 'logs/error-', pattern: 'yyyy-MM-dd.log', maxLogSize: 10 * 1000 * 1000, numBackups: 7, alwaysIncludePattern: true } }, categories: { default: {appenders: ['console'], level: 'info'}, debug: {appenders: ['console', 'debugFile'], level: 'debug'}, error: {appenders: ['console', 'errorFile'], level: 'error'} } });
|
PM2 cluster模式注意
PM2 cluster模式下要加 disableClustering: true:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| log4js.configure({ disableClustering: true, appenders: { console: {type: 'console'}, errorFile: { type: 'dateFile', filename: 'logs/error', pattern: 'yyyy-MM-dd.log' } }, categories: { default: {appenders: ['console'], level: 'info'}, error: {appenders: ['console', 'errorFile'], level: 'error'} } });
|
日志级别
| 级别 |
数值 |
输出 |
场景 |
| OFF |
-1 |
不输出 |
关闭日志 |
| DEBUG |
1 |
所有 |
开发调试 |
| INFO |
2 |
INFO及以上 |
日常运行 |
| WARN |
3 |
WARN和ERROR |
生产环境 |
| ERROR |
4 |
仅ERROR |
错误监控 |
并发锁机制
高并发下共享资源访问要加锁,不然会出数据竞争问题。
使用:
1 2 3 4 5 6 7 8 9 10
| const AsyncLock = require('async-lock'); const lock = new AsyncLock();
async function updateUserBalance(userId, amount) { await lock.acquire(userId, async () => { const user = await getUser(userId); user.balance += amount; await saveUser(user); }); }
|
锁的最佳实践:
- 按用户ID加锁,别搞全局锁
- 设置超时:
1 2 3 4 5 6
| await lock.acquire(key, async () => { }, { timeout: 5000, timeoutCallback: () => console.error('获取锁超时') });
|
Socket服务开发
基础Socket服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const net = require('net'); const port = 1580;
require('http').globalAgent.maxSockets = Infinity;
const server = net.createServer({ allowHalfOpen: false, pauseOnConnect: false });
server.on('connection', (socket) => { console.log('新连接:', socket.remoteAddress, socket.remotePort);
socket.on('data', async (data) => { try { const message = data.toString(); const response = await handleMessage(message); socket.write(JSON.stringify(response)); } catch (error) { socket.write(JSON.stringify({ error: error.message })); } });
socket.on('close', (hadError) => { console.log('连接关闭:', hadError ? '异常' : '正常'); });
socket.on('error', (err) => { console.error('Socket错误:', err); }); });
server.listen(port, '0.0.0.0', () => { console.log(`Socket服务器运行在端口 ${port}`); });
|
阿里云服务器注意
阿里云ECS上要用内网IP,别用127.0.0.1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const os = require('os');
function getInternalIP() { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { for (const iface of interfaces[name]) { if (iface.family === 'IPv4' && !iface.internal) { return iface.address; } } } return '0.0.0.0'; }
server.listen(port, getInternalIP(), () => { console.log(`Socket服务器运行在 ${getInternalIP()}:${port}`); });
|
压力测试
loadtest工具
1 2 3 4 5 6 7
| npm install -g loadtest
loadtest http://localhost:3000/api/users -c 10 --rps 100
|
指标解读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "totalRequests": 4900, "totalErrors": 0, "totalTimeSeconds": 5.002, "rps": 100, "meanLatencyMs": 317, "maxLatencyMs": 610, "minLatencyMs": 146, "percentiles": { "50": 300, "90": 474, "95": 550, "99": 610 } }
|
| 指标 |
说明 |
建议阈值 |
| rps |
每秒请求 |
达到目标 |
| meanLatencyMs |
平均延迟 |
< 500ms |
| 90%延迟 |
90%请求延迟 |
< 1000ms |
| totalErrors |
错误数 |
0 |
POST测试
1 2 3 4 5
| loadtest -t 20 -c 10 --rps 10 \ http://localhost:6010/playerLogin.action \ -T 'application/x-www-form-urlencoded' \ -m POST \ --data '{"mCommand":"playerLogin","mLoginId":"test001"}'
|
参数调优
V8引擎参数
| 参数 |
作用 |
推荐值 |
--nouse-idle-notification |
关闭空闲通知 |
生产启用 |
--gc_global |
全局GC |
内存足时启用 |
--max-old-space-size |
老生代上限 |
2048+ |
--max-new-space-size |
新生代上限 |
1024 |
PM2生产配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| { "apps": [{ "name": "api-server", "script": "server.js", "port": 4000, "exec_mode": "cluster_mode", "instances": "max", "node_args": [ "--nouse-idle-notification", "--gc_global", "--max-old-space-size=4096", "--max-new-space-size=2048" ], "env": { "NODE_ENV": "production" }, "log_file": "./logs/combined.log", "out_file": "./logs/out.log", "error_file": "./logs/error.log", "log_date_format": "YYYY-MM-DD HH:mm:ss Z" }] }
|
HTTP连接池
1 2 3 4 5 6 7 8 9 10 11
| require("http").globalAgent.maxSockets = Infinity;
const http = require('http'); const agent = new http.Agent({ keepAlive: true, maxSockets: 100, maxFreeSockets: 10, timeout: 60000, freeSocketTimeout: 30000 });
|
Buffer操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const buf1 = Buffer.alloc(1024); const buf2 = Buffer.from([0x62, 0x75, 0x66]); const buf3 = Buffer.from('Hello', 'utf8');
buf1.fill(0); buf1.write('Hello World', 0, 'utf8');
const str = buf1.toString('utf8', 0, 11);
const json = JSON.stringify(buf3);
const list = [Buffer.from('Hello '), Buffer.from('World')]; const merged = Buffer.concat(list);
|
对象操作技巧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const data = { users: {}, orders: {}, products: {} }; Object.keys(data).length;
function getAllKeyCount(obj) { let count = Object.keys(obj).length; for (const key in obj) { if (obj[key] && typeof obj[key] === 'object') { count += getAllKeyCount(obj[key]); } } return count; }
function safeCall(func, ...args) { try { if (func && typeof func === 'function') { return func(...args); } } catch (e) { console.error('调用失败:', e.message); } }
|
VSCode效率插件
Beautify格式化
快捷键:Shift + Alt + F
CodeMetrics圈复杂度
| 复杂度 |
质量 |
建议 |
| 0-10 |
不错 |
保持 |
| 11-15 |
关注 |
考虑重构 |
| 16+ |
必须重构 |
拆分函数 |
降低复杂度示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function processData(data) { if (data) { if (data.type === 'A') return handleA(data); else if (data.type === 'B') return handleB(data); else return handleDefault(data); } else return null; }
const handlers = { 'A': handleA, 'B': handleB, 'default': handleDefault }; function processData(data) { if (!data) return null; return (handlers[data.type] || handlers.default)(data); }
|
经验总结
开发Node.js高并发服务,这几点比较关键:
- 性能分析:先用Profile找到瓶颈,别盲目优化
- 日志分级:DEBUG/INFO/WARN/ERROR分开,生产开WARN级别
- 并发控制:共享资源加锁,锁粒度尽量细
- Socket注意:阿里云用内网IP监听
- 压测验证:上线前用loadtest压一下,看延迟和错误率
- 参数调优:根据服务器配置调整V8内存参数
做到这些,Node.js服务在生产环境基本能扛得住。