上一篇 下一篇 分享链接 返回 返回顶部

跨境ERP系统如何在香港服务器的Debian系统中配置多站点数据库复制,解决跨境事务一致性?

发布人:Minchunlin 发布时间:2025-09-04 10:26 阅读量:781


上个星期五,财务对账单又一次不对:深圳仓的出库扣减比香港主库晚了 40 多秒,越南工厂的回执入账更是延迟到分钟级。跨境链路没出“大问题”,只是稳定的 35~80ms RTT,让“强一致”这三个字显得有点奢侈。我的目标很明确:用户下单体验不受影响(P95 < 120ms)、账务类交易强保证不丢不重、跨境复制在网络抖动下仍可自愈。

于是我把部署清单摊开:Debian 12(bookworm) 系统的 5 台香港服务器做核心(强一致域),再向深圳与胡志明市各拉一条逻辑复制通道。物理同步保证香港核心账务 RPO=0,跨境逻辑复制配合 Outbox 事件表 + 幂等去重 保证最终一致。

一、业务背景与技术目标

业务画像:跨境 ERP(订单、库存、支付、对账),多仓、多币种、跨时区(HKT、ICT、UTC-8)。

一致性目标:

  • 香港站内(主域)账务类事务:强一致(本地同步复制,RPO=0)。
  • 跨境站点:最终一致(秒级收敛,幂等保障,不重不丢)。
  • 性能目标:下单主路径 P95 < 120ms;账务写入 P95 < 200ms(含同步确认)。
  • 可用性目标:香港域任一节点宕机 RTO < 60s,跨境链路断开自动追赶。

二、拓扑设计(HK 强一致域 + 跨境最终一致域)

拓扑:
                [ 互联网 / APP / 网关 ]
                         │
                    [MySQL Router/读写路由]
                         │
                 ┌────────┴────────┐
                 │                     │
           【HK 强一致域】        【只读/异地域】
           Debian 12 + PG16         Debian 12 + PG16
           ┌──────────────┐          ┌───────────────┐
           │ Primary (HK1)│  WAL     │  Replica (SZ) │  逻辑订阅
           ├──────────────┤<========>├───────────────┤  (最终一致)
           │ Sync Standby │<====┐    │  Replica (HCM)│
           │    (HK2)     │     │    └───────────────┘
           ├──────────────┤     │
           │ Sync Standby │<====┘
           │    (HK3)     │
           └──────────────┘
           (HK1 对 HK2/HK3 同步物理复制;
             SZ/HCM 通过逻辑复制订阅关键表)

说明:本文示例用 PostgreSQL 16(主域强一致用同步物理复制;跨境站点用逻辑复制)。若你更熟 MySQL,可把“物理同步”替换为 InnoDB Group Replication(单主)、“逻辑复制”替换为 异步通道 + GTID,思路一致。

三、硬件与系统参数(我在香港机房的标配)

角色 型号 CPU 内存 磁盘 网卡 备注
HK1/HK2/HK3 2U 定制 AMD EPYC 7452(32C) 256GB DDR4 2×3.84TB NVMe U.2(ZFS Mirror) 2×10GbE 机架同 AZ 内 0.2~0.5ms RTT
SZ 1U Intel Xeon 4314(16C) 128GB 1×3.84TB NVMe 1×10GbE 至 HK RTT 35~45ms
HCM 1U AMD EPYC 7313P(16C) 128GB 1×3.84TB NVMe 1×10GbE 至 HK RTT 60~80ms

Debian 12,内核 6.1,启用以下内核参数(/etc/sysctl.d/99-tuning.conf):

vm.swappiness=1
vm.dirty_background_ratio=5
vm.dirty_ratio=15
kernel.numa_balancing=0
net.core.rmem_max=4194304
net.core.wmem_max=4194304
net.ipv4.tcp_congestion_control=bbr
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_tw_reuse=1

坑:ZFS 上跑 PG 要注意 recordsize=16K 与 logbias=throughput,并将 sync=always 给 WAL 盘;否则崩机回放时延迟会出人意料的长。

