一、问题解析
在业务初一、问题解析期,考虑到投入成本,很多公司通常只用一个机房提供服务。但随着业务发展,流量不断增加,我们对服务的响应速度和可用性有了更高的要求,这时候我们就要开始考虑将服务分布在不同的地区来提供更好的服务,这是互联网公司在流量增长阶段的必经之路。
之前我所在的公司,流量连续三年不断增长。一次,机房对外网络突然断开,线上服务全部离线,网络供应商失联。因为没有备用机房,我们经过三天紧急协调,拉起新的线路才恢复了服务。这次事故影响很大,公司损失达千万元。
经过这次惨痛的教训,我们将服务迁移到了大机房,并决定在同城建设双机房提高可用性。这样当一个机房出现问题无法访问时,用户端可以通过HttpDNS接口快速切换到无故障机房。
为了保证在一个机房损坏的情况下,另外一个机房能直接接手流量,这两个机房的设备必须是1:1采购。但让其中一个机房长时间冷备不工作过于浪费,因此我们期望两个机房能同时对外提供服务,也就是实现同城双机房双活。
对此,我们碰到的一个关键问题就是,如何实现同城双活的机房数据库同步?
24.1 核心数据中心设计
因为数据库的主从架构,全网必须只能有一个主库,所以我们只能有一个机房存放更新数据的主库,再由这个机房同步给其他备份机房。虽然机房之间有专线连接,但并不能保证网络完全稳定。如果网络出现故障,我们要想办法确保机房之间能在网络修复后快速恢复数据同步。
有人可能会说,直接采用分布式数据库不就得了。要知道改变现有服务体系,投入到分布式数据库的大流中需要相当长的时间,成本也非常高昂,对大部分公司来说是不切实际的。所以我们要看看怎么对现有系统进行改造,实现同城双活的机房数据库同步,这也是我们这节课的目标。
核心数据库中心方案是常见的实现方式,这种方案只适合相距不超过50公里的机房。
在这个方案中,数据库主库集中在一个机房,其他机房的数据库都是从库。当有数据修改请求时,核心机房的主库先完成修改,然后通过数据库主从同步把修改后的数据传给备份机房的从库。由于用户平时访问的信息都是从缓存中获取的,为了降低主从延迟,备份机房会把修改后的数据先更新到本地缓存。
与此同时,客户端会在本地记录下数据修改的最后时间戳(如果没有就取当前时间)。当客户端请求服务端时,服务端会自动对比缓存中对应数据的更新时间,是否小于客户端本地记录的修改时间。
如果缓存更新时间小于客户端内的修改时间,服务端会触发同步指令尝试在从库中查找最新数据;如果没有找到,就把从主库获取的最新数据放到被访问机房的缓存中。这种方式可以避免机房之间用户数据更新不及时的问题。
除此之外,客户端还会通过请求调度接口,让一个用户在短期内只访问一个机房,防止用户在多机房间来回切换的过程中,数据在两个机房同时修改引发更新合并冲突。
总体来看,这是一个相对简单的设计,但缺点也很多。比如如果核心机房离线,其他机房就无法更新,故障期间需要人工切换各个proxy内的主从库配置才能恢复服务,并且在故障过后还需要人工介入恢复主从同步。
此外,因为主从同步延迟较大,业务中刚更新的数据要延迟一段时间,才能在备用机房查到,这会导致我们业务需要人工兼顾这种情况,整体实现十分不便。
这里我给你一个常见的网络延迟参考:
- 同机房服务器:0.1 ms
- 同城服务器(100公里以内) :1ms(10倍 同机房)
- 北京到上海: 38ms(380倍 同机房)
- 北京到广州:53ms(530倍 同机房)
注意,上面只是一次RTT请求,而机房间的同步是多次顺序地叠加请求。如果要大规模更新数据,主从库的同步延迟还会加大,所以这种双活机房的数据量不能太大,并且业务不能频繁更新数据。
此外还要注意,如果服务有强一致性的要求,所有操作都必须在主库“远程执行”,那么这些操作也会加大主从同步延迟。
除了以上问题外,双机房之间的专线还会偶发故障。我碰到过机房之间专线断开两小时的情况,期间只能临时用公网保持同步,但公网同步十分不稳定,网络延迟一直在10ms~500ms之间波动,主从延迟达到了1分钟以上。好在用户中心服务主要以长期缓存的方式存储数据,业务的主要流程没有出现太大问题,只是用户修改信息太慢了。
有时候,双机房还会偶发主从同步断开,对此建议做告警处理。一旦出现这种情况,就发送通知到故障警报群,由DBA人工修复处理。
另外,我还碰到过主从不同步期间,有用户注册自增ID出现重复,导致主键冲突这种情况。这里我推荐将自增ID更换为“由SnowFlake算法计算出的ID”,这样可以减少机房不同步导致的主键冲突问题。
可以看到,核心数据库的中心方案虽然实现了同城双机房双活,但是人力投入很大。DBA需要手动维护同步,主从同步断开后恢复起来也十分麻烦,耗时耗力,而且研发人员需要时刻关注主从不同步的情况,整体维护起来十分不便,所以我在这里推荐另外一个解决方案:数据库同步工具Otter。
24.2 跨机房同步神器:Otter
Otter是阿里开发的数据库同步工具,它可以快速实现跨机房、跨城市、跨国家的数据同步。如下图所示,其核心实现是通过Canal监控主库MySQL的Row binlog,将数据更新并行同步给其他机房的MySQL。
因为我们要实现同城双机房双活,所以这里我们用Otter来实现同城双主(注意:双主不通用,不推荐一致要求高的业务使用),这样双活机房可以双向同步:
如上图,每个机房内都有自己的主库和从库,缓存可以是跨机房主从,也可以是本地主从,这取决于业务形态。Otter通过Canal将机房内主库的数据变更同步到Otter Node内,然后经由Otter的SETL整理后,再同步到对面机房的Node节点中,从而实现双机房之间的数据同步。
讲到这里不得不说一下,Otter是怎么解决两个机房同时修改同一条数据所造成的冲突的。
在Otter中数据冲突有两种:一种是行冲突,另一种是字段冲突。行冲突可以通过对比数据修改时间来解决,或者是在冲突时回源查询覆盖目标库;对于字段冲突,我们可以根据修改时间覆盖或把多个修改动作合并,比如a机房-1,b机房-1,合并后就是-2,以此来实现数据的最终一致性。
但是请注意,这种合并方式并不适合库存一类的数据管理,因为这样会出现超卖现象。如果有类似需求,建议用长期缓存解决。
Otter不仅能支持双主机房,还可以支持多机房同步,比如星形双向同步、级联同步(如下图)等。但是这几种方式并不实用,因为排查问题比较困难,而且当唯一决策库出现问题时,恢复起来很麻烦。所以若非必要,不推荐用这类复杂的结构。
另外,我还要强调一点,我们讲的双活双向同步方案只适合同城。一般来说,50~100公里以内的机房同步都属于同城内。
超过这个距离的话,建议只做数据同步备份,因为同步延迟过高,业务需要在每一步关注延迟的代价过大。如果我们的业务对一致性的要求极高,那么建议在设计时,把这种一致性要求限制在同一个机房内,其他数据库只用于保存结果状态。
那为什么机房间的距离必须是100公里以内呢?你看看Otter对于不同距离的同步性能和延迟参考,应该就能理解了。
具体表格如下所示:
为了提高跨机房数据同步的效率,Otter对用于主从同步的操作日志做了合并,把同一条数据的多次修改合并成了一条日志,同时对网络传输和同步策略做了滑窗并行优化。
对比MySQL的同步,Otter有5倍的性能提升。通过上面的表格可以看到,通过Otter实现的数据同步并发性能好、延迟低,只要我们将用户一段时间内的请求都控制在一个机房内不频繁切换,那么相同数据的修改冲突就会少很多。
用Otter实现双向同步时,我们的业务不需要做太多改造就能适应双主双活机房。具体来说,业务只需要操作本地主库,把“自增主键”换成“snowflake算法生成的主键”、“唯一索引互斥”换成“分布式互斥锁”,即可满足大部分需求。
但是要注意,采用同城双活双向同步方案时,数据更新不能过于频繁,否则会出现更大的同步延迟。当业务操作的数据量不大时,才会有更好的效果。
说到这里,我们再讲一讲Otter的故障切换。目前Otter提供了简单的主从故障切换功能,在Manager中点击“切换”,即可实现Canal和数据库的主从同步方式切换。如果是同城双活,那关于数据库操作的原有代码我们不需要做更改,因为这个同步是双向的。
当一个机房出现故障时,先将故障机房的用户流量引到正常运转的机房,待故障修复后再恢复数据同步即可,不用切换业务代码的MySQL主从库IP。切记,如果双活机房有一个出现故障了,其他城市的机房只能用于备份或临时独立运行,不要跨城市做双活,因为同步延迟过高会导致业务数据损坏的后果。
最后,我再啰嗦一下使用Otter的注意事项:第一,为了保证数据的完整性,变更表结构时,我们一般会先从从库修改表结构,因此在设置Otter同步时,建议将pipeline同步设置为忽略DDL同步错误;第二,数据库表新增字段时,只能在表结尾新增,不能删除老字段,并且建议先把新增字段同步到目标库,然后再同步到主库,因为只有这样才不会丢数据;第三,双向同步的表在新增字段时不要有默认值,同时Otter不支持没有主键的表同步。
二、粉丝福利
- 我根据我从小白到架构师多年的学习经验整理出来了一份50W字面试解析文档、简历模板、学习路线图、java必看学习书籍 、 需要的小伙伴斯我一下,或者评论区扣“求分享”