🏰 设计高可用消息队列系统:打造坚不可摧的MQ!

38 阅读9分钟

副标题:主从复制、消息持久化、故障转移,缺一不可!💪


🎬 开场:MQ宕机的噩梦

凌晨3点,生产事故 💥:

23:58  MQ主节点磁盘满了
00:00  消息开始堆积
00:05  MQ进程崩溃
00:10  所有服务无法发送消息
00:15  订单无法创建
00:20  用户疯狂投诉
00:30  老板打电话: "怎么回事?!"
01:00  紧急重启MQ
01:30  发现数据丢失了几千条消息
02:00  连夜恢复数据...

损失: 100万+
教训: 惨痛!

这些问题暴露了什么?

问题原因后果
单点故障只有一个节点节点挂了全挂 💀
数据丢失没有持久化重启后数据没了 💔
无法故障转移没有备份节点恢复时间长 ⏰
监控缺失没有告警问题发现太晚 ⚠️

今天我们来设计一个真正高可用的MQ系统!


📊 高可用的核心指标

高可用系统的"三高":
├── 高可靠(Reliability)   → 消息不丢失
├── 高性能(Performance)    → 吞吐量高、延迟低
└── 高可用(Availability)   → 服务不中断

可用性等级

等级可用性年度宕机时间适用场景
2个999%3.65天测试环境
3个999.9%8.76小时一般业务
4个999.99%52.56分钟重要业务 ⭐
5个999.999%5.26分钟核心业务 ⭐⭐
6个999.9999%31.5秒金融级 ⭐⭐⭐

🏗️ 整体架构设计

1. 集群架构

              ┌─────────────────────────────┐
              │      Load Balancer          │
              │    (HAProxy/Nginx)          │
              └─────────┬───────────────────┘
                        │
            ┌───────────┼───────────┐
            │           │           │
     ┌──────▼─────┐ ┌──▼──────┐ ┌─▼─────────┐
     │  Broker 1  │ │ Broker 2│ │ Broker 3  │
     │  (Master)  │ │(Master) │ │ (Master)  │
     └─────┬──────┘ └──┬──────┘ └───┬───────┘
           │           │             │
     ┌─────▼──────┐ ┌──▼──────┐ ┌───▼───────┐
     │  Broker 1  │ │ Broker 2│ │ Broker 3  │
     │  (Slave)   │ │ (Slave) │ │ (Slave)   │
     └────────────┘ └─────────┘ └───────────┘
           │           │             │
           └───────────┼─────────────┘
                       │
              ┌────────▼──────────┐
              │    ZooKeeper      │
              │  (协调/选主)      │
              └───────────────────┘

架构说明

  1. 负载均衡层

    • 分发请求到多个Broker
    • 健康检查
    • 故障自动摘除
  2. Broker集群

    • 多个Master节点(负责读写)
    • 每个Master有Slave节点(负责备份)
    • 数据同步
  3. 协调服务

    • ZooKeeper/etcd/Consul
    • 服务发现
    • 主从选举

💾 消息持久化设计

1. 存储架构

消息存储层次:

┌──────────────────────────────────┐
│         Memory Queue             │  ← 内存队列(最快)
│    (快速读写,断电丢失)          │
└──────────┬───────────────────────┘
           │ 异步刷盘
┌──────────▼───────────────────────┐
│        Page Cache                │  ← 系统缓存(快)
│   (操作系统管理,性能好)         │
└──────────┬───────────────────────┘
           │ 操作系统刷盘
┌──────────▼───────────────────────┐
│      磁盘文件                    │  ← 持久化(安全)
│   CommitLog + ConsumeQueue       │
└──────────────────────────────────┘

2. 持久化策略

同步刷盘 vs 异步刷盘

策略安全性性能适用场景
同步刷盘高 ⭐⭐⭐⭐⭐低 ⭐⭐金融交易
异步刷盘中 ⭐⭐⭐高 ⭐⭐⭐⭐⭐日志收集

代码实现

/**
 * 消息持久化管理器
 */
public class MessageStore {
    
    private final FileChannel commitLogChannel;
    private final MappedByteBuffer mappedBuffer;
    
    /**
     * 同步刷盘(安全但慢)
     */
    public void appendMessageSync(Message message) throws IOException {
        // 1. 序列化消息
        ByteBuffer buffer = serializeMessage(message);
        
        // 2. 写入文件
        commitLogChannel.write(buffer);
        
        // 3. 强制刷盘(同步)
        commitLogChannel.force(true);  // 等待刷盘完成
        
        log.info("消息已同步刷盘: messageId={}", message.getId());
    }
    
