如何为部署在香港服务器上的手游搭建多分区逻辑服架构,避免不同区域玩家同步异常
技术教程 2025-09-17 10:18 184


我把最后一台逻辑服的机箱推回托架时,Grafana 的 P99 线终于趴平,我关上工具箱当作给自己画了个句号。香港这家机房在荃湾,半夜两点,走廊里只有我和机柜的指示灯在对话。前一周,菲律宾分区的玩家投诉“体力不刷新、跨区公会战积分乱跳”。那一刻我意识到:我们欠一套真正意义上的多分区逻辑服架构,既要让各区独立稳定低延迟,又要在必要的跨区场景下做到可控的一致性。这篇文章,是我这次从设计、选型、落地、踩坑到回归稳定的完整实操记录。希望无论你是第一次搭手游后端的新同学,还是在大促、合服、跨区战里摸爬滚打多年的同路人,都能从中拿到可直接复用的“菜谱”。

一、目标与约束

目标

  • 多分区(例如:HK、TW、SEA、JP)逻辑服相互隔离,同区请求闭环,跨区事件可控异步。
  • 单区P99 延迟 ≤ 80ms(香港/台北/马尼拉/东京覆盖),跨区事件允许数百毫秒~数秒的最终一致。
  • 避免同步异常:时间漂移、重复消费、写放大、缓存击穿、雪崩、跨区“脑裂”。

约束

  • 物理部署在香港机房(低时延覆盖东亚/东南亚),系统:CentOS 7(注意内核特性)。
  • 尽量使用社区稳健组件:Nginx/Envoy、Kafka、Redis Cluster、MySQL 5.7/8.0(或 MGR)、Prometheus + Grafana、Loki/ELK。
  • 预算友好,10G 接入 + 专线/多线 BGP;GPU 非刚需。

二、总体架构(Region-Partition + EventBus + Outbox)

分层与关键点:

Client
  ↓ (GSLB/DNS/Anycast)
Edge(香港): Nginx/Envoy Access GW  ──→  Auth/Session
                           │
                           ├──> Partition Router(按玩家UID映射分区)
                           │
                           └──> Logic Cluster[Region=HK/TW/SEA/JP]
                                   ├─ Lobby/Match
                                   ├─ Battle/Instance
                                   ├─ Economy(支付/道具)
                                   └─ Social(公会/好友)
                                   │
                                   ├── Redis Cluster(分区内) - 热数据/令牌/锁
                                   └── MySQL Shards(分区内)  - 冷热分层+分库分表
                                           │
                                           └── Outbox(本地事务表) → Kafka(全局Bus)
                                                      ↑
                                           Cross-Region Aggregators(异步汇总/跨区玩法)
  • 分区(Partition)是逻辑概念:每个分区有独立的逻辑服、Redis Cluster、MySQL分片。跨区只通过Kafka 事件总线沟通,避免直接跨库/跨缓存写。
  • Outbox Pattern:业务写本地库 & 记录 outbox 表(同一事务提交),由后台 relay 稳定投递到 Kafka,实现“至少一次 + 幂等消费”。
  • 幂等键:idempotency_key = partition_id + table + business_pk + version。消费者用去重表或 Redis set 带 TTL 去重。

三、硬件与网络选型(香港机房实配示例)

3.1 机型与存储(单区目标并发 10~20 万 QPS/请求峰值 1~2 万)

角色 型号/CPU 内存 系统盘 数据盘 网卡 备注
Access GW 2×Intel Xeon Silver 4310 64GB 2×480G SSD - 2×10G Nginx/Envoy+Keepalived
Logic 节点×12/区 AMD EPYC 7313P 256GB 2×960G SSD 2×3.84TB NVMe U.2 2×10G Go/Java 微服务
Redis×6/区 Intel Xeon Gold 6230R 128GB 2×480G SSD 4×1.92TB NVMe 2×10G Cluster 3主3从
MySQL×6/区 AMD EPYC 7402P 256GB 2×960G SSD 4×3.84TB NVMe 2×10G Shard ×3,主从
Kafka×5(全局共用) AMD EPYC 7313P 128GB 2×480G SSD 4×1.92TB NVMe 2×10G 3AZ 架构 / 机柜隔离
监控/日志 任意 64GB 2×480G SSD 2×3.84TB SSD 2×10G Prom/Loki/MinIO

网络:ToR 双上联,BGP 多线,内网 MTU 保持 1500(除非全链路可达 9000),Bonding LACP,VRRP 做VIP漂移。

四、IP 规划与端口

