likes
comments
collection
share

node 异常数据响应排查(pm2 Cluster Mode、异步)

作者站长头像
站长
· 阅读数 20

昨天收到一个铁子的反馈

node 里面写的一个 js 文件里面的方法,这个 js 文件里面有声明一个 var 全局变量(global),然后上面说到的方法就是先判断全局变量是否有值,要是就值就直接返回,要是没有值就通过接口去获取值; 然后在页面上特定的一个操作之后,会把这个全局变量的值清空为null,然后就走上面的获取接口的值本地正常,服务器上错误

他们给出的信息肯定是不够的,我就顺嘴问了几个信息

  • 是不是报错?因为报错会导致 Node 应用重启,继而导致状态失效。

    没报错
  • 能否提供复现代码?这里一般取决于项目的体量,或者目前问题定位进度

    无法提供。本地正常,服务器上错误
  • 执行环境是什么?commonjs?ESM?Ts?这里想看看是不是有什么骚操作,比如说 serverless 之类的无法保存状态。

    Node
  • 然后还问了本地和服务器通过什么启动的服务?这里我想确认一下是不是 Cluster ,因为 Cluster 状态是不共享,需要特殊方案。

    node

其实到这里我就知道,这个人不是专业做 Node 的,前面的信息有可能也有毒。

这个时候突然给我发来了日志截图,这直接破案了。Cluster 模式数据共享问题,本地 node 起的服务所以不存在这个问题,服务器应该是 pm2 start index.js -i 4 之类的。

0| www xxxxx
1| www xxxxx
3| www xxxxx
0| www xxxxx

接下来就是最小复现 demo 排查问题,修复方案了。

复现 Cluster 数据共享问题

其实在他让我看到是 Cluster 的时候就已经定位到问题了,非常明显的数据共享问题

下面来看我们的复现例子,可以发现单个实例输出是正确的,正是因为请求落到不同的机器(实例)导致不同的响应

if (!global.a) {
    global.a = 1
}
console.log(global.a, Date.now())
function randomTask() {
    console.log(++global.a, Date.now())
    if (global.a < 5) {
        setTimeout(randomTask, Math.random() * 1000)
    }
}
randomTask();

node 异常数据响应排查(pm2 Cluster Mode、异步)

Statelessify your applicationBe sure your application is stateless meaning that no local data is stored in the process, for example sessions/websocket connections, session-memory and related. Use Redis, Mongo or other databases to share states between processes.Another resource on how to write efficient, production ready stateless application is The Twelve Factor Application manifesto.

修复

不启动 Cluster 集群模式

因为本地是非 Cluster 集群模式,所以表现正常。那么第一个解决办法就是生产环境也不开启集群模式,但是一般来说这个方案是不可取的,生产环境的请求比较高,集群模式才是最优解法。

增加单实例的数据服务 | 降为单实例模式

类似于 redis ,只不过是新建一个单实例的 nodeJs 脚本。获取数据&更新数据都是请求这个脚本服务。

因为不使用集群模式所以也就不存在共享问题了。同时也避免了上一个解法的问题,因为数据服务不对外开放,只给内网的服务开通,所以请求量级不会太大。

redis

Published & subscribe

通过 redis 来实现发布订阅功能。更新数据的时候 Published 所有 Worker 更新数据。Subscribe 收到更新的时候更新自己的数据。

代码如下。至于为什么会有多个 redis 实例呢?这是因为一个 redis 实例只能为发布者或者订阅者,所以我们需要有两个实例,一个用来发布更新后的数据,一个用来监听其他 worker 发来的更新。

// ioredis
const Redis = require("ioredis")
let redisClient3 = new Redis()
let redisClient4 = new Redis()

setInterval(() => {
    const message = { foo: Math.random(), pid: process.pid };
    const channel = `my-channel-${1 + Math.round(Math.random())}`;
    
    redisClient3.publish(channel, JSON.stringify(message));
    console.log("Published %s to %s", message, channel);
}, 5000);

redisClient4.subscribe("my-channel-1", "my-channel-2", (err, count) => {
    if (err) {
        console.error("Failed to subscribe: %s", err.message);
    } else {
        console.log(
            `Subscribed successfully! This client is currently subscribed to ${count} channels.`
        );
    }
});

redisClient4.on("message", (channel, message) => {
    console.log(`Received ${message} from ${channel}`);
});

fs

因为集群实例间无法通信,所以需要找到一个可以共同访问的,那么本地磁盘也是一个可行的方案。但是 fs 有可能会存在冲突,还是不建议使用了。

node 异常数据响应排查(pm2 Cluster Mode、异步)

试一下了好像也不会报错,也不会出现内容错乱,就是有可能取出来的内容是空。

const fs = require('fs');
const str = `process.pid: ${process.pid}`.repeat(999) + '\n';
console.log(`process.pid: ${process.pid}`)
const test = ()=>{
    for(var i = 0; i < 10; i++){
        console.log(`process.pid: ${process.pid} ${i}`)
        fs.writeFile('message.txt', `${i} ${str}`, (err)=>{
            if(err) console.log(err)
        });
    }
    setTimeout(test, Math.random() * 100);
    // setTimeout(test);
}
test();

cluster 模块

因为 pm2 启动的全是 Worker 所以这个方案不太适合我们。

if (cluster.isMaster) {
  const worker = cluster.fork();
  worker.send('你好');
} else if (cluster.isWorker) {
  process.on('message', (msg) => {
    process.send(msg);
  });
}