    /**
     * 异步刷盘(快但有风险)
     */
    public void appendMessageAsync(Message message) throws IOException {
        // 1. 序列化消息
        ByteBuffer buffer = serializeMessage(message);
        
        // 2. 写入PageCache
        commitLogChannel.write(buffer);
        
        // 3. 不等待刷盘,立即返回
        // 操作系统会在后台异步刷盘
        
        log.info("消息已写入PageCache: messageId={}", message.getId());
    }
    
    /**
     * 定时刷盘(折中方案)
     */
    @Scheduled(fixedDelay = 1000)  // 每秒刷盘一次
    public void flushPeriodically() {
        try {
            commitLogChannel.force(false);
            log.debug("定时刷盘完成");
        } catch (IOException e) {
            log.error("刷盘失败", e);
        }
    }
}

3. 文件存储结构

MQ数据目录:
/data/mq/
├── commitlog/          # 消息存储
│   ├── 00000000000000000000
│   ├── 00000000001073741824  # 每个文件1GB
│   └── 00000000002147483648
│
├── consumequeue/       # 消费队列索引
│   ├── topic1/
│   │   ├── 0/          # 队列0
│   │   ├── 1/          # 队列1
│   │   └── 2/          # 队列2
│   └── topic2/
│
└── index/              # 消息索引
    ├── 20240101000000
    └── 20240102000000

CommitLog文件格式

┌───────────────────────────────────────┐
│ Message 1                             │
│  ├─ TotalSize (4 bytes)               │
│  ├─ MagicCode (4 bytes)               │
│  ├─ BodyCRC (4 bytes)                 │
│  ├─ QueueId (4 bytes)                 │
│  ├─ Flag (4 bytes)                    │
│  ├─ QueueOffset (8 bytes)             │
│  ├─ PhysicalOffset (8 bytes)          │
│  ├─ SysFlag (4 bytes)                 │
│  ├─ BornTimestamp (8 bytes)           │
│  ├─ BornHost (8 bytes)                │
│  ├─ StoreTimestamp (8 bytes)          │
│  ├─ StoreHost (8 bytes)               │
│  ├─ ReconsumeTimes (4 bytes)          │
│  ├─ PreparedTransactionOffset(8bytes) │
│  ├─ BodyLength (4 bytes)              │
│  ├─ Body (variable)                   │
│  ├─ TopicLength (1 byte)              │
│  ├─ Topic (variable)                  │
│  ├─ PropertiesLength (2 bytes)        │
│  └─ Properties (variable)             │
├───────────────────────────────────────┤
│ Message 2                             │
│  ├─ ...                               │
└───────────────────────────────────────┘

🔄 主从复制机制

1. 同步复制 vs 异步复制

同步复制(Sync Replication):

Producer → Master → Slave 1
              ↓     ↘ Slave 2
              │      ↘ Slave 3
              │ 等待所有Slave确认
              ↓
            返回ACK

优点:数据绝对安全
缺点:性能低,延迟高

异步复制(Async Replication):

Producer → Master → 立即返回ACK
              ↓
          后台同步 → Slave 1
                   → Slave 2
                   → Slave 3

优点:性能高,延迟低
缺点:可能丢数据(Master挂了)

2. 主从同步实现

/**
 * Master节点:数据同步发送器
 */
@Service
public class MasterReplicationService {
    
    private final List<SlaveConnection> slaveConnections = new ArrayList<>();
    