组件 VIP 端口
Access VIP(游戏 TCP/WS) 10.10.1.10 7000-7099
Access VIP(HTTP/REST) 10.10.1.11 80/443
Redis Cluster 10.10.2.0/24 6379, 16379-16479
MySQL Shard0/1/2 10.10.3.0/24 3306
Kafka Brokers 10.10.4.0/24 9092, 9093
Prometheus/Grafana 10.10.5.10 9090/3000

五、时间与时区策略(避免“时间相关的同步异常”第一原则)

  • 所有服务器统一 UTC+0,客户端用本地时区渲染;活动窗口按 UTC 精确落地。
  • Chrony(CentOS 7 默认可用):

/etc/chrony.conf(香港近源 + 局域对时)

server 0.hk.pool.ntp.org iburst
server 1.hk.pool.ntp.org iburst
server 2.asia.pool.ntp.org iburst
makestep 1.0 3
rtcsync
local stratum 10
allow 10.10.0.0/16
systemctl enable chronyd --now
chronyc tracking
chronyc sourcestats

验收:所有节点时钟偏移 |offset| < 50ms。超过阈值自动降级:拒绝跨区强一致事务,仅允许读。

六、数据分区与路由

6.1 玩家路由(稳定 + 均衡)

PlayerUID 为 64-bit:region_code(8bit) + server_id(8bit) + snowflake(48bit)

逻辑分区 partition_id = region_code * 100 + server_id

网关路由根据玩家 token 中的 partition_id 选择后端 upstream。

6.2 数据库分片(MySQL)

单区 3 个 Shard(示例):user_db_{0..2}

规则:shard_idx = uid % 3,表内再做 range/hash 分表。

跨区不跨库写,跨区玩法使用聚合服务从 Kafka 订阅事件,写独立聚合库。

6.3 缓存(Redis Cluster)

Key 设计:{uid}:profile(利用哈希槽 tag 收敛到同节点,减少跨槽多键操作)

强约束:只在同分区进行多键脚本/事务,跨区用事件驱动。

七、组件与内核调优(CentOS 7)

内核 3.10 默认不带 BBR,若需 BBR 建议用 ELRepo kernel-ml ≥ 4.14。否则使用 fq_codel。

/etc/sysctl.d/90-game.conf:

# 文件句柄与连接跟随业务侧调大
fs.file-max = 2097152
net.core.somaxconn = 65535
net.ipv4.ip_local_port_range = 10000 65535

# 队列、缓冲
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456

# TCP
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_max_syn_backlog = 262144

# 队列算法
net.core.default_qdisc = fq_codel
net.ipv4.tcp_congestion_control = cubic

sysctl --system
ulimit -n 1048576

Nginx/Envoy 侧禁 Nagle(上行 WebSocket/长连接),业务 SDK 开启 TCP_NODELAY。

八、部署分步走

8.1 访问层(Nginx Stream + Keepalived)

/etc/nginx/nginx.conf(简化示例,按分区分 upstream):

worker_processes auto;
events { worker_connections  65535; use epoll; }
stream {
    upstream logic_hk {
        hash $remote_addr consistent;
        server 10.10.20.11:7001 max_fails=3 fail_timeout=30s;
        server 10.10.20.12:7001 max_fails=3 fail_timeout=30s;
        # ...
    }
    upstream logic_tw { server 10.10.21.11:7001; server 10.10.21.12:7001; }
    # ...

    map $ssl_preread_server_name $backend {
        default logic_hk;     # 首次登录默认 HK,再由分区路由重定向
        tw     logic_tw;
        sea    logic_sea;
        jp     logic_jp;
    }
    server {
        listen 7000;
        proxy_pass $backend;
        proxy_timeout 60s;
        proxy_connect_timeout 5s;
    }
}

Keepalived(VIP 漂移):

vrrp_instance VI_1 {
    state MASTER
    interface bond0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication { auth_type PASS auth_pass 1a2b3c4d }
    virtual_ipaddress { 10.10.1.10/24 dev bond0 label bond0:1 }
    track_script { chk_nginx }
}

8.2 分区路由服务(Partition Router)

登录鉴权后,Router 依据 uid.partition_id 把客户端302/重连指向目标域名(tw.game.example.com),或在网关层Header里下发 X-Partition: tw 走不同 upstream。不要在登录后期再切分区,避免状态迁移。

8.3 Kafka(全局事件总线)

关键配置(每区生产、全局消费):

num.partitions=24   # 按业务与吞吐规划
default.replication.factor=3
min.insync.replicas=2
unclean.leader.election.enable=false
message.max.bytes=10485760

主题设计:

economy.events.{region}、guild.events.{region}、pvp.match.{region}

Key:uid 或者 guild_id,保证分区内顺序性。

8.4 Redis Cluster(分区内)

