Node.js后端开发踩坑记录:服务监控、定时任务与文件操作

Node.js后端开发踩坑记录:服务监控、定时任务与文件操作

这篇文章是我在2021年进行Node.js后端开发时遇到的一些问题和解决方案的整理。

服务监控与自动重启

服务状态检测脚本

检测Node.js、MongoDB、Nginx服务状态,异常时自动重启并发送邮件通知。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const { exec } = require('child_process');
const nodemailer = require('nodemailer');

// 配置邮件发送
const transporter = nodemailer.createTransport({
host: 'smtp.qq.com',
port: 465,
secure: true,
auth: {
user: 'your_email@qq.com',
pass: 'your_auth_code'
}
});

// 发送邮件通知
async function sendAlert(serviceName) {
await transporter.sendMail({
from: 'your_email@qq.com',
to: 'admin@example.com',
subject: '服务器服务异常告警',
text: `服务 ${serviceName} 异常,时间:${new Date().toLocaleString()}`
});
}

// 检测服务状态
function checkService(command, serviceName) {
return new Promise((resolve) => {
exec(command, (error, stdout) => {
const count = parseInt(stdout.trim()) || 0;
resolve({ name: serviceName, running: count > 0, count });
});
});
}

// 主检测函数
async function monitorServices() {
const services = [
{ cmd: "ps -ef|grep node|grep -v grep|wc -l", name: 'Node.js' },
{ cmd: "netstat -lntup|grep mongod|wc -l", name: 'MongoDB' },
{ cmd: "netstat -lntup|grep nginx|wc -l", name: 'Nginx' }
];

const results = await Promise.all(
services.map(s => checkService(s.cmd, s.name))
);

for (const result of results) {
if (!result.running) {
console.log(`${result.name} 服务异常,准备重启...`);
await sendAlert(result.name);
await restartService(result.name);
}
}
}

// 重启服务
async function restartService(serviceName) {
const commands = {
'Node.js': 'cd /data/nodejs/ && sudo sh api.sh',
'MongoDB': 'sudo systemctl restart mongod',
'Nginx': 'sudo systemctl restart nginx'
};

if (commands[serviceName]) {
exec(commands[serviceName], (error) => {
if (error) {
console.error(`${serviceName} 重启失败:`, error);
} else {
console.log(`${serviceName} 重启成功`);
}
});
}
}

// 定时执行(每分钟)
setInterval(monitorServices, 60000);
monitorServices(); // 立即执行一次

Bash版本监控脚本

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
36
37
38
39
40
41
42
#!/bin/bash

code1=`ps -ef|grep node|wc -l`
code2=`netstat -lntup|grep mongod|wc -l`
code3=`netstat -lntup|grep nginx|wc -l`

my_mail(){
mail_list=("111@qq.com")
now_date=`date "+%Y-%m-%d %T"`
for i in ${mail_list[@]}
do
echo -e "服务器服务异常,IP 13.127.1.1,$now_date" |
mail -s "服务器服务异常" $i
done
}

my_node(){
cd /data/nodejs/
sudo sh xxx-api.sh
sudo sh xxx-service.sh
}

n=1
count=1
while ((n<4))
do
if [ $code1 -gt 3 ] && [ $code2 -gt 0 ] && [ $code3 -gt 0 ]
then
n=$(($n+1))
sleep 2
else
count=$(($count+1))
n=$(($n+1))
sleep 2
if [ $count -eq 3 ];then
my_mail
if [ $code1 -lt 3 ];then
my_node
fi
fi
fi
done

PM2进程管理

PM2基础命令

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
# 启动应用
pm2 start app.js

# 指定进程名启动
pm2 start app.js --name my-api

# 启动多个实例
pm2 start app.js -i 4

# 查看状态
pm2 status
pm2 list

# 查看日志
pm2 logs
pm2 logs app-name

# 重启应用
pm2 restart app-name

# 停止应用
pm2 stop app-name

# 删除应用
pm2 delete app-name

# 保存当前配置
pm2 save

# 开机自启
pm2 startup

PM2定时重启

1
2
3
4
5
# 每周五定时重启
pm2 start app.js --node-args="--nouse-idle-notification" --cron '0 0 0 0 0 5'

# 每天凌晨重启
pm2 start app.js --cron '0 0 * * *'

PM2配置文件

ecosystem.config.js:

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
module.exports = {
apps: [{
name: 'api-server',
script: './app.js',
instances: 4,
exec_mode: 'cluster',
watch: true,
ignore_watch: ['node_modules', 'logs'],
max_memory_restart: '500M',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 80
},
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
cron_restart: '0 3 * * *', // 每天凌晨3点重启
autorestart: true,
max_restarts: 10
}]
};

解决sudo pm2找不到命令

原因: sudo使用secure_path限制命令路径

解决方案:

1
2
3
cd /usr/bin
ln -s /usr/local/bin/node node
ln -s /usr/local/bin/pm2 pm2

文件系统操作

递归删除目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const fs = require('fs');
const path = require('path');

