美团外卖订单中心的演进

前言

自2013年9月美团外卖第一单成交以来,已经三年了。在此期间,业务发展迅速,美团外卖从日均几单发展到日均500万单O2O互联网外卖服务平台。平台支持的品类也从最初的外卖单品扩展到全品类。

随着订单量的增长、业务复杂度的提升。外卖订单系统也在不断演变进化,从早期一个订单业务模块到现在分布式可扩展的高性能、高可用、高稳定订单系统。整个发展过程中,订单系统经历了几个明显的阶段,下面本篇文章将为大家介绍一下订单系统的演进过程,重点关注各阶段的业务特征、挑战及应对之道。

为了方便大家更好地了解整个演变过程,我们先来看看外卖业务。

外卖订单业务

外卖订单业务是一项需要实时交付的业务,对实时性有很高的要求。从用户订购到最终交付,通常在1小时内。如果最终交付时间更长,它将带来用户体验。在1小时内,订单将快速通过多个阶段,直到最终交付给用户。每个阶段都需要密切合作,以确保订单的顺利完成。

下图是用户视角的订单流程图。

从普通用户的角度来看,外卖订单将通过多个阶段完成支付、商户订单接收、配送、用户收货、售后和订单。从技术的角度来看,每个阶段都依赖于多个子服务。例如,订单将依赖于购物车、订单预览和订单确认服务,这些子服务将依赖于基础系统来完成其功能。

外卖业务的另一个重要特点是,订单量将在一天内定期变化,订单将集中在中午和晚上,而其他时间的订单量较少。这样,餐点附近的系统压力就会相对较大。

下图是一天内外卖订单量分布图

综上所述,外卖业务具有以下特点:

流程长,实时要求高;订单量高,集中。

下面将解释订单系统经历的各个阶段、业务特征、挑战和应对方式。

订单系统雏形

在外卖业务发展的早期阶段,第一个目标是快速验证业务的可行性。从技术上讲,我们需要确保架构足够灵活和快速迭代,以满足业务快速试错的需要。

在这个阶段,我们将订单相关功能组织成模块,与其他模块(商店模块等)形成公共模块jar然后引入每个系统jar使用订单功能。

早期系统的整体架构图如下:

早期,外卖的整体结构简单灵活,公共业务逻辑通过jar包实现后集成到各端应用,应用开发部署相对简单。更适合早期逻辑简单、业务量小、需要快速迭代的情况。然而,随着业务逻辑的复杂性和业务量的增长,单一应用架构的缺点逐渐暴露出来。系统复杂后,共享大型项目进行开发部署,协调成本增加;业务之间的相互影响问题逐渐增加。

早期业务正处于试错、快速变化和快速迭代阶段。通过上述结构,我们可以跟上业务,快速满足业务需求。随着业务的发展和业务的逐步成熟,我们逐步升级系统,以更好地支持业务。

独立的订单系统

20142000年4月,外卖订单量达到10万单/天,订单量持续增长。此时,大型业务框架基本形成,业务在大型框架的基础上快速迭代。我们共享一个大项目进行开发和部署,相互影响,协调成本增加;多个业务部署在同一个业务部署VM,相互影响也在增加。

为了解决开发、部署和运行过程中相互影响的问题。我们独立拆分订单系统,以免受到其他业务的影响。

系统拆分主要有以下原则:

相关业务拆分独立系统;优先级一致的业务拆分独立系统;拆分系统包括业务服务和数据。

基于上述原则,我们独立拆分订单系统,所有订单服务都通过RPC界面提供给外部使用。在订单系统中,我们将功能分为不同的子系统,以避免相互影响。订单系统通过MQ(队列)通知外部订单状态变化。

独立拆分后的订单系统架构如下:

其中,底层是数据存储层,与订单相关的数据独立存储。在订单服务层,我们将订单服务分为交易系统、查询系统和异步处理系统三个系统。

独立拆分后,可以避免业务间的相互影响。在保证系统稳定性的同时,快速支持业务迭代需求。

订单系统性能高,可用性高,稳定性高

在上述独立拆分后,订单系统有效地避免了业务之间的相互干扰,确保了迭代速度,并确保了系统的稳定性。此时,我们的订单数量已经超过了100万,并且仍在继续增长。在订单数量增加后,以前的一些小问题被放大,从而影响了用户体验。例如,在用户支付成功后,极端情况(如网络和数据库问题)将导致支付成功的信息处理失败,用户在支付成功后仍显示未支付。订单数量增加后,问题订单相应增加。我们需要提高系统的可靠性,以确保订单功能的稳定性和可用性。

此外,随着订单量的增加和订单业务的复杂性,对订单系统的性能、稳定性和可用性提出了更高的要求。

为了提供更稳定、更可靠的订单服务,我们进一步升级了的订单系统。以下将分别介绍升级的主要内容。

性能优化

系统独立拆分后,可以轻松优化升级订单系统。我们对独立拆分后的订单系统进行了大量的性能优化,以提高服务的整体性能。优化主要涉及以下几个方面。