# 6节点,3主3从
redis-cli --cluster create \
10.10.2.11:6379 10.10.2.12:6379 10.10.2.13:6379 \
10.10.2.14:6379 10.10.2.15:6379 10.10.2.16:6379 \
--cluster-replicas 1

redis.conf 要点:maxmemory-policy allkeys-lfu,tcp-keepalive 60,hash-max-ziplist-entries 512(8.x 改名),latency-monitor-threshold 50。

8.5 MySQL(分区内分片)

5.7/8.0 均可;推荐 GTID + 主从,上 ProxySQL。

关键参数(示例):

[mysqld]
server_id=101
gtid_mode=ON
enforce_gtid_consistency=ON
log_bin=mysql-bin
binlog_format=ROW
binlog_row_image=MINIMAL
innodb_flush_log_at_trx_commit=1
sync_binlog=1
innodb_buffer_pool_size=128G
innodb_io_capacity=2000
max_connections=8000
table_open_cache=20000

表设计(示例):

CREATE TABLE player_profile_00 (
  uid BIGINT UNSIGNED NOT NULL,
  nickname VARCHAR(32),
  level INT,
  stamina INT,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (uid)
) PARTITION BY HASH(uid) PARTITIONS 64;

8.6 Outbox + 消费幂等(Go 伪代码)

写入(同事务):

BEGIN;
UPDATE player_profile_00 SET stamina = stamina - 10 WHERE uid=? AND stamina>=10;
INSERT INTO outbox (id, aggregate, aggregate_id, event_type, payload, created_at)
VALUES (UUID(), 'player', ?, 'STAMINA_SPENT', JSON_OBJECT('uid', ?, 'delta', 10), NOW(6));
COMMIT;

Relay 投递:

for {
  rows := db.Query(`SELECT id, payload FROM outbox WHERE sent = 0 ORDER BY created_at LIMIT 100`)
  for rows.Next() {
    // 1) 先写 Kafka (Key=uid)
    // 2) 成功后标记 sent=1
  }
}

消费者幂等:

CREATE TABLE consumer_dedup (
  idempotency_key VARCHAR(128) PRIMARY KEY,
  processed_at TIMESTAMP(6) NOT NULL
);

k := fmt.Sprintf("%s:%d:%s", region, shard, eventID)
if !insertIgnore("consumer_dedup", k) { return } // 已处理
// 执行业务落库

九、玩法与同步策略(避免“跨区同步异常”的实操要点)

  • 同区强一致:同区所有修改都在单区事务 + 单区缓存内闭环,必要时用 Redis Lua 分布式锁(注意过期与看门狗)。
  • 跨区仅异步:公会排行榜、跨区锦标赛只消费各区的事件流,汇总库写入最终态,并向客户端声明“最多延迟 N 秒”。
  • 绝不做跨区分布式事务:拒绝两地两阶段提交(2PC)牵扯,宁可用补偿事件(Saga)与人工运营回滚工具。
  • 时间窗固定 UTC:活动窗口用 UTC,避免夏令时/本地时区差异导致“提前结算”。

十、监控、容量与验收

10.1 指标基线

指标 目标
Access P99 ≤ 20ms
Logic P99 ≤ 60ms
Redis GET/SET P99 ≤ 2/3ms
MySQL 写 P99 ≤ 15ms
Kafka end-to-end ≤ 1s(跨区玩法可放宽到 3s)
时钟偏移 < 50ms

10.2 压测模型(分区维度)

  • 并发:单区 20k 并发,混合 70% 读、30% 写、5% 跨区事件。
  • 脚本要点:长连接、心跳、突发 3× 峰值 5 分钟、活动结算 1 分钟“尖峰”。

十一、优化清单(现场证明有效)

  • 连接池:应用层 gRPC/DB 连接池热身预建;MySQL max_connections 与 ProxySQL 配比 1:3。
  • SQL:避免 N+1,所有排行榜走批量读 + 异步写回。
  • 缓存:热点 Key 加随机过期抖动(±10%),避免同刻雪崩;低频 Key 加 Bloom 降低穿透。
  • 网络:统一 MTU=1500,确认专线/云边都不启用 9000,否则全链路一致再开。
  • GC:Go 服务固定 GOGC=100~150,热点大对象池化;Java 选 ZGC/G1 并设 -XX:MaxGCPauseMillis=50。
  • 限流:网关与逻辑双层令牌桶,保护数据库与 Redis。
  • 可观测性:每个跨区汇总服务都暴露 落后滞后秒数(lag_seconds),超过阈值自动降级玩法或隐藏入口。

十二、坑与现场复盘(真事儿)

时钟漂移 300ms 导致签名校验失败
某批逻辑服 BIOS 里 RTC 未与 UTC 同步,chrony 初次校时没 makestep。玩家登录频繁 401。
解决:如上配置 makestep,上线前 chronyc tracking 强校验,CI 加“时钟偏移守护”。

