4. 同城双活架构:架构设计、数据同步、流量调度策略
同城双活,说白了就是在一个城市里搞两个数据中心。两个中心都活着,都能处理交易。这不是简单的「主备切换」,而是真正的「双活」。我当年第一次接触这个架构时,心里也犯嘀咕:两个中心同时写数据,不乱套才怪?后来踩过坑才明白,关键在于怎么切分流量、怎么同步数据。
4.1 架构设计的核心思路
同城双活的核心,是「业务无感、数据一致、流量可控」。我习惯把架构分成三层来看:
- 接入层:负责流量入口,做负载均衡和健康检查
- 应用层:交易网关集群,无状态设计,随便扩缩
- 数据层:数据库、缓存、消息队列,这是最头疼的部分
你想想看,应用层无状态,随便哪个中心都能跑。但数据层有状态,两个中心同时写,怎么保证不冲突?
关键原则:同城双活不是「两个中心完全对称」,而是「业务单元化」。每个中心负责一部分用户或一部分业务,数据按规则归属。
举个例子。我在项目中做过一个期货交易网关。我们把用户按ID哈希分片,A中心负责奇数ID,B中心负责偶数ID。这样每个用户的数据只在一个中心写入,另一个中心只读备份。这就避免了双写冲突。
4.2 数据同步:心跳、复制与延迟
数据同步是同城双活最坑的地方。我记得有一次,两个中心之间的光纤被施工队挖断了,数据延迟飙到几百毫秒,交易直接卡死。嗯,从那以后我对数据同步格外敏感。
同城双活常用的数据同步方式有三种:
| 方式 | 延迟 | 一致性 | 适用场景 |
|---|---|---|---|
| 同步复制 | < 1ms | 强一致 | 核心交易、账户余额 |
| 半同步复制 | 1-5ms | 最终一致 | 订单状态、持仓信息 |
| 异步复制 | 5-50ms | 最终一致 | 行情数据、历史记录 |
我个人建议,核心交易用同步复制。虽然性能有损耗,但数据不会丢。非核心业务用异步复制,扛得住就行。
避坑指南:我曾经遇到过同步复制导致数据库连接池爆满的问题。原因是两个中心之间的网络抖动,同步请求排队,把连接池撑爆了。后来加了「超时熔断」机制,超过10ms就降级为异步,保证主库不被打死。
数据同步还有一个容易被忽略的点:心跳检测。两个中心之间要互相发心跳包,检测对方是否活着。我习惯用「三路心跳」:应用层心跳、数据库心跳、网络层心跳。三层都通才算正常,任何一层断了都要触发告警。
4.3 流量调度策略
流量调度,说白了就是「怎么把用户请求分到正确的中心」。我常用的策略有三种:
4.3.1 DNS 调度
最外层,通过DNS解析把域名指向不同中心的VIP。优点是简单,缺点是生效慢(DNS缓存)。我一般只用来做「兜底调度」,平时不用。
4.3.2 负载均衡调度
在接入层用LVS或Nginx,根据用户IP或Cookie做一致性哈希。比如:用户IP哈希后落在A中心,那他的所有请求都去A中心。这样能保证数据局部性。
# Nginx 一致性哈希配置示例
upstream backend {
hash $remote_addr consistent;
server 10.0.1.10:8080; # A中心
server 10.0.2.10:8080; # B中心
}
4.3.3 应用层路由
这是最灵活的方式。在交易网关内部,根据用户ID或业务类型,动态路由到对应的数据中心。我习惯在网关里维护一个「路由表」,定期从配置中心拉取。
// 路由表示例(伪代码)
Map<Integer, String> routeTable = new HashMap<>();
routeTable.put(0, "center_a"); // 用户ID哈希值0-4999 -> A中心
routeTable.put(1, "center_b"); // 用户ID哈希值5000-9999 -> B中心
String targetCenter = routeTable.get(userId % 2);
// 转发请求到 targetCenter
注意:流量调度最怕「脑裂」。两个中心都以为对方挂了,都抢着接管全部流量。我见过一次事故,两个中心同时接管,数据乱成一锅粥。解决方案是「仲裁机制」:引入第三个节点(比如ZooKeeper)做投票,超过半数同意才算故障。
4.4 整体架构图
下面这张图,是我画的一个同城双活交易网关的简化架构。你可以看到流量怎么从用户到网关,再到数据中心,以及两个中心之间怎么同步数据。
4.5 流量调度与数据同步的配合
光有架构图还不够,你得让流量调度和数据同步「配合起来」。我总结了一个口诀:读本地、写归属、同步保底。
- 读本地:用户请求到了哪个中心,就从哪个中心读数据。数据可能不是最新的,但延迟最低。
- 写归属:写入操作必须路由到数据归属的中心。比如用户ID哈希到A中心,那他的订单只能在A中心创建。
- 同步保底:两个中心之间通过数据同步,保证最终一致性。如果A中心挂了,B中心能接管全部流量。
个人经验:我习惯在网关里加一个「路由缓存」。每次路由决策后,把结果缓存10秒。这样即使配置中心挂了,网关也能按缓存继续路由。缓存过期后,如果配置中心还没恢复,就降级为轮询。这个机制帮我扛过两次配置中心宕机。
4.6 容灾切换的「三板斧」
同城双活最终要回答一个问题:一个中心挂了,怎么切?我总结了三板斧:
- DNS切换:把域名全部指向活着的中心。生效慢,但兜底。
- 负载均衡切换:修改Nginx/LVS配置,把流量全部分到活着的中心。秒级生效。
- 应用层路由切换:修改网关的路由表,把所有用户都路由到活着的中心。毫秒级生效。
我建议按「先应用层、再负载均衡、最后DNS」的顺序切。应用层最快,DNS最慢。但要注意,切换后数据可能不一致,得等数据同步追上。
血的教训:我曾经在切换时忘了关「数据同步校验」。结果两个中心的数据还在互相同步,导致死循环。从那以后,我每次切换前都会先暂停数据同步,切换完成后再恢复。这个步骤写进了我们的「切换SOP」,谁都不能跳过。
好了,同城双活的核心就这些。架构设计要分层,数据同步要分场景,流量调度要灵活。这三样做好了,同城双活就能跑得稳。下次遇到数据中心故障,你也能从容应对。