摘要:饿了么是一家创业公司,业务发展很是快,可能准备不是很充分,好比说监控、日志、告警、框架、消息、数据库,不少基础设施还在建设之中。在这个过程当中出现一些问题是在所不免的,对系统的要求不是不能挂、不能出问题,而是出了问题要第一时间能恢复。数据库
关于它的服务架构的演进:缓存
图中所示是订单的早期架构图,比较简单。这个架构在2014年的时候支撑了日均10万的订单,是一套很不错的架构,依然在不少系统中完美运行。可是对于后来发展的场景,它已经曝露问题了,好比业务逻辑严重耦合、代码管理很困难,由于数据库都在一块儿,操做变动很难追溯。服务器
更进一步的是,性能的瓶颈只能是靠服务器去硬抗,从物理架构到逻辑架构,都已经成为业务发展的掣肘了。因而,为了业务的发展,咱们作了一些演进的工做。网络
演进工做的核心就是一个字“拆”,跟“拆”对等的就是分治的思想。怎么拆分呢?面向服务有不少拆分原则。我将拆分过程当中最具帮助和指导性的点罗列了如下几条:架构
1 明确的定义 以前也确实犯了一些错误,为了拆而拆。其实咱们须要更明确,什么才算是一个服务?服务必定具备很是独立的技术能力或者业务能力,并且必定意义上可以很抽象负载均衡
2松耦合 最基本的松耦合就是Customer的消费不依赖于Provider的某一个特定实现,这样服务器的内部变动不会影响外部消费,消费者能够切换到其余服务能力的提供方,这是最基本的松耦合。还有时间上的松耦合或者位置上的松耦合,咱们但愿的松耦合是消费方和服务方是能够分离的框架
3基于领域认知 这对于整个产品起到很是大的做用。由于当时整个饿了么全部系统是在一块儿的,基于领域的认知,在面向用户的维度和面向商户的维度作了切分,还有基于交易链路作了切分ide
4单一职责与关注分离 简单说,咱们但愿一个服务或者一个模块拥有单一的能力,而不是承担过多的职责,不然责任不清晰,致使能力也不清晰。性能
5可被验证的结果 在订单拆分的过程当中咱们犯了一些错误,当时认为这样拆分是没有问题的,可是过1、两个月,并无带来效率和能力的提高,反而是跨团队的要求愈来愈多,能力要求也愈来愈多。这时候多是拆错了。若是是一个好的拆分必定有利于发展,拆分以后的发展是更迅速的。学习
基于这几条原则,他们对饿了么的总体服务作拆分以后,如上图所示,架构就有了一些变化,看起来跟刚才架构区别不大。把Order Service作了分离。当时拆分虽然比较垂直,可是用户、商户、金融、订单等仍是有一些横向交互。
一个接口有一个很是明确的Owner,一个表、一个库也能保证仅有单一的操做方,让我感觉比较直接的是,为服务的治理奠基了基础,之后能够针对某项特定业务作一些降级、熔断,以及单独的监控。拆分其实是让各自模块的掌控力变得更强了,对业务起到更好的支撑做用。
这时每一个部门或者每一个团队都负责本身独立的领域,代码和数据都拆分完毕是否是就能够了?可是后来发现好像还不对。由于虽然大的领域上确实已经干净了,可是在小的领域上依然问题不少,订单并不只仅只有一张表,一个单一的模块,其实还有不少复杂的内容。
在一些技术工做上,这些问题曝露得并非那么明显,那时候你们对于一些领域认知或者业务边界的认识仍是模糊的,没有人界定这些。可是当更进一步地去发展一个领域的时候,仍是会有职责不清晰或者能力模糊的地方。咱们思考,还要基于业务进行更细腻的规划。
因而咱们把订单自己作了一些业务层次的拆分,拆分以前首先要确认订单到底在整个系统中,尤为是交易系统、O2O系统中承担什么角色,担负什么职责。
在这个思考过程当中,咱们的认知大概是如下四点
第一,订单是整个交易链路的核心,围绕了一些相关服务,好比金额计算服务、催单服务、售中异常服务等,咱们但愿这些服务之间有明确的区别。
第二,订单实时处理是整个链路的中心,咱们将这个过程定义得尽可能简洁。一笔交易中,订单被推动得越复杂,说明系统设计得越复杂,出问题的几率也会越高。因此咱们但愿订单核心流程很是简单、轻薄,把复杂的东西剥离出来,把简单和复杂明确成两个部分。
第三,考虑到交易的时效性和异常场景愈来愈复杂,将交易分红正向交易流程和逆向交易流程两个部分。正向交易流程,99%的订单会根据这个流程走完生命周期;逆向交易流程,好比说退单要求时效性比较低,处理会牵扯多方业务可能很复杂,因此经过一个逆向的交易流程来解决。
第四,可以在功能和业务上独立的部分,尽量抽象为单独的模块或服务。简单来讲,好比催单的服务,它其实对交易链路没法起到推动做用,它只是一个动做或者附带服务,咱们把它单独抽象出来,为后面的发展作出铺垫。
基于这些以后,咱们对订单进行完整的认知,对订单的服务架构和业务架构作成图中的样子,大概是三层。下面一层是基本数据;中间层是正向逆向的流程、最核心的状态和最关联的交易链上耦合的服务;上层是用户服务、商户服务,包括跟交易链相关的,好比饿了么最近推出的“准时达”的服务。
咱们同时对其余服务模块也作了演进。一些是以前设计的不合理,如图所示是当时缓存服务的逻辑架构,节点比较多。简单解释一下最初的作法: 提交订单的时候清除缓存,获取订单的时候若是没有缓存的话,会经过消息机制来更新缓存。中间还有一个Replicator,起到重复合并的做用。
后来咱们发现,原本能够轻量级实现的内容,可是用了相对复杂的实现,链路长,组件多,受网络影响很是大。一旦一个节点缓存数据不一致,感知会比较困难,尤为是业务体量大的时候。
业务体量小的时候同时处理的量并很少,问题曝露并不明显,可是体量变大的时候,这个设计马上带来不少困扰。因此咱们对缓存作了简化,就是把没必要要的内容砍掉,作一个最基本的缓存服务。
这是一个最基本的缓存的套路,在数据库更精准的状况下更新缓存,若是从DB获取不到就从缓存获取。这个架构虽然简单了,可是效率比以前高不少,以前数据库和缓存之间延迟在200毫秒左右,而这个简单实现延迟控制在10毫秒之内。
以前订单最大的瓶颈是在数据库,咱们主要作了DAL中间层组件。图中这个中间件对咱们影响很是大,日均300万单的时候数据库量比较大,引入DAL中间件作什么呢?有几个做用:数据库管理和负载均衡以及读写分离,水平分表对用户和商户两个维度作评估,为用户存储至少半年以上的数据。解决了数据库的瓶颈,系统总体负载能力提高了不少。
这张图说明了订单具体改造的时候DAL中间件起的做用,有读写分离端口、绑定主库端口、水平分表、限流削峰以及负载均衡等功能。
监控和告警的峰值很是明显,午间和晚间两个高峰,其余时间流量相对平缓。下面主要讲三个部分。
第一,对于订单而言,吞吐量是最须要重点关注的指标。一开始对业务指标的感知并非特别清晰,就在某一个接口耗费了不少时间。后来发现一些很小BD的问题不太容易从小接口感知到,可是从业务方面感知就比较明显,因此就更多关注业务指标的控制。
第二,一般咱们重视系统指标,而容易忽视业务指标,其实业务指标更能反映出隐晦的问题。
第三,目前咱们致力于基于监控和数据学习的过载保护和业务自动降级。虽然如今尚未彻底作好,可是已经能感受到一些效果。若是商户长时间不接单,用户会自动取消订单,自动取消功能的开关目前是人工控制的,咱们更但愿是系统来控制。
好比说有大量订单取消了,有多是接单功能出了问题,就须要临时关闭这个功能,若是仍是依靠人来作,每每已经来不及,这时候就应该基于数据的学习,让系统自动降级这个功能。
当作完这一切,订单的架构就变成了上面这个样子。咱们把整个Service集群作了分组,有面向用户的、面向商户的,还有物流和其余方面的
就订单系统而言主要有如下四个内容。第一是消息广播补偿,第二是主流程补偿,第三是灾备,第四是随机故障测试系统。
首先是消息广播补偿。对于订单来讲,MQ是很是核心的基础组件,若是它出现问题,一些订单处理就会受影响。为了不这种状况发生,咱们作了一个补偿的内容,这个补偿其实很简单,就是在订单状态发生消息变化的时候,咱们会同时落一份消息数据,目前会存储最近一小时的消息。
若是MQ系统或者集群当前有问题或者抖动,消息广播补偿能够起到一个备线的做用。消息广播补偿不能应付全部问题,可是对于订单系统的稳定和健壮而言仍是很是有用的。
第二是主流程的补偿。什么是主流程?就是交易的正向流程。99%的交易都会是正向的,就是下单、付款,顺利吃饭。在这个过程当中,只要用户有经过饿了么吃饭的意向,就尽全力必定让他完成最终的交易,不要由于系统的缘由影响到他。
主流程主要是针对链路自己出问题的状况,以最大程度保证交易的进行,也是对主要链路的保护。
好比有一次出现这个问题:用户已经支付过了,但订单没有感觉到这个结果,订单显示还在待支付,当时支付服务本应该把结果推送过来,订单就能够继续往前走,可是系统在那里卡住了,这对用户就是比较差的体验。
因此后来咱们作了主流程的补偿,以确保交易的信息链路一直完整。咱们的原则是,对订单的各个状态变动进行推送或拉取,保证最终的一致性,链路和介质要独立于原流程。咱们从两个方面来解决这个问题。
在部署方面,把提供补偿功能的服务和主服务分开部署,依赖的服务也须要使用独立实例,以保证高可用性。在效果方面,用户支付成功前的全部信息都应该尽可能入库,能够对支付、待接单、接单等一系列环节均可以作补偿。
这是主流程补偿的图,最大的关联方就是支付和商户,支付就是表明用户。商户有推送订单信息,支付也有推送订单信息,若是出现问题,补偿服务能够拉取结果,订单甚至能够自动接单。这个补偿通过屡次的演变,目前依然在运做,对于一些比较特殊的状况仍是颇有用的,能够在第一时间处理问题,保证交易的完成。
第三是灾备。目前订单系统作了一个比较简单的灾备,就是两个机房的切换。切换的时候是全流量切换的,咱们会把流量从A机房切到B机房。订单的主要操做是在切换的过程当中要进行修复数据。
好比,一些订单开始是在A机房,被切换到B机房去操做,这就可能会形成两个机群数据不一致的状况,因此会专门对信息作补全,当一笔交易切换到另外一个机房后,咱们要确保短期内将数据对比并修复完成,固然主要仍是确保数据最终一致。
第四是随机故障测试系统。左图是Netflix的猴子家族,右图是咱们作的Kennel系统,一个是猴子窝,一个是狗窝。你们对猴子家族了解吗?Netflix如今几乎把全部内容都部署在云上,对系统和架构的要求很高,他们能够随时破坏一些节点,以测试是否能依然为用户提供服务。
咱们也参考他们的作法,有很大的启发,避免失败最好的办法就是常常失败。饿了么的发展速度比很是快,技术还不完善,设计也会有缺口。我我的以为,一个好的系统或者好的设计不是一开始被大牛设计出来的,必定是随着发展和演进逐渐被迭代出来的。
参考了Netflix的猴子家族,咱们研发了本身的Kennel系统。猴子家族主要是针对节点的攻击,咱们的Kennel主要是对网络、内存等作了调整,还结合本身的服务,对应用和接口也作了一些攻击。攻击分两部分。
第一部分是物理层面的,咱们能够对指定节点IO作攻击,或者把CPU打到很高;对于服务和接口而言,能够把某个接口固定增长500毫秒或者更要的响应延迟,这样作的目的是什么?在整个链路中,咱们但愿架构设计或者节点都是高可用的,高可用就须要被测试,经过大量的测试人为攻击节点或者服务,来看预先设计好的那些措施或者补偿的能力是否是真的有用。
整个Kennel的设计是,首先会有一个控制中心来作总的调度,配置模块能够配置各类计划,能够控制CPU或者网络丢包等,能够设置在每周六8-10am的某个时间点攻击系统十五分钟。它还有一些操做模块,好比执行计划模块、任务执行模块、节点管理模块、执行记录模块等。
Kennel有四个主要的做用。
首先,帮助咱们发现链路中隐蔽的缺陷,将小几率事件放大。好比说缓存不一致的问题,以前极少出现,一旦出现以后,处理手段比较缺少,那就能够经过Kennel来模拟。网络的抖动是很随机的,那么Kennel能够在某个时间段专门进行模拟,把小几率事件放大。若是怀疑某个地方出了问题,能够经过它来测试是否是真的能查出问题。
第二,重大功能能够在发布以前经过其进行测试,迫使你更深刻地设计和编码。经过模拟流量或者线上流量回放,来检验系统运行是否如你设计那样工做,好比监控的曲线或者告警以及相关服务之间的依赖等。
第三,咱们作了不少失败的准备和设计,要看到底会不会起做用、起多大做用。能够经过Kennel进行校验,在某个时间经过随机手段攻击相关服务,服务方不知道具体的攻击内容,这时本来设定的监控告警,降级熔断等措施有没有及时起做用就是一个很好的校验。同时还能够检验以前准备的容错或者补偿措施是否能按照预期工做。
第四,须要验证FailOver的设计,只有验证经过才能够依靠。全部的设计都是经历了一次一次的失败,一些设计原觉得有用,可是真实问题发生时并无起到做用。真正有意义的FailOver设计必定是通过验证的。