MTU 不一致导致间歇性延迟
Kafka Broker 网卡 MTU=9000,但 GW 与交换机仍 1500。峰值时多片段重组,P99 飙升。
解决:全部回到 1500;或者完善全链路 9000 并做 PMTU 探测。

Redis Cluster 插槽迁移时脚本多键失败
公会模块的 Lua 里跨键未用哈希 Tag,迁槽期间 CROSSSLOT。
解决:统一 Key 模板 {guild:%d}:*,强制同槽;脚本加失败重试与熔断。

Kafka ISR 配置不当引发隐性丢事件
min.insync.replicas=1 + acks=1,broker 挂掉时仍返回成功。
解决:生产者 acks=all,集群 min.insync.replicas=2,并开启 unclean.leader.election=false。

TIME_WAIT 激增导致端口耗尽
活动结算扫尾瞬间短连接暴增。
解决:全量长连;必要时增加 ip_local_port_range 与 tcp_tw_reuse=1。

UID Hash 偏斜导致某 Shard 热点
早期 UID 连号,取模偏斜。
解决:Snowflake 打散;老数据迁移脚本夜间批量重分布。

十三、上线脚本与运维手册片段

13.1 systemd(逻辑服)

/etc/systemd/system/logic@.service:

[Unit]
Description=Game Logic %i
After=network.target

[Service]
User=game
WorkingDirectory=/srv/logic/%i
ExecStart=/srv/logic/%i/logic --partition=%i --conf=conf.yml
Restart=always
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

for p in hk-01 hk-02 hk-03; do systemctl enable --now logic@$p; done

13.2 Prometheus 采集(示例)

scrape_configs:
  - job_name: 'logic'
    static_configs:
      - targets: ['10.10.20.11:9100','10.10.20.12:9100']
  - job_name: 'kafka'
    static_configs:
      - targets: ['10.10.4.11:9308','10.10.4.12:9308']

十四、表格:分区容量与阈值

分区 在线峰值 QPS 峰值 Redis 内存上限 MySQL TPS 峰值 Kafka 主题/分区
HK 120k 18k 256GB 6k economy.hk × 24
TW 80k 12k 192GB 4k economy.tw × 16
SEA 150k 22k 256GB 7k economy.sea × 24
JP 60k 9k 128GB 3k economy.jp × 12

告警阈值:Redis 使用率 > 75%,MySQL P99 > 30ms,Kafka lag_seconds > 2s,跨区玩法自动降级。

十五、灾备与演练

  • 同城双机柜:Kafka、MySQL、Redis 跨机柜副本;交换机与电源冗余。
  • 冷备跨城:最关键库做 binlog 级别离线增量同步到海外对象存储,每日演练恢复。
  • 演练:每月一次 broker 下线演练、每季度一次“分区丢一半节点”的黑天鹅演练。

十六、从零到上线 Checklist(可直接对照执行)

  • 机房/网络:BGP/带宽/MTU/VRRP 验收 ✅
  • 系统基线:内核/chrony/ulimit/sysctl/tuned ✅
  • 访问层与VIP:Nginx/Envoy + Keepalived 对外连通 ✅
  • 分区路由:登录→分区→重连流程自测 ✅
  • Redis Cluster:插槽分布/Lua/Tag 规则验收 ✅
  • MySQL:分片规则/主从/延迟/DDL 演练 ✅
  • Kafka:ISR/acks/主题分区/监控 ✅
  • Outbox:双写一致、重复投递与幂等验证 ✅
  • 压测:20k 并发/3×尖峰/活动结算 ✅
  • 可观测:Dashboards/告警策略/值班手册 ✅
  • 演练:降级/熔断/只读模式/灰度开关 ✅
  • 运营工具:补单/回滚/公告/热修复开关 ✅

十七、凌晨三点半的合拢

活动结算那晚,我盯着 Grafana 的 P99 线像盯着心电图。跨区汇总的 lag 从 1.8s 缓慢降到 600ms,公会战的积分不再乱跳。机房的灯还是冷的,但聊天室里菲律宾分区的玩家开始刷“GG”、“nice fix”。我把最后的告警阈值再调窄了一点,给值班同学发了条“可以收尾了”。
架构不是一次性的漂亮图,而是每一个边界的取舍:分区内强一致、跨区异步、可观测、可降级、可恢复。只有在凌晨三点半的机房里,你才能听见系统稳定运行时的那种安静。这套多分区逻辑服架构,我用过、修过,也愿意把每个坑都摊开给你看。
下一次你在机房里听见那种安静,希望也有我的这份“菜谱”在你手边。