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

跨境电商网站如何在香港服务器上部署多活数据库,避免全球订单数据不一致?

发布人:Minchunlin 发布时间:2025-09-15 08:52 阅读量:710


去年的“双十一”前夜,深圳同事在群里吼:“欧洲站出现重复订单,美国站查不到刚刚付款的单!”我在人和某湾区机房间来回切 VPN,看着监控盘里落下的红点,心里很清楚:以前那种“香港主库 + 海外异步只读”的架构,已经顶不住全球多站点、强一致下单的需求。
我决定把“香港”变成亚太的数据库枢纽,并在此落地一套真正能跨洲多活、强一致事务的分布式数据库。目标只有一个:全球任一站点下的订单,都要在读写路径上保持一致性;不再出现重复下单或“你付了钱但我查不到订单”的惊悚现场。

结论先行:我选用的技术与拓扑

数据库:CockroachDB(PostgreSQL 协议,天然分布式、事务级强一致;支持多区域/分区路由、REGIONAL BY ROW 局部化写读;对跨洲多活比“传统主从/半同步”靠谱得多)。

注:如果你团队更偏向 MySQL 生态,又能接受更复杂的运维链路,可以评估 TiDB(TiKV/PD/TiDB)。而 MySQL Group Replication/Galera 更适合同城/同园区近距多活,跨洲延迟会很痛。

机房与地域:

  • 香港(AP 东亚):核心路由与网关,低时延触达内地/东南亚。
  • 新加坡(AP 东南亚):就近覆盖 SEA。
  • 洛杉矶(US 西):覆盖美西/拉美;
  • 法兰克福(EU 中):覆盖欧洲。

每地 3 节点(最小 3 节点起步,跨 3 地区形成多数派仲裁,避免单地故障带来写入停摆)。

应用层策略:订单写入采用幂等键与串行化事务;读请求就近且尽量本地(Follower Reads/本地 Leaseholder);支付对账用Outbox + 变更订阅(CHANGEFEED → Kafka)保证最终一致对接。

基线硬件与网络(我实际在线上使用的参数)

区域 机型示例 CPU 内存 NVMe RAID 网卡 OS
香港 hk1/hk2/hk3 单路 EPYC 7502P 32C/64T 128GB 2×1.92TB RAID1(mdadm) 10GbE(BGP 优化线路) CentOS 7.9
新加坡 sg1/sg2/sg3 同上 同上 同上 同上 同上 同上 CentOS 7.9
洛杉矶 us1/us2/us3 同上 同上 同上 同上 同上 同上 CentOS 7.9
法兰克福 eu1/eu2/eu3 同上 同上 同上 同上 同上 同上 CentOS 7.9

时延(经验值,仅供容量与参数估算):

源 → 目标 HK SG LA FRA
HK 30–45 ms 120–140 ms 160–190 ms
SG 30–45 ms 170–190 ms 190–220 ms
LA 120–140 ms 170–190 ms 140–170 ms
FRA 160–190 ms 190–220 ms 140–170 ms

核心提示:选择 CockroachDB 等强一致分布式 SQL的关键点,是把写请求路由到“本地 Region 的副本多数”,避免跨洲往返影响下单延时;而读尽量用本地 Follower/Leaseholder。

操作系统与磁盘/网络基线(CentOS 7)

1)磁盘与文件系统

# 创建 RAID1
mdadm --create /dev/md0 --level=1 --raid-devices=2 /dev/nvme0n1 /dev/nvme1n1
mkfs.ext4 -E lazy_itable_init=0,lazy_journal_init=0 /dev/md0
mkdir -p /data
echo '/dev/md0 /data ext4 defaults,noatime,nodiratime 0 0' >> /etc/fstab
mount -a

2)内核参数(数据库友好):

cat >/etc/sysctl.d/99-db.conf <<'EOF'
vm.swappiness=1
vm.dirty_ratio=10
vm.dirty_background_ratio=5
fs.aio-max-nr=1048576
net.core.somaxconn=65535
net.core.netdev_max_backlog=250000
net.ipv4.tcp_max_syn_backlog=4096
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_tw_reuse=1
EOF
sysctl --system

3)关闭透明大页(THP)+ 启用 NTP(CockroachDB 对时钟偏移很敏感):

# 关闭 THP(开机生效)
echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled
echo 'never' > /sys/kernel/mm/transparent_hugepage/defrag
cat >/etc/rc.d/rc.local <<'EOF'
#!/bin/bash
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF
chmod +x /etc/rc.d/rc.local

# NTP:chrony
yum install -y chrony
systemctl enable chronyd --now
chronyc sources -v

坑 1:时钟偏移

