GMTC 全球大前端技术大会 8 折涨价倒计时 2 天,现在购票立减 ¥960 ! 了解详情
写点什么

超 3 亿活跃用户的多活架构,数据同步与流量调度怎么做?

2021 年 4 月 12 日

超3亿活跃用户的多活架构,数据同步与流量调度怎么做?

一、多活业务架构

1、OPPO 多活架构原则

第一,主线多活。


多活成本比较高的,双活是两倍,三活可能成本会低一些,但三活的难度更大。因此没有办法对所有业务进行多活,只能对主线做多活。


第二,是保障多数用户。


举个例子,系统有个充值的功能,充值功能本身是强一致的,完全不能允许任何的延迟或者是副本的读。


但是多活切换之后,只有少数用户在切换的前几分钟有充值的,这部分用户余额可能没有通过过去,只需要对这部分用户进行服务降级,其他绝大多数用户是可以使用完整的服务的。


第三,数据分类,应用不同的 CAP 模型。


CAP 定理不是针对的业务功能,比如说账号、支付、登录,CAP 定理是对数据的要求。一个功能可能用到多个数据,数据本身的一致性、可用性、延迟的容忍是不一样的。


所以需要对业务功能用到的数据进行分类,比如余额数据、流水数据、日志数据、个人资料数据……我们对每个数据进行一致性、可用性的需求分析,一致性要求很强,这个数据就选用同城高可用的数据库服务。这个数据一致性要求不高、允许延迟,就可以选择异地高可用的数据库服务。


所以这个业务来说不是整体使用一个 CAP 模型,在业务内部,因为不同的数据分类,使用了不同的模型,因此业务有时候存在部分降级的情况。


第四,平台业务 SDK 化。


OPPO 的业务比较多,比如浏览器、软件商店、广告务、音乐、视频等非常多的业务,这些业务都用了平台化的服务,比如评论系统、消息系统,还有账号鉴权的系统等等。


OPPO 公司的机房比较多,主要的就有好几个机房,我们的上层业务是分布在不同的机房里面去,这对平台业务来说就比较麻烦,上层业务可能只需要做双活就行了,而平台业务可能就要做七活、甚至八活,而且七、八个机房都要有读和写,难度就非常大。


为解决这个问题,我们提出平台业务进行 SDK 化思路,把这种平台型业务,拆分成独立的域名,从 SDK 开始拆分,这样我们平台业务只需要单独做多活就行了,不需要在每个机房都提供读写的能力。


第五,数据最终一致。


第六,我们的记录日志、流水,避免修改、计数操作。

2、同城多活业务架构


上图是典型同城多活的业务架构,应用层是完全无状态的,随便打流量。四层采用 DPDK 技术开发,七层包括 Nginx 和 API 网关两个组件,Nginx 只用来做 SSL 卸载、WAF 防火墙,其他功能都是 API 网关来提供。


数据层以主备为主,写流量只会写到 Master 节点,但是读的流量可以访问 slave 节点,但是也不一定,看业务本身数据一致性要求,如果要求非常强一致的,我们的读也只会指向 Master 节点。


需要注意,我们把 Nginx 和 API 网关都放到同一个容器中,两只之间采用进程间通信。这样的好处是,我们扩容的时候,我们可以将整个七层同步去扩容,而不会存在某一层组件容量不足的情况。


另外就是注册中心,我们没有使用 k8s 本身的一个注册功能,而是自己基于数据库,实现了 AP 模型的注册中心,保证注册中心的跨机房高可用。同时注册中心兼容 Consul 协议,从而更好的融入开源生态。多个 k8s 集群的实例,都会注册到统一的注册中心里面去。这个注册的动作,是由发布平台完成的,好处是应用发布的时候,发布平台可以提前摘掉流量,避免重启影响服务的成功率。

3、异地多活业务架构——单元化


异地多活,比较典型的架构是单元化,就是将用户进行分片,将不同的用户分片放到不同的机房里面去,这样可以做到一个完全的扩展,随着用户规模的增加,我们可以很容易去扩展机房的数量,这些都可以持续的去增加的,包括每个机房的容量也可能不一样。比如有的机房大,有的机房小,我们可以调整每个机房存放的单元数量。


这里确实实现了多活,每个机房都有流量,每个机房也是读写,是完全的多活,但是单元高可用的问题如何解决,单元的归属机房故障了,如果把这个单元转移另一个机房继续提供服务。

4、异地双活业务架构


上图是我们使用较多的异地双活架构,首先我们将用户按照地域维度进行了一个单元划分,比如说按照地域将用户划分为七个大区单元。


