面试官问:“什么是异地多活?”
这题要是只回答一句“多个机房同时提供服务,提高系统可用性”,基本等于没答。
我更关心的是后面那半句:两个机房都在接流量,数据到底怎么写?
这才是异地多活真正麻烦的地方。
假设一个交易系统部署在北京和上海。
北京机房有:
Gateway
Order Service
Redis
MySQL
MQ
上海也有完整的一套。
正常情况下,北京用户可以进北京机房,上海用户进上海机房。两个机房都真正处理业务,不是一个干活、另一个机器开着吃灰。
这叫“多活”。
如果北京机房整体断电,流量切到上海,上海还能继续提供服务,这才体现出异地部署的价值。
但这里有个很容易混的概念。
北京主机房提供服务,上海只同步数据库,平时完全不接业务,这更接近异地灾备或者主备,不算真正意义上的多活。
我面试别人时,一般会继续追一句:
“那订单数据怎么办?北京上海两个库都能写吗?”
如果直接回答“双向同步”,我反而会有点担心。
因为数据库双写远没有画架构图那么轻松。
比如用户刚在北京修改收货地址:
北京:address = 上海浦东
网络还没同步过去,他的下一个请求被调度到了上海,而上海数据库里还是:
上海:address = 北京朝阳
用户一刷新,地址又变回去了。
更麻烦的是两个机房同时修改同一条数据。
到底听谁的?
靠时间戳?
机器时间未必绝对一致。
靠版本号?
那冲突怎么合并?
所以做异地多活,我一般不会一上来就追求“所有数据所有机房随便写”。
这种设计看着自由,后面处理数据冲突的时候一点都不自由。
比较常见的做法是给数据划归属。
比如按用户 ID 分片:
public final class RegionRouter {
private static final int BUCKETS = 1024;
public String locate(long userId) {
int slot = Math.floorMod(Long.hashCode(userId), BUCKETS);
if (slot < 512) {
return "cn-beijing";
}
return "cn-shanghai";
}
}
假设一个用户归北京,那么他的订单、账户之类的核心写请求,尽量固定落北京。
上海可以保留数据副本,也可以承担读请求,但别让同一份核心数据平时在两个地方乱写。
路由层大概会有这样的判断:
public OrderResult create(OrderCommand cmd) {
String home = regionRouter.locate(cmd.userId());
if (!localRegion.equals(home) && regionState.isHealthy(home)) {
return remoteOrderClient.forward(home, cmd);
}
return orderWriter.create(cmd);
}
代码不复杂,麻烦的是这个规则必须从入口一直贯彻到数据库、缓存、MQ。
否则 Gateway 把用户路由到北京,订单写北京库,结果库存服务又随机跑到了上海,前面的归属规则就白做了。
还有一个坑是跨机房调用。
北京 Order Service 每创建一个订单,都同步调用上海的会员服务:
北京 Order
|
| 跨机房 RPC
v
上海 Member
这种架构我第一眼就不太信。
机房内部一次 RPC 可能很快,跨城之后网络延迟、抖动、专线故障全进来了。调用链再长一点,一个请求跨机房来回跑几趟,接口耗时很容易被拖起来。
所以异地多活通常还有一条挺重要的原则:
请求尽量在一个单元内闭环。
能本地完成,就别跨机房同步调用。
需要同步的数据,通过 MQ、CDC、Binlog 之类异步过去。
例如订单状态同步,消费端首先要考虑的甚至不是“怎么消费”,而是幂等:
@Transactional
public void apply(OrderSyncEvent event) {
if (syncRecordDao.exists(event.eventId())) {
return;
}
orderReplicaDao.saveOrUpdate(
event.orderId(),
event.status(),
event.version()
);
syncRecordDao.markDone(event.eventId());
}
因为跨机房复制出现重试、重复消息很正常。
不做幂等,故障切换还没开始,数据先被自己重复处理坏了。
那机房真挂了怎么办?
比如北京彻底不可用。
这里也不是 DNS 一切,上海接住流量就结束了。
至少得判断几件事:
北京最后一批数据有没有同步完成?
上海的数据落后多少?
那些原本属于北京的数据,现在是否允许上海接管写权限?
北京恢复以后,新增数据怎么回同步?
双边会不会同时认为自己是主?
最后这个最危险。
两个机房网络断了,但机器都活着,北京觉得上海死了,上海也觉得北京死了,然后两边同时开放写入。这就是典型的脑裂。
所以异地多活真正难的从来不是“多部署几套服务”。
机器复制一套不难。
难的是流量路由、数据分片、跨机房同步、故障检测、写权限转移、脑裂保护,以及故障恢复之后怎么把数据重新收回来。
面试里如果让我压缩成一句话,我会这么回答:
异地多活是把完整业务能力部署到多个地域,并让多个地域同时承载真实流量;它解决的是单机房故障下的业务连续性,但真正的设计重点不是部署,而是怎么控制数据写入和故障切换。
答到这里,面试官如果继续问“双活数据库怎么保证一致性”,这题才算真正开始。类似这类架构问题,在云栈社区也经常被拿出来讨论。