强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 6 章:集群管理

第 6 章:集群管理

掌握 rqlite 集群的节点管理、Leader 选举、数据同步和成员变更操作。


6.1 集群基础

6.1.1 集群概念

rqlite 集群由多个节点(Node)组成,通过 Raft 协议实现数据复制。每个集群有且仅有一个 Leader 节点负责处理写请求。

┌─────────────────────────────────────────────────┐
│                rqlite 集群                        │
│                                                  │
│   ┌─────────┐  ┌─────────┐  ┌─────────┐         │
│   │ Node 1  │  │ Node 2  │  │ Node 3  │         │
│   │ Leader  │  │Follower │  │Follower │         │
│   │         │  │         │  │         │         │
│   │ HTTP:4001│  │ HTTP:4011│  │ HTTP:4021│        │
│   │ Raft:4002│  │ Raft:4012│  │ Raft:4022│       │
│   └────┬────┘  └────┬────┘  └────┬────┘         │
│        │            │            │               │
│        └────────────┼────────────┘               │
│           Raft 通信(心跳 + 日志复制)              │
└─────────────────────────────────────────────────┘

6.1.2 节点角色

角色写请求读请求(strong)读请求(weak)读请求(none)
Leader
Follower❌ 转发❌ 重定向❌ 重定向
Candidate

6.2 节点加入与移除

6.2.1 添加节点到集群

方式一:启动时通过 -join 参数加入

# 新节点启动时直接加入集群
rqlited -node-id=node4 \
    -http-addr=0.0.0.0:4031 \
    -raft-addr=0.0.0.0:4032 \
    -join=http://127.0.0.1:4001 \
    /tmp/rqlite/node4

方式二:运行时通过 API 加入

# 确保新节点已独立启动
rqlited -node-id=node4 \
    -http-addr=0.0.0.0:4031 \
    -raft-addr=0.0.0.0:4032 \
    -disco-mode=off \
    /tmp/rqlite/node4 &

# 通过 Leader 的 HTTP API 将新节点加入集群
curl -XPOST 'localhost:4001/join' \
    -H 'Content-Type: application/json' \
    -d '{"id": "node4", "address": "127.0.0.1:4032"}'

注意: /join 请求中的 address 是 Raft 地址,不是 HTTP 地址。

6.2.2 从集群移除节点

# 移除指定节点
curl -XPOST 'localhost:4001/remove' \
    -H 'Content-Type: application/json' \
    -d '{"id": "node4"}'

移除节点后的操作:

  1. 该节点的 Raft 成员资格被撤销
  2. 该节点收到通知后自动关闭
  3. 如需重新加入,需要清除数据目录后重新启动

6.2.3 查看集群节点

# 查看所有节点信息
curl -s 'localhost:4001/nodes?pretty' | python3 -m json.tool

输出示例:

{
    "nodes": [
        {
            "id": "node1",
            "api_addr": "http://127.0.0.1:4001",
            "addr": "127.0.0.1:4002",
            "voter": true,
            "reachable": true,
            "leader": true
        },
        {
            "id": "node2",
            "api_addr": "http://127.0.0.1:4011",
            "addr": "127.0.0.1:4012",
            "voter": true,
            "reachable": true,
            "leader": false
        },
        {
            "id": "node3",
            "api_addr": "http://127.0.0.1:4021",
            "addr": "127.0.0.1:4022",
            "voter": true,
            "reachable": true,
            "leader": false
        }
    ]
}

6.3 Leader 选举

6.3.1 选举过程

当集群启动或当前 Leader 宕机时,Raft 协议会触发 Leader 选举:

时间线 ─────────────────────────────────────────────►

1. 集群启动
   Node1 ─────────── 成为 Leader(任期 1)
   Node2 ─────────── 成为 Follower
   Node3 ─────────── 成为 Follower

2. Node1 宕机
   Node1 ──────── ✕ (故障)
   Node2 ─────────── 选举超时 → 成为 Candidate → 请求投票
   Node3 ─────────── 收到投票请求 → 投票给 Node2
   Node2 ─────────── 获得多数票 → 成为 Leader(任期 2)

3. Node1 恢复
   Node1 ─────────── 重新启动 → 发现更高任期 → 成为 Follower

6.3.2 选举配置

# 查看当前 Raft 配置
curl -s 'localhost:4001/status?pretty' | python3 -c "
import json, sys
data = json.load(sys.stdin)
raft = data.get('store', {}).get('raft', {})
for key, val in raft.items():
    print(f'{key}: {val}')
