“
本文是依据石佳宁在全球架构师(深圳)峰会上所做的演讲整理出来的 。
老司机简介
石佳宁是饿了么后台支撑研发部的负责人,他目前在饿了么任职,担任平台研发中心后台支撑部门的负责人,主要负责饿了么外卖订单系统的设计和研发工作,负责统一客服系统的设计和研发工作,负责BD销售系统的设计和研发工作,负责管理工具系统的设计和研发工作,负责代理商管理平台系统的设计和研发工作。
先进行一下自我介绍,我在2014年加入了饿了么,那个时候是饿了么快速发展的开端。我始终从事后台领域的研发工作,像BD系统、客服系统以及订单系统等,如今专注于交易架构相关的工作。
今天要讲的内容主要分成两大部分 第一部分是在高速增长 交易场景愈加复杂的情况下 饿了么订单的服务架构是怎样演进的 到底是什么支持我们发展的
快速增长下的业务场景
在具体讲述之前,我先介绍一下我们的场景,因为若脱离具体场景,所有架构演进都毫无意义 。上面这两个图表并非饿了么的数据,而是第三方分析整个外卖市场得出的数据图 。左边的图表自2011年起,整个O2O市场以及外卖的份额逐年递增 。在2013年和2014年时出现了较大飞跃,饿了么也是在这个时间段订单量开始急剧增加 。右边的图表是用户注重外卖平台的因素分布。
从图中能够看出,用户十分在意配送速度,也在意交易的时效性。对于O2O或者饿了么订单,交易的要求比传统电商更高,原因是交易通常一两个小时就会结束。在2014年初,饿了么订单量日均仅有10万单,到2014年底超过了百万单,这实现了质的飞跃,10万订单的量级与百万订单的量级要求差异极大。在2015年的时候,突破了日均300万,到今年5月,单日峰值突破了500万。
快速发展会涉及诸多问题,我们作为一家创业公司,业务发展极为迅速,或许准备得并不十分充分,像监控、日志、告警、框架、消息、数据库等,诸多基础设施仍在建设进程中。在此过程里出现一些问题是难以避免的,对系统的要求并非不能挂、不能出问题,而是出了问题要能够在第一时间恢复,这乃是整个系统架构的前提条件。
服务架构的演进
图中展示的是订单的早期架构图,该架构较为简单,在2014年时能支撑日均10万的订单,是一套相当不错的架构,至今仍在许多系统中完美运行。然而,对于后来发展出的场景,它已暴露出问题,诸如业务逻辑严重耦合、代码管理困难,原因在于数据库都集中在一起,操作变更难以追溯 。
性能的瓶颈只能依靠服务器去顽强抵抗,物理架构成了业务发展的阻碍,逻辑架构也成了业务发展的阻碍,为了业务的发展,我们开展了一些演进的工作 。
演进工作的核心是“拆”,与“拆”相对应的是分治思想。如何进行拆分呢?面向服务存在诸多拆分原则。我把拆分过程中最具帮助性和指导性的要点罗列了以下几条。
基于这几条原则,我们对饿了么的整体服务进行了拆分,拆分后架构出现了一些变化,如上图所示,看起来与刚才的架构区别不大。做了分离。当时拆分比较垂直,不过用户、商户、金融、订单等仍存在一些横向交互。
一个接口有非常明确的特点,一个表、一个库能保证仅有单一的操作方,这让我感受比较直接,它为服务的治理奠定了基础,以后可针对某项特定业务做一些降级、熔断,以及单独的监控。拆分实际上使各自模块的掌控力变得更强,对业务起到更好的支撑作用。
这时,每个部门负责自己独立的领域,每个团队也负责自己独立的领域,代码拆分完毕,数据拆分完毕,这样就可以了吗?但后来发现并非如此。因为大的领域确实已干净,然而小的领域问题仍很多,订单不只有一张表,不是一个单一模块,实际上还有很多复杂内容。
在一些技术工作当中,这些问题暴露得并不显著,当时大家对一些领域的认知以及业务边界的认识仍很模糊,没人对这些进行界定,然而当进一步发展一个领域时,仍会存在职责不清晰或者能力模糊的情况,我们思考,还需基于业务开展更细致的规划。
于是我们对订单本身进行了一些业务层次的拆分,在拆分之前,首先要确认订单在整个系统中的角色,特别是在交易系统、O2O系统中的角色,还要确认订单在整个系统中的职责,特别是在交易系统、O2O系统中的职责。
在这个思考过程中,我们的认知大概是以下四点。
第一,订单是整个交易链路的核心,围绕订单存在一些相关服务,像金额计算服务,催单服务,售中异常服务等,我们期望这些服务之间有清晰的区分。
第二,订单实时处理是整个链路的中心,我们把这个过程定义得尽可能简洁。在一笔交易中,订单推进得越复杂,表明系统设计得越复杂,出问题的概率也就越高。因此,我们期望订单核心流程非常简单、轻便,将复杂的部分剥离出来,把简单与复杂明确划分为两个部分。
第三,鉴于交易的时效性以及异常场景日益复杂,故而将交易划分为正向交易流程与逆向交易流程两部分。其中,正向交易流程中,99%的订单会依照此流程完成生命周期。而逆向交易流程方面,例如退单,其时效性要求较低,且处理过程可能会涉及多方业务,较为复杂,所以借助一个逆向的交易流程来予以解决。
第四,对于能够在功能和业务上实现独立的部分,要尽可能将其抽象成单独的模块或者服务。简单来讲,就像催单的服务,它实际上无法对交易链路起到推进作用,它仅仅是一个动作或者附带的服务,我们把它单独抽象出来,以此为后面的发展做好铺垫。
基于这些之后 ,我们对订单有了完整的认知 ,将订单的服务架构和业务架构做成了图中的样子 ,大概是三层 。下面一层是基本数据 ;中间层是正向逆向的流程 、最核心的状态以及最关联的交易链上耦合的服务 ;上层是用户服务 、商户服务 ,其中包括跟交易链相关的 ,比如饿了么最近推出的 “准时达” 服务 。
我们同时也对其他服务模块进行了演进。有些服务模块之前设计得不合理,如图展示的是当时缓存服务的逻辑架构,其节点较多。下面简单解释一下最初的做法:提交订单时会清除缓存,获取订单时若没有缓存,便会通过消息机制更新缓存。中间还有一个环节,能起到重复合并的作用。
后来我们发现,原本能够以轻量级方式实现的内容,却采用了相对复杂的实现方式,链路很长,组件众多,受网络影响极大。一旦有一个节点的缓存数据不一致,要察觉到会比较困难,特别是在业务体量大的时候。
业务体量较小时,同时处理的量不多,问题暴露不明显,然而体量变大时,这个设计立即带来诸多困扰,于是我们对缓存进行简化,即砍掉不必要的内容,打造一个最基本的缓存服务。
这是一个最基本的缓存套路,当数据库更精准时更新缓存,若从DB获取不到则从缓存获取。这个架构虽简单,但其效率比之前高很多,之前数据库和缓存之间延迟约200毫秒,而这个简单实现延迟控制在10毫秒以内。
之前订单最大的瓶颈在于数据库,为此我们着重开发了DAL中间层组件。图中的这个中间件对我们意义重大,当日均订单量达到300万单时,数据库的数据量颇为庞大。引入DAL中间件有何作用呢?它具备几个方面的作用:包括数据库管理、负载均衡以及读写分离。水平分表会从用户和商户两个维度展开评估,为用户存储至少半年以上的数据 。解决了数据库的瓶颈,系统整体负载能力提升了很多。
这张图说明了在订单具体改造时,DAL中间件起到了作用,它具备读写分离端口功能,具备绑定主库端口功能,具备水平分表功能,具备限流削峰功能,还具备负载均衡功能。
监控的峰值非常明显,告警的峰值也非常明显,存在午间和晚间这两个高峰,在其他时间流量相对平缓 。下面主要讲三个部分。
对于订单来说,吞吐量是最需重点关注的指标。起初对业务指标的感知不清晰,在某一个接口耗费了大量时间。后来发现一些小BD问题不易从小接口感知到,从业务方面感知却比较明显,所以更多关注业务指标的控制。
第二,通常我们重视系统指标,容易忽视业务指标,实际上业务指标更能反映隐晦问题。
第三,目前我们致力于过载保护和业务自动降级,这是基于监控和数据学习的。虽然现在还没有完全做好,不过已经能感觉到一些效果。要是商户长时间不接单,用户会自动取消订单。自动取消功能的开关目前由人工控制,我们更希望由系统来控制。
有大量订单被取消,这有可能是接单功能出现问题所致,此时需要临时关闭该功能。若仍依靠人工处理,往往会来不及,这种情况下应基于数据学习,让系统自动降低该功能的级别。
做完这一切后,订单的架构呈现出上面的样子。我们对整个集群进行了分组,其中包括面向用户的分组,面向商户的分组,还有物流方面的分组以及其他方面的分组。
for
就订单系统来说,主要包含以下四个方面的内容。其一为消息广播补偿,其二为主流程补偿,其三是灾备,其四是随机故障测试系统。
首先是消息广播补偿,MQ 是订单非常核心的基础组件,若它出现问题,一些订单处理会受影响,为避免这种情况发生,我们做了补偿内容,该补偿很简单,即订单状态发生消息变化时,我们会同时落一份消息数据,目前会存储最近一小时的消息 。
若MQ系统存在问题,或者集群当前出现抖动,消息广播补偿能起到备线作用。消息广播补偿虽无法应付所有问题,但对订单系统的稳定,以及订单系统的健壮而言,还是颇为有用的。
第二是主流程的补偿,什么是主流程,主流程就是交易的正向流程,99%的交易都是正向的,也就是下单、付款,然后顺利吃饭,在这个过程中,只要用户有通过饿了么吃饭的意向,就要尽全力让他完成最终交易,不能因为系统原因影响到他。
主流程主要针对链路本身出现问题的情形,其目的是最大程度保证交易能够进行,这也是对主要链路的一种保护 。
用户已经完成支付,然而订单却未接收到该结果,仍显示处于待支付状态。当时支付服务理应推送支付结果,如此订单便能继续流转,可系统却在此处卡顿,这给用户带来了极为糟糕的体验。
所以后来我们进行了主流程的补偿,目的是确保交易的信息链路始终保持完整,我们遵循的原则是,针对订单的各个状态变更实施推送或拉取操作,以此保证最终的一致性,并且链路和介质需独立于原流程,我们从两个方面着手解决这个问题。
在部署方面,要将提供补偿功能的服务与主服务分开进行部署,依赖的服务同样需要使用独立实例,以此来保证高可用性。在效果方面,用户支付成功之前的所有信息都应当尽可能入库,对于支付、待接单、接单等一系列环节都能够进行补偿。
这是一张关于主流程补偿的图,其中最大的关联方是支付和商户,支付代表用户。商户会推送订单信息,支付同样会推送订单信息。若出现问题,补偿服务能够拉取结果,订单甚至能自动接单。这个补偿历经多次演变,至今仍在运作,对一些特殊情况颇为有用,能在第一时间处理问题,确保交易完成。
第三是灾备,目前订单系统做了较为简单的灾备,即两个机房的切换,切换时是全流量切换,会将流量从A机房切到B机房,订单的主要操作是在切换过程中修复数据。
一些订单起初在A机房,随后被切换至B机房操作,这或许会致使两个机群数据不一致,故而会专门对信息进行补全,当一笔交易切换至另一机房后,我们要保证在短时间内完成数据对比与修复,当然关键是确保数据最终保持一致。
第四是随机故障测试系统,左图展示的是猴子家族,右图呈现的是我们所做的系统,一个好似猴子窝,一个犹如狗窝。大家对猴子家族熟悉吗?当下几乎已将所有内容都部署到了云上,这对系统和架构有着很高的要求,它们能够随时破坏一些节点,以此来测试是否依旧可为用户提供服务 。
我们参考他们的做法后得到很大启发,避免失败最好的办法是经常失败,饿了么发展速度极快,但其技术尚不完善,设计也存在缺口,我个人认为,一个好的系统或好的设计并非一开始就由大牛设计出来,而是随着发展和演进逐渐迭代产生的。
参考了猴子家族,我们研发了自己的系统,猴子家族主要针对节点进行攻击,我们的系统主要对网络、内存等进行调整,还结合自身服务,对应用和接口也进行一些攻击,攻击分为两部分。
第一部分属于物理层面,我们能够对指定节点IO展开攻击,或者将CPU的使用率提升至很高水平;针对服务和接口来说,可以使某个接口的响应延迟固定增加500毫秒或者更长时间,这样做的目的究竟是什么?在整个链路中,我们期望架构设计具备高可用性,节点也具备高可用性,高可用性需要接受测试,通过大量测试对节点或者服务进行人为攻击,以此查看预先设计好的措施或者补偿能力是否真的有效。
整个设计中,首先会有一个控制中心进行总的调度,配置模块能够配置各种计划,能控制CPU或者网络丢包等,还能设置在每周六上午8点到10点的某个时间点攻击系统十五分钟,它还有一些操作模块,例如执行计划模块、任务执行模块、节点管理模块、执行记录模块等 。
有四个主要的作用。
首先,它能帮助我们发现链路中隐蔽的缺陷,还能将小概率事件放大。例如缓存不一致的问题,以前极少出现,一旦出现后,处理手段较为缺乏,此时就可通过它来模拟。网络抖动很随机,所以能在某个时间段专门进行模拟,以此放大小概率事件。要是怀疑某个地方有问题,借助它就能测试是否真能查出问题。
第二,重大功能在发布之前可借助其展开测试,这会促使你更深入地进行设计与编码。通过模拟流量或者回放线上流量,以此检验系统运行是否如你设计的那般工作,例如监控的曲线、告警以及相关服务之间的依赖等 。
第三,我们做了许多失败的准备与设计,想看看是否会起作用,以及作用有多大。可以开展校验,在某个时间通过随机方式攻击相关服务,服务方不清楚具体攻击内容,此时原本设定的监控告警、降级熔断等措施能否及时发挥作用,便是很好的校验。同时还能检验之前准备的容错或补偿措施能否按预期运行。
第四,存在需要验证的设计,只有验证通过了,才能够依靠。所有设计都经历过一次次失败,有些设计原本认为有用,然而在真实问题出现时却没起到作用。真正有意义的设计必定是经过验证的。
8月27日20:00至21:00,百川解码将带领大家与技术大咖直接沟通交流,共同探讨那些年热修复造成的问题,剖析最流行的阿里多平台热修复方案!
工作时间:8:00-18:00
电子邮件
扫码二维码
获取最新动态