    /**
     * 同步复制
     */
    public boolean replicateSync(Message message) {
        CountDownLatch latch = new CountDownLatch(slaveConnections.size());
        AtomicInteger successCount = new AtomicInteger(0);
        
        // 并发发送给所有Slave
        for (SlaveConnection slave : slaveConnections) {
            CompletableFuture.runAsync(() -> {
                try {
                    slave.send(message);
                    successCount.incrementAndGet();
                } catch (Exception e) {
                    log.error("同步到Slave失败: {}", slave.getAddress(), e);
                } finally {
                    latch.countDown();
                }
            });
        }
        
        try {
            // 等待所有Slave响应,最多等待3秒
            latch.await(3, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        // 至少一半Slave成功
        int requiredCount = (slaveConnections.size() + 1) / 2;
        return successCount.get() >= requiredCount;
    }
    
    /**
     * 异步复制
     */
    public void replicateAsync(Message message) {
        for (SlaveConnection slave : slaveConnections) {
            // 异步发送,不等待响应
            CompletableFuture.runAsync(() -> {
                try {
                    slave.send(message);
                } catch (Exception e) {
                    log.error("异步同步失败: {}", slave.getAddress(), e);
                }
            });
        }
    }
}

/**
 * Slave节点:数据接收器
 */
@Service
public class SlaveReplicationService {
    
    @Autowired
    private MessageStore messageStore;
    
    /**
     * 接收Master的数据
     */
    public void receiveFromMaster(ReplicationData data) {
        try {
            // 1. 验证数据完整性
            if (!validateData(data)) {
                throw new IllegalArgumentException("数据校验失败");
            }
            
            // 2. 写入本地存储
            messageStore.append(data.getMessage());
            
            // 3. 返回确认
            sendAck(data.getMessageId());
            
            log.info("数据同步成功: offset={}", data.getOffset());
            
        } catch (Exception e) {
            log.error("接收数据失败", e);
            throw e;
        }
    }
}

3. 同步位点管理

/**
 * 同步位点追踪
 */
public class ReplicationProgress {
    
    // Master的写入位点
    private volatile long masterOffset = 0;
    
    // 每个Slave的同步位点
    private final ConcurrentHashMap<String, Long> slaveOffsets = new ConcurrentHashMap<>();
    
    /**
     * 更新Master位点
     */
    public void updateMasterOffset(long offset) {
        this.masterOffset = offset;
    }
    
    /**
     * 更新Slave位点
     */
    public void updateSlaveOffset(String slaveId, long offset) {
        slaveOffsets.put(slaveId, offset);
    }
    
    /**
     * 获取最小同步位点
     */
    public long getMinSlaveOffset() {
        return slaveOffsets.values().stream()
            .min(Long::compare)
            .orElse(0L);
    }
    
    /**
     * 获取同步延迟
     */
    public Map<String, Long> getReplicationLag() {
        Map<String, Long> lags = new HashMap<>();
        
        slaveOffsets.forEach((slaveId, offset) -> {
            long lag = masterOffset - offset;
            lags.put(slaveId, lag);
        });
        
        return lags;
    }
}

🚨 故障检测与转移

1. 心跳检测

/**
 * 心跳检测服务
 */
@Service
public class HeartbeatService {
    
    private final ConcurrentHashMap<String, NodeStatus> nodeStatuses = new ConcurrentHashMap<>();
    
    /**
     * 发送心跳
     */
    @Scheduled(fixedDelay = 1000)  // 每秒发送一次
    public void sendHeartbeat() {
        for (String nodeId : getClusterNodes()) {
            try {
                HeartbeatResponse response = sendHeartbeat(nodeId);
                
                NodeStatus status = nodeStatuses.computeIfAbsent(
                    nodeId,
                    k -> new NodeStatus(nodeId)
                );
                
                status.updateLastHeartbeat();
                status.setAlive(true);
                
            } catch (Exception e) {
                handleHeartbeatFailed(nodeId);
            }
        }
    }
    
    /**
     * 心跳失败处理
     */
    private void handleHeartbeatFailed(String nodeId) {
        NodeStatus status = nodeStatuses.get(nodeId);
        
        if (status != null) {
            status.incrementFailedCount();
            
            // 连续3次失败,标记为down
            if (status.getFailedCount() >= 3) {
                status.setAlive(false);
                
                // 触发故障转移
                triggerFailover(nodeId);
            }
        }
    }
}

/**
 * 节点状态
 */
public class NodeStatus {
    private final String nodeId;
    private volatile boolean alive = true;
    private volatile long lastHeartbeatTime;
    private final AtomicInteger failedCount = new AtomicInteger(0);
    
    public void updateLastHeartbeat() {
        this.lastHeartbeatTime = System.currentTimeMillis();
        this.failedCount.set(0);  // 重置失败计数
    }
    
    public boolean isTimeout(long timeoutMs) {
        return System.currentTimeMillis() - lastHeartbeatTime > timeoutMs;
    }
}

2. 主从切换

/**
 * 故障转移管理器
 */
@Service
public class FailoverManager {
    
    @Autowired
    private ZooKeeperClient zkClient;
    
    @Autowired
    private BrokerController brokerController;
    
    /**
     * 执行故障转移
     */
    public void failover(String failedMasterId) {
        log.warn("检测到Master节点故障: {}", failedMasterId);
        
        try {
            // 1. 从Slave中选举新的Master
            String newMasterId = electNewMaster(failedMasterId);
            
            if (newMasterId == null) {
                log.error("没有可用的Slave节点");
                return;
            }
            
            // 2. 提升Slave为Master
            promoteToMaster(newMasterId);
            
            // 3. 更新路由信息
            updateRouteInfo(failedMasterId, newMasterId);
            
            // 4. 通知所有客户端
            notifyClients(failedMasterId, newMasterId);
            
            log.info("故障转移完成: {} -> {}", failedMasterId, newMasterId);
            
        } catch (Exception e) {
            log.error("故障转移失败", e);
        }
    }
    
    /**
     * 选举新Master
     */
    private String electNewMaster(String failedMasterId) {
        // 1. 获取所有Slave
        List<String> slaves = getSlaves(failedMasterId);
        
        if (slaves.isEmpty()) {
            return null;
        }
        
        // 2. 选择同步位点最新的Slave
        String bestSlave = null;
        long maxOffset = -1;
        
        for (String slaveId : slaves) {
            long offset = getReplicationOffset(slaveId);
            if (offset > maxOffset) {
                maxOffset = offset;
                bestSlave = slaveId;
            }
        }
        
        return bestSlave;
    }
    
    /**
     * 提升为Master
     */
    private void promoteToMaster(String slaveId) throws Exception {
        // 1. 在ZooKeeper创建Master节点
        zkClient.create(
            "/mq/masters/" + slaveId,
            "MASTER",
            CreateMode.EPHEMERAL
        );
        
        // 2. 更新Broker配置
        brokerController.setRole(BrokerRole.MASTER);
        
        // 3. 开始接受写请求
        brokerController.enableWrite();
        
        log.info("Slave已提升为Master: {}", slaveId);
    }
}

3. 脑裂预防

/**
 * 脑裂预防机制
 */
@Service
public class SplitBrainPrevention {
    
    @Autowired
    private ZooKeeperClient zkClient;
    
    /**
     * 使用ZooKeeper分布式锁
     */
    public boolean acquireMasterLock(String brokerId) {
        String lockPath = "/mq/locks/master";
        
        try {
            // 尝试创建临时节点
            zkClient.create(
                lockPath,
                brokerId,
                CreateMode.EPHEMERAL  // 连接断开自动删除
            );
            
            log.info("成功获取Master锁: {}", brokerId);
            return true;
            
        } catch (NodeExistsException e) {
            // 锁已被其他节点持有
            String currentMaster = zkClient.getData(lockPath);
            log.warn("Master锁已被持有: {}", currentMaster);
            return false;
        }
    }
    
    /**
     * 检查是否真正的Master
     */
    public boolean isTrueMaster(String brokerId) {
        String lockPath = "/mq/locks/master";
        
        try {
            String currentMaster = zkClient.getData(lockPath);
            return brokerId.equals(currentMaster);
        } catch (Exception e) {
            return false;
        }
    }
}

📊 监控告警

1. 核心指标监控

/**
 * MQ监控指标
 */
@Component
public class MQMetrics {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    // 消息发送速率
    private final Counter messageSentCounter;
    
    // 消息消费速率
    private final Counter messageConsumedCounter;
    
    // 消息堆积量
    private final Gauge messageBacklogGauge;
    
    // 磁盘使用率
    private final Gauge diskUsageGauge;
    
    // 同步延迟
    private final Gauge replicationLagGauge;
    
    public MQMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        // 注册指标
        messageSentCounter = Counter.builder("mq.messages.sent")
            .description("消息发送总数")
            .register(meterRegistry);
        
        messageConsumedCounter = Counter.builder("mq.messages.consumed")
            .description("消息消费总数")
            .register(meterRegistry);
        
        messageBacklogGauge = Gauge.builder("mq.messages.backlog", this::getMessageBacklog)
            .description("消息堆积量")
            .register(meterRegistry);
        
        diskUsageGauge = Gauge.builder("mq.disk.usage", this::getDiskUsage)
            .description("磁盘使用率")
            .register(meterRegistry);
    }
    
    /**
     * 记录消息发送
     */
    public void recordMessageSent(String topic) {
        messageSentCounter.increment();
        
        Counter.builder("mq.messages.sent.by.topic")
            .tag("topic", topic)
            .register(meterRegistry)
            .increment();
    }
    
    /**
     * 获取消息堆积量
     */
    private long getMessageBacklog() {
        // 计算未消费的消息数
        return producerOffset - consumerOffset;
    }
    
    /**
     * 获取磁盘使用率
     */
    private double getDiskUsage() {
        File dataDir = new File("/data/mq");
        long total = dataDir.getTotalSpace();
        long free = dataDir.getFreeSpace();
        return (double) (total - free) / total * 100;
    }
}

2. 告警规则

/**
 * 告警管理器
 */
@Service
public class AlertManager {
    