CockroachDB 对节点间时钟偏移有严格限制(默认阈值为百毫秒级)。我们曾因海外节点 NTP 源不稳定,触发“clock offset”告警导致事务受限。务必保证各地 NTP 源就近、稳定,并在防火墙放行。

安装 CockroachDB(多区域)

1)安装二进制与用户

useradd -r -m -d /var/lib/cockroach cockroach
cd /usr/local/bin
curl -O https://binaries.cockroachdb.com/cockroach-v23.2.linux-amd64.tgz
tar xzf cockroach-*.tgz --strip 1
chown root:root cockroach
mkdir -p /var/lib/cockroach/{data,certs}
chown -R cockroach:cockroach /var/lib/cockroach

2)生成证书(自签,实际可接企业 CA)

每地做一次 ca,或在香港集中生成并安全分发。

sudo -u cockroach cockroach cert create-ca --certs-dir=/var/lib/cockroach/certs --ca-key=/var/lib/cockroach/certs/ca.key
sudo -u cockroach cockroach cert create-node \
  hk1 hk1.local 127.0.0.1 \
  --certs-dir=/var/lib/cockroach/certs --ca-key=/var/lib/cockroach/certs/ca.key

sudo -u cockroach cockroach cert create-client root \
  --certs-dir=/var/lib/cockroach/certs --ca-key=/var/lib/cockroach/certs/ca.key

3)systemd 服务(示例:香港节点 hk1)

cat >/etc/systemd/system/cockroach.service <<'EOF'
[Unit]
Description=CockroachDB Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=cockroach
Group=cockroach
ExecStart=/usr/local/bin/cockroach start \
  --store=/data/cockroach \
  --advertise-addr=hk1.example.com \
  --listen-addr=0.0.0.0:26257 \
  --http-addr=0.0.0.0:8080 \
  --certs-dir=/var/lib/cockroach/certs \
  --max-sql-memory=25% \
  --cache=25% \
  --locality=region=ap-east,zone=hk1 \
  --join=hk1.example.com:26257,hk2.example.com:26257,hk3.example.com:26257,sg1.example.com:26257,us1.example.com:26257,eu1.example.com:26257
Restart=always
LimitNOFILE=262144

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable cockroach --now

4)初始化集群(在任一已启动节点执行一次):

cockroach init --host=hk1.example.com:26257 --certs-dir=/var/lib/cockroach/certs

5)放行端口:

firewall-cmd --permanent --add-port=26257/tcp
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --reload

坑 2:证书 SAN 不匹配

别忘了把所有对等名与 IP都加入 create-node 的 SAN;否则连接会随机失败。

多区域数据库与本地化路由

1)创建多区域数据库与主区域

-- 用 root 连接后执行(psql / cockroach sql 都可):
CREATE DATABASE ecommerce;
ALTER DATABASE ecommerce SET PRIMARY REGION "ap-east";
ALTER DATABASE ecommerce ADD REGION "ap-southeast";
ALTER DATABASE ecommerce ADD REGION "us-west";
ALTER DATABASE ecommerce ADD REGION "eu-central";

2)基于行的区域本地化(REGIONAL BY ROW)

USE ecommerce;

-- 区域类型(CockroachDB 提供内置类型,名称示例视版本而定)
CREATE TYPE public.region_enum AS ENUM ('ap-east','ap-southeast','us-west','eu-central');

-- 订单表:按 crdb_region 分散到各自区域,多数副本在本地
CREATE TABLE orders (
  order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id  UUID NOT NULL,
  status   STRING NOT NULL DEFAULT 'PENDING',
  currency STRING NOT NULL,
  amount_cents INT NOT NULL,
  payment_ref STRING,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  idempotency_key STRING NOT NULL,
  crdb_region public.region_enum NOT NULL,
  UNIQUE (idempotency_key) -- 幂等键,应用保证“一次且仅一次”下单
) LOCALITY REGIONAL BY ROW;

-- 用户表:单区域本地(例如“就近读多、写少”)
CREATE TABLE users (
  user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email STRING UNIQUE,
  name  STRING,
  crdb_region public.region_enum NOT NULL
) LOCALITY REGIONAL BY ROW;

-- 索引与热点键优化
CREATE INDEX ON orders (user_id);
CREATE INDEX ON orders (created_at);

3)应用侧路由策略

  • 进入站点时,根据用户地理/店铺归属标注 crdb_region,写入时把该列带上;
  • 这样 CockroachDB 会把这行数据的Leaseholder/多数副本安置在对应区域,写入只需本地多数派往返;
  • 跨区查询可使用只读或 AS OF SYSTEM TIME(牺牲一点点新鲜度、换就近低延迟)。

应用层写入:事务 + 幂等键(Go 示例)

CockroachDB 兼容 PostgreSQL 协议,这里用 pgx。关键点:串行化事务 + 业务侧幂等键 + 唯一约束。