注意这个单元划分是用户首次访问服务的时候进行的,然后客户端就保存了单元号,就不会产生变化了,所以用户出差,换到因为另外一个地域里面去,它所属的单元号我们是不会变化的,还是访问单元归属的机房,这个时候可能就不是访问最优的机房。


这样的好处是,当一个用户移动的时候,数据访问就不会在两个机房之间跳来跳去,避免双向同步的数据冲突问题,很容易实施。


数据层在两个机房,都是完全全量的,两个机房间数据是做双向同步,没有谁是主谁是从的区分,是完全对等的架构。


用户流量调度按单元进行,这样可以保证一个用户,他只会访问其中一个机房,不会在南北两个机房之间跳来跳去,就算是用户出差也是如此,按照首次访问服务时的地域来划分的单元。只要我们的调度规则没有变更的情况下,一个用户他永远只会在其中一个机房读写,这样的好处就是,第一个可以避免我们的同步的冲突,第二个好处就是容忍了数据延迟的情况,比如说一个用户他永远是看到北方机房,南北之间数据同步的延迟日常情况下其实是感知不到的。


这个架构是非常简单,只需要在客户端网络库里面做一些封装,对用户进行单元划分,按单元进行流量调度就可以了,双向同步比较好实施,延迟、冲突,这些问题都可以避免。


除了地域之外,也可以按照账号或者设备来划分单元。按账号或者设备划分单元的好处是,如果按照地域划分单元,在用户删除手机 APP 这种情况下,APP 里面保存的单元号就没有了,下次访问服务的时候就需要重新分配单元号,因为地域可能和之前不同了,就可能分配到不同的单元号,按账号或者设备划分就没有这个问题,重新分配还是原来的单元号。


前面说到,南北机房的数据层都是全量的,一般情况下,按地域的划分单元的模式,就算重新分配了单元号,也不影响数据的读写访问。

5、异地双活——评论系统案例


上图是平台型业务-评论系统异地生活的案例,评论系统从 SDK 开始,就进行了域名拆分,避免了在业务域名所在机房内部去做跨机房的评论服务调用,影响服务的可用性和性能。


如上图所示,我们只对 MySQL 原始数据层做了南北双机房同步,第二层的评论元数据表,还有第三层的一个 Cache,这两层实际上没做同步的。两个机房分别基于 MySQL 数据独自去重建第二层的元数据表,第三层的 Cache,以及重建其他的数据源。


这样好处就是,我们只有一个数据源做了南北机房的同步,就可以避免双数据源同步的时候,两个数据源之间会存在同步的进度不一致,从而两个数据源之间的依赖关系出现问题。


举个例子,我们上面的评论表、点赞表这一层,最上面这一层做了同步以后,我们中间第二层如果也做了同步,然后第二层同步以后,两个数据可能存在差异,比如说第一层同步快一点,第二层同步得慢一点,同样是南方的用户,他们看到这个数据之间的存在不匹配的问题。


因为用户流量调度是按单元进行的,两个机房的数据虽然有差异,有延迟,但是用户感知不到的。一个用户要么看到南方机房,要么看到北方机房,我们评论数量两个机房有差异,点赞数量有差异,回复数都有差异,但是无所谓,用户是感知不到差异的。需要注意的一点,就是当多活切换的时候,用户能感知到一个差异,但日常情况下用户感知不到这个差异。

6、异地 N 活业务架构


上图是比较复杂异地 N 活业务架构。它基本的思路就是对用户进行两级的划分。第一级按照设备和账号划分单元,其中单元里面既有登录的用户,也有未登录的用户。


在第二级划分的单元内部,我们再应用异地双活的模式,或者是同城多活的模式,比如说左边单元 1,按照地域做第二级的划分,把它划分成南北两个副本,既然是副本,肯定数据是全量的,是异地双活模式,两个副本数据做双向同步,这种模式适用非强一致的业务。


那么强一致的业务怎么办呢?比如右边的单元 4,跨同城的两个机房,单元内部采用同城多活的模式,就是共享跨机房高可用的数据层,是主备的的。这种模式适合强一致的业务。


前面说了单元内部主要两种模式,第一种是异地双活,双向同步,主主模式,读写在本机房,然后做双向同步;第二种是同城多活,主备的模式,跨机房共享主备切换的数据层。除此之外,单元内部还可以选择主从,冷备等模式。

7、服务部署


上图是服务部署架构。服务部署分为几大部分。


第一部分是中心域。


中心域主要是部署一些运营管理后台,还有一些爬虫,还有一些非常长尾的应用,但这些业务可能不太重要,也不需要做一个多活。中心域的读写都是在中心机房,然后把数据单向同步到其他单元机房。


第二部分是全局域。