四、技术选型与一致性策略

  • 数据库:PostgreSQL 16
  • 站内强一致:物理同步复制(synchronous_commit=on,synchronous_standby_names='FIRST 1 (hk2,hk3)')
  • 跨境复制:逻辑复制(Publication/Subscription),仅订阅核心业务表(订单、支付、库存流水),降噪、减压。
  • 事务模型:账务写入在 HK 主域提交;跨境地区通过 Outbox 事件 驱动消费,幂等处理 确保最终一致。
  • 路由:PgBouncer 连接池 + 应用侧读写分离(强一致读需 read from primary)。
  • 故障切换:Patroni + etcd(或 Keepalived VIP),1 主 2 同步备。
  • 审计与回放:保留 24 小时 WAL(wal_keep_size),跨境断链自动追赶。

五、实施步骤(从干净的 Debian 12 开始)

1)系统准备与时间同步

apt update && apt install -y chrony htop iotop net-tools gnupg lsof jq curl
sed -i 's/^pool.*/pool time.google.com iburst/' /etc/chrony/chrony.conf
systemctl enable --now chrony

时间漂移是复制的一号敌人。监控 NTP 偏差,目标 < 50ms。

2)安装 PostgreSQL 16

# 官方仓库
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/pg.gpg
echo "deb [signed-by=/usr/share/keyrings/pg.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" \
  > /etc/apt/sources.list.d/pgdg.list
apt update && apt install -y postgresql-16 postgresql-16-wal2json pgbouncer

3)主库(HK1)基础配置 /etc/postgresql/16/main/postgresql.conf

listen_addresses = '0.0.0.0'
max_connections = 1000
shared_buffers = 64GB
work_mem = 64MB
maintenance_work_mem = 2GB
effective_cache_size = 180GB
wal_level = logical
max_wal_senders = 32
max_replication_slots = 32
wal_compression = on
wal_keep_size = '24GB'
checkpoint_timeout = '15min'
max_wal_size = '32GB'
min_wal_size = '4GB'
synchronous_commit = on
synchronous_standby_names = 'FIRST 1 (hk2,hk3)'
archive_mode = off
# 延迟敏感的 OLTP 调参
random_page_cost = 1.1
seq_page_cost = 1.0

访问控制 /etc/postgresql/16/main/pg_hba.conf

# 复制专用用户
host    replication     repl_user       10.10.0.0/16           scram-sha-256
# 应用侧
host    appdb           app_user        10.20.0.0/16           scram-sha-256

创建用户与库:

CREATE ROLE repl_user WITH REPLICATION LOGIN PASSWORD '***';
CREATE ROLE app_user LOGIN PASSWORD '***';
CREATE DATABASE appdb OWNER app_user;

4)在香港域内配置同步物理复制(HK2/HK3)

在 HK2/HK3 清空 data 目录,用 pg_basebackup 拉链:

