通常情况下我们在谈论高可用架构设计的时候,主要关注的是系统结构的高可用,例如主备架构、集群架构、多中心架构。我们做架构设计的时候,也主要是从系统结构本身出发,例如我们把单机改为双机、双机改为集群、单机房改为异地多机房等等。
这种以系统结构为目标的高可用架构设计,更多的是从技术角度出发,但其实我们都知道,无论技术多先进,架构多强大,都不可能保证100%不出问题的。蓝翔的挖掘机一铲子下去,支付宝就要故障2小时;程序员的误操作,携程12小时不能提供业务;某个黑客1000台肉鸡攻过来,网站可能就拒绝服务了......
因此,真正做到高可用,不能单纯从系统结构的角度出发来考虑问题,不能局限于某个系统的高可用,而应该从业务的角度出发,全方位的来考虑高可用的架构设计。前者我称之为“面向系统的高可用架构”,后者我称之为“面向业务的高可用架构”。
阿里九游游戏接入系统(以下简称“游戏接入系统”)负责所有九游平台游戏的接入(包括用户的登录、注册、支付等业务,也包括游戏开发商(以下简称“CP”)用户验证、支付等业务),对可用性的要求非常高,一旦故障,大量用户就不能愉快的玩游戏,投诉就会满天飞,论坛就被报障的帖子刷爆了,因为对于很多用户来说,不上微信可能问题不大,但要是几小时不能玩游戏,那就要爆粗口了。如何保证游戏接入系统的高可用,让用户能够愉快的玩游戏,成为了我们的一个巨大的挑战。
为了实现游戏接入业务的高可用,我们跳出传统的面向系统的高可用的思路,转而从业务的角度来整体考虑高可用,最终实现了一套“立体化的高可用架构”。接下来我将逐一展示这套立体化高可用架构的一些具体实践。
业界高可用的通用指标是几个9,例如5个9代表一年业务不可用的时间是5分钟,4个9代表一年业务的不可用时间是50分钟。我们最初也是使用这个指标来作为我们高可用的目标,但是在实际的操作和讨论过程中,发现这几个指标虽然简单,但是并不能直观的理解,而且对于我们分析问题和设计方案没有很强的指导意义,因此我们决定找更加容易理解和操作的目标。
但说起来容易做起来难,高可用本身就是一个主观色彩比较强的概念,不同的人理解都不一致,要确定一个更加容易理解和操作、大家又能达成一致意见的目标,竟然成了我们首要面对的难题。我们先后讨论了多次,前后使用了“提高可用性”、“具备XX能力”、“解决存在的可用性问题”……等多个目标,但这些目标最后都被否决了,主要原因就是我们的BOSS认为这些都 没法量化,没法评估,不能认为做了事情就一定能够达到目标。 当时研发团队和BOSS还有一点小分歧:研发团队认为除了几个9外,高可用没法量化;而BOSS认为一切都可以量化,只是我们还没找到方法。不得已,团队又继续头脑风暴,功夫不负有心人,终于在一次讨论中想出了一个可量化可衡量的高可用目标: 3分钟定位问题、5分钟恢复业务、平均最多2个月发生一次问题 ,这样计算下来一年不可用的时间大约就是50分钟,正好契合4个9的业界通用的可用性目标。
在后来的项目执行过程中,我们发现这个目标真的是非常有用,非常具有指导意义,具体表现为:
后来的项目总结时,我们都认为这个目标是项目成功的第一关键。
将我们的高可用目标分解一下,其实有3个子目标:
1. 尽量避免发生问题
不出问题当然是高可用的首要目标了,不然的话天天出问题,处理再快也没意义。
2. 快速定位问题
出了问题要能够快速发现和定位,不要报警或者用户投诉过来后还要花半天才能定位问题,在问题发生后尽快定位初步的原因所在,尽快处理问题,防止问题恶化。
3. 快速恢复业务
特别注意这里我们强调的是“恢复业务”,而不是“解决问题”。很多人在处理生产问题或者故障的时候有一个误区:那就是一定要找到问题根因,然后解决。实际上大部分的时候这样做都很难的,也很耗费时间。比如说某个机器响应很慢,可能的原因有:机器磁盘有问题、机器的CPU被耗光了、这台机器上的程序陷入死循环、jvm垃圾回收时间较长......要在短短几分钟内排查这么多可能的原因是很难的,但我们不知道真正的原因也可以恢复业务,比如说最简单的是直接把这台机器立刻下线,让流量分配到其它的机器。
当我们审视这3个目标的时候,就会发现没有哪个系统的架构能够独立的满足这目标,必须从业务的角度全方位、立体化的来分析和设计高可用方案。结合我们的业务,最终的架构实现方案如图1所示。
通过这个图我们可以看到,这个架构设计方案并不是传统意义上的软件架构,而是一个高可用的业务架构,真正和传统系统架构相关的就只有“异地多活”,其它的功能或者系统并不属于狭义上的架构设计范畴,但这些功能和系统组合起来,共同保障了业务的整体高可用。
当我们的业务发生问题的时候,用户侧肯定是感知最快的,如果能够在用户侧感知并立刻处理,问题的影响将大大降低。用户侧最简单的处理问题就是重试,当遇到某些错误的时候用户侧再重试一次,错误可以是通用的错误,例如HTTP 404/500,也可以是业务上的错误码,只需要客户端和服务端预先约定即可。
用户侧重试是处理问题速度是最快的,但一个最大的问题就是DNS的不可靠性。简单来说,如果通过DNS拿到了错误的主机地址,即使重试也一样同样是错误的。导致DNS错误的原因主要有如下几种:
1. 用户侧DNS被劫持,hosts被篡改
如下是我们的域名在用户机器上被篡改的实例,可以看到我们的域名被篡改成了127.0.0.1。
2. 缓存DNS服务器污染,返回客户端错误IP
2014年1月21日下午3点,国内顶级域的根服务器出现异常,许多知名网站的域名均被劫持到一个错误的IP地址上,至少有2/3的国内网站受到影响,用户无法正常访问。根服务器恢复后,由于DNS缓存问题,部分地区用户“断网”现象仍持续了几个小时。
3. DNS缓存时间较长,短则10分钟,长则几小时
DNS的解析机制为了提升效率,在很多地方会有缓存,例如本机的缓存,DNS服务器上的缓存。缓存带来了效率上的提升,但同时却给故障处理带来了不小的麻烦,即:当我们将故障的机器下线或者将DNS指向的主机地址修改以后,用户并不能立刻感知新的主机地址,在缓存有效期内还是会继续访问旧的主机。
DNS的这几个问题虽然我们都很清楚,但是也无能为力,因为我们不能控制DNS的设备,也不能修改DNS的实现机制。要想解决这个问题,只能另想办法,我们的解决方案就是HTTP-DNS。
故名思议,HTTP-DNS就是通过HTTP的方式来自己实现一套DNS的功能,简单来说就是客户端通过HTTP接口来获取指定域名对应的主机地址,而不再通过传统的DNS设备来获取主机地址。相比传统DNS,HTTP-DNS具备如下优势:
1. 自己实现,控制力度强,可以根据业务特点灵活实现
可以根据业务进行细粒度的调度,例如发现A业务某个集群请求量较多,可以动态的将请求分配到其它集群。
2. 更新快,故障处理及时
当更新域名对应的主机信息后,客户端能够立刻拿到最新的信息。例如下线一台机器后,客户端再来获取主机地址就不会获取已经下线的机器地址,能够实现秒级的故障处理速度。
当然,HTTP-DNS的这些优势是在特定场景下才能体现的,并不能完全取代传统的DNS。因为如果每次访问都先通过HTTP-DNS拿取主机地址的话,效率和性能都太低,对于手机类智能设备还会导致耗电和流量增加。因此我们需要结合传统DNS和HTTP-DNS的优势,既要保证大部分情况下的性能和效率,也要保证异常情况下的故障快速处理。
具体的做法为:正常情况下我们通过传统DNS完成业务请求,异常重试的时候通过HTTP-DNS完成请求。如图3所示简要的说明了整体流程。
我们的高可用目标中,首要的目标就是“尽量避免发生问题”,但对于如何避免发生问题,刚开始的时候团队并没有明确的方向,有的同学从项目管理角度提出“结对编程”、“上线评审”等流程保障机制;也有同学从测试角度提出“加强测试力度”、“自动化测试”等测试手段;我们甚至还想到了“提升人员水平”这些人力资源措施......但这些措施都强依赖于人的因素,而人的因素往往是最不可控的,相比之下,我们认为通过技术的手段是更可控的,所以制定了“技术驱动”的整体策略。
通过对系统业务的仔细分析,以及对过往线上故障的问题的分析,我们制定了“功能分离 + 功能降级”的方案,具体解释如下;
1. 功能分离:划分核心功能和非核心功能,将核心功能和非核心功能物理隔离
这里有两个关键点:
整体的架构示意见图4。
2. 功能降级:当出现故障的时候,可以将非核心功能直接降级,保护核心功能不受影响
拆分为核心功能和非核心功能后,虽然物理上两者隔离了,但有的业务还是需要核心功能和非核心功能配合才能完成,这就存在了一定的风险。比如说消息下发(非核心功能)需要获取用户的信息(核心功能),如果大量消息下发的话,就会给核心业务系统产生较大的压力。这种情况下我们可以通过将非核心功能停掉,以保证核心功能不受影响。
将某个功能停掉的传统方式就是打patch,但做过的同学都知道,这样做的效率太低了:研发改代码、测试验证、运维部署,一路走下来,至少1小时以上,还容易出错,而且事后还要打另外一个patch恢复功能。为了实现“5分钟恢复业务”的目标,我们开发了一个后台管理程序,当需要停用某个功能的时候,只需要在后台上点击一个按钮就能够完成,花费时间只需要几秒钟。
通常我们讲系统架构的时候,异地多活往往都是最吸引人的,因为异地多活看起来很美好,一个机房故障,其它机房能够完全接管业务,故障处理简单快速。但异地多活往往实现都是很复杂的,其复杂性不是体现在业务处理层面,而是体现在数据处理层面。简单来说:异地多活需要保证数据的一致性和实时性,但异地机房间的距离天然决定了了数据的一致性和实时性是无法保证的。
目前业界常见的异地多活方案总体上可以分为两种:
1. 跨城多机房
在不同的城市搭建多个机房,机房间通过网络进行数据复制(例如MySQL主备复制),但由于跨城网络时延的问题,业务上需要做一定的妥协和兼容,不需要数据的实时强一致性,保证最终一致性。
例如微博类产品,B用户关注了A用户,A用户在北京机房发布了一条微博,B在广州机房不需要立刻看到A用户发的微博,等10分钟看到也可以。
这种方式实现简单,但和业务有很强的相关性,例如微博可以这样做,支付宝就不能这样做。
2. 同城多机房
同一个城市多个机房,距离不会太远,可以投入重金,搭建私有的高速网络,基本上能够做到和同机房一样的效果。
这种方式对业务影响很小,但投入较大,如果不是土豪公司,一般是承受不起的;而且遇到极端的地震、2013年汕头水灾这样自然灾害,同城多机房也是有很大风险的。
我们暂时还不是土豪,所以同城多机房就没法用了。同时,游戏接入数据还要求实时强一致性,例如A用户在北京机房注册成功了,然后到广州机房登录游戏,要求能够立刻登录,不可能让用户等10分钟才能登录,所以跨城多机房也不能用了。怎么办?方案似乎陷入了僵局。
我们转向了第三种方式: 由业务层来控制数据的强一致性和最终一致性,而不是由数据层来控制数据强一致性和最终一致性。 具体来说就是3个手段:
1. 异步分发
一个机房生成的数据,会通过程序异步分发到其它机房,机房间没有底层的数据复制通道,不存在主数据库和备数据库的概念,也就没有主机房和备机房的概念,每个机房都不依赖其它机房就可以独立提供服务。
通过这种异步分发的方式,可以保证数据的最终一致性。当其中一个机房宕机时,除了宕机时刻还没有分发的数据外,其它数据在其它机房都已经有了,其它机房可以接管宕机的机房的业务。
与传统的MySQL复制相比,业务层数据分发可以更加灵活,我们可以根据业务来选择真正需要分发的数据,或者在分发前做一些预处理操作。业务层分发性能也可以做到比MySQL更高,因为我们可以并发的进行分发。
2. 二次读取
异步分发只能保证数据的最终一致性,不能保证数据的实时一致性,因此我们设计了业务层二次读取的机制。即:A机房读取不到用户的数据,则到B机房或者C机房去读取。注意这里的跨机房读取操作都是通过接口,而不是通过访问数据库完成的。
通过二次读取的方式,可以保证数据的实时一致性。
3. 重复生成数据
对于全局唯一且又保证必须一致的数据,我们不是依赖数据库的自增id,或者zookeeper的全局id之类的机制,而是通过算法生成,这样保证不管在哪个机房都可以生成相同的数据。当其中一个机房宕机时,如果数据还没有分发到其它机房,此时虽然需要用户重新操作一次,但由于生成了相同的数据,业务上不会产生问题,用户能够继续正常使用业务。
而对于那些业务上可以重复生成的数据,例如session、ticket类数据,处理就更加简单了,不同机房间都不需要同步此类数据,如果没有,直接二次生成即可。虽然要求用户多登录一次会带来体验上的不友好,但我们的核心目标是不影响用户玩游戏,相比我们的核心目标来说,这点影响微不足道。
在我们最开始设计这套架构的时候,有的同学忧虑地说“这也叫架构啊,看起来好像一点都不高大上哦”,但最后实现的效果大大超出了我们的意料。通过上述三种手段,我们实现了即使一个机房完全宕机,另外一个机房也可以接管全部业务的目标。自从有了这套异地多活的架构,运维做机房切换非常简单和轻松,有时候为了验证系统抗压能力,我们在正常的时候也将某个机房的业务全部切换到另外一个机房,甚至运维同学可以开玩笑的说“没事做的时候切换一下玩玩也可以”!
处理过线上问题的朋友可能都有这样的经历:客户投诉现在业务有问题,于是研发测试运维开始投入定位和分析问题,一场忙碌但又混乱的活动开始了。
A研发去查日志,但是线上机器好多,一台一台的看,1小时过去了;如果想把日志下载下来,一下几个G的文件,网速又慢,只能干等……
B研发同学觉得数据库可能有问题,但是自己又不能直接操作数据库,只能找DBA,但是DBA不在线啊,赶紧打电话,DBA说我正好休假呢……
C运维同学更头大,一边要应付研发和测试的各种问题,一边还要自己查机器CPU、内存、io、网络、程序状态,而且还那么多机器。
这样的做法肯定达不到“3分钟定位问题”的要求,怎么办呢?我们的运维大神给出了“立体化、自动化、可视化监控”的方向:在故障发生的时候,定位问题的所需要的各种信息都已经准备好了,不需要再到各种各样的系统中去执行各种各样的命令了;而且这些信息都必须是可视化的,能够一目了然的看出问题所在。具体实现如下:
1. 立体化
立体化就是将故障分析和定位时涉及的所有的相关信息都要监控起来,共分为5层,具体各层和含义如下:
收集和分析业务层的访问量、成功率等指标。例如当系统被刷的时候,业务层能够一目了然的看出访问量会增加很多
应用服务层指的是以URI为维度的分析,可以看到某个URI的访问量、HTTP响应码分布、HTTP响应时间等指标。应用服务层与业务层并不是一 一对应的关系,一个业务可能对应多个应用服务层的URI,一个URI也可能对应多个业务层的业务。
接口调用层指的是系统依赖的外部系统接口,收集的信息包括访问情况,包括时延、错误码、次数等,当外部系统故障导致我们的业务故障时,通过接口调用层就能够快速的定位具体问题。
基础组件层指系统依赖的底层组件,例如容器、数据库、缓存、消息队列。不同的组件收集的信息不一样,例如数据库MySQL的监控指标包括连接数、请求数、查询行数、更新行数等,而缓存memcached包括使用率、踢出率、命中率等。
基础设施层指操作系统状态、网络状态,收集的信息包括cpu使用率、内存使用率、网卡流量、连接数等。
2. 自动化
自动化的含义就是不要再由人工去分析日志或者执行命令了,而是由程序自动完成这些动作。当故障定位的时候需要这些信息时,可以立即看到,节省故障定位时间。为此我们开发了一套数据收集和分析系统,这套系统可以从其它各个系统(包括业务系统、运维系统等)获取并分析数据,例如日志数据、状态数据等。
数据自动化收集和分析系统的结构如下:
其中Logstash用于采集日志,redis用于缓存日志,elasticsearch用于存储和分析日志。
3. 可视化
可视化的含义就是故障定位所需要的信息能够通过图表和数字直观的展示出来。有了自动化的收集和分析作为基础,可视化只需要将数据做成图表展示即可。除此以外,同比、环比这类数据也可以通过系统直观的展示出来,方便快速判断问题所在。
我们来回顾一下整个这套架构方案是如何实现我们最初的目标的:
通过运维已有的拨测脚本,加上立体化&自动化&可视化的监控,绝大部分故障能够在3分钟内发现并定位初步的原因。例如是因为访问量暴涨,还是数据库变慢了,还是网络被SYN FLOOD攻击了。
如果是某台机器或者某几台机器故障,我们可以将故障机器下线;
某个接口或者某些业务故障,如果不是核心业务,我们可以采用功能降级快速处理;
如果是大面积的业务故障,甚至是核心功能也故障了,我们可以通过将故障机房的业务切换到其它机房;
如果是所有的机房核心业务都出故障了,那就只能具体问题具体处理了,例如如果是新版本引起的,首先立刻回滚版本;如果是被攻击了,需要采用防攻击手段等。
通过核心功能和非核心功能分离,能够尽最大可能的保护核心功能的可靠性。当然除了这个措施外,我们还有其它更多常规的保证措施,例如项目流程上的部署评审,自动化测试,灰度发布,tcpcopy环境预发布、MySQL高可用、Memcached高可用等措施。
架构设计主要参与的人员有:王金银、王力志、聂勇、詹青朋、李运华、李俊,整个项目得到了研发BOSS郑从威的大力支持,这也是项目成功的关键因素!
作者简介: 李运华,目前就职于阿里巴巴集团移动事业群,担任资深软件工程师,主要从事后台架构设计、开发工作,同时带领团队负责部门的公共组件设计和开发。热爱技术但不拘泥于技术,爱好分享,喜欢读书,技术控、读书狂。
【相关阅读】 专访李运华:程序员如何在技术上提升自己
(责编/钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,交流探讨可加微信qshuguang2008,备注姓名+公司+职位)
本文来自 《程序员》电子刊10月B 架构专题文章, 程序员2015年电子版订阅火热进行中 ,包含:iPad版、Android版、PDF版。
值得一提的是,由CSDN举办的 SDCC 2015中国软件开发者嘉年华 将于11月19-21日在北京举行,本次大会涵盖:新型数据库、编程语言、工具与平台、产品与设计、前端开发、算法、微信开发、架构实践、安全等九大分 论坛,届时国外知名讲师将分享所在领域的最佳实践。 【点击这里抢票】