全局域主要存放非单元分片维度的数据,比如评论、消息等。这些数据不能按统一维度进行拆分,需要全量的访问,放到全局域的数据都是全量的。


第三部分是单元域。


存放按单元拆分的数据,比如用户订单、收藏、下载记录等。

8、服务路由


用户会先请求到 API 网关,API 网关根据请求的单元号参数,判断是是否访问错了机房,如果访问错了,就做重定向,或者跨机房转发,用户自己选择的其中一种模式。转发的模式比较依赖于两个机房之间的专线的带宽和稳定性,重定向模式机房之间的带宽要求会低一些,客户端重新发起请求,这两个机房之间的网络专线要求低一些。


前面说到用户首次请求的时候,会给客户端分配一个单元号,这个单元号将会存储起来,以后每次业务请求都会带上这个单元号。


请求到了单元内部,单元号会做一个全链路的传递,全链路传递是通过调用链来实现的,调用链可以把一些参数做全链路的传递。应用实例打上了单元号的标签,微服务调用方通过单元号对实例进行筛选,防止请求打到其他单元。


数据访问层要做一个兜底的操作,可能由于服务路由还是其他的一些原因,不小心访问错了单元,这个数据层有可能访问错,所以数据访问层要做一个兜底,根据传过来单元号,做拒绝或者转发。

9、用单元化解决业务扩展性问题


单元化不只是可以用来解决多活的问题,也可以用来解决业务扩展性问题。在一个机房内部,如果服务 1000 万用户,他可能需要 10 个数据库,服务 1 亿个用户,需要 100 个数据库,如果 100 个数据库让每个应用实例都连上的话,连接数就太多了。


可以在一个机房内部也拆分多个单元,每个单元保证 1000 万、2000 万左右的用户,随着用户的增长,我们再将单元数量进行增加就行了,这样就可以保证每一个单元内部的服务规模受控。

二、多活数据同步

1、MySQL 同城多活


上图是 MySQL 同城多活架构,MySQL 对外看上去是一个集群,只有一个 IP。我们需要解决的问题是:怎么让跨机房的集群看到的是同一个 IP?这里就用到了 Anycast 技术,IP 的作用可以理解为域名,我们把一个 VIP 用 Anycast 技术,将它路由到两个机房,或者是三个机房。我们是路由到三个机房,然后就到了机房内部,再通过 ECMP 协议将流量再分到多个四层负载均衡节点。


通过 Anycast 第一层路由到不同的机房,第二层的 ECMP 再路由到基于 DPDK 技术开发的四层负载均衡节点。这样我们整个的数据库对外看到的 VIP 就是同一个了,所有机房看到 VIP 都是同一个。利用 Anycast 和 ECMP 两个技术,实现跨 AZ 共享 VIP。


然后是数据层,数据层我们现在是一主三从,然后需要 2 个以上 slave 同步成功,才能完成最终的成功。


MySQL 版本需要 5.7 以上,操作系统内核需要打一个 toa 补丁,这样经过四层负载均衡之后,MySQL Server 才能拿到真正来源 IP。因为我们这边要做一个 IP 白名单的授权,如果不打补丁,拿到的来源 IP 就是四层负载均衡的 IP,就没法做 IP 白名单授权了。当然 top 补丁有一个缺陷,就是只能支持 ipv4,这在内网使用问题不大。


底层采用了开源的 MySQL 拓扑管理组件,通过检测我们数据库节点的情况,然后做重新选组做切换,然后通知 SLB 改变后端指向,流量打到新的 master 节点,


Anycast 不是必须的,也可以用域名代替,但是域名有个问题,需要重新接连的时候才会发起解析,所以域名切换的时候可能会切不干净。Anycast 做切换是立即生效的,因为这是路由协议的一个变更,马上就能切过去,不存在解析不干净和生效不一致的问题。


Anycast 除了内网之外,外网也用的比较多,比如说谷歌上负载均衡器,它发布的 IP 就是 Anycast 的 IP,在公网环境下,在不同的地区路由到不同的一个真实地址,包括我们 DNS Server 也是用 Anycast 去发布的,在不同的区域,路由到就近的 IDC,所以 Anycast 技术应用还比较广泛。

2、MySQL 异地多活


上图是 MySQL 的异地多活架构,重点在于提升同步的性能,从源库订阅到数据以后,不是直接写目标库,而是先存起来,在目标机房部署中继日志模块。这样的好处是,我们可以在网络上快速的传输过去,中继日志并行去写目标库。


这个设计性能提升非常大,OPPO 实际业务场景下,这个模式比订阅后直接写目标库提升了几倍。因为引入了中继日志,就存在两阶段提交的问题。比如中继日志写成功,但是中继日志写目标库没有成功。这就存在数据一致性问题,需要用到两阶段提交。


