副标题:主从复制、消息持久化、故障转移,缺一不可!💪
🎬 开场: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个9 | 99% | 3.65天 | 测试环境 |
| 3个9 | 99.9% | 8.76小时 | 一般业务 |
| 4个9 | 99.99% | 52.56分钟 | 重要业务 ⭐ |
| 5个9 | 99.999% | 5.26分钟 | 核心业务 ⭐⭐ |
| 6个9 | 99.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 │
│ (协调/选主) │
└───────────────────┘
架构说明:
-
负载均衡层:
- 分发请求到多个Broker
- 健康检查
- 故障自动摘除
-
Broker集群:
- 多个Master节点(负责读写)
- 每个Master有Slave节点(负责备份)
- 数据同步
-
协调服务:
- 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. 运维检查清单
日常检查:
□ 检查磁盘使用率
□ 检查消息堆积量
□ 检查主从同步状态
□ 检查节点健康状态
□ 检查错误日志
周检查:
□ 清理过期数据
□ 检查系统资源
□ 检查网络延迟
□ 备份配置文件
月检查:
□ 压测验证性能
□ 演练故障切换
□ 更新监控规则
□ 优化配置参数
🎯 总结
核心要点 ✨
-
高可用架构:
- 多Master + 多Slave
- 负载均衡
- ZooKeeper协调
-
数据可靠性:
- 消息持久化
- 主从复制
- 同步/异步策略
-
故障处理:
- 心跳检测
- 自动故障转移
- 脑裂预防
-
监控告警:
- 关键指标监控
- 实时告警
- 日志审计
记忆口诀 📝
高可用MQ要做好,
架构设计很重要。
主从集群要部署,
数据复制不能少。
持久化要做到位,
消息丢失不会有。
心跳检测要及时,
故障转移要自动。
监控告警不能忘,
问题发现早知道。
容量规划要合理,
性能压测要做好。
运维流程要规范,
系统稳定才是宝!
愿你的MQ系统坚如磐石,永不宕机! 🏰✨