    @Autowired
    private DingTalkService dingTalkService;
    
    @Autowired
    private MQMetrics mqMetrics;
    
    /**
     * 定期检查告警条件
     */
    @Scheduled(fixedDelay = 60000)  // 每分钟
    public void checkAlerts() {
        // 1. 检查消息堆积
        long backlog = mqMetrics.getMessageBacklog();
        if (backlog > 10000) {
            sendAlert(
                "消息堆积告警",
                "当前堆积: " + backlog + " 条,超过阈值10000"
            );
        }
        
        // 2. 检查磁盘使用
        double diskUsage = mqMetrics.getDiskUsage();
        if (diskUsage > 80) {
            sendAlert(
                "磁盘使用率告警",
                "当前使用率: " + diskUsage + "%,超过阈值80%"
            );
        }
        
        // 3. 检查同步延迟
        long replicationLag = mqMetrics.getReplicationLag();
        if (replicationLag > 1000000) {  // 1MB
            sendAlert(
                "主从同步延迟告警",
                "当前延迟: " + replicationLag + " bytes"
            );
        }
    }
    
    private void sendAlert(String title, String content) {
        dingTalkService.send(title, content);
        log.warn("发送告警: {} - {}", title, content);
    }
}

💡 生产环境最佳实践

1. 配置建议

# MQ Broker配置
broker:
  # 集群配置
  cluster-name: mq-cluster
  broker-name: broker-1
  broker-role: SYNC_MASTER  # SYNC_MASTER/ASYNC_MASTER/SLAVE
  