还有就是数据压缩和加密,对数据的安全和同步性能也非常重要。

然后是多消费者支持,订阅模块会保存数据,每个订阅方可以维持自己的消费位点,彼此之间没有干扰,从而减少多订阅方同步对 Source DB 的压力。

3、MySQL 订阅——数据最终一致


以前面提到的评论系统为例,数据同步只同步 MySQL 那一层,而其他的数据源 Cache、MQ、ES、排序服务等,分别订阅 MySQL binlog 重新构建。


原则上,我们尽量只同步底层的一份 MySQL 数据,其他数据源订阅 MySQL 重建。前面说到,MySQL 只需要订阅一次,Jins 程序自己存储了一份数据到本地文件队列,然后分别重放到 Cache、MQ、ES 等其他数据源,也可以多次重放数据。


如果多数据源分别进行同步的话,多个数据源同步的进度是没法保证协调一致的,必然有的数据源快,有的数据源慢,这有可能导致两个数据源之间的关联关系出现一些程序错误。所以我们尽量只同步一个数据源,再基于 MySQL 重建其他的数据源,避免进度不一致的问题。

4、MySQL 数据对比 &修复


OPPO 的业务场景,很多地方都非常依赖底层的 MySQL 数据同步,两个机房之间之间到底有没有差异,是蛮重要的。


因此我们设计了一个独立的 MySQL 比对修复工具,就执行上图这样一个 SQL 语句,通过这个 SQL 语句,对一段时间之内的所有数据算一个异或的值,通过异或值去比对两个机房之间数据差异,如果比对有差异,我们再缩小比对范围,逐步逼近到差异的记录行,这个语句的执行效率还是蛮高的。


但是这个方案有个不足,要求我们数据库里面有一个时间戳的字段,程序会对比前一个周期内的所有记录的异或值,判断两个机房之间数据是否有差异。


另外一重要场景就是数据修复,因为业务可能配置错了数据库、应用实例配置生效不一致,再比如 A 单元数据写到 B 单元,这个时候需要修复数据,通过这个工具,把两个数据库不一致的数据行整理出来,然后人工做识别或者批量修复。

5、Redis 多活


Redis 同城多活的架构如上图所示,我们在 Redis Server 上面做了一层代理,下层 Redis Server 没有使用 Redis cluster 技术,代理将流量进行分片,分发到了不同的 Redis Group 里面去,每个 Group 里面就是普通的 Redis 主从。


主从之间采用 binlog 的同步,因为 Redis 本身没有 binlog,我们把 AOF 做了改造,把让它变成 binlog 的这种格式,这里改造的工作量不大。


然后代理也支持两种模式,一种是重定向模式,一种是转发模式。转发模式就是写主读从,它只会把写流量转到了主机房里面去,但是从机房是能读的。重定向模式就不一样,重定向模式是非常更强一致的,读写都只能在主机房。


前面反复提到,CAP 是针对数据的,是指数据本身的延迟或者差异的容忍度,所以这两种模式都需要支持,有的数据它就是要强一致,一定要到主库里面的去读,但有的数据它允许从库读,允许延迟。



异地多活也很简单,异地多活两个机房各部署一个组件去订阅同机房的 Redis,订阅 Redis 的 binlog,订阅的数据写到 MQ 里面去,两个机房分别重放 binlog,实现起来并不复杂。


最后简单说一下 binlog 的格式,里面包括了命令、数据产生的机房、递增的序号,还有一个时间戳。还需要注意的一点,Redis 持久化 RDB 也要改造一下,RDB 需要包含一个 binlog offset,binlog 读取偏移量,需要把它记下来,因为主从颠倒的时候,我们订阅程序要重新从 offset 开始继续订阅下面的命令。

三、GSLB 流量调度

1、Http DNS


最后讲我们的 GSLB 流量调度,首先是为什么要使用 Http DNS。


第一个是防劫持。


DNS 劫持,DNS 是多级缓存,部分环节存在解析劫持的情况。


DNS 黑洞,这个大家可能遇到比较少,什么叫 DNS 黑洞呢?就是运营商监控到某个域名有恶意的请求,封杀他的时候不小心扩大了封杀的范围,我们已经出现过几次这种情况,有时候某个地区甚至可以把整个 cn 顶级域名全封杀,这种封杀的范围很大,称之为 DNS 黑洞。整个 2020 年已经发生过多次这种情况了,某个地域整个顶级域名都给你封杀掉,大家都解决不了。


第二个是快速生效。


