设计一个 1 亿全球 DAU 的社交电商,用户分布北美 / 欧盟 / 亚太三大区。目标:就近读 p99 < 150ms(跨洋 RTT 就有 120-180ms,单靠一个中心区域必然超标);单区域整体故障 RTO < 5 分钟;欧盟用户 PII 必须留在欧盟境内(GDPR 第 44 条跨境传输限制)。
为什么这是 System Design 里最难的一类题:光速不可谈判。北京到弗吉尼亚单程约 90ms,一次跨区同步写要 180ms RTT——你无法用工程手段消除,只能用架构绕开。多区域的全部难点就是:哪些操作允许跨区、哪些必须留在本区、数据冲突谁赢。
组件职责:GeoDNS/Anycast 按用户地理位置把请求路由到最近区域;每个区域是自包含的全栈(边缘→服务→数据库),本区故障不拖垮其他区(Netflix 的 Isolation 原则)。跨区数据默认异步复制——同步复制会把每次写变成跨洋 RTT。欧盟主库对 PII 只做选择性复制:非敏感数据(商品目录)全局同步,PII 留在 eu-west。核心张力:就近=低延迟但数据分散,集中=强一致但高延迟。
核心 trade-off:把用户钉在「归属区」读写最简单一致,但归属区挂了要能秒切别的区,切换时的会话/数据一致性是坑。
【原理】三层路由手段:GeoDNS(按 resolver 地理位置返回不同 IP,粒度粗、有 DNS 缓存 TTL 滞后);Anycast(同一 IP 全球广播,BGP 自动选最近入口,切换快但连接可能中途跳区);应用层路由(边缘网关按 user_id 查「归属区」表再转发,最精确)。生产里通常组合用:Anycast 进边缘,边缘再按用户归属区做 L7 转发。
| 方案 | 切换速度 | 精度 | 代价 |
|---|---|---|---|
| GeoDNS | 慢(TTL 分钟级) | 粗(按 resolver) | 客户端缓存旧 IP |
| Anycast | 秒级(BGP) | 中 | TCP 连接可能跳区断 |
| L7 归属区路由 | 快(配置热更新) | 细(按 user) | 需维护路由表 |
# 边缘网关:按用户归属区路由,归属区故障则降级到备区
def route(user_id):
home = region_map.get(user_id) or geo_nearest(client_ip)
if health[home].ok:
return home
# 故障转移:选延迟次优且健康的区
for r in sorted(regions, key=lambda r: rtt[client_ip][r]):
if health[r].ok and r != home:
return r # 注意:切区后可能读到复制滞后的数据
现实案例:Netflix 用 Zuul 做边缘路由,可运行时改路由、故障时把整个区流量 evacuate 到另一区(Active-Active 博客)。Cloudflare 全球 Anycast,同一 IP 落到最近 PoP。AWS Route 53 提供 latency-based + geolocation routing 组合。
核心 trade-off:真·多活(各区都可写同一份数据)延迟最低但要解冲突;单写归属区无冲突但归属区不可用时写请求受阻;全局强一致(Spanner)无冲突但每次提交付跨区延迟。
【原理】三种范式:(a) Active-Active + LWW(Last-Writer-Wins):各区都能写,异步双向复制,冲突按时间戳取最新——简单但会丢更新(两区并发改同一 key,输的那次静默消失)。(b) 单写归属区(home region):每条数据有唯一 owner 区,写只在 owner 发生,其他区读副本、写要转发——无冲突,代价是跨区写延迟 + owner 故障时的可用性。(c) 全局共识 + TrueTime:Spanner 用 GPS+原子钟把时钟不确定性收敛到几 ms,Paxos 跨区同步提交,给外部一致性——延迟是跨区 RTT 量级。
# LWW 冲突解决(DynamoDB Global Tables 语义)
def merge(local, remote):
# 每条 item 带系统时间戳,取时间戳更大的胜出
return remote if remote.ts > local.ts else local
# 危险:ts 相等或时钟偏移时行为未定义 → 用逻辑时钟/version vector 更稳
选型心法:能容忍偶发丢更新的数据(点赞数、last-seen)用 LWW;不能丢的(账户余额、库存)用单写归属区或强一致 store;跨区计数用 CRDT(G-Counter 各区独立累加、合并取和,天然无冲突)。
现实案例:DynamoDB Global Tables 默认 MREC 模式 = 多活 + LWW,1-2 秒内跨区收敛(AWS 文档)。Google Spanner 用 TrueTime 做全球外部一致性事务(OSDI 2012 论文)。Netflix 用 Cassandra 多向异步复制承接跨区最终一致。
核心 trade-off:全局复制体验最好但违反 GDPR;严格驻留合规但欧盟用户去美国出差就访问不到自己数据(需就近 + 归属区兜底)。
【原理】合规要求 PII 物理留在特定法域。做法是按地理分区(geo-partitioning):以 region 作为分片键的一部分,欧盟用户的行只落在 eu 节点,复制拓扑也被约束在法域内。关键区分:敏感数据(PII)驻留 + 不出境,非敏感数据(商品、汇率)全局复制。难点是关联查询——订单在 eu、推荐模型在 us,得靠 ID 引用而非跨境 JOIN。
-- CockroachDB:把表按 region 分区并钉住位置
ALTER TABLE users SET LOCALITY REGIONAL BY ROW;
-- 每行有 crdb_region 列,eu 用户的行物理留在 eu 节点
-- Super Region 保证:既满足驻留,又能在 eu 内部跨区存活
陷阱:备份和日志也是 PII!很多团队主库驻留了,却把 binlog / 审计日志 / 分析管道统一汇到美国数仓——照样违规。合规边界要覆盖全数据生命周期。
现实案例:CockroachDB 的 REGIONAL BY ROW + Super Region 专门解「驻留 vs 区域存活」的两难(数据驻留文档)。Stripe / Salesforce 为欧盟客户提供 EU-only 数据处理选项。
核心 trade-off:读本地副本快但可能 stale;每次跨区确认最新则被 RTT 支配。优化的本质是把跨区往返从「每请求」降到「后台异步」。
【原理】三招:(1) 读本地写归属——读永远打本区副本(0 跨区),写才可能跨区,因为读远多于写。(2) 批量/流水线——把 N 次跨区调用合并成一次,或用异步复制而非同步等待。(3) 边缘计算——静态与可缓存内容在 PoP 就地返回,动态请求才回源。核心指标是 跨区往返次数,不是带宽——一次 180ms RTT 比传 1MB 还贵。
# 反模式:一个请求里串行 3 次跨区调用 = 3×180ms = 540ms
inv = call_us(check_inventory) # 跨区
price = call_us(get_price) # 跨区
tax = call_us(calc_tax) # 跨区
# 正解:本区副本 + 后台异步同步,请求路径 0 跨区
现实案例:Cloudflare Workers 在 300+ PoP 就近执行逻辑,避免回源跨洋。Netflix 把用户可见路径全部服务多区部署,请求不跨区(Active-Active)。
演进路线:单区 → 主备(DR,跨区异步复制 + 手动 failover)→ 读多活(写归属区、读全球副本)→ 全多活(各区可写 + 冲突解决)。多数公司停在「读多活」就够了——全多活的冲突治理成本极高。下一步瓶颈:跨区复制带宽与滞后(用 CDC + Kafka MirrorMaker 做管道);failover 演练(Netflix 用 Chaos Kong 定期真的关掉一个区验证);全局二级索引(跨区维护索引一致性是深水区,常退化成异步最终一致)。