"

6.3.3 手动触发 Leader 转移

# 在当前 Leader 上触发转移
curl -XPOST 'localhost:4001/raft/transfer-leadership'

场景: 需要对 Leader 节点进行维护时,先转移 Leader 再停机。

6.3.4 防止脑裂

rqlite 通过以下机制防止脑裂(Split-Brain):

机制说明
Quorum 多数派写入必须获得多数节点确认
Leader 租约Leader 在租约期内独占写入权
任期递增新 Leader 的任期号必须更高
奇数节点推荐使用奇数节点,避免对等分裂

6.4 数据同步

6.4.1 同步机制

写入请求 → Leader → Raft 日志 → 复制到 Follower → 多数确认 → 提交 → 应用到 SQLite
                                    │
                                    ▼
                              Follower 落后时
                                    │
                              ┌─────▼──────┐
                              │  日志追赶    │
                              │ (Catch-Up)  │
                              └─────┬──────┘
                                    │
                                    ▼
                              如果日志已被截断
                                    │
                              ┌─────▼──────┐
                              │  快照传输    │
                              │ (Snapshot)  │
                              └────────────┘

6.4.2 新节点数据同步

当新节点加入集群时:

  1. Leader 将最新快照(Snapshot)发送给新节点
  2. 新节点应用快照数据到本地 SQLite
  3. Leader 将快照之后的日志条目发送给新节点
  4. 新节点追上后进入正常同步状态
# 查看同步状态
curl -s 'localhost:4001/status?pretty' | python3 -c "
import json, sys
data = json.load(sys.stdin)
store = data.get('store', {})
print(f'Raft State: {store.get(\"raft_state\")}')
print(f'Last Log Index: {store.get(\"last_log_index\")}')
print(f'Last Log Term: {store.get(\"last_log_term\")}')
print(f'Applied Index: {store.get(\"applied_index\")}')
print(f'Commit Index: {store.get(\"commit_index\")}')
"

6.4.3 处理网络分区

当网络分区发生时:

┌──────────────────────┐  ┌──────────────────────┐
│   分区 A (多数派)      │  │   分区 B (少数派)      │
│                      │  │                      │
│  ┌────────┐          │  │  ┌────────┐          │
│  │ Node 2 │ (Follower)│  │  │ Node 1 │ (Leader) │
│  └────────┘          │  │  └────────┘          │
│  ┌────────┐          │  │                      │
│  │ Node 3 │ (Follower)│  │                      │
│  └────────┘          │  │                      │
│                      │  │                      │
│  选举超时后:          │  │  无法获得多数确认      │
│  Node 2 成为新 Leader │  │  写入操作失败          │
└──────────────────────┘  └──────────────────────┘

关键规则: 拥有多数节点的分区会继续正常工作。少数派分区中的 Leader 会退位,写入操作失败但不会产生数据不一致。


6.5 集群运维操作

6.5.1 滚动升级

升级集群时,逐个替换节点:

# 步骤 1: 升级 Follower 节点
# 停止 node3
systemctl stop rqlited

# 替换二进制
sudo mv /tmp/rqlite-new /usr/local/bin/rqlited

# 重启 node3
systemctl start rqlited

# 确认 node3 恢复正常
curl -s 'localhost:4021/status?pretty' | grep raft_state

# 步骤 2: 升级 node2(同上)
# 步骤 3: 转移 Leader 到已升级节点,再升级原 Leader
curl -XPOST 'localhost:4001/raft/transfer-leadership'
# 等待转移完成
systemctl stop rqlited
sudo mv /tmp/rqlite-new /usr/local/bin/rqlited
systemctl start rqlited

6.5.2 故障节点恢复