首先是 DNS 本身的多级缓存,这个时间不受控制,但它可能不是主要问题,更主要的问题是客户端长连接。


我们还没上 Http DNS 之前,业务使用了客户端长连接,需要 20 分钟甚至一个小时才能大部分流量调度走。主要的原因就在客户端长连接,DNS 做了变更以后,只有客户端重新发起连接的时候,它才会发重新发起解析,才拿到新的 IP,如果连接没断开,就一直不会转移,所以这部分长连接用户根本就切不走。


如果是机房入口网络故障还好,连接天然会断开,如果是因为业务自己的问题,需要把流量切走,这种情况下就会发现根本切不走,所以客户端长连接是比较重要的问题。所以客户端网络库需要处理一下,解析变更的时候,需要主动去关闭连接,但是传统 DNS,没有解析变更的通知机制,不发起解析就不知道解析变更了,这里就进入了循环了,需要仔细的思考一下流程。


第三个是精准调度。


传统的 DNS 解析只能获取到 IP 这一个参数,首先 IP 信息不准确,包括运营商归属、地域归属,都不是很准确,国外运营商特别多,情况更严重。现在 IPv6 也在快速的推广,信息不准确的情况更为严重。其次传统 DNS 无法做到用户维度、设备维度的解析。


最后是生效一致性。


单元一旦发生调度以后,在单元内的所有用户要同时调走,不能说一部分先调走,一部分后调走,这样数据写入就乱了,需要保证全体用户生效的一致性。

2、单元调度


下面讲单元调度的主要流程


第一步: 划分用户单元


划分用户单元主要有三种模式:


  • 按设备划分单元;

  • 按账号划分单元;

  • 按地域划分单元。


这里有个地方需要注意,我们为了划分单元,客户端肯定要传一些参数,如果按账号划分单元,需要传账号 ID;按设备划分单元,需要传设备的 IMEI,或者国内 Android 厂商推行的 OpenID;按地域划分单元很简单,直接从 IP 里面可以获取,不用客户端传递参数。


因为隐私合规要求,比如说海外业务,直接传用户的 ID 或设备信息,是违规的,因为我们这个调度的域名它是一个独立的域名,它不是业务本身的,这个域名很难跟用户解释,即使跟用户签了协议,因为业务主体的不同,可能也不一定包含了这个域名,所以我们做了一个匿名化处理,设计了两个新参数,一个叫 ADG(匿名设备分组),一个叫 AUG(匿名用户分组)。


我们将账号 ID 和 10 万取模的值定义为 AUG,设备 ID 与 10 万取模的值定义为 ADG。通过这种方式,把设备和账号分成 10 万个桶,然后对桶分单元,比如说 1~5000 桶是单元 1,5000~1 万桶是单元 2。这样我们就不用传真实的设备 ID 和真实账号。


第二步:客户端获取单元号。


客户端首次访问业务的时候要分一个单元号,这样就算按地域划分单元,基本上也不会出现变更,只要用户的 APP 不被删除,我们 OPPO 手机的好处就是,我们的 APP 是不怕被删除的,我们的数据不会被清掉;但如果是一个外发的 APP,可能就存在 APP 删除,这个可以考虑用设备或者账号分单元。获取到单元号之后,就永久保存在客户端。


第三步:客户端解析域名 IP。


域名解析的时候会带上单元号的参数,获取这个单元对应的 IP 列表,然后客户端缓存 IP 列表。需要注意的一点是缓存机制,建议根据网络环境进行缓存,比如 WiFi 名称,或者运营商的名称,底层的缓存数据结构就是域名加上网络环境的名称。这样的好处就是,用户网络切换的时候,比如说家里面是 WiFi,我们拿的是 IP1,我们一出门,网络环境变了,我们取出的缓存 IP 就是 IP2,在每个网络环境都是缓存最优的 IP。


另外一点需要注意是:我们为每个单元还分配了一个单元域名,这是一个传统 DNS 域名,主要是降级的时候使用。可以设想一下,如果我们没有为每个单元单独分配一个传统 DNS 域名,一旦降级的时候就会走到业务的主域名,而传统 DNS 是不能携带任何参数的,无法做到按单元进行解析,用户流量就全都乱了。


所以每个单元分配一个域名的好处就是,降级的时候只要降级到我们这个单元的域名,这样大多数用户解析结果还是准确的,不准确的一部分通过 API 网关重定向或者内部转发,只要很少用户需要走这个路径,绝大多数用户还是最佳的路径。


第四步:客户端重定向。


