构建百分级并发的 Node.js 应用(Nginx 版本)

189 阅读3分钟

在高并发场景下,Node.js 应用往往需要借助网关/负载均衡器来分发流量。相比应用层实现分发逻辑,用 Nginx 处理“百分比分流”更稳定、更高效。本文将介绍一个完整的高并发方案:

  1. Nginx 百分比分发
  2. 消息队列处理并发写
  3. Redis 热点缓存
  4. 数据库读写分离

概念

  • Nginx 百分比分发:通过 upstreamweight 配置,控制不同后端服务接收多少流量(例如 70% 到 A,30% 到 B)。
  • 消息队列:把并发写请求入队,顺序消费,避免数据冲突。
  • Redis 缓存:高频读操作优先读缓存,减少 DB 压力。
  • 读写分离:主库写,从库读,应用层或中间件自动分流。

原理

  • Nginx 负载均衡:基于权重(weight),按照比例把请求分配到不同的 Node.js 服务。
  • MQ 串行化:写请求进入队列 → 单线程消费 → 顺序写入数据库。
  • 缓存:Cache-Aside 模式(读缓存 → DB → 回填)。
  • 数据库:主从复制,应用决定走主库还是从库。

实践

1. Nginx 配置:百分比分发

/etc/nginx/conf.d/node.conf 新建配置:

upstream node_app {
    server 127.0.0.1:4001 weight=70 max_fails=3 fail_timeout=30s;   # 服务 A,70% 流量
    server 127.0.0.1:4002 weight=30max_fails=3 fail_timeout=30s;   # 服务 B,30% 流量
}

server {
    listen 3000;

    location / {
        proxy_pass http://node_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

🔎 说明

  • max_fails=3:30 秒内失败 3 次即认为该节点不可用。
  • fail_timeout=30s:在 30 秒内暂停向该节点转发请求。
  • 到期后 Nginx 会重新尝试,节点恢复后自动重新加入。

👉 启动方式:

nginx -s reload   # 重载配置
curl http://127.0.0.1:3000/api/test   # 请求将按 70/30 分发到两个 Node 服务

2. MQ:并发写请求串行化

生产者

// mq/producer.js
const amqp = require('amqplib');

async function produce(queue, payload) {
  const conn = await amqp.connect('amqp://user:pass@localhost:5672');
  const ch = await conn.createChannel();
  await ch.assertQueue(queue, { durable: true });
  ch.sendToQueue(queue, Buffer.from(JSON.stringify(payload)), { persistent: true });
  await ch.close();
  await conn.close();
}

module.exports = { produce };

消费者

// mq/consumer.js
const amqp = require('amqplib');
const { updateUser } = require('../db');

async function startConsumer(queue) {
  const conn = await amqp.connect('amqp://user:pass@localhost:5672');
  const ch = await conn.createChannel();
  await ch.assertQueue(queue, { durable: true });
  ch.prefetch(1);

  ch.consume(queue, async (msg) => {
    if (!msg) return;
    const payload = JSON.parse(msg.content.toString());
    try {
      await updateUser(payload.userId, payload.changes);
      ch.ack(msg);
    } catch (err) {
      console.error(`[Consumer Error] ${err.message}`);
      ch.nack(msg, false, false);
    }
  });
}

startConsumer('user-update').catch(console.error);

3. Redis 缓存

// cache.js
const Redis = require('ioredis');
const redis = new Redis();
const { getUserById, updateUserInDB } = require('./db');

async function getUser(userId) {
  const key = `user:${userId}`;
  let cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const user = await getUserById(userId);
  if (!user) return null;

  await redis.set(key, JSON.stringify(user), 'EX', 60);
  return user;
}

async function updateUser(userId, changes) {
  await updateUserInDB(userId, changes);
  await redis.del(`user:${userId}`); // 删除缓存,下次请求自动回填
}

4. 数据库读写分离

// db.js
const { Pool } = require('pg');

const writePool = new Pool({ host: 'db-primary', user: 'app', password: 'pwd', database: 'mydb' });
const readPools = [
  new Pool({ host: 'db-replica-1', user: 'app', password: 'pwd', database: 'mydb' }),
  new Pool({ host: 'db-replica-2', user: 'app', password: 'pwd', database: 'mydb' }),
];

let rr = 0;
function getReadPool() {
  rr = (rr + 1) % readPools.length;
  return readPools[rr];
}

async function getUserById(id) {
  return (await getReadPool().query('SELECT * FROM users WHERE id=$1', [id])).rows[0];
}

async function updateUserInDB(id, changes) {
  await writePool.query('UPDATE users SET data=$1 WHERE id=$2', [changes, id]);
}

module.exports = { getUserById, updateUserInDB, updateUser: updateUserInDB };

拓展

  • 灰度发布:动态调整 weight 实现平滑升级。
  • 一致性:对强一致场景,写后读要走主库。
  • 缓存雪崩防护:给 TTL 加随机偏移。
  • MQ 幂等处理:写请求必须带唯一 ID,避免重复消费。

潜在问题

  • Nginx 单点:可部署多实例,前面加云 LB。
  • 读写延迟:从库可能存在复制延迟,需在关键场景强制走主库。
  • 消息丢失:需开启 RabbitMQ 持久化,并配置死信队列。
  • 缓存击穿:热点数据需加互斥锁。

总结

通过 Nginx 百分比分发 + RabbitMQ 串行化写 + Redis 热点缓存 + 数据库读写分离,我们就能在 Node.js 系统中搭建一个支撑“百分级并发”的生产级架构。

Nginx 的引入让分发层更高效、更稳定,也能更好地支持灰度发布和回滚。

最后这种是由于购买大型服务器超贵的解决方案,如果金额足够使用一台大型服务器就足够了。


本文部分内容借助 AI 辅助生成,并由作者整理审核。