# 如果 node3 数据损坏
# 方法 1: 清除数据目录,重新加入
rm -rf /tmp/rqlite/node3/*
rqlited -node-id=node3 \
    -http-addr=0.0.0.0:4021 \
    -raft-addr=0.0.0.0:4022 \
    -join=http://127.0.0.1:4001 \
    /tmp/rqlite/node3

# 方法 2: 从备份恢复后加入
# 先从备份加载数据,再加入集群

6.5.3 节点替换(更换节点 ID)

# 1. 从集群移除旧节点
curl -XPOST 'localhost:4001/remove' \
    -H 'Content-Type: application/json' \
    -d '{"id": "node3"}'

# 2. 清除旧节点数据
rm -rf /tmp/rqlite/node3/*

# 3. 使用新节点 ID 加入
rqlited -node-id=node3-new \
    -http-addr=0.0.0.0:4021 \
    -raft-addr=0.0.0.0:4022 \
    -join=http://127.0.0.1:4001 \
    /tmp/rqlite/node3

6.6 集群健康检查脚本

#!/bin/bash
# cluster-health.sh — rqlite 集群健康检查
NODES=("localhost:4001" "localhost:4011" "localhost:4021")

echo "=== rqlite 集群健康检查 ==="
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""

for node in "${NODES[@]}"; do
    echo "--- 节点: $node ---"
    
    # 状态检查
    status=$(curl -s -o /dev/null -w "%{http_code}" "http://$node/status" --connect-timeout 3)
    if [ "$status" = "200" ]; then
        echo "  状态: ✅ 在线"
        
        # 获取 Raft 状态
        raft_state=$(curl -s "http://$node/status" | python3 -c "
import json, sys
data = json.load(sys.stdin)
store = data.get('store', {})
print(store.get('raft_state', 'unknown'))
" 2>/dev/null)
        echo "  Raft 状态: $raft_state"
    else
        echo "  状态: ❌ 离线 (HTTP $status)"
    fi
    echo ""
done

# 检查 Leader
echo "--- Leader 检查 ---"
leader=$(curl -s "http://${NODES[0]}/nodes" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for node in data.get('nodes', []):
    if node.get('leader'):
        print(f\"  Leader: {node['id']} ({node['api_addr']})\")
        break
else:
    print('  Leader: ⚠️ 未找到 Leader')
" 2>/dev/null)
echo "$leader"

6.7 业务场景:电商平台高可用方案

                 ┌─────────────────┐
                 │   负载均衡器      │
                 │   (Nginx/HAProxy)│
                 └────────┬────────┘
                          │
         ┌────────────────┼────────────────┐
         │                │                │
    ┌────▼────┐     ┌────▼────┐     ┌────▼────┐
    │ Node 1  │     │ Node 2  │     │ Node 3  │
    │ Beijing │     │Shanghai │     │ Guangzhou│
    │ (Leader)│     │Follower │     │Follower │
    └─────────┘     └─────────┘     └─────────┘
    
    数据: 订单、库存、用户地址
    写入: 全部路由到 Leader
    读取: 可从 Follower 读(none/weak 级别)

关键配置:

# Node 1 (Beijing — Leader)
rqlited -node-id=beijing \
    -http-addr=0.0.0.0:4001 \
    -raft-addr=0.0.0.0:4002 \
    -disco-mode=off \
    /var/lib/rqlite/data

# Node 2 (Shanghai — Follower)
rqlited -node-id=shanghai \
    -http-addr=0.0.0.0:4001 \
    -raft-addr=0.0.0.0:4002 \
    -join=http://beijing-node:4001 \
    /var/lib/rqlite/data

# Node 3 (Guangzhou — Follower)
rqlited -node-id=guangzhou \
    -http-addr=0.0.0.0:4001 \
    -raft-addr=0.0.0.0:4002 \
    -join=http://beijing-node:4001 \
    /var/lib/rqlite/data

读写策略:

操作类型路由目标一致性级别
创建订单Leader强一致性
扣减库存Leader强一致性
查询商品列表任意 Followernone
查询订单详情Leaderstrong
查询统计数据任意 Followernone

6.8 集群运维速查表

操作命令
查看节点列表curl localhost:4001/nodes?pretty
查看集群状态curl localhost:4001/status?pretty
添加节点curl -XPOST localhost:4001/join -d '{"id":"n4","addr":"x:4002"}'
移除节点curl -XPOST localhost:4001/remove -d '{"id":"n4"}'
转移 Leadercurl -XPOST localhost:4001/raft/transfer-leadership
健康检查curl localhost:4001/status/ready
Leader 检查curl localhost:4001/status/leader

6.9 本章小结

要点内容
集群规模推荐 3 或 5 节点(奇数)
Leader 选举Raft 自动选举,支持手动转移
数据同步日志复制 + 快照传输
网络分区多数派分区继续工作,少数派停止写入
节点管理API 方式动态加入/移除节点
滚动升级逐节点升级,先升级 Follower 再升级 Leader

上一章:第 5 章:HTTP API 详解 下一章:第 7 章:备份与恢复