因为调度过程当中还有一部分用户在访问旧的 IP,我们是通过 API 网关,把新机房 IP 直接告诉客户端,客户端立即用新 IP 重试,并且异步去刷新解析,如果只是反馈一个状态码,告诉客户端需要重新刷新解析,客户端的总请求时间就会拉得比较长,这就是重定向模式。


但除了重定向模式,还有转发模式,但是转发模式比较依赖机房之间的专线带宽和稳定性,如果公司规模不是很大的话,机房之间的专线带宽和稳定性可能赶不上公网,重定向模式可能更适合一些。

3、单元调度注意事项

数据层联动,举一个用户余额充值的例子,这是非常强一致的,我们可以维护一个数据不一致用户清单,比如说有用户刚刚进行了充值,这个数据还没在各个机房达成一致,机房调度的时候,只是这一部分用户需要进行服务降级,其他用户还可以继续提供完整的服务。

4、域名解析刷新时机


接下来讲域名解析刷新时机。因为 HttpDNS 是直连解析的,不像传统 DNS 有多级的缓存,如果我们还沿用传统 DNS 的 TTL 方式来刷新解析,这个 TTL 就不能设置的太短,太短了 HttpDNS Server 的压力非常大。TTL 设置过长又不能满足业务快速恢复的要求。


所以域名解析的及时刷新依赖另外两种途径,第一种途径是失败。我们请求一个服务,要么连接错误,要么响应内容出现错误,比如说我们响应了 500,或者其他我们认可的一个响应值(客户端可以自己定一个规则),我们访问失败的时候,就需要立即去刷新一下域名解析,因为请求失败的时候可能需要做一个机房调度,不管是业务后端出现了问题,或者是连接不上,这种情况都需要做机房调度,需要客户端刷新解析。


第二种途径是指令,如果是因为我们带宽不足,做活动,或者其他原因的,需要把流量切走,这时我们可以通过 API 网关下发指令,下发指令也是随着 API 网关的正常的业务请求,响应 Header 带下去,不是单独的通道,也不是通过 Push 推送。


这样我们就可以兜底,要么会请求失败,会立即刷新解析。要么请求成功,响应 header 就会携带指令。所以用户一定能走到失败和指令其中一条路径。因此我们做了调度变更以后,用户一定会刷新,不再依赖 TTL 了,过期时间可以设置非常长,这样我们绝大多数请求,都不会发生真正的解析请求。


通常情况下,传统 DNS 有 2%~3%的解析失败率,还是挺高的。通过这种方式,我们就可以把解析成功率做到 99.5%以上,日常情况下甚至能做到接近 100%。

5、调度生效一致性


下面讲一讲调度生效的一致性,当我们的客户端降级到传统 DNS 的时候,就会解析到错误的机房,在调用生效过程当中,也会访问到旧的机房,所以我们在 API 网关会做一个拦截,因为每个请求都带上了单元号,API 网关就可以判断这个请求是否请求到了正确的机房,如果请求错了机房,API 网关把请求定向单元当前归属的机房。


定向用户请求有两种模式,一种是转发模式,API 网关直接转发到新机房的业务后端实例。另一种是重定向模式,API 网关在响应 header 携带了重定向指令,以及新机房的 IP(避免客户端多一次的请求),客户端立即重试新 IP。


转发模式需要消耗较多的机房专线带宽,重定向模式的总体时长更高,业务可以自由选择两种模式。


解析刷新采用并行跑马的模式,客户端会并行请求两个 HttpDNS Server 和一个传统 DNS,三个请求同时发出去。如果 HttpDNS Server 请求成功,哪个先到就用哪个,如果两个 HttpDNS Server 请求都失败,就使用传统 DNS 解析结果。因为每个单元都分配一个传统域名,所以传统 DNS 解析结果和 HttpDNS 解析结果也基本是一致,只有极少数用户会解析错误,API 网关重定向一次以后也能纠正过来。

6、调度决策大脑

调度决策大脑会收集很多路的原始监控数据,比如客户端调用链的数据、外网拨测平台的数据、机房网络监控的数据等等,多路数据汇总到决策大脑里,进行比对分析,得出故障的结论。


调度决策大脑一定要依赖多路监控数据源,因为单路数据的质量无法保证,比如可能会出现拨测用例配置错误、网络监控数据丢失等,所以单路数据都是不可信的,需要多路数据源做交叉的比对,过滤抖动、防止误判。


调度决策大脑最终会输出一个指令,指令只会告诉你故障类型,比如:机房故障、运营商线路故障、机房之间网络(DCI)故障,或者是容量不足、业务自身出现了问题等。业务自身出现问题,比如业务的数据库故障,也需要切到另外的机房去。


决策指令同时发到两个地方,既要发给接入层,也要发给数据层,为什么需要这样呢?