// go get github.com/jackc/pgx/v5
ctx := context.Background()
conn, _ := pgx.Connect(ctx, "postgresql://root@lb.hk.example.com:26257/ecommerce?sslmode=verify-full")

type OrderReq struct {
  UserID string
  Amount int
  Currency string
  IdemKey string   // 前端/支付回调生成,确保每个“下单意图”只对应一次成功
  Region string    // e.g. "ap-east"
}

func CreateOrder(ctx context.Context, req OrderReq) (string, error) {
  tx, err := conn.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
  if err != nil { return "", err }
  defer tx.Rollback(ctx)

  // 1) 幂等:如果这个 idempotency_key 已经用过,直接返回老订单
  var existing uuid.UUID
  err = tx.QueryRow(ctx, `SELECT order_id FROM orders WHERE idempotency_key=$1`, req.IdemKey).Scan(&existing)
  if err == nil {
    return existing.String(), nil
  }

  // 2) 插入订单(携带 crdb_region)
  var orderID uuid.UUID
  err = tx.QueryRow(ctx, `
    INSERT INTO orders(user_id, amount_cents, currency, idempotency_key, crdb_region)
    VALUES ($1,$2,$3,$4,$5)
    RETURNING order_id
  `, req.UserID, req.Amount, req.Currency, req.IdemKey, req.Region).Scan(&orderID)
  if err != nil { return "", err }

  // 3) 其他写操作……(库存预留、优惠核销等,务必同一事务或采用 SAGA/Outbox)
  if _, err = tx.Exec(ctx, `UPDATE inventory SET reserved=reserved+1 WHERE sku=$1`, "SKU-001"); err != nil {
    return "", err
  }

  if err = tx.Commit(ctx); err != nil {
    // Serializable 冲突可重试
    if pgerr, ok := err.(*pgconn.PgError); ok && pgerr.Code == "40001" {
      // 简化:递归重试一次
      return CreateOrder(ctx, req)
    }
    return "", err
  }
  return orderID.String(), nil
}

读请求就近与一致性取舍

  • 强一致读:默认从 Leaseholder 读取,若表/行本地化,则基本是本地。
  • 延时敏感读:允许轻微陈旧(比如 5 秒)换本地 follower 高速读取:
-- 举例:非关键看板、搜索页
SELECT count(*) FROM orders AS OF SYSTEM TIME '-5s' WHERE status='PAID';

负载均衡(HAProxy)与健康检查

在每个区域放一个 HAProxy(或 pgbouncer),用 DNS 将应用连到最近的 LB;LB 后面挂本地与其它区的 Cockroach 节点(优先本地)。

/etc/haproxy/haproxy.cfg 片段:

frontend pgsql
  bind *:26257
  mode tcp
  default_backend cockroach_be

backend cockroach_be
  mode tcp
  balance roundrobin
  option tcp-check
  tcp-check connect port 26257
  server hk1 hk1.example.com:26257 check
  server hk2 hk2.example.com:26257 check
  server hk3 hk3.example.com:26257 check
  # 可按需加跨区节点为兜底

坑 3:跨区健康探测导致错误切流

健康检查尽量不同权重或分前后池(本地优先,跨区为备用),避免跨区节点短暂更“健康”时被误选为主路径。

变更订阅(Outbox/CDC)对接支付与分析

订单写库成功 ≠ 接入方已经收到消息。我在订单表或 Outbox 表上创建 CDC(CHANGEFEED)推到 Kafka:

-- 打开 rangefeed(新版本默认一般已可用,按版本调整)
SET CLUSTER SETTING kv.rangefeed.enabled = true;

CREATE CHANGEFEED FOR TABLE orders
INTO 'kafka://kafka-1:9092'
WITH updated, resolved='10s', format='json';

支付回调、风控、BI 订阅该主题,实现最终一致分发;失败可用 Kafka 重试/死信队列。

备份与恢复(S3 兼容对象存储)

-- 例:全量 + 增量
BACKUP DATABASE ecommerce TO 's3://x-border-backup/full?AWS_ACCESS_KEY_ID=...&AWS_SECRET_ACCESS_KEY=...';

-- 周期性(用 crontab 或调度器触发)
BACKUP DATABASE ecommerce
TO 's3://x-border-backup/inc?AWS_ACCESS_KEY_ID=...&AWS_SECRET_ACCESS_KEY=...'
INCREMENTAL FROM LATEST;

坑 4:跨境带宽与费用

备份桶尽量区域就近(香港/新加坡)且有生命周期策略;跨洲恢复演练要在低峰期,避免拉满专线。

监控与告警

Prometheus 抓取 Cockroach 指标(默认 8080 暴露 /metrics):

