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

罗代均 2021-03-30 09:43:40

本文根据罗代均老师在〖deeplus直播第261期〗线上分享演讲内容整理而成。(文末有获取本期PPT&回放的方式,不要错过)



 
 

一、多活业务架构

 

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的格式,

↓点这里回看本期直播

阅读原文

活动预告