异步化

服务需要处理的工作越少,其性能自然就越高。部分操作可以异步化,以减少需要同步的操作,从而提高服务的性能。异步化有两种方案。

线程或线程池:将异步操作放中处理异步操作,避免堵塞服务线程;信息异步:通过接收信息完成异步操作。

异步化带来隐患,如何保证异步操作的实施。该场景主要发生在应用重启时,通过线程或线程池进行异步化,JVM重启时,后台执行的异步操作可能还没有完成。此时,需要通过JVM优雅关闭,确保异步操作完成后,JVM再次关闭。通过消息,消息本身已经提供了不受应用重启影响的持久性。

具体到订单系统,我们可以通过异步化一些不需要同步的操作来提高外部服务界面的性能。异步操作可以不立即生效,如发放红包PUSH推送、统计等。

以订单配送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月,这个问题更加突出。当时,数据库服务器接近单机上限。业务预期将继续快速增长。为了确保业务的快速增长,我们开始对订单系统进行第二次升级。目标是确保系统具有足够的可扩展性,以支持业务的快速发展。

分布式系统的可扩展性取决于分布式系统中各组件的可扩展性。对于订单系统,其主要组件包括三类:存储层、中间组件层和服务层。下面将解释如何提高每个层的可扩展性。

存储层

订单系统的存储层主要依赖于订单系统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),我们实现了一个轻量级的分库分表访问插件,避免将分库分表逻辑嵌入到业务代码。分库分表插件的实现包括如下几个要点。

配置文件管理分库分表配置信息;J ** A注解说明SQL语句分库分表信息;J ** A AOP解析注解+查询配置文件,获取数据源及表名;MYBATIS动态替换表名;SPRING动态替换数据源。

通过分库分表,解决了写容量扩展问题。但是分表后,会给查询带来一定的限制,只能支持主要维度的查询,其它维度的查询效率存在问题。

ES搜索

订单表分表之后,对于ID、用户ID、门店ID外的查询(比如按照手机号前缀查询)存在效率问题。这部分通常是复杂查询,可以通过全文搜索来支持。在订单系统中,我们通过ES来解决分表后非分表维度的复杂查询效率问题。具体来说,使用ES,主要涉及如下几点。

通过databus将订单数据同步到ES。同步数据时,通过批量写入来降低ES写入压力。通过ES的分片机制来支持扩展性。小结

通过对存储层的可扩展性改造,使得订单系统存储层具有较好的可扩展性。对于中间层的可扩展性与上面提到的中间层可用性一样,中间层本身已提供解决方案,直接复用即可。对于服务层,订单系统服务层提供的都是无状态服务,对于无状态服务,通过增加机器,即可获得更高的容量,完成扩容。

通过对订单系统各层可扩展性改造,使得订单系统具备了较好的可扩展性,能够支持业务的持续发展,当前,订单系统已具体千万单/日的容量。

上面几部分都是在介绍如何通过架构、技术实现等手段来搭建一个可靠、完善的订单系统。但是,要保障系统的持续健康运行,光搭建系统还不够,运维也是很重要的一环。

智能运维的订单系统

早期,对系统及业务的运维主要是采用人肉的方式,即外部反馈问题,RD通过排查日志等来定位问题。随着系统的复杂、业务的增长,问题排查难度不断加大,同时反馈问题的数量也在逐步增多。通过人肉方式效率偏低,并不能很好的满足业务的需求。

为提升运维效率、降低人力成本,我们对系统及业务运维进行自动化、智能化改进,改进包括事前、事中、事后措施。

事前措施

事前措施的目的是为提前发现隐患,提前解决,避免问题恶化。

在事前措施这块,我们主要采取如下几个手段:

定期线上压测:通过线上压测,准确评估系统容量,提前发现系统隐患;周期性系统健康体检:通过周期检测CPU利用率、内存利用率、接口QPS、接口TP95、异常数,取消订单数等指标是否异常,可以提前发现提前发现潜在问题、提前解决;全链路关键日志:通过记录全链路关键日志,根据日志,自动分析反馈订单问题原因,给出处理结果,有效提高反馈处理效率。事中措施

事中措施的目的是为及时发现问题、快速解决问题。

事中这块,我们采取的手段包括:

订单监控大盘:实时监控订单业务指标,异常时报警;系统监控大盘:实时监控订单系统指标,异常时报警;完善的SOP:报警后,通过标准流程,快速定位问题、解决问题。事后措施

事后措施是指问题发生后,分析问题原因,彻底解决。并将相关经验教训反哺给事前、事中措施,不断加强事先、事中措施,争取尽量提前发现问题,将问题扼杀在萌芽阶段。

通过将之前人肉进行的运维操作自动化、智能化,提升了处理效率、减少了运维的人力投入。

看完以后是不是想拍个砖、留个言,抒发下己见?可以来微信公众号“美团点评技术团队”给我们评论,还能在第一时间获取我们发布的一些实践经验总结、最新的活动报名信息。

.

扫码免费用

源码支持二开

申请免费使用

在线咨询