假设我们同城两个机房之间,专线出现了故障,两个机房的数据库肯定达不成一致,同步不过去了。这个情况下,假设我们的数据库选主 B 机房,而接入层保留 A 机房, A 机房的数据库完全写不进去,即使写进去也是错误的,这里我们要保证数据层和接入层两边选择的机房要一致。


所以这种专线故障情况下,我们是调度决策大脑来通知,做统一的决策,同时通知接入层、数据层做联动,选择同一机房,这个主机房的选择是事先配置好的,它不是由我们刚刚说的 Raft 组件来解决的。

7、调度效果

上图是我们 9 月份做过的一次机房调度的效果,基本上做到分钟级(实际上是秒级的)的生效,是很陡的一个曲线。

四、总结

最后,给大家总结一下今天分享的内容:


>>>>

Q&A


Q1:Http DNS 也有缓存的吧?

A1 :对,刚刚提到我们 Http DNS 缓存时间非常长,缓存了一周的时间,而且缓存的时候是根据环境来缓存的,就是按照 WiFi 名称、运营商的名称来做缓存,这样网络切换的时候可以拿到最优的 IP。

缓存的时间非常长,是因为域名解析的刷新,是不依赖缓存过期的,如果能请求成功,API 网关在响应 Header 就会带上调度指令,如果请求失败客户端也会主动去刷新解析。因此解析的刷新,是不依赖缓存过期时间的。


Q2:同城多活网络是怎么配置的?两个机房使用相同的 ip 地址,还是不同的?

A2 :对于跨机房高可用的数据库来说,用户看到的是同一个 IP,第一层使用 Anycast 路由到机房,第二层使用 ECMP 路由到多个四层负载均衡节点,单个四层负载均衡的流量扛不住,四层负载均衡是一个集群,通过 ECMP 实现流量分发。

多余入口流量来说,前面架构图可以看到,接入层在两个机房从四层、七层都是独立的,接入层有 2 组出口 IP,如果其中一个机房运营商线路出现问题,根据调度决策系统的指令,自动停止该运营商线路的 IP 解析。


Q3:老师能介绍一下多活带来什么业务收益吗?是什么契机促使 OPPO 开始做异地多活?

A3 :OPPO 业务多活的三个核心诉求是成本、扩展、容灾。

成本是指业务总体技术运营成本,包括基础设施的资源成本、研发成本,还包括业务中断的成本、品牌和口碑的成本;

扩展是因为业务规模过大,一个服务需要调用数百个三方实例、一个数据库被数百个实例连接、一个服务需要连接几十个数据库,这就需要对用户进行分片,缩小业务规模,自然演进到单元化多活的架构;

容灾一方面是极端情况下用户数据可靠性保障的需求,另一方面还是业务过于复杂、处理的链路很长,总有一些意想不到情况的发生,频率还挺高,问题定位到恢复的时间达不到公司 RTO 的要求。机房内部共享了运营商线路、DNS、SNAT 防火墙、负载均衡、K8S 集群、注册中心、监控等等资源,而机房之间是相对隔离的环境,同时出问题的概率大为降低。在业务出现无法自动恢复的故障时,先切换机房恢复业务,然后再从容定位问题根因。


Q4:随着业务发展启用多个订阅时,如何减少对数据库的压力?

A4 :我们从数据库源库订阅出来以后,先落地到本地文件队列,然后多个订阅方可以维持自己的同步位点,所以对于源数据库来说,只会有一次订阅。


Q5:请问同城双活方案 MHA manager 部署在哪个数据中心?

A5 :我们这里不是 MHA,我们用的是一个开源的 Raft 组件,部署在同城的 3 个机房,通过 Raft 组件检测数据库的状态、触发切换。


Q6:Http DNS 和 Local DNS 的区别是什么?

A6 :Http DNS 走的是 HTTP 协议,客户端直连解析,没有运营商的多级缓存。Local DNS 就是运营商的 DNS,成功率低,还有劫持、黑洞等问题,而且这两年黑洞频率是越来越高了,前几年基本上很少出现黑洞情况。传统 DNS 劫持情况现在好一些了,像移动端的接口劫持相对来说会少一些,H5 的劫持多一点。

Http DNS 就是依赖 HTTP 协议做解析,但这个压力会比较大,因为 Http DNS 没有多级缓存,所有请求都到我们的机房,所以刷新机制的设计就非常关键,前面一个章节详细介绍解析刷新的时机。

HttpDNS 还有一个好处,因为是自定义的协议,可以传递其他参数,比如设备信息、账号信息,这样才能够实现按用户单元进行解析、调度。


Q7:能否制定统一的用户单元划分规则?

