Node.js高并发服务踩坑记录

Node.js高并发服务踩坑记录

Node.js做单服高并发有不少坑,这里整理下实际项目中踩过的雷。

性能分析

做性能优化前,得先知道瓶颈在哪。常用两种方法:Node自带Profile和AliNode。

方法 特点 场景
Node自带Profile 列出函数执行占比 快速定位
AliNode CPU Profile 完整调用栈 深度分析

Node自带Profile

1
2
3
4
5
6
7
8
9
# 1. Profile模式启动
node --prof index.js

# 2. 压测施压
npm install -g loadtest
loadtest http://127.0.0.1:6001 --rps 100

# 3. 处理日志
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 # 6989是进程ID
# 结果在 /tmp/cpu-profile-6989-XXX.cpuprofile

实际案例:

发现最耗性能的是个 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
npm install async-lock

使用:

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);
});
}

锁的最佳实践:

  1. 按用户ID加锁,别搞全局锁
  2. 设置超时:
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

# -c 10: 10个并发
# --rps 100: 每秒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);

// Buffer转JSON
const json = JSON.stringify(buf3);
// {"type":"Buffer","data":[72,101,108,108,111]}

// 合并
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
// 获取对象key数量
const data = { users: {}, orders: {}, products: {} };
Object.keys(data).length; // 3

// 递归获取所有层级key数量
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高并发服务,这几点比较关键:

  1. 性能分析:先用Profile找到瓶颈,别盲目优化
  2. 日志分级:DEBUG/INFO/WARN/ERROR分开,生产开WARN级别
  3. 并发控制:共享资源加锁,锁粒度尽量细
  4. Socket注意:阿里云用内网IP监听
  5. 压测验证:上线前用loadtest压一下,看延迟和错误率
  6. 参数调优:根据服务器配置调整V8内存参数

做到这些,Node.js服务在生产环境基本能扛得住。