  # 持久化配置
  flush-disk-type: ASYNC_FLUSH  # SYNC_FLUSH/ASYNC_FLUSH
  flush-interval: 1000  # 异步刷盘间隔(毫秒)
  
  # 复制配置
  replication-type: SYNC  # SYNC/ASYNC
  min-replicas: 2  # 最少副本数
  
  # 存储配置
  store-path: /data/mq/store
  commitlog-size: 1073741824  # 1GB
  
  # 性能配置
  send-message-thread-pool-nums: 16
  pull-message-thread-pool-nums: 16
  
  # 监控配置
  enable-metrics: true
  metrics-port: 9999

2. 容量规划

容量计算公式:

所需磁盘 = 日消息量 × 消息大小 × 保留天数 × 副本数 × 1.5(冗余)

示例:
- 日消息量: 1亿条
- 消息大小: 1KB
- 保留天数: 7天
- 副本数: 3
- 冗余系数: 1.5

所需磁盘 = 100000000 × 1KB × 7 × 3 × 1.5
         = 3.15 TB

建议配置: 4TB SSD

3. 运维检查清单

日常检查:
□ 检查磁盘使用率
□ 检查消息堆积量
□ 检查主从同步状态
□ 检查节点健康状态
□ 检查错误日志

周检查:
□ 清理过期数据
□ 检查系统资源
□ 检查网络延迟
□ 备份配置文件

月检查:
□ 压测验证性能
□ 演练故障切换
□ 更新监控规则
□ 优化配置参数

🎯 总结

核心要点 ✨

  1. 高可用架构

    • 多Master + 多Slave
    • 负载均衡
    • ZooKeeper协调
  2. 数据可靠性

    • 消息持久化
    • 主从复制
    • 同步/异步策略
  3. 故障处理

    • 心跳检测
    • 自动故障转移
    • 脑裂预防
  4. 监控告警

    • 关键指标监控
    • 实时告警
    • 日志审计

记忆口诀 📝

高可用MQ要做好,
架构设计很重要。

主从集群要部署,
数据复制不能少。
持久化要做到位,
消息丢失不会有。

心跳检测要及时,
故障转移要自动。
监控告警不能忘,
问题发现早知道。

容量规划要合理,
性能压测要做好。
运维流程要规范,
系统稳定才是宝!

愿你的MQ系统坚如磐石,永不宕机! 🏰✨