function deleteFolderRecursive(folderPath) {
if (fs.existsSync(folderPath)) {
fs.readdirSync(folderPath).forEach((file) => {
const curPath = path.join(folderPath, file);
if (fs.statSync(curPath).isDirectory()) {
// 递归删除子目录
deleteFolderRecursive(curPath);
} else {
// 删除文件
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(folderPath);
}
}

// 使用
deleteFolderRecursive('./temp');

递归复制目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fs = require('fs');
const path = require('path');

function copyFolderRecursive(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}

const entries = fs.readdirSync(src, { withFileTypes: true });

for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);

if (entry.isDirectory()) {
copyFolderRecursive(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}

// 使用
copyFolderRecursive('./source', './destination');

递归读取文件内容

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 fs = require('fs');
const path = require('path');

function readFilesRecursive(dir, callback) {
if (!fs.existsSync(dir)) return;

fs.readdirSync(dir).forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (stat.isDirectory()) {
readFilesRecursive(filePath, callback);
} else {
const content = fs.readFileSync(filePath, 'utf-8');
callback(filePath, content);
}
});
}

// 使用示例:统计所有JS文件中的特定字符
const charMap = {};
readFilesRecursive('./src', (filePath, content) => {
if (filePath.endsWith('.js')) {
for (const char of content) {
charMap[char] = (charMap[char] || 0) + 1;
}
}
});

创建多级目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function makeDir(dirpath) {
if (!fs.existsSync(dirpath)) {
let pathtmp;
dirpath.split(path.sep).forEach(dirname => {
if (pathtmp) {
pathtmp = path.join(pathtmp, dirname);
} else {
pathtmp = dirname || path.sep;
}
if (!fs.existsSync(pathtmp)) {
fs.mkdirSync(pathtmp);
}
});
}
}

执行系统命令

使用child_process模块

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
const { exec, execSync, spawn } = require('child_process');

// 异步执行
exec('ls -la', (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});

// 同步执行
try {
const result = execSync('ls -la');
console.log(result.toString());
} catch (error) {
console.error(error);
}

// 使用spawn(流式输出)
const ls = spawn('ls', ['-la', '/usr']);

ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
console.log(`子进程退出,退出码 ${code}`);
});

使用node-cmd包

1
npm install node-cmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cmd = require('node-cmd');

// 同步执行
const syncData = cmd.runSync('cd ./example & dir');
console.log(`
Sync Err: ${syncData.err}
Sync stderr: ${syncData.stderr}
Sync Data: ${syncData.data}
`);

// 异步执行
cmd.run('ipconfig', (err, data, stderr) => {
console.log(data);
});

性能优化

代码执行时间检测

1
2
3
4
5
console.time("saveData time");
this.saveData();
console.timeEnd("saveData time");

// 输出:saveData time: 1.19677734375ms

内存使用监控

1
2
3
4
5
6
7
8
9
10
11
12
// 打印内存使用情况
function printMemoryUsage() {
const usage = process.memoryUsage();
console.log('内存使用情况:');
console.log(`RSS: ${(usage.rss / 1024 / 1024).toFixed(2)} MB`);
console.log(`堆已用: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
console.log(`堆总量: ${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`);
console.log(`外部: ${(usage.external / 1024 / 1024).toFixed(2)} MB`);
}

// 定时打印
setInterval(printMemoryUsage, 30000);

垃圾回收优化

1
2
3
4
5
6
// 手动触发垃圾回收(需要--expose-gc参数启动)
if (global.gc) {
global.gc();
}

// 启动命令:node --expose-gc app.js

流式处理大文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');
const readline = require('readline');

// 高效处理大文件
async function processLargeFile(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});

for await (const line of rl) {
// 处理每一行数据
processLine(line);
}
}

TypeScript支持

全局安装TypeScript

1
npm install typescript -g

初始化tsconfig.json

1
tsc -init

tsconfig.json配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

VS Code编译配置

  1. 编写好.ts文件后
  2. 按下Ctrl+Shift+B快捷键
  3. 选择监视模式,自动生成对应的.js文件

错误处理

处理EPERM错误

错误: node EPERM operation not permitted

解决方案: 以管理员身份运行CMD或PowerShell

处理Cannot read property错误

解决方案:

1
2
# 卸载Node.js
# 从官网下载最新版本重新安装

全局异常捕获

1
2
3
4
5
6
7
8
9
10
11
12
// 捕获未处理的Promise异常
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
// 记录日志或发送告警
});

// 捕获未捕获的异常
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', err);
// 优雅退出
process.exit(1);
});

最佳实践

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
project/
├── src/ # 源代码
│ ├── config/ # 配置文件
│ ├── controllers/ # 控制器
│ ├── models/ # 数据模型
│ ├── routes/ # 路由
│ ├── services/ # 业务逻辑
│ └── utils/ # 工具函数
├── tests/ # 测试文件
├── logs/ # 日志目录
├── scripts/ # 脚本文件
├── .env # 环境变量
├── package.json
└── ecosystem.config.js # PM2配置

环境变量管理

1
2
3
4
5
6
7
8
9
10
11
12
13
// config/index.js
require('dotenv').config();

module.exports = {
port: process.env.PORT || 3000,
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb'
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
}
};

以上是我在2021年Node.js后端开发中的一些踩坑记录和解决方案。