高可用与分布式部署指南
RustBill 从设计之初就面向水平扩展和高可用部署。本文档涵盖从单机到 Citus 分布式集群的完整演进路径。
架构总览
DNS Round-Robin / VIP
│
┌──────────────────┼──────────────────┐
│ │ │
Caddy-1 Caddy-2 Caddy-N
(HTTP/3+TLS) (HTTP/3+TLS) (HTTP/3+TLS)
│ │ │
└──────┬───────────┴──────────┬───────┘
│ health-check │
│ least_conn LB │
│ │
┌──────┴──────────────────────┴───────┐
│ rustbill-server × N │
│ (stateless, tonic gRPC :50051) │
└──────┬──────────────────────┬───────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Redis │ │ OTLP │
│ (optional) │ │ Collector │
└─────────────┘ └─────────────┘
│
┌──────┴──────────────────────────────┐
│ Citus Coordinator │
│ (PG protocol routing, metadata) │
└──────┬───────────────────┬──────────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Worker-1 │ │ Worker-2 │
│(shard 1-32) │ │(shard 33-64)│
│ + replica │ │ + replica │
└─────────────┘ └─────────────┘
│ │
┌──────┴───────────────────┴──────┐
│ etcd cluster × 3 │
│ (Patroni distributed │
│ consensus for failover) │
└─────────────────────────────────┘
### 核心设计原则
| 原则 | 实现方式 |
|------|---------|
| **无状态应用层** | rustbill-server 不存本地状态,Session 入 Redis/DB |
| **PG 做共识** | 领导选举和事件外发箱全部基于 PostgreSQL 实现,无额外依赖 |
| **优雅降级** | Redis 不可用时自动回退到 PG;Rate Limit DB 模式 |
| **故障自愈** | 熔断器自动切断故障下游,恢复后半开探测 |
| **可观测性** | Request ID 全链路追踪 + OTLP 导出 + 健康检查端点 |
---
## 1. 多实例水平扩展
### 1.1 应用层无状态
rustbill-server 每个实例完全无状态:
- **Session** — Redis cache-aside 优先,miss 回源 `sessions` 表。Redis 不可用时直接查 DB。
- **JWT** — 自包含 token,任意实例可独立验证。
- **插件** — Rune 脚本源码存储于 DB `plugin_interfaces.script_source`。`ScriptEngine` 内存缓存,热重载自动同步。
- **后台任务** — 通过领导选举确保单实例执行(见第 3 节)。
所有实例共享同一份 `config.toml`,指向同一个 DB 和 Redis。无状态使得扩缩容只需增减 Caddy upstream 列表。
### 1.2 Caddy 反向代理
```caddyfile
# /etc/caddy/Caddyfile
rustbill.example.com {
# HTTP/3 QUIC
protocols h1 h2 h3
# 上游 gRPC 服务
reverse_proxy /rustbill.* {
to localhost:50051 localhost:50052
health_uri /health
health_interval 5s
lb_policy least_conn
# gRPC 需要 HTTP/2 传输
transport http {
versions h2c
}
}
# Admin 静态资源(内嵌于 server)
reverse_proxy /admin* {
to localhost:50051 localhost:50052
lb_policy least_conn
}
# 日志
log {
output file /var/log/caddy/rustbill.log
format json
}
}
关键配置项:
| 配置 | 推荐值 | 说明 |
|---|---|---|
health_uri | /health | 短路端点,仅返回 200,不做任何 DB 查询 |
health_interval | 5s | 健康检查频率。过短增加负载,过长延迟故障检测 |
lb_policy | least_conn | 最少连接数算法,适合 gRPC 长连接场景 |
transport http versions | h2c | gRPC 要求 HTTP/2 传输层 |
1.3 并发控制
[server]
max_concurrency = 30 # 每实例并发上限
每个 rustbill-server 实例在 Tower 中间件栈入口处设置 ConcurrencyLimitLayer。超过上限的请求进入 FIFO 队列等待(背压),而非直接拒绝。集群总并发容量 = N × max_concurrency。
1.4 速率限制
[rate_limit]
enabled = true
login_per_sec = 5 # 登录 5 次/秒/IP
register_per_sec = 1 # 注册 1 次/秒/IP
双后端模式:
| 后端 | 配置 | 适用场景 |
|---|---|---|
| 内存 | [redis] enabled = false | 单实例或允许独立计数 |
| Redis | [redis] enabled = true | 多实例共享计数,全局精确限流 |
Redis 不可用时自动降级到内存(per-instance 计数,非精确)。
2. 实例标识与心跳
2.1 InstanceIdentity
#![allow(unused)]
fn main() {
// 启动时生成 UUID v7(时间有序)
pub struct InstanceIdentity {
pub id: Uuid, // UUID v7
}
}
每个 rustbill-server 实例启动时生成一个 UUID v7,注册到 instance_heartbeat 表:
CREATE TABLE instance_heartbeat (
instance_id UUID PRIMARY KEY,
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
heartbeat_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
pool_idle INTEGER NOT NULL DEFAULT 0, -- 空闲连接数
pool_active INTEGER NOT NULL DEFAULT 0, -- 活跃连接数
pool_max INTEGER NOT NULL DEFAULT 0 -- 最大连接数
);
UUID v7 采用时间有序编码,便于日志排序和调试。
2.2 心跳机制
每 10 秒更新 heartbeat_at + 连接池快照(pool_idle / pool_active):
启动 → InstanceIdentity::new(注册)
→ spawn InstanceHeartbeat (interval 10s)
→ 更新 heartbeat_at + pool_{idle,active}
心跳数据被领导选举用于负载感知调度(见第 3 节)。心跳超时 30 秒的实例被视为失联,其持有的 leader 角色自动释放。
3. 领导选举
3.1 基于 PostgreSQL 表锁
RustBill 不引入 ZooKeeper/etcd 等外部共识组件,直接利用 PostgreSQL 的行锁实现领导选举。
#![allow(unused)]
fn main() {
pub struct LeaderElection {
pool: PgPool,
instance_id: Uuid,
held: Arc<RwLock<HashSet<String>>>, // 当前持有的角色
}
}
选举 SQL(try_acquire):
UPDATE leader_role
SET holder = $instance_id, heartbeat_at = NOW(), ttl_secs = 30
WHERE role = $role
AND heartbeat_at < NOW() - make_interval(secs => COALESCE(
(SELECT ttl_secs FROM leader_role WHERE role = $role), 30))
AND holder IN (
SELECT instance_id FROM instance_heartbeat
WHERE heartbeat_at > NOW() - INTERVAL '30 seconds'
ORDER BY pool_idle DESC, instance_id ASC LIMIT 1
)
关键机制:
FOR UPDATE隐式行锁 —UPDATE在 PG 中自动获取行级排他锁,多实例并发执行时只有一个成功。- 心跳过期 — TTL 30 秒。leader 不续期时租约自动过期,其他实例可抢占。
- 存活检查 — 仅心跳正常的实例(
instance_heartbeat.heartbeat_at > NOW() - 30s)可参与选举。 - 负载感知 —
ORDER BY pool_idle DESC, instance_id ASC优先选举空闲连接最多的实例。
3.2 三种 Leader 角色
| 角色 | 职责 | 由谁持有 |
|---|---|---|
migration | 数据库迁移执行 | 启动时最先选举,仅 leader 执行 sqlx::migrate! |
billing | 定时账单生成(月末批量) | 仅 leader 运行 billing cron job |
event_worker | 事件队列消费、支付超时扫描 | 仅 leader 运行 event worker loop |
三种角色独立选举,可分布在不同实例上,避免单点瓶颈。
3.3 续期与释放
#![allow(unused)]
fn main() {
// 每 10 秒续期,保持租约活跃
leader_election.spawn_renewal("event_worker".to_string(), 10);
// 实例关闭时自动释放
impl Drop for LeaderElection {
// release all held roles
}
}
续期后台任务每 10 秒更新 heartbeat_at = NOW()。若 leader 崩溃,TTL 30 秒后自动释放,follower 接管。
3.4 启动流程
create_pool → InstanceIdentity::new(注册) → LeaderElection(migration)
├── leader: run_migrations + spawn_renewal("migration")
└── follower: poll leader_role 等待 leader(最多 60s)
→ spawn InstanceHeartbeat(10s)
→ 正常启动 gRPC server
→ spawn PluginScanner
→ spawn event_worker(竞争 leader 后才开始消费)
→ spawn 其他后台任务
Follower 在迁移完成前阻塞,确保不会在旧 schema 上运行。
4. 事务外发箱与事件 Worker
4.1 事件队列表
CREATE TABLE event_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type TEXT NOT NULL, -- order_paid / order_provisioned / order_cancelled
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending/processing/done/failed
order_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
业务操作在同一数据库事务中写入业务数据 + event_queue 记录,保证原子性。典型流程:
BEGIN;
-- 余额扣减
UPDATE customers SET balance = balance - $amount;
-- 创建支付记录
INSERT INTO payments ...;
-- 更新订单状态
UPDATE orders SET status = 'provisioning';
-- 插入事件
INSERT INTO event_queue (event_type, payload, order_id)
VALUES ('order_paid', $payload, $order_id);
COMMIT;
4.2 Worker 消费
#![allow(unused)]
fn main() {
// 每 5 秒轮询
loop {
// 1. 检查是否仍是 event_worker leader
if !leader_election.is_leader("event_worker").await { break; }
// 2. 获取 pending 事件(并发安全)
let events = sqlx::query(
"SELECT id, event_type, payload, order_id
FROM event_queue
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT 10
FOR UPDATE SKIP LOCKED"
).fetch_all(&pool).await?;
// 3. 按 order_id 获取 advisory lock → 串行化同订单事件
for event in events {
let key = uuid_to_advisory_key(&event.order_id);
sqlx::query("SELECT pg_try_advisory_xact_lock($1)")
.bind(key).fetch_one(&pool).await?;
// dispatch: provision → notify
}
}
}
并发安全保证:
FOR UPDATE SKIP LOCKED— 多个 worker 不会重复消费同一事件pg_try_advisory_xact_lock(hash(order_id))— 同订单事件串行执行,跨订单并发执行- 事务释放时自动释放 advisory lock,无需显式解锁
4.3 事件类型与处理
| 事件 | 触发时机 | 处理逻辑 | 成功后 |
|---|---|---|---|
order_paid | 支付完成 | 调用 provider 开通实例 | 写入 order_provisioned 事件 |
order_provisioned | 开通成功 | 广播通知(客户 + 管理员) | 标记 done |
order_cancelled | 订单取消 | 退款 + 广播通知 | 标记 done |
开通失败时自动退款 + 写入 order_cancelled 事件触发通知(Saga 补偿)。
4.4 支付超时扫描
Worker 同时扫描 24 小时未支付的订单:
UPDATE orders SET status = 'cancelled'
WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '24 hours'
RETURNING id
取消的订单同样写入 order_cancelled 事件,走退款+通知流程。
5. 熔断器
5.1 三段状态机
┌──────────┐ 5 次失败 ┌──────────┐
│ Closed │ ──────────────→ │ Open │
│ (正常) │ │ (熔断) │
└──────────┘ └─────┬─────┘
↑ │
│ 30s 冷却后 │
│ ┌──────────────────┐ │
└────┤ HalfOpen │←─────┘
│ (探测) │
└────┬─────────────┘
│
成功 ───┴─── 失败 → Open
- Closed — 正常状态,失败计数器递增。累计 5 次失败 → Open。
- Open — 熔断状态,直接拒绝请求(抛出
CircuitOpen错误),无需等待超时。 - HalfOpen — 30 秒冷却后进入,允许 1 次探测请求。成功 → Closed;失败 → Open。
5.2 按 host 粒度的熔断器管理
#![allow(unused)]
fn main() {
pub struct BreakerManager {
breakers: RwLock<HashMap<String, Arc<CircuitBreaker>>>,
}
}
熔断器按下游 host 隔离,一个支付网关故障不影响其他网关。用于所有出站 HTTP 调用:
- 支付网关(易支付、自定义网关)
- 上游供应商(RustBill 上游实例)
- 通知渠道(Webhook)
5.3 CoreHttpClient
#![allow(unused)]
fn main() {
pub struct CoreHttpClient {
client: reqwest::Client,
cert_clients: RwLock<HashMap<String, reqwest::Client>>, // mTLS client 缓存
breakers: BreakerManager,
}
}
统一出站 HTTP 客户端,提供:
- 3 次重试 — 指数退避,透明重试网络瞬时错误
- 熔断检查 — 请求前检查 host 熔断器状态,Open 时快速失败
- mTLS 证书缓存 — 按 cert hash 缓存
reqwest::Client实例,避免每次重建 TLS 握手
6. Redis 缓存层(可选)
Redis 是可选组件。不配置时全部功能降级运行,零阻塞。
6.1 功能矩阵
| 功能 | Redis 可用 | Redis 不可用(降级) |
|---|---|---|
| Session 缓存 | cache-aside(快) | 纯 DB 查询(慢,但可用) |
| 分布式锁 | Redis SETNX | PG advisory lock |
| 跨实例限流 | Redis 全局计数 | 内存 per-instance 计数(近似) |
6.2 配置
[redis]
enabled = true
url = "redis://localhost:6379"
max_connections = 10
prefix = "rustbill"
prefix 用于多环境隔离(如 rustbill-prod / rustbill-staging 共享同一 Redis)。
6.3 降级策略
Session 读取:
Redis GET → hit → 返回
→ miss → DB 查询 → Redis SET (异步, 忽略失败) → 返回
Rate Limit:
Redis INCR → 成功 → 全局精确
→ 失败 → 内存计数器 (per-instance)
分布式锁:
Redis SETNX → 成功 → 获取锁
→ 失败 → PG pg_try_advisory_lock
所有 Redis 操作失败后自动降级,不阻塞业务。恢复后自动切回。
7. Citus 分布式数据库
当单 PostgreSQL 实例无法承载数据量或并发时,迁移到 Citus。
7.1 分片策略
┌──────────────────┐
│ Coordinator │
│ (metadata only) │
└──┬────────────┬──┘
│ │
┌────────┴──┐ ┌────┴──────────┐
│ Worker-1 │ │ Worker-2 │
│ shard 1-32│ │ shard 33-64 │
│ (PG 16) │ │ (PG 16) │
│ + replica │ │ + replica │
└───────────┘ └───────────────┘
分片键:customer_id。按 customer_id 哈希分片,每个客户的所有关联数据落在同一 Worker。
7.2 分布式表(Distributed Tables)
按 customer_id 分片的表:
| 表名 | 分片键 | 说明 |
|---|---|---|
customers | customer_id (id) | 客户主体 |
customer_users | customer_id | 客户子账户 |
orders | customer_id | 订单 |
invoices | customer_id | 账单 |
invoice_items | customer_id | 账单明细 |
payments | customer_id | 支付记录 |
instances | customer_id | 云实例 |
balance_transactions | customer_id | 余额变动 |
tickets | customer_id | 工单 |
ticket_replies | customer_id | 工单回复 |
同客户数据共置:一个客户的所有订单、支付、实例、工单存于同一 Worker。跨 Worker Join 几乎不发生。
7.3 参考表(Reference Tables)
复制到所有 Worker 的表(全量同步,高频读取):
| 表名 | 说明 |
|---|---|
admin_users | 管理员账户 |
sessions | Session 记录 |
products | 商品定义 |
product_categories | 商品分类 |
product_groups | 商品分组 |
plugin_interfaces | 插件接口实例 |
plugins | 插件定义 |
instance_heartbeat | 实例心跳 |
leader_role | 领导选举 |
event_queue | 事件队列 |
api_keys | API 密钥 |
7.4 迁移 004 前置条件
004_citus_prepare.sql 确保所有涉及 customer_id 的列均为 NOT NULL,这是 Citus 创建分布式表的前提。此迁移与单节点 PG 完全兼容。
7.5 初始化集群
-- 在 Coordinator 上执行
-- 添加 Worker 节点
SELECT citus_add_node('worker-1.internal', 5432);
SELECT citus_add_node('worker-2.internal', 5432);
-- 创建分布式表(示例)
SELECT create_distributed_table('customers', 'id');
SELECT create_distributed_table('orders', 'customer_id');
SELECT create_distributed_table('payments', 'customer_id');
SELECT create_distributed_table('instances', 'customer_id');
-- ... 其余分布式表
-- 创建参考表
SELECT create_reference_table('admin_users');
SELECT create_reference_table('products');
SELECT create_reference_table('plugin_interfaces');
-- ... 其余参考表
推荐分片数:64(2 Worker 各 32,后续可水平扩容)。
7.6 在线扩容
-- 添加新 Worker
SELECT citus_add_node('worker-3.internal', 5432);
-- 开始数据重分布(零停机)
SELECT citus_rebalance_start();
-- 监控进度
SELECT * FROM citus_rebalance_status();
Citus 在后台迁移分片,读写正常进行。完成后分片自动重新分布到全部 Worker。
8. Worker 高可用 — Patroni + etcd
每个 Citus Worker 独立配置 PG 流复制 + Patroni 自动故障切换。
8.1 架构
┌─────────────────────────────────┐
│ etcd cluster │
│ (3 nodes, distributed concensus) │
└──┬─────────────┬─────────────┬──┘
│ │ │
┌───────┴──────┐ ┌────┴──────┐ ┌────┴──────┐
│ Worker-1 │ │ Worker-2 │ │ Worker-3 │
│ ┌───────────┐ │ │ │ │ │
│ │ Patroni │ │ │ Patroni │ │ Patroni │
│ │ (leader) │ │ │ (replica) │ │ (replica) │
│ └─────┬─────┘ │ │ │ │ │
│ │ │ │ │ │ │
│ ┌────┴────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │ PG 16 │ │ │ │ PG 16 │ │ │ │ PG 16 │ │
│ │ (master) │──┼─┼→│(standby)│ │ │ │(standby)│ │
│ └─────────┘ │ │ └────────┘ │ │ └────────┘ │
└───────────────┘ └────────────┘ └────────────┘
streaming streaming streaming
replication replication replication
8.2 Patroni 配置
# /etc/patroni/patroni.yml
scope: rustbill-worker1
name: worker1-node1
restapi:
listen: 0.0.0.0:8008
connect_address: worker1-node1.internal:8008
etcd:
hosts: etcd1.internal:2379,etcd2.internal:2379,etcd3.internal:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1MB
postgresql:
use_pg_rewind: true
parameters:
max_connections: 200
shared_buffers: 8GB
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
wal_keep_size: 1GB
postgresql:
listen: 0.0.0.0:5432
connect_address: worker1-node1.internal:5432
data_dir: /var/lib/postgresql/16/main
pg_hba:
- host replication replicator 0.0.0.0/0 md5
- host all all 0.0.0.0/0 md5
8.3 故障切换流程
1. Patroni 检测 leader 心跳丢失(etcd TTL 30s)
2. 选举新 leader(etcd CAS 操作,只有一个 replica 成功)
3. 新 leader 提升为 master(pg_ctl promote)
4. 其他 replica 切换到新 master(rewind + follow)
5. Coordinator 自动感知(连接池重连到新 master VIP)
切换时间: 通常 30-60 秒。
8.4 etcd 集群
# 三节点 etcd 集群部署
# etcd1
etcd --name etcd1 \
--initial-cluster etcd1=http://etcd1.internal:2380,etcd2=http://etcd2.internal:2380,etcd3=http://etcd3.internal:2380 \
--initial-cluster-state new
# etcd2, etcd3 同理
etcd 集群容忍 1 节点故障(3 节点 majority = 2)。
9. 监控与可观测性
9.1 健康检查端点
| 端点 | 用途 | 行为 |
|---|---|---|
GET /health | Caddy 健康检查 | 立即返回 200,短路所有中间件 |
GET /ready | 就绪检查 | 检查 DB 连接池连通性 |
# Caddy 使用 /health
curl -s http://localhost:50051/health
# → 200 OK
# k8s readiness probe 使用 /ready
curl -s http://localhost:50051/ready
# → 200 OK 或 503(DB 不通)
9.2 请求追踪
每个请求携带:
X-Request-Id— UUID v7,由服务端生成traceparent— W3C Trace Context 标准头,格式00-{trace_id}-{span_id}-01
# 示例响应头
X-Request-Id: 018f6a3c-8b7e-7d01-9a2c-c3f1e4b8d5a2
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
9.3 OpenTelemetry
[telemetry]
enabled = true
otlp_endpoint = "http://localhost:4317"
service_name = "rustbill"
导出到 OTLP Collector(Jaeger / Grafana Tempo),包含:
- gRPC 请求 span(method、status code、duration)
- DB 查询 span(sqlx 自动注入)
- 出站 HTTP span(CoreHttpClient 自动注入)
- 事件处理 span
9.4 systemd 健康检查定时器
# /etc/systemd/system/rustbill-healthcheck.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/grpcurl -plaintext localhost:50051 rustbill.identity.IdentityService/GetMe
User=rustbill
# /etc/systemd/system/rustbill-healthcheck.timer
[Timer]
OnCalendar=*:0/5 # 每 5 分钟
Persistent=true
[Install]
WantedBy=timers.target
9.5 数据库监控查询
-- 活跃 Worker 节点
SELECT node_name, node_port, is_active, should_haveshards
FROM citus_get_active_worker_nodes();
-- 分片分布
SELECT shard_count, nodename, nodeport
FROM citus_shards
GROUP BY nodename, nodeport;
-- 租户数量(每个 shard 的行数)
SELECT shardid, shard_size, result::json->0->>'row_count' AS rows
FROM citus_shard_sizes()
JOIN LATERAL json_array_elements(shard_sizes::json) AS result ON true
WHERE table_name = 'customers'::regclass;
-- 跨分片查询监控(慢查询诊断)
SELECT queryid, query, calls, mean_exec_time
FROM citus_stat_statements
WHERE calls > 0
ORDER BY mean_exec_time DESC
LIMIT 10;
9.6 资源监控
# 服务状态(内存/CPU)
systemctl status rustbill-server
# 连接数
ss -tnp | grep :50051 | wc -l
# 数据库连接
psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'rustbill'"
10. 故障恢复
10.1 故障场景与响应
| 场景 | 影响 | 自动恢复 | 手动操作 |
|---|---|---|---|
| 单个 server 实例宕机 | 无(Caddy 自动摘除) | Caddy health check 检测后摘除 | 重启故障实例 |
| 所有 server 实例宕机 | 服务完全中断 | 无 | 逐台重启 |
| Worker 宕机 + 有 replica | 无(Patroni 自动切换) | 30-60s 自动 failover | 修复故障节点后重新加入 |
| Worker 宕机 + 无 replica | 该 Worker 的分片不可用 | 无 | 从备份恢复 |
| Coordinator 宕机 | 所有查询中断 | 无(需手动切换) | DNS/VIP 切换到备用 Coordinator |
| Redis 宕机 | 性能下降(无中断) | 自动降级到 PG | 恢复 Redis |
| etcd 单节点宕机 | 无(集群仍可工作) | 集群自愈 | 恢复故障节点 |
| etcd 多节点宕机 | Patroni 无法选举 | 无 | 恢复 etcd 集群 |
| 下游 API 熔断 | 该下游请求快速失败 | 30s 后半开探测,成功后恢复 | 修复下游后等待自动恢复 |
10.2 Worker 故障恢复步骤
# 1. 确认 Worker 状态
psql -h coordinator -c "SELECT * FROM citus_get_active_worker_nodes();"
# 2. 如果 Worker 不可恢复(数据丢失)
psql -h coordinator <<SQL
SELECT citus_remove_node('broken-worker.internal', 5432);
SELECT citus_add_node('new-worker.internal', 5432);
SELECT citus_rebalance_start();
SQL
# 3. 监控重分布
psql -h coordinator -c "SELECT * FROM citus_rebalance_status();"
10.3 Coordinator 切换
# 方案 A: DNS 切换(推荐)
# 将 DNS A 记录从 coordinator-1 指向 coordinator-2
# TTL 设置 60s 以加速切换
# 方案 B: VIP 漂移
# keepalived 或类似工具将 VIP 从 coordinator-1 迁移到 coordinator-2
# 切换后重启所有 rustbill-server 实例(或等待连接池自动重连)
sudo systemctl restart rustbill-server
10.4 从 Citus 回退到单机 PG
# 从 Coordinator 导出全量数据
pg_dump -h coordinator.internal -U rustbill -d rustbill \
--no-owner --no-acl -F c -f rustbill_full.dump
# 导入到单机 PG
pg_restore -h single-pg.internal -U rustbill -d rustbill \
--no-owner --no-acl -j 4 rustbill_full.dump
# 更新 config.toml 的 DB URL
# 重启所有 rustbill-server 实例
11. 性能基准
测试环境:3 个 Citus Worker(8C32G),64 分片,100 万订单,50 个客户。
| 操作 | 单机 PG | Citus 3 Worker | 说明 |
|---|---|---|---|
| 客户查询自己的订单 | 8ms | 9ms | 单 shard 查询,性能接近 |
| 管理员查询全部订单 | 45ms | 62ms | 跨 shard 聚合,略有增加 |
| 创建订单 | 4ms | 5ms | 单 shard 写入 |
| 支付回调(余额扣减+建支付+更新订单) | 3ms | 3ms | 同 shard 事务 |
| Dashboard COUNT(6 表聚合) | 120ms | 180ms | 跨 shard COUNT 开销 |
| 订单列表分页(100 条/页) | 12ms | 15ms | 推送到 Coordinator 排序 |
结论: 单客户操作延迟几乎不变(+1ms)。跨客户聚合查询因 Coordinator 汇总而产生额外延迟(+30-60%),但仍在可接受范围内。
12. 部署检查清单
应用层
- Caddy 配置
health_uri /health和health_interval 5s - Caddy
lb_policy least_conn -
[jwt].secret所有实例一致 -
[db].url指向同一个 Citus Coordinator 或 PG 实例 -
[redis].enabled按需配置(多实例建议启用) -
[telemetry].enabled按需开启 OTLP
数据库层
- Citus Coordinator 已添加所有 Worker 节点
- 分布式表
customer_id列全部NOT NULL - 参考表已
create_reference_table - 每个 Worker 配置了至少一个 streaming replica
- Patroni + etcd 集群正常运行
监控
-
/health端点可被 LB 访问 -
/ready端点配置为 k8s readiness probe - OTLP exporter 成功连接到 Collector
- systemd healthcheck timer 启用
- 告警规则配置(实例宕机、Worker 宕机、熔断器 Open)
备份
- Coordinator 每日 pg_dump
- Worker WAL 归档到远程存储
- 备份恢复流程已文档化并测试
13. 运维命令速查
# === 实例管理 ===
systemctl status rustbill-server # 实例状态
systemctl restart rustbill-server # 重启(优雅关闭 + 启动)
journalctl -u rustbill-server -f # 实时日志
# === Citus 集群 ===
psql -h coordinator -c "SELECT * FROM citus_get_active_worker_nodes();"
psql -h coordinator -c "SELECT count(*) FROM citus_shards;"
psql -h coordinator -c "SELECT * FROM citus_rebalance_status();"
# === Patroni ===
patronictl -c /etc/patroni/patroni.yml list # 集群状态
patronictl -c /etc/patroni/patroni.yml switchover # 手动切换
# === 健康检查 ===
curl -s http://localhost:50051/health
grpcurl -plaintext localhost:50051 list
# === 领导选举 ===
psql -c "SELECT role, holder, heartbeat_at, ttl_secs FROM leader_role;"
# === 熔断器状态 ===
# 通过 gRPC IntegrationService 或日志查看
journalctl -u rustbill-server | grep -i "circuit"
术语对照
| 术语 | 说明 |
|---|---|
| Coordinator | Citus 协调节点,接收查询并路由到 Worker |
| Worker | Citus 工作节点,存储实际数据分片 |
| Shard / 分片 | 数据分区单元,按 customer_id 哈希分配 |
| Reference Table / 参考表 | 复制到所有 Worker 的小表 |
| Distributed Table / 分布式表 | 按分片键分布到 Worker 的大表 |
| Patroni | PostgreSQL 高可用管理工具 |
| etcd | 分布式键值存储,用于 Patroni 共识 |
| TTL | 租约有效期(Time To Live) |
| Advisory Lock | PostgreSQL 应用层锁,用于并发控制 |
| Saga | 分布式事务补偿模式 |