scrape_configs:
  - job_name: 'cockroach'
    static_configs:
      - targets:
        - hk1.example.com:8080
        - hk2.example.com:8080
        - sg1.example.com:8080
        - us1.example.com:8080
        - eu1.example.com:8080

关注:sql_ 延时分位、raft_ 相关提交/心跳、clock_offset、GC/compaction、store 容量与写放大、replicas_per_store、ranges 过热迁移等。

容量与副本策略要点

  • 副本与投票:对 orders 这类写多且延时敏感表,采用 REGIONAL BY ROW,多数副本就近;
  • 跨区容灾:系统表/全局轻写入元数据可以 GLOBAL 或 REGIONAL BY TABLE;
  • 扩容:新增节点带 --locality=region=...,zone=...,观察 rebalancing;
  • 限速:跨洲链路窄时,调低快照/重平衡速率,防止业务尖刺期被复制流量顶死。

常见坑与现场处理记录

  • “clock offset too high”:NTP 源抖动 → 统一改为就近权威 NTP、Chrony 校准,阈值告警上提到监控。
  • 证书 SAN 漏加 LB 名称:导致偶发握手失败 → 重做 create-node,把 LB FQDN 一并加入。
  • 跨区健康探测切流:LB 权重与优先级不当 → 分“本地池/跨区池”,仅当本地全挂时才兜底跨区。
  • 磁盘写放大导致 NVMe 磨损快:把 compaction/GC 指标纳入看板,业务侧冷热分层、归档历史订单到 OLAP。
  • 支付回调二次触发:应用侧幂等键 + 唯一约束救命;并在回调里也使用同一幂等键。
  • 热点用户/店铺:对 user_id 做二级索引 + 适度散列前缀,或在上层做分片键(例如按店铺 ID hash)。

面向新同事的一页纸(速记)

  • 下单写路径:前端→最近区应用→最近区 LB→Cockroach 本地副本多数 → 幂等键去重 → 成功后 Outbox/CDC。
  • 读路径:本地强一致读;非关键查询用 AS OF SYSTEM TIME '-5s'。
  • 跨区灾备:任何一地故障,其他地仍可写(只要多数派可达);香港作为亚太主枢纽不等于单点。
  • 不要做:把 MySQL 半同步跨太平洋硬扛;把 Galera 当全球多活;把幂等当“以后再做”。

如果你更倾向 MySQL 生态(简版指引)

TiDB 方案:TiDB(SQL 前端)+ PD(调度)+ TiKV(存储),多 Region 部署,按 label/placement rules 让订单表按行区域化,写在本地,强一致事务。

Group Replication/Galera:更适合同城多活;跨洲延迟下吞吐与冲突会难看,不建议用于全球订单写路径。

CDC:Debezium + Kafka 或 TiCDC(TiDB),对接支付/风控/数据仓库。

压测与表格数据(我线上做过的近似指标,供你对标)

场景 区域 写 TPS(下单事务) P95 写延时 读 QPS 说明
峰值大促 香港 ~2.8k 35–55 ms 8k REGIONAL BY ROW 本地多数派提交
常态 新加坡 ~1.2k 30–45 ms 3k -
跨区读 LA 读 FRA 数据 - - 1.5k AS OF SYSTEM TIME '-5s'
故障演练 下线 sg2 ~1.1k 40–60 ms 2.5k 仍可写,流量落到 sg1/sg3

指标随 CPU/NVMe 规格、表设计与业务逻辑不同而波动,上表仅作量级参考。

完整上线 Checklist

  •  各区 3 节点、--locality 正确
  •  NTP 稳定,时钟偏移告警
  •  证书 SAN 覆盖节点与 LB 名称
  •  orders/users 等表 REGIONAL BY ROW,crdb_region 正确传递
  •  幂等键唯一约束 + 事务冲突重试
  •  CDC 到 Kafka,消费者幂等
  •  备份到对象存储,做恢复演练
  •  Prometheus/Grafana 告警就绪
  •  LB 本地优先,跨区兜底
  •  压测通过:P95 写延时与 TPS 达标

第二天早上 6 点,大促入口准时打开。看板上,香港的写延时稳稳落在 50ms 以内,新加坡、洛杉矶、法兰克福也各自平滑起伏。有人在群里发了句“这次下单体验很稳”,我才想起桌上那杯已经凉了的咖啡。
这套架构不是“银弹”,但它把“全球订单数据不一致”的大坑,变成了可控的小沟。今天把完整过程写下来,你可以原样落地,也可以按你的业务自由裁剪。
当你凌晨两点看到告警,却不再为订单重复与丢失冒汗时,记得也给自己倒一杯咖啡。那是我们做工程的味道。

目录结构
全文