大神分享美团外卖订单中心演进之路
前言
美团外卖从2013年9月成交首单以来,已走过了三个年头。时期,事务飞速开展,美团外卖由日均几单开展为日均500万单(9月11日已突破600万)的大型O2O互联网外卖服务渠道。渠道支撑的品类也由开始外卖单品拓宽为全品类。
跟着订单量的增加、事务复杂度的提高,外卖订单体系也在不断演化进化,从前期一个订单事务模块到现在分布式可扩展的高性能、高可用、高安稳订单体系。全部开展过程中,订单体系阅历了几个显着的期间,下面本篇文章将为我们介绍一下订单体系的演进过程,重点重视各期间的事务特征、应战及应对之道。
为便利我们非常好地了解全部演进过程,我们首要看一下外卖事务。
外卖订单业务
外卖订单业务对于及时性的要求很高,因此从技术角度来看,实时性的优先级很高,从用户订餐开始算起,直至送达,时效通常在一个小时左右。如果超时,用户体验就会受到伤害,成为一次糟糕的购物经历。
在一个小时之内,订单会不断的变更状态,一直到最终送达。这期间要求各阶段配合紧密,一定要保证订单有效且及时送达。
下图是一个用户视角的订单流程图。
我们从一个普通用户的思维出发,当他下达订单之后,会有一个什么流程?
用户需要下单然后支付,这是整个订单发出的路径,在这之后,商家接收订单,完成制作,再之后,我们的物流团队进行配送,送至终端用户,然后就是确认收货、包括后续售后或完结订单等操作。
从技术上团队来看,各个阶段其实是多个子服务共同配的结果:比如说用户在订单下达的阶段可能会有需要各个购物车、预览、接受订单服务共同匹配完成,这一系列的子服务又依赖于我们整体的底层基础服务来完成。
另一个问题在于,外卖业务的时效性非常显著,订单在中午、傍晚时段会大批量的集中爆发,在其他时段数量则非常之少。这导致,到某时间段之后,整个系统的压力会急剧上升。
下图是一天内的外卖订单量分布图
总结而言,外卖业务具有如下特征:
- 流程较长且实时性要求高;
- 订单量高且集中。
下面将按时间脉络为大家讲解订单系统经历的各个阶段、各阶段业务特征、挑战以及应对之道。
订单系统雏型
在早期,外卖业务起步阶段技术团队的第一目标是要能够急速验证我们整体业务的可行性,所以这阶段技术团队的主要任务就是保证我们的服务架构有足够的灵活性,方便快速迭代,完成业务快速试错过程中的需求。
因此,我们将订单相关功能组织成为模块化的服务,将其与其他的模块一起拼装成jar包,这个包是公用的,可以通过我们的各个系统来使用这个包,从而提供订单功能。
早期系统的整体架构图如下所示:
在业务前期使用这种架构,整体来讲比较简单、灵活,整个公共服务的逻辑通过集成分配到各端,整个应用的开发部署相对比较简单,十分适合我们前期的需求:前面已经提过,是逻辑简单、业务量不大,方便与快速迭代。
但是,随着业务量持续上升,整个服务体系逻辑变得复杂,业务量开始增加,因此我们现有架构的漏洞开始出现,整个业务体系互相干扰,出现了很多问题。
我们考虑,在前期整个业务处在试探阶段,初步的架构可以满足业务需求,也能够给提供快速迭代的支持,但随着整个业务的成长、服务的成熟,我们应该对这个体系进行升级,满足现阶段的需求。
独立的订单系统
在14年的4月左右,外卖订单数量增长迅速,几乎达到每天10万订单量的水平,而且由于业务发展良好,这个数字每天都在不断攀升。整体业务的大框架此时已经基本成型,集体业务在整体框架上快速迭代。
业务团队使用一个大项目进行开发部署,在此基础上互相影响,业务沟通成本上升,大多数业务公用VM的情况,也对业务整体产生了一定影响。
为解决开发、部署、运行时相互影响的问题。我们将订单系统进行独立拆分,从而独立开发、部署、运行,避免受其它业务影响。
系统拆分主要有如下几个原则:
- 相关业务拆分独立系统;
- 优先级一致的业务拆分独立系统;
- 拆分系统包括业务服务和数据。
出于以上考虑,我们把订单系统进行了整体的的独立拆分,将接收到的所有订单服务通过接口提供给外部使用。
在内部订单系统中,我们评估了功能优先级,按照优先级拆分成了不同的子系统,以防止业务之间的相互影响,通过队列消息的形式,订单系统可以通知外部订单状态的变更等情况。
独立拆分后的订单系统架构如下所示:
这其中,最底层是数据存储层,负责订单相关数据的独立存储。订单服务层,该层按照优先级划分为三个体系,分别称之为交易体系、查询体系和异步处理的体系。
独立拆分后,能够避免事务间的相互影响。迅速支持事务迭代需要的一起,保障体系稳定性。
高性能、高可用、高稳定的订单系统
在我们将订单系统进行拆分后,业务之间的相互干扰大大降低,不仅保证了迭代的速度,而且还很好的维持了系统的稳定性,在这期间,整个业务的订单量开始逐渐想着百万大关迈进,并逐渐超出。本来的小问题,在大量订单出现的时候覆盖面积加大,对用户的试用体验相当不友好。举例说明,再支付成功后,某些极端情况会导致支付成功的消息无法返回,在终端显示没有支付。在巨量订单的前提下,这种问题开始增加,我们必须进一步优化系统的可靠性,保证我们的订单功能不会影响用户使用。
不仅如此,订单量仍在不断增加的同时,一些业务逻辑相对复杂、混乱,对于整个系统的使用提出了新的、更高的要求。
因此,为了能够建设一个更加稳定并且可信赖的订单服务系统,我们必须再次对整个系统进行升级。我们后文就会按照各个升级需求对整个升级过程进行描述。
性能优化
系统独立拆分后,可以方便地对订单系统进行优化升级。我们对独立拆分后的订单系统进行了很多的性能优化工作,提升服务整体性能,优化工作主要涉及如下几个方面。
异步化
一个显而易见的道理是,同一时间需要处理的事情越少,服务性能就会越高、速度越快,所以我们将通过将部分操作进行异步化处理减少整个操作进行的压力。通过异步来提升服务气的性能。
以下使我们提出的两种方案:
- 线程或线程池:将异步操作放在单独线程中处理,避免阻塞服务线程;
- 消息异步:异步操作通过接收消息完成。
注意合理有一个隐患,我们如何保障异步操作的执行?设想一下场景,当应用重启时,如果是线程或者线程池的行的异步化,在后台执行未完成的情况下,jvm重启会造成相当大的影响。
因此需要采取优雅关闭开保障业务的进行。当然,如果采取消息异步,则无须考虑这个问题。
具体到该系统,我们选择将某些没有必要同步进行的操作异步化,从而提供更强的性能,例如我们发放红包或者推送消息等等全部采用了异步处理。
以订单配送PUSH推送为例,将PUSH推送异步化后的处理流程变更如下所示:
PUSH异步化后,线程#1在更新订单状态、发送消息后立即返回,而不用同步等待PUSH推送完成。而PUSH推送异步在线程#2中完成。
并行化
操作并行化也是提升性能的一大利器,并行化将原本串行的工作并行执行,降低整体处理时间。我们对所有订单服务进行分析,将其中非相互依赖的操作并行化,从而提升整体的响应时间。
以用户下单为例,第一步是从各个依赖服务获取信息,包括门店、菜品、用户信息等。获取这些信息并不需要相互依赖,故可以将其并行化,并行后的处理流程变更如下所示:
通过将获取信息并行化,可有效缩短下单时间,提升下单接口性能。
缓存
经过将核算信息进行提早核算后缓存,防止获取数据时进行实时核算,然后提升获取核算数据的效劳功能。比如关于首单、用户已减免配送费等,经过提早核算后缓存,能够简化实时获取数据逻辑,节省时刻。
以用户已减免配送费为例,假如需求实时核算,则需求取到用户一切订单后,再进行核算,这么实时核算成本较高。技术团队经过提早核算,缓存用户已减免配送费。需求取用户已减免配送费时,从缓存中取即可,不用实时核算。具体来说,包含如下几点:
- 通过缓存保存用户已减免配送费;
- 用户下单时,如果订单有减免配送费,增加缓存中用户减免配送费金额(异步进行);
- 订单取消时,如果订单有减免配送费,减少缓存中用户减免配送费金额(异步进行);
一致性优化
订单体系触及买卖,需要确保数据的共同性。不然,一旦出现疑问,可能会致使订单不能及时配送、买卖金额不对等。
买卖一个很主要的特征是其操作具有业务性,订单体系是一个杂乱的分布式体系,比方付出触及订单体系、付出渠道、付出宝/网银等第三方。仅经过传统的数据库业务来确保不太可行。关于订单买卖体系的业务性,并不请求严厉满意传统数据库业务的ACID性质,只需要终究成果共同即可。对于订单体系的特征,咱们经过如下种方法来确保终究成果的共同性。
重试/幂等
经过延时重试,确保操作终究会最执行。比方退款操作,如退款时遇到网络或付出渠道毛病等疑问,会延时进行重试,确保退款终究会被完结。重试又会带来另一个疑问,即有些操作重复进行,需要对操作进行幂等处理,确保重试的正确性。
以退款操作为例,参加重试/幂等后的处理流程如下所示:
退款操作首先会查看是否现已退款,假如现已退款,直接返回。不然,向付出渠道建议退款,从而确保操作幂等,防止重复操作带来疑问。假如建议退款失利(比方网络或付出渠道毛病),会将使命放入延时行列,稍后重试。不然,直接返回。
经过重试+幂等,能够确保退款操作终究一定会完结。
2PC
2PC是指分布式业务的两期间提交,经过2PC来确保多个体系的数据一致性。比方下单过程中,触及库存、优惠资历等多个资本,下单时会首先预占资本(对应2PC的第一期间),下单失利后会释放资本(对应2PC的回滚期间),成功后会运用资本(对应2PC的提交期间)。关于2PC,网上有很多的说明,这儿不再持续打开。
高可用
分布式体系的可用性由其各个组件的可用性一起决议,要提高分布式体系的可用性,需求归纳提高构成分布式体系的各个组件的可用性。
对于订单体系而言,其主要构成组件包括三类:存储层、中间件层、效劳层。下面将分层说明订单体系的可用性。
存储层
存储层的组件如MySQL、ES等自身现已完成了高可用,比方MySQL经过主从集群、ES经过分片仿制来完成高可用。存储层的高可用依靠各个存储组件即可。
中间件层
分布式体系会大量用到各类中间件,比方服务调用结构等,这类中间件通常使用开源商品或由公司根底渠道供给,自身已具有高可用。
服务层
在分布式体系中,各个服务间经过彼此调用来完成事务功用,一旦某个服务出现问题,会级联影响调用方的其他服务,进而致使体系溃散。分布式体系中的依靠容灾是影响服务高可用的一个重要方面。
依靠容灾主要有如下几个思路:
- 依赖超时设置;
- 依赖灾备;
- 依赖降级;
- 限制依赖使用资源;
订单系统会依赖多个其它服务,也存在这个问题。当前订单系统通过同时采用上述四种方法,来避免底层服务出现问题时,影响整体服务。具体实现上,我们采用Hystrix框架来完成依赖容灾功能。Hystrix框架采用上述四种方法,有效实现依赖容灾。订单系统依赖容灾示意图如下所示
通过为每个依赖服务设置独立的线程池、合理的超时时间及出错时回退方法,有效避免服务出现问题时,级联影响,导致整体服务不可用,从而实现服务高可用。
另外,订单系统服务层都是无状态服务,通过集群+多机房部署,可以避免单点问题及机房故障,实现高可用。
小结
上面都是经过架构、技能完成层面来保证订单体系的功能、安稳性、可用性。实际中,有很多的事端是人为因素致使的,除了好的架构、技能完成外,经过标准、准则来躲避人为事端也是保证功能、安稳性、可用性的重要方面。订单体系经过完善需要review、计划评定、代码review、测验上线、后续跟进流程来防止人为因素影响订单体系安稳性。
经过以上办法,技术团队将订单体系建造成了一个高功能、高安稳、高可用的分布式体系。其中,交易体系tp99为150ms、查询体系tp99时刻为40ms。全体体系可用性为6个9。
可扩展的订单系统
订单体系通过上面介绍的全体晋级后,已经是一个高功能、高安稳、高可用的分布式体系。可是体系的的可拓展性还存在必定疑问,部分效劳只能通过笔直拓展(添加效劳器装备)而不能通过水平拓展(加机器)来进行扩容。可是,效劳器装备有上限,致使效劳全体容量受到约束。
到2015年5月的时分,这个疑问就比较突出了。其时,数据库效劳器写挨近单机上限。事务预期还会持续快速增长。为确保事务的快速增长,咱们对订单体系开始进行第2次晋级。方针是确保体系有满足的拓展性,然后支持事务的快速开展。
分布式体系的拓展性依赖于分布式体系中各个组件的可拓展性,关于订单体系而言,其首要组成组件包含三类:存储层、中间件层、效劳层。下面将分层阐明怎么进步各层的可拓展性。
存储层
订单体系存储层首要依赖于MySQL耐久化、tair/redis cluster缓存。tair/redis cluster缓存自身即提供了极好的拓展性。MySQL能够通过添加从库来处理读拓展疑问。可是,关于写MySQL存在单机容量的约束。另外,数据库的全体容量受限于单机硬盘的约束。
存储层的可拓展性改造首要是对MySQL拓展性改造。
- 分库分表
写容量约束是受限于MySQL数据库单机处理才能约束。假如能将数据拆为多份,不一样数据放在不一样机器上,就能够便利对容量进行拓展。
对数据进行拆分通常分为两步,第一步是分库,行将不一样表放不一样库不一样机器上。通过第一步分库后,容量得到必定提高。可是,分库并不能处理单表容量超越单机约束的疑问,跟着事务的开展,订单体系中的订单表即遇到了这个疑问。
关于订单表超越单库容量的疑问,需求进行分表操作,行将订单表数据进行拆分。单表数据拆分后,处理了写的疑问,可是假如查询数据不在同一个分片,会带来查询功率的疑问(需求聚合多张表)。因为外卖在线事务对实时性、功能请求较高。咱们关于每个首要的查询维度均保留一份数据(每份数据按查询维度进行分片),便利查询。
具体来说,外卖首要涉及三个查询维度:订单ID、用户ID、门店ID。对订单表分表时,关于一个订单,咱们存三份,别离依照订单ID、用户ID、 门店ID以必定规矩存储在每个维度不一样分片中。这么,能够涣散写压力,一起,依照订单ID、用户ID、门店ID三个维度查询时,数据均在一个分片,确保较高的查询功率。
订单表分表后,订单表的存储架构如下所示:
可以看到,分表后,每个维度共有100张表,分别放在4个库上面。对于同一个订单,冗余存储了三份。未来,随着业务发展,还可以继续通过将表分到不同机器上来持续获得容量的提升。
分库分表后,订单数据存储到多个库多个表中,为应用层查询带来一定麻烦,解决分库分表后的查询主要有三种方案:
- MySQL服务器端支持:目前不支持。
- 中间件。
- 应用层。
由于MySQL服务器端不能支持,我们只剩下中间件和应用层两个方案。中间件方案对应用透明,但是开发难度相对较大,当时这块没有资源去支持。于是,我们采用应用层方案来快速支持。结合应用开发框架(SPRING+MYBATIS),我们实现了一个轻量级的分库分表访问插件,避免将分库分表逻辑嵌入到业务代码。分库分表插件的实现包括如下几个要点。
- 配置文件管理分库分表配置信息;
- JAVA注解说明SQL语句分库分表信息;
- JAVA AOP解析注解+查询配置文件,获取数据源及表名;
- MYBATIS动态替换表名;
- SPRING动态替换数据源。
通过分库分表,解决了写容量扩展问题。但是分表后,会给查询带来一定的限制,只能支持主要维度的查询,其它维度的查询效率存在问题。
- ES搜索
订单表分表之后,对于ID、用户ID、门店ID外的查询(比如按照手机号前缀查询)存在效率问题。这部分通常是复杂查询,可以通过全文搜索来支持。在订单系统中,我们通过ES来解决分表后非分表维度的复杂查询效率问题。具体来说,使用ES,主要涉及如下几点。
- 通过databus将订单数据同步到ES。
- 同步数据时,通过批量写入来降低ES写入压力。
- 通过ES的分片机制来支持扩展性。
小结
通过对存储层的可扩展性改造,使得订单系统存储层具有较好的可扩展性。对于中间层的可扩展性与上面提到的中间层可用性一样,中间层本身已提供解决方案,直接复用即可。对于服务层,订单系统服务层提供的都是无状态服务,对于无状态服务,通过增加机器,即可获得更高的容量,完成扩容。
通过对订单系统各层可扩展性改造,使得订单系统具备了较好的可扩展性,能够支持业务的持续发展,当前,订单系统已具体千万单/日的容量。
上面几部分都是在介绍如何通过架构、技术实现等手段来搭建一个可靠、完善的订单系统。但是,要保障系统的持续健康运行,光搭建系统还不够,运维也是很重要的一环。
智能运维的订单系统
早期,对系统及业务的运维主要是采用人肉的方式,即外部反馈问题,RD通过排查日志等来定位问题。随着系统的复杂、业务的增长,问题排查难度不断加大,同时反馈问题的数量也在逐步增多。通过人肉方式效率偏低,并不能很好的满足业务的需求。
为提升运维效率、降低人力成本,我们对系统及业务运维进行自动化、智能化改进,改进包括事前、事中、事后措施。
- 事前措施
事前措施的目的是为提前发现隐患,提前解决,避免问题恶化。
在事前措施这块,我们主要采取如下几个手段:
- 定期线上压测:通过线上压测,准确评估系统容量,提前发现系统隐患;
- 周期性系统健康体检:通过周期检测CPU利用率、内存利用率、接口QPS、接口TP95、异常数,取消订单数等指标是否异常,可以提前发现提前发现潜在问题、提前解决;
- 全链路关键日志:通过记录全链路关键日志,根据日志,自动分析反馈订单问题原因,给出处理结果,有效提高反馈处理效率。
- 事中措施
事中措施的目的是为及时发现问题、快速解决问题。
事中这块,我们采取的手段包括:
- 订单监控大盘:实时监控订单业务指标,异常时报警;
- 系统监控大盘:实时监控订单系统指标,异常时报警;
- 完善的SOP:报警后,通过标准流程,快速定位问题、解决问题。
- 事后措施
事后措施是指问题发生后,分析问题原因,彻底解决。并将相关经验教训反哺给事前、事中措施,不断加强事先、事中措施,争取尽量提前发现问题,将问题扼杀在萌芽阶段。
通过将之前人肉进行的运维操作自动化、智能化,提升了处理效率、减少了运维的人力投入。