A7 :这个问题比较好,我们最开始也是想这样子的,我们有云服务、广告、信息流、音乐、视频等业务,起初也想整个公司使用一套单元划分规则,这样业务之间可以做到单元内封闭调用,避免跨机房的调用。


最终的方案,业务之间没有使用同一套单元划分规则。主要原因是:比如说有个业务他经常会做活动,做活动的时候他需要将一部分用户调度走,如果全公司用一套规则的话,所有业务都要跟着调度走,其他业务是不同意的。所以我们是每个业务自己制定单元划分规则。


那这里怎样解决业务之间跨机房调用呢?前面说到了平台型业务 SDK 化,上层业务之间本身没有强依赖,音乐、软件商店、视频之间本身没有强依赖,他们主要是依赖平台型服务,如账号、评论服务、消息中心、推送服务等。这些平台型业务我们最开始也是提供机房内部 API 去给其他业务器调用,这就导致我们的平台型服务在每一个机房都要去部署,每个机房都要提供读写功能。所以我们将平台型域名拆分出来,从 SDK 就开始就和业务域名分开,平台型自己做多活。当然平台型业务无法做到 100%的 SDK 化拆分,平台型服务的部分数据也需要单向同步到各机房,提供本地查询的服务。


Q8:Redis 日志是哪个开源组件做到的来的?

A8 :Redis binlog 是 OPPO 自己修改的,基于 AOF 修改,简单说一下 binlog 的格式,里面包括了命令、数据产生的机房、递增的序号,还有一个时间戳。


相关的组件今年会开源出去,OPPO 微服务体系 ESA Stack、存储已经对外开源,可以搜索一下。


Q9:MQ 数据同步了,如果切换,在 A 机房写入了,但是还没消费,此时切到 B 机房,B 机房咋知道从那个点开始消费呢?

A9 :OPPO 主要使用 RocketMQ 定制版本,节点跨同城 AZ 部署即可。


嘉宾介绍:

罗代均,OPPO 安第斯系统资深工程师。负责 OPPO 后端体系建设,包括 API 网卡、用户流量调度、微服框架、调用链路跟踪等,经历了 OPPO 用户从千万级到亿级的增长历程。


本文转载自:dbaplus 社群(ID:dbaplus)

原文链接:超3亿活跃用户的多活架构,数据同步与流量调度怎么做?

2021 年 4 月 12 日 13:001878

评论

发布
暂无评论
发现更多内容

食堂就餐系统设计

Hugo

初步架构想法

极客大学架构师训练营

食堂就餐卡系统设计

Coder的技术之路

架构师训练营第一周 - 学习总结

Eric

极客大学架构师训练营

低调的网易又要上市了

池建强

创业 网易 慢公司

比Webpack更高效的Rollup入门指南

费马

Rollup 打包 前端工程化 webpack

[Go] 写一个守护协程的通用套路是什么?

eddix

golang pattern

老当益壮的 Servlet

侯树成

Java Java 25 周年 Servlet

架构师训练营第一周总结

Hugo

架构师训练营第一课

Coder的技术之路

食堂就餐卡系统设计

LEAF

聊聊Java中的Thread类

geekymv

线程 Java25周年 Thread Runnable

架构师训练营 No.1 周作业

连增申

架构师作业一:食堂就餐卡系统设计

李锦

Hello World!

东哥

极客大学架构师训练营

为什么建立自己的规则很重要

Neco.W

自我管理 行动派 执行力

架构师训练营-作业-第一讲

吕浩

极客大学架构师训练营

UML 建模

师哥

剖析Golang Context:从使用场景到源码分析

伴鱼技术团队

golang 源码分析 并发编程 程序语言 Context

架构课程心得

dj_cd

极客大学架构师训练营

论一个前端工程师的自我修养

萧文翰

ios android 开发者 前端 Web

神奇的梦想

霍太稳@极客邦科技

身心健康 个人成长 目标管理

解决出海网络难题 融云保障 MiniJoy 千万印度用户流畅互动

Geek_116789

第一周总结

LEAF

架构师训练营第一周总结

Kiroro

四个和成长有关的小故事

霍太稳@极客邦科技

团队管理 TGO鲲鹏会 团队组织 职业成长

讲一个程序员如何副业月赚三万的真实故事

非著名程序员

程序员 独立开发者 副业赚钱 提升认知

第1周 - 学习总结

大海

week01 UML 学习总结

李锦

8000字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理

古时的风筝

函数式接口 Lambda stream Java 25 周年

食堂就餐卡系统架构设计

dj_cd

极客大学架构师训练营

超3亿活跃用户的多活架构,数据同步与流量调度怎么做?-InfoQ