systemctl stop postgresql
rm -rf /var/lib/postgresql/16/main/*
pg_basebackup -h hk1 -U repl_user -D /var/lib/postgresql/16/main -Fp -Xs -P -R
chown -R postgres:postgres /var/lib/postgresql/16/main
# 标识节点名(备用)
echo "primary_conninfo = 'host=hk1 user=repl_user password=*** application_name=hk2'" \
  >> /var/lib/postgresql/16/main/postgresql.auto.conf
systemctl start postgresql

HK1 上验证:

SELECT application_name, sync_state, state, flush_lag
FROM pg_stat_replication;

你应该看到 hk2/hk3 其中一个是 sync,另一个 potential,确保 synchronous_standby_names='FIRST 1 (hk2,hk3)' 生效。

现场坑 ①:

  • 症状:主库写入 P95 突然飙升到 300ms。
  • 排查:发现同步链路上的 hk2 写入放大,NVMe GC;
  • 处置:把 FIRST 1 (hk2,hk3) 改为 FIRST 1 (hk3,hk2),临时让 hk3 负担同步确认;稍后重平衡。记住:同步副本的 IO 抖动会直接放大主库延迟。

5)跨境逻辑复制(只复制关键表)

在 HK1 创建 publication:

-- 核心表(示例)
CREATE TABLE IF NOT EXISTS orders (
  id BIGSERIAL PRIMARY KEY,
  order_no TEXT UNIQUE NOT NULL,
  user_id BIGINT NOT NULL,
  amount NUMERIC(18,2) NOT NULL,
  currency TEXT NOT NULL,
  status TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS inventory_ledger (
  id BIGSERIAL PRIMARY KEY,
  sku TEXT NOT NULL,
  warehouse TEXT NOT NULL,
  delta INT NOT NULL,
  reason TEXT NOT NULL,
  ref_order TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 只发布需要跨境消费的表
CREATE PUBLICATION pub_core FOR TABLE orders, inventory_ledger;

在 深圳 SZ 与 胡志明 HCM 建库并创建订阅:

CREATE ROLE sub_user LOGIN PASSWORD '***';
CREATE DATABASE appdb OWNER sub_user;
\c appdb
CREATE SUBSCRIPTION sub_core
  CONNECTION 'host=hk1 port=5432 user=sub_user password=*** dbname=appdb sslmode=require'
  PUBLICATION pub_core
  WITH (copy_data=true, create_slot=true, enabled=true);

现场坑 ②:排序规则/编码不一致 会导致订阅初始化失败。确保各站点 lc_collate、lc_ctype、server_encoding 一致(推荐 en_US.UTF-8)。

现场坑 ③:逻辑复制不复制序列(sequence)。跨境写入端若需要本地创建数据,必须:

改用 ULID/Snowflake 类型全局 ID;或

复制前手动对齐序列起点:SELECT setval('orders_id_seq', (SELECT max(id) FROM orders));

6)Outbox 事件表 + 幂等去重(解决跨境最终一致)

为什么需要 Outbox? 因为跨境逻辑复制到达时间不可控。把“事实”与“事件”分离,事件保证和事务同生死,消费者按 去重键 幂等处理。

建表与触发器:

CREATE TABLE IF NOT EXISTS outbox (
  id BIGSERIAL PRIMARY KEY,
  event_key TEXT NOT NULL,           -- 幂等键(如: orders:ORDER_NO)
  event_type TEXT NOT NULL,          -- 订单创建/支付成功/库存扣减等
  payload JSONB NOT NULL,            -- 业务负载
  occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  processed BOOLEAN NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX ux_outbox_event_key ON outbox(event_key);

-- 示例:订单创建时写 outbox(与订单在同一事务)
CREATE OR REPLACE FUNCTION f_order_created()
RETURNS trigger AS $$
BEGIN
  INSERT INTO outbox(event_key, event_type, payload)
  VALUES (concat('orders:', NEW.order_no), 'ORDER_CREATED',
          jsonb_build_object('order_no', NEW.order_no,
                              'amount', NEW.amount,
                              'currency', NEW.currency,
                              'status', NEW.status));
  RETURN NEW;
END; $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_order_created
AFTER INSERT ON orders
FOR EACH ROW EXECUTE FUNCTION f_order_created();

跨境站点消费者(伪码):

-- 每次取 500 条未处理事件,幂等 upsert
WITH cte AS (
  SELECT id, event_key, event_type, payload
  FROM outbox
  WHERE processed = false
  ORDER BY id
  LIMIT 500
  FOR UPDATE SKIP LOCKED
)
-- 以 event_key 做幂等约束
INSERT INTO inbox (event_key, event_type, payload, received_at)
SELECT event_key, event_type, payload, now() FROM cte
ON CONFLICT (event_key) DO NOTHING;

-- 按事件类型驱动本地投影(读模型)
-- 例如:
-- INSERT INTO projection_orders(...) SELECT ... FROM inbox WHERE event_type='ORDER_CREATED' AND NOT applied;

关键点:Outbox 与业务提交在同一事务,保证“有事实就必有事件”。消费者必须以 event_key 作为 唯一幂等键,避免跨境重复投影。

7)扣库存的一致性(强一致域事务 + 行级锁)

库存扣减要么完全成功要么完全失败:

BEGIN;
-- 锁住该 SKU 在指定仓的行,避免并发扣减
SELECT id FROM inventory WHERE sku=$1 AND warehouse=$2 FOR UPDATE;
-- 业务检查:可用库存是否足够
UPDATE inventory SET available = available - $3 WHERE sku=$1 AND warehouse=$2 AND available >= $3;
-- 记录流水
INSERT INTO inventory_ledger(sku, warehouse, delta, reason, ref_order)
VALUES ($1, $2, -$3, 'ORDER_ALLOCATE', $4);
COMMIT;

跨境站点只消费 inventory_ledger 流水来构建只读的“可视化库存”,不直接改写主域库存表。

8)连接池与读写分离(PgBouncer)

/etc/pgbouncer/pgbouncer.ini

[databases]
appdb = host=hk1 port=5432 dbname=appdb auth_user=pgbouncer

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction
max_client_conn = 5000
default_pool_size = 200
server_reset_query = DISCARD ALL

业务要强一致读时,直连主库端点;容忍读延迟的报表/查询走跨境只读副本。

9)自动故障转移(Patroni 简配)

/etc/patroni/hk1.yml(节选):

scope: hk-cluster
name: hk1
restapi:
  listen: 0.0.0.0:8008
  connect_address: hk1:8008
etcd:
  hosts: 10.10.0.10:2379,10.10.0.11:2379,10.10.0.12:2379
postgresql:
  listen: 0.0.0.0:5432
  connect_address: hk1:5432
  data_dir: /var/lib/postgresql/16/main
  parameters:
    wal_level: logical
    synchronous_commit: on
    synchronous_standby_names: 'FIRST 1 (hk2,hk3)'
bootstrap:
  dcs:
    synchronous_mode: true
    synchronous_mode_strict: true
  initdb:
  - encoding: UTF8
  - data-checksums

选择 synchronous_mode_strict=true 确保没有同步副本时拒绝提交,代价是可用性下降但一致性提升。我们只对账务路径启用。

10)监控与告警

  • 主从延迟:pg_stat_replication 的 write_lag/flush_lag/replay_lag。
  • 逻辑复制延迟:pg_stat_subscription 的 latency。
  • WAL 积压:pg_stat_replication.sent_lsn - replay_lsn。
  • 磁盘与 IO:node_exporter + postgres_exporter + Grafana。

示例查询:

SELECT subname, stats.apply_lag
FROM pg_stat_subscription AS stats;

11)压测与演练

模拟跨境网络:

# 在 SZ 节点对 HK 方向增加 40ms 往返延迟、0.1% 丢包
apt install -y iproute2
tc qdisc add dev eth0 root netem delay 20ms 1ms loss 0.1%

回放订单写入 5 万条,观察:

指标 站内强一致 跨境订阅
写入 P95 142ms N/A
复制追赶 即时 4.2s 收敛(5w/秒级)
WAL 峰值 18GB ——

六、常见坑与现场解决记录

  • 序列值不一致:已在上文提及,用全局 ID 或手动对齐。
  • 冲突写(多活误用):跨境站点严禁改写主域事实表。需要多活请引入冲突解决策略(如 pglogical 的 conflict_resolution),但复杂度陡增。
  • 大事务复制阻塞:拆分为批量小事务;或为 inventory_ledger 单独 publication。
  • 跨境 SSL:sslmode=require 并配置 CA;否则中间人风险难以接受。
  • 磁盘写放大:WAL 压缩开启;ZFS 选好 recordsize;为 WAL 单独盘。
  • 同步副本抖动:随时切换 FIRST 1 的顺序或临时降级为 LOCAL(业务允许时)。

七、验收标准与回归清单

一致性:

  • 香港站内模拟宕机 + 账务写入不丢(RPO=0)。
  • 跨境链路中断 30 分钟内恢复,事件全量回放成功;inbox 去重命中率 100%。

性能:

  • 下单主链路 P95 < 120ms;账务 P95 < 200ms。
  • 观测:Grafana 面板显示复制延迟 < 5s;WAL 保留安全阈值 > 2 小时。
  • 演练:每月一次 patroni failover;每季度一次跨境断链回放演练。

八、参数速查表(可直接抄用再微调)

类别 参数 建议值 说明
WAL wal_level logical 同时支持物理/逻辑复制
WAL wal_keep_size 24GB 保障追赶空间
复制 max_wal_senders 32 并发发送上限
复制 max_replication_slots 32 逻辑槽数量
同步 synchronous_commit on 强一致提交
同步 synchronous_standby_names FIRST 1 (hk2,hk3) 站内一票通过
存储 wal_compression on 降低写放大
池化 PgBouncer.pool_mode transaction 降低连接开销

九、收尾:把账对上号的那个周末

周日晚 11 点,我把最后一条演练记录贴进了维基:RPO=0、跨境 5 秒收敛、库存流水与订单行数一致。财务那边发来一张截图,月末对账首次零差异。

那一刻我知道,这套在 Debian + PostgreSQL 上“站内强一致 + 跨境最终一致”的组合,是我们这个跨境 ERP 的最小复杂度解:该强则强,该松则松。以后再有人问我“跨境一致性怎么做”,我会把这篇笔记丢给他——然后补一句:先把同步副本的 IO 打稳,再谈架构。

目录结构
全文