前言
今天我给大家分享的主题是阿里游戏异地多活设计的原则和一些理念。对于做技术的同学来说,设计一个异地多活的方案其实是很具有挑战性的和成就感的。
异地多活系统确实比较复杂,作为运维的同学来说,如果自己负责维护的系统具备异地多活的能力,那是一件很幸福的事情,因为异地多活确实很强大,在大的公司重要的业务系统都会思考这个问题。
在之前参加的技术大会上和技术的朋友做交流的时候,大家都对异地多活挺感兴趣的,对这个话题也是滔滔不绝的讨论,各抒已见。在交流的过程中,我发现普通存在两个问题:
都觉得异地多活方案挺好的,但是要真正的去自己思考的话,不知道从何处下手
怎么去想,都难以设计一个完美的异地多活的方案
今天我的分享就是基于我们做异地多活的经验和自己的思考,来尝试解答上述两个问题。
我的演讲将会从以下四个方面进行解剖:
一. 异地多活——— 1个原理
二. 异地多活——— 3大原则
三. 异地多活——— 4大步骤
四. 阿里游戏实践
首先介绍一下我在异地多活设计方案中的一些经验,我带领团队做了阿里游戏第一个真正意义上的异地多活方案,后来又参与了一些其它几个异地多活方案的设计,这几个业务特点差异很大,有的是游戏的,有的是支付的,还有的是中间件的。
具体设计中会面临很多的问题,有的跟业务相关的,有的跟系统相关的。通过实践完成这几个系统以后,结合自己的一些经验和思考,提炼了一个叫阿里游戏异地多活设计之道,也就是今天我要跟大家分享的主题。
我的分享分四个部分,前面三个部分都是讲所谓的异地多活设计之道的,我把异地多活设计抽象成三个部分:
第一个是异地多活它设计背后的一个原理。 第二个就是异地多活设计的三大原则,就是说不管你做什么样的异地多活设计方案,也不管你是做微信、银行、支付宝等等业务场景这三大原则是不可违背的。 第三个就是讲一下异地多活方案设计具体的实施步骤,这些步骤与业务不关联,不管你是什么样的业务,通过这样的几个步骤都可以实现异地多活的设计方案。 第四部分是个实际案例来展示前面的内容。
一. 异地多活———1个原理
1.1 大道至简 ——— CAP理论
异地多活本质上是一个分布式的AP方案,分布式系统要遵循的就是CAP理论,CAP理论的细节我们不展开,主要讲一下与异地多活方案相关的一部分。CAP理论最常见的应用场景一个是牺牲可用性达到一致性的要求,另一个是牺牲一致性达到可用性的要求。
本质上异地多活是一个AP的方案,理解了这个,对我们后续做方案分析和设计是非常关键的一步,这是因为做异地多活方案时,必须要做出一部分的取舍,这个取舍就是对一致性上的取舍。
1.2 大道至深———CAP细节
CAP理论为了便于描述方向忽略了很多细节,而这些细节在我们落实和实践方案的过程中,是不可避免的会遇到和要去面对的,了解了上述这些细节点,对我们做异地多活设计时是非常关键的。
细节一:
CAP关注的粒度是数据,而不是系统。因为CAP都是node1、node2,system1、system2,做异地多活我们的业务系统有很多的数据,不可能说我整个系统都是支持CP和AP的,我只能说有一部分是支持CP的,另一部分是支持AP的。
之前很多的朋友在这方面有疑惑,他设计系统时想设计一个异地多活的方案,但是实践时发现如果满足了A数据的要求,就无法满足B数据的异地多活的要求,满足了B可能又无法满足C了,这样就陷入了迷茫或者怎么做都做不好的状态。
实际上我们在设计异地多活方案的时,不能说整个系统都能支持AP和CP的,只能说我这个方案里面有一部分支持AP,另一部分是支持CP的。
细节二:
CAP是忽略网络延迟的。比如你选择了CAP方案,对CAP理论而言,它认为你的数据从A节点复制到B节点是没有时延的,只要用户写入到A节点上的数据,B节点瞬间可以复制到这个数据,通过这种方式来保证系统数据的一致性。
但是在做具体方案的时候,时延是不可避免的,不管你是在本地机房还是跨机房或者异地机房,时延都存在只是时延长短而已。比如说本地机房是几毫秒,而异地多活的机房,比如从广州到上海,正常是50毫秒,如果网络抖动可能会有100毫秒甚至1秒钟。
细节三:
正常运行情况下,可以同时满足CA。很多朋友谈CAP理论的时候,我选择了AP就放弃了C,实际上他所谓的放弃都是指在故障的情况下。
细节四:
如果我们是AP方案,我们会放弃了C,这里的放弃并不是你什么都不用做,而是为了分区分布以后做修复和补偿的操作的。
这四个细节点和我们做异地多活的设计方案实施时所面对的问题是强相关的,后面我们也可以看到。
接下来讲讲设计异地多活的三大原则。
二. 异地多活——— 3大原则
2.1 原则1 — 只保证核心业务
第一个是只保证核心业务,我在一开始写PPT的时候特意写了一个“只”,后来感觉可以改为“优先”,因为有的公司财大气粗,何必是只保证呢,应该投入大价钱来保证所有的核心业务,反正哥有钱!
但是我最终还是决定保证“只”,这是因为:
保证所有的业务全部实现异地多活,这个在理论上是不可能的。
我们做这个方案是要考虑成本和收益的,如果说投入很大的价钱把整个系统,所有的业务都做成异地多活的话,这个成本是相当大的。如果公司真的是有那么多钱的话,给研发同学加点工资,给运维同学发点奖金更好。
我们举一个例子看看,上面这个图是简单的用户管理系统,我们假设它只有下面三个功能:
用户注册功能
登录功能
用户信息管理功能
我们可以看到它的架构是很简单的,有一个路由层根据用户的ID路由到不同的数据中心;接下来是业务层,有三个数据中心,每个中心都有节点的数据,相互之间通过复制来达到全局数据,就这么一个简单的业务,简单的架构,如果要实现它的异地多活,应该怎么做呢?
首先来看看注册,注册其实是一个一致性要求非常高的业务。比如说这个手机号码只能在这个系统上注册一个账号,我们假设有一个用户是归属于A中心,他在A中心注册,这个时候假设我们注册也要做异地多活,会出现什么样的情况呢?用户已经在A中心注册了,这个时候A中心的数据还没有复制到B中心,如果此时A中心宕机或者是挂掉了,用户登录到B中心会发现我刚刚明明已经注册了,B中心却提示他没有注册,假设这个时候再次注册,会出现什么样的情况呢?一旦A中心恢复就会发现明显的冲突了。本来约束一个手机号码只能注册一个账号,但是现在一个手机号码在两个地方注册了,这样账号就出现了冲突。
接下来看看用户信息管理,也会存在类似的现象。比如说用户修改了他的昵称和简介,A中心修改了一版说我叫杨幂,然后B中心他又改成 angelababy,然后A和B恢复了以后,发现这两个昵称冲突了,这个怎么解决呢?谁最后修改以谁为准。大家都知道跨异地的数据中心,让所有的服务器保持同样的时钟,也是挺麻烦和挺复杂的一件事情的。
再来看看登录。其实登录是最好做的,因为它只需要账号和密码,账号和密码一旦成功的复制到所有的数据中心,不管是在A中心登录还是在B中心登录都没有问题的。
通过上面的一个简单的业务我们可以发现,做登录的异地多活是最简单的,做注册的异地多活是没办法做的,做用户信息管理的异地多活就比较复杂,要尝试解决数据的冲突。
在面临这样一个系统,我们在考虑做异地多活方案时,应该怎样选择呢?答案是丢车保帅,只保证登录的异地多活。如果你的日活是100万,对应到每天注册的用户就一两万,修改用户信息的用户只有一万,这样一个系统要做异地多活,最优先保证的是登录业务,因为用户不能登录的话,他后面所有的业务都不能实行。
如果用户没法注册的话影响也不是很大,第一个注册的用户数不多,第二个用户在没有注册的情况下,他其实没有使用后面相关的业务,也没有产生相关的数据,即时注册不了,对个人的体验来说也影响不大的。
所以第一个原则叫只保证核心业务的异地多活,当然这里会遗留一个小疑问,就算登录做异地多活也会面临一种情况,比如有一个用户在A中心登录了,但是如果A中心挂了,这个用户在另外一个中心也是无法登录的,这个涉及到另外一个原则,我们一会儿再来解答。
2.2 原则2 - 只能做到最终一致性
第二个原则叫只能做到最终一致性。这一点其实就是对应刚才CAP细节上所讲的,理论上是忽略时延的,但是做具体方案时这个是无法忽略也是无法避免的。
我们来看一个简单的例子,比如说现在有两个数据中心,一个是Data Center 1,一个是Data Center 2。现在有三个节点,Node A、Node B、Node C,它们三个通过复制来同步数据,数据从Node A同步到Node B和Node C节点。如果用户在Node A 节点写了一个数据X,在Data Center 1 里面复制到Node B节点,同时也复制到Data Center 2中的Node C节点上。Node A到Node B是几毫秒的时延,Node A到Node C就是几十毫秒或者是一百毫秒时延,考虑时延,对于异地多活设计的方案只能做到最终一致性,而不能做到实时的一致性。
这一点很多朋友都提到过,就是他的业务可能要求实时一致性,但是一旦考虑异地多活,复制这种时延,就没办法做到实时一致性的。这种情况下怎么办呢?如果真的是这种场景下的数据,就没办法做到异地多活。
举个简单的例子,银行的账户余额、商品的库存,这两个数据其实是没办法做到真正的异地多活。大家也会看到蓝翔的挖掘机一铲子下去,支付宝百分之多少的客户一小时都有影响。
这里面还有另一个疑问,如果是只能实现最终一致性,那在复制时延的窗内,用户的业务提交是有问题的,比如说用户在Data Center 1 改了数据,复制到Data Center 2 还是时延的,客户就会投诉,如果涉及到资金可能还会报警,那我们怎么办呢?这也是遗留的问题,后面统一解答。
2.3 原则3 - 只保证绝大部分用户
刚才原则1和原则2都遗留了些问题:
原则1的问题是只保证核心业务的情况下,非核心业务受损怎么办?
原则2的问题是在时延窗口期内,用户业务有问题那该怎样处理?
其实我的答案是没办法处理的。我们只能保证绝大部分用户业务正常,核心业务是正常可用的,而不能保证所有用户,所有业务都是可用的。
因此,我们在做异地多活设计方案时,不要为了0.01%的用户导致99.9%的用户都没法使用。
在昨天下午的时候,我一做金融的同学跟我交流异地多活的方案,他正好也提到了这个问题,就是说他们的复制从A中心到B中心,有一到两秒的延迟,用户在A中心下了一笔订单,在B中心不能立马看到,这个时候怎么办?我的答案是没有办法,只能让用户等一等了。
那么,对于这0.01%的用户怎么办呢?当然也不能说要么等要么滚了,对于这部分的用户我们也是不抛弃的。那怎么做呢?下面详细看一下。
三. 异地多活——— 四大步骤
来看看异地多活设计的四大步骤,这也是回答很多朋友在思考异地多活设计方案时,感觉千头万绪无从下手。
第一步业务分级,其实就是对应原则1,我们要只保证核心业务的异地多活,首先要把核心业务给挑选出来。
第二步数据分类,前面我们提到CAP理论,它其实关注的是数据,那我们这个系统中有很多的数据,我们要挑选对核心业务关键的一部分数据进行设计。
第三步数据同步,挑选出来的核心数据后设计同步方案,为什么要设计同步方案呢?简单的来说单纯的使用数据库或者底层存储的设计方案是没办法满足我们在某些场景下异地多活设计方案的复制要求。
第四步异常处理,我们考虑方案做完以后有哪些异常,这些情况下用户会受到什么影响,对于这些影响我们该采用什么样的措施进行弥补,才能真正做到不抛弃不放弃用户。
3.1 步骤1———业务分级
业务分级一般有以下几个维度:
访问量大。最简单的例子就是我们前面举的用户管理系统,对于一个用户管理系统来说,登录的访问量要远大于注册的访问量,注册的访问量略微大于修改密码的访问量。对于这样的系统,我们优先保证登录用户的异地多活。
核心场景。有的非核心比核心的还要大,这种我们就不能按照访问量来评估业务,而是应该以用户场景,这里举的是微信的例子,可能每天访问朋友圈的量比聊天的量还要大,但是对于微信来说核心场景还是聊天。我认为聊天大于朋友圈和摇一摇的。当然,具体划分时可能不同业务有不同的理解。如果微信团队认为朋友圈是他们的核心场景,那优先保证朋友圈的异地多活也没问题的。
收入来源。说白了与钱相关的,类似于淘宝这样的电商网站,下订单是大于搜索的优先级,用户下单就会支付,卖家和平台可以拿到钱了。如果搜索功能有问题,用户还可以通过列表和类来查看商品,搜索又大于编辑,编辑就是卖家修改他的商品信息,短时间之内系统有问题,卖家不修改商品信息,这对于整个业务来说影响不大的。对于电商的一个系统来说,如果要做异地多活,应该是优先保证订单,淘宝的异地多活是优先保证买家下单的业务。
3.2 步骤2———数据分类
挑选出核心业务以后,我们要看看核心业务有哪些数据,然后这些数据具有什么样的特点。数据分类有以下几个原则:
数据量。这里关注的是修改的数据量,只有修改了的数据才会进行复制和同步。
一致性。为什么要特别强调一致性?因为对于余额和库存强一致性的要求,单个数据是没办法做异地多活的。举个最简单的例子,用户的余额,你就算有一秒的延迟也无法做异地多活,如果做了异地多活,一秒之内能够重复扣款超过他的余额,一旦漏洞被黑客识别到,很容易发起攻击,让你系统处于不一致延迟的状态,就会让你转账损失很多的钱。前面说的昵称这块做到最终一致性就可以了。
唯一性。账号要求全局唯一的,这也是要求你在注册时,一个用户的账号如果在A中心注册了,他在B中心就不能再重复注册了。对于这种全局唯一的数据的话,对复制的要求就高了,而对于昵称这种全局不唯一的数据,每个数据中心生成都没有问题。
可丢失性。账户余额变更这实际上是不可丢失的,如果丢失用户要么多扣了钱要么少扣了钱,而对于微博这类来说,是可丢失的,这条微博丢失了对用户的影响不太大,对于session这类也是可以丢失的,让用户重新登录一下就可以了。
可恢复性。如果你发了一条微博,这时微博系统挂了导致这个微博丢失了,等系统恢复了重新发一条就可以了。对于系统复制或者宕机导致用户密码丢了,用户可以通过找回密码就能恢复密码了。而对于新闻类的数据来说,实际上是可以让运营人员重新发一下的,这些可恢复性的数据都会有一些影响。比如让微博用户重新发一次用户体验差,用户密码不对让用户重新找回他要操作一遍,对于新闻类的数据来说,让运营人员重发会增加他们的工作量。但是回到原则3上,做异地多活的时候,不能保证用户所有的业务都不受影响,也就说这部分的损失,不管是体验上的损失还是工作量的损失,都是要必须容忍的。
为什么要进行数据分类呢?其实是为了不同的数据做不同的处理方式,就是不同的数据我们会采用不同的同步方式的。
3.3 步骤3———数据同步
数据同步的方案,总结一句话就是多管齐下,不择手段。
在设计异地多活方案时,面临的困惑是底层存储不可控,一旦想到我要复制一些数据的时候,就会想到用MySQL或者Oracle,但是MySQL和Oracle都会存在问题的,MySQL在网络抖动和数据量很大的时候,时延比较高。
如果做方案时只考虑用数据库的方案,其实是没有办法做到真正意义上的异地多活,或者是快速的数据复制。这种情况下,我们就要开拓思路了,不要用底层存储的方案,用另外的方案,我们自己开发了一套存储方案,也是为了实现更多业务上的要求。
来举一个例子,其实就是对应最开始的用户管理系统的一个复制的,从下往上看,最下面的一层是数据库复制,我们只用来复制密码和用户信息,因为密码和用户信息的修改频率不高,也就是数据复制量不会太大。
再往上我们会有一个消息队列,这个只用于复制新建账号,为什么用消息队列呢?就是因为底层的MySQL数据延迟严重,另外一点MySQL通道延迟大,那我用消息队列走另外一个通道,不仅复制很快,而且还相当于在数据库同步通道以外又额外的增加了一条通道,起到双保险的作用。通过消息队列将新的用户快速复制到其它的数据中心,尽量减少复制延迟的时间窗。
再往上是二次读取。假设系统没有宕机只是网络延迟,比如说延迟了几百毫秒,用户在A中心刚注册就跑到B中心,他发现没有数据,这个时候用户体验不好,那系统怎么做呢?他就拿到用户标识以后,根据路由算法,算出它所属的数据中心在A机房,然后通过接口去A机房主动读取用户的数据,因为用户在A机房注册了,其实他是可以读到的。
再往上有一个回源读取,这是用来同步session的,用户在A机房登录会产生一个session,而B机房没有,可以统一session的标识首字母,来判断session在哪里产生的,然后回源到这个机房读取。
最好一个方案是重新生成,也是应对于session的,其实它不是同步的方案,但是可以应用于极端情况下session丢失的问题。假设C机房宕机了,有几十万的数据,那session丢失了怎么办呢?session可以重新生成就可以了,对于session的数据我们就可以不采用同步的方案,也就不需要数据库同步。
大家知道一个系统里面的session数据是很庞大的,一天有一百万的用户,就产生了一百万的session,如果同步这么大的数据,对底层数据库存储也好还是对消息队列也好,同步的压力都比较大。
3.4 步骤4———异常处理
这个对应原则3,我们保证99.9%的用户不受影响,但是还有一部分用户是会受到影响的,对于这部分用户我们怎么做呢?采取的方式就是不抛弃不放弃。
业务逻辑上要兼容,数据短时间不一致的情况下,我们可能不要报一个错误给他,而是显示一些老的数据给他。如果数据无法获取,那就可能要修改一些业务逻辑。举个例子———-转账,这个其实是没有办法做到真正意义上的异地多活的,那我们在正常的情况下,我们可以直接让用户实施转账,在异常情况下,我们就不让用户进行实时转账,而是有一个转账申请的功能,用户发起转账并不立即执行,而是后台记录下来,然后看这个用户所属的数据中心是否恢复了,恢复了后再去真正的执行转账的操作。类似的还有支付这块,经常网购的朋友会发现,像我以前在京东购物时,有的时候我支付完成了,订单状态变成支付确认中,其实这个时候没有真正的扣款,可能比如说支付系统有问题之类的,他只是记录了你这个状态,但是没有真正的完成支付。
事后补偿,对于受损部分的用户我们会进行适当的补偿,弥补用户的损失。比如游戏行业常用的礼包或者红包等等。前不久的暴雪炉石回档案例,有关注的同学印象应该比较深刻,提供的补偿是非常丰富的,搞得玩家期望暴雪再来一次回档。
人工修正。前面提到异地多活要实现最终一致性,这在故障期间会有一些数据,不管是因为时延还是故障也好没有同步过去,对于这部分的数据,CAP理论在正常情况下是可以保证CA的。如果系统能够自动的订正和恢复那当然是最好不过了,但是投入的成本和复杂性就比较高了。我们之前尝试过系统能否自动修复,发现异常情况和逻辑太复杂了,用人处理的效率反而更高。
只是我们要尽量的加大复制的速度,减少不一致的数据量。这里最重要的就是日志,每个地方都要部署写日志,只有每个地方有日志记录下来,才能进行修复和补偿。
总的原则就是客户第一,要亏自己亏。对比暴雪的案例,网易的《阴阳师》就是一个负面的案例,网易的《阴阳师》的bug处理措施,让玩家不满。因此客户第一,要亏自己亏,不管是你去发礼包,去修复都要自己去做而不是要求用户怎样怎样。
四. 阿里游戏实践
最后来看看阿里游戏具体的案例。这个业务叫阿里游戏接入系统,简单的介绍下就是大家玩网游的时候,进入游戏之前会有一个账号登录,你可以用百度账号,可以用QQ,可以用微信,也可以用UC的账号登录,这个登录就是接入系统,这个登录是不同的系统,比如说登录到百度、腾讯的系统,登录完成才能进入游戏,这个过程我们叫游戏接入。
我们来看一下这样的一个接入系统怎样来做异地多活,还是按照上面所说的步骤一步一步的来。
业务分级,我们挑选的就是登录和支付,我们认为这两个业务是我们游戏接入的一个核心业务。其实挑了这两个是基于不同的原则的,第一个就是登录量大,第二个是支付,只有用户支付了才能赚到钱。
内部讨论的时候,如果系统面临冲突的时候,就是做登录的异地多活和支付的异地多活冲突的时候,还是优先保证登录,也就是刚才说的要亏自己亏,首先要保证用户能够进入游戏玩。
数据分类,第一个是按照session数据,第二个是角色ID,每个用户进这个游戏的ID,第三个是账号密码,就是用户登录一定要用的。
这三类数据有什么特点呢?
session是用户登录以后生成的数据,它就是前面说到的不需要同步,只需要重复生成就可以了。
角色ID要求是全局唯一的,同一个账号和同一个游戏里面只能有一个角色ID,如果生成两个,用户的游戏信息就会混乱。这块我们采取的是消息队列同步,在A机房生成的角色信息,通过消息队列同步到其它机房。还有算法重复生成,因为数据是全局唯一的,假设用户在A机房生成,没有同步到B机房,这时候用户到A机房玩游戏,我们是保证用算法同样生成同一份数据,为什么算法同样生成同一份数据还用同步呢?主要的目的是一旦这个数据生成就不可变了,生成以后再同步,后面访问的时候再查询比算法生成效率更高,因为可以用缓存。
账号密码和支付,这块很简单,因为这是第三方管理的,你可以认为是支付宝做的,这块我们无需处理。
异常补救我们采取的方式有两种,一个是人工修复,在极端情况下,比如机房掉电或者是攻击了,有一部分用户会来投诉也可能不投诉,我们有详细的日志记录玩家在这个过程中的关键数据进行人工修复。第二个是对于受损的玩家我们可以送他礼包和代金券,这都是可以在游戏里面使用的。
如下架构图示我们做异地多活前的系统和异地多活后的系统。
左边的系统有一个全局集中的点就是A集群的主库,所有的数据写都是写主库,这不是真正的异地多活,因为主库挂掉,另外一个机房也没办法写入,所有的业务不能用了。
右边是改造后的系统,有两个主库,而且在本机房同步,两个主库数据库之间并不进行同步,然后我们在业务层会加两个方式,就是上面的一个是同步,这个同步就是消息队列的同步,另一个是二次读取,就是刚才我在介绍数据同步方案里面提到的,做完这个方案,切换的话是执行一个命令可能几秒就能切换过来,整体流量可能是一到两分钟就可以全切换过去。
在做了这个系统以后,我们也经历过机房交换机的故障,还有DDoS攻击。我们系统都是简单的执行命令把流量切走,基本上对业务没有影响。而那些没有做异地多活的系统,就只能干等着,有可能需要等两三个小时。
最后,简单的说把异地多活设计提炼成一个理论,三个原则,多个步骤。这些与我们的业务并不绑定的,相信大家在落实异地多活的方案时,参考我提炼的东西应该都能够做出不能说是完美至少是可落地的异地多活的方案的。