随着订单量的增长、业务复杂度的提升,订单系统也在不断演变进化,从早期一个订单业务模块到现在分布式可扩展的高并发、高性能、高可用订单系统。整个发展过程中,订单系统经历了几个明显的阶段,通过不同的技术优化方案解决业务上遇到的问题。
1、京东到家系统架构
- 这个系统架构主要由几个部分构成:用户端分别是C端用户和B端用户(B端用户针对的是像沃尔玛、永辉超市等的一些商家)
- 商家生产需要用到我们的一些拣货APP和拣货助手,后面商家履约完成会用到配送端
- 配送端就是给骑手接单抢单,最后是结算部分,分别给骑手和商家结算
- C端针对的是用户,用户进来浏览、下单到支付,整个过程是用户的操作行为。
- 基于用户的操作行为,我们有几大模块来支撑,首先是京东到家APP的后端业务支撑的基础服务,另外就是营销系统、业务系统等等。
- 基于上面这些,我们需要有很多系统来支撑,比如运营支撑系统、管理后台的支撑系统、对商家的履约支撑系统。
- 这些业务系统的底层大概有三块的持久化,分别是缓存(LocalCache、Redis等)、DB(MySQL、MongoDB等数据库)、ES。
2、订单数据入库流程
用户提单以后数据怎么流转?提单其实是一个把用户下单数据存储到数据库,提单系统做了一些分库分表。那么提完单的数据怎么下发到订单系统生产?
首先我们会有一个管道,提单通过一个分布式异步任务来下发订单管道里。所有的订单下来都会放到管道里,我们通过一个异步的任务,按照一定的速率,均匀地把订单下发到订单生产系统。
这样设计有一个好处,比如像大促时可能会有大量数据一下子下发到订单生产系统,对订单生产库有很大压力,所以我们中间设计出一个管道,通过异步任务来分发生产订单。
其实个人订单DB跟订单系统是不同维度的数据,因为个人订单其实是基于用户去做了一个分库分表,它每一个查询的订单都是基于这种个人,跟订单生产是不一样的,所以每个维度的数据都是单独的存储,来提高系统的稳定性,以及适合它自身业务特性的设计。
那么订单系统跟个人中心是怎么交互的?首先异步,我们是通过MQ来交互这些订单状态的变更。另外C端的订单取消,是怎么同步到订单生产系统的?我们是通过RPC的调用来保证订单实时取消,有一个返回结果。
3、订单系统微服务及架构演进
订单系统的架构演进如下图所示
2017-2018年,我们根据2016年遇到的问题做了一些拆分,比如按领域拆分不同的APP应用。这样拆分做到的就是系统没有单点,负载均衡可以横向扩展,多点部署。包括引入Redis,其实我们用到了Redis的分布式锁、缓存、有序队列、定时任务。
我们数据库为什么升级?因为数据库的数据量越来越大,比如添加一些字段,它其实会做一些锁表操作,随着数据量越大,单表的数据越来越多,数据主从延迟以及一些锁表的时间会越来越长
所以在加字段的时候对生产影响特别大,我们就会对数据做一个分离,把一些冷的数据单独做一个历史库,剩下的生产库只留最近几天的一些生产需要的数据,这样生产库的订单数据量就会很小,每次修改表的时间是可控的,所以我们会把数据按照冷备进行拆分。
至于为什么引入ES,是因为订单在生产方面会有一些很复杂的查询,复杂查询对数据库的性能影响非常大,引入ES就可以很好地解决这个问题。
2018-2019年,我们发现之前在引入数据库时,用数据冗余来保证一些数据应用可互备互降。比如我们之前在用ES低版本1.7的时候,其实就是一个单点,当集群有问题时是会影响生产的。
我们后来引入了一个双集群,双ES集群互备,当一个集群有问题时,另一个集群可以直接顶上来,确保了应用的高可用和生产没有问题。
数据的演进最终结构如上图,当这是基于目前业务的一个支撑,在未来业务不断发展的情况下,这个数据库架构是远远不够的。
基于以上架构,我们主要是做到了一主多从的主备实时切换,同时确保主从在不同机房来保证数据库的容灾能力。同时通过业务隔离查询不同的从库给主库减轻压力,以及冷备数据的隔离的一个思路来保证订单数据库的稳定性。
4、ElasticSearch集群
最开始我们是单ES集群,DB会通过一个同步写写到ES集群。这个时候我们的ES是一个单机群,如果写失败的话,我们会起一个异步任务来保证数据的最终一致性,是这样的一个架构。
在ES集群没问题的情况下,这个架构也是没问题的,但当集群有问题时,其实就没有可降级的了
为了解决这个问题,我们引入了ES的冷备两个集群,热集群只保存跟数据库一样的生产库的数据,比如说我们现在保证的就是5天的生产数据,其它所有数据都归档在一个ES的冷集群里。
通过这种异步跟同步写,通过异步任务来保证最终的集群的数据一致性。这就是ES的架构升级
5、订单系统结构
如上图所示,是我们整个订单系统的结构。整个过程我们是通过业务网关、RPC高可用、业务聚合、DB冗余、多机房部署,来保证整个订单应用的一些系统架构高可用。上述就是整体的订单架构演进过程。
6、订单系统容灾能力
就像上面提到的,我们对开放平台、商家中心、京明管家等业务系统的支撑怎么做到互备?
其实就是通过ES的冷热集群,冷集群存全量的数据,热集群存最近几天的生产数据。
而Redis是做业务隔离,Redis 存储有一些大key会影响核心业务,我们就会把非核心的业务拆出来,拆到另外一个Redis集群。
这就是我们系统的业务隔离和集群的互备。
7、QA
Q1:集群规模大概是什么样的?各集群节点规模如何?
A:京东到家订单中心ES 集群目前大约有将近30亿文档数,数据大小约1.3TB,集群结构是8个主分片,每个主分片有两个副本,共24个分片。
每个机器上分布1-2个分片,如果企业不差钱最好的状态就是每个分片独占一台机器。这些集群规模和架构设计不应该是固定的,每一个业务系统应该根据自身实际业务去规划设计
这样做确定分片数:
- ES是静态分片,一旦分片数在创建索引时确定那么后继不能修改;
- 数据量在亿级别,8或者16分片够用,分片数最好是2的n次方;
- 如果后继数据量的增长超过创建索引的预期,那么需要创建新索引并重灌数据;
- 创建mapping是请自行制定分片数,否则创建的索引的分片数是ES的默认值。这其实并不符合需求;
- 副本数:一般设置为1,特色要求除外。
Q4:ES主要是用于明细单查询,还是聚合统计?Join对资源耗用大吗?如何控制内存及优化?
A:ES在订单系统中的实践主要是解决复杂查询的问题,ES不建议使用聚合统计,如果非要使用那我也拦不住你,哈哈哈。
8、参考:京东把 Elasticsearch 用得真牛逼!日均5亿订单查询完美解决!
京东到家订单中心系统业务中,无论是外部商家的订单生产,或是内部上下游系统的依赖,订单查询的调用量都非常大,造成了订单数据读多写少的情况。
我们把订单数据存储在MySQL中,但显然只通过DB来支撑大量的查询是不可取的。同时对于一些复杂的查询,MySQL支持得不够友好,所以订单中心系统使用了Elasticsearch来承载订单查询的主要压力
ES查询的原理,当请求打到某号分片的时候,如果没有指定分片类型(Preference参数)查询,请求会负载到对应分片号的各个节点上。
而集群默认副本配置是一主一副,针对此情况,我们想到了扩容副本的方式,由默认的一主一副变为一主二副,同时增加相应物理机
下图为订单中心ES集群各阶段性能示意图,直观地展示了各阶段优化后ES集群性能的显著提升:
当然分片数量和分片副本数量并不是越多越好。分片数可以理解为MySQL中的分库分表,而当前订单中心ES查询主要分为两类:单ID查询以及分页查询。
分片数越大,集群横向扩容规模也更大,根据分片路由的单ID查询吞吐量也能大大提升,但聚合的分页查询性能则将降低;
分片数越小,集群横向扩容规模也更小,单ID的查询性能也会下降,但分页查询的性能将会提升。
以如何均衡分片数量和现有查询业务,我们做了很多次调整压测,最终选择了集群性能较好的分片数。
发文时业务体量:美团外卖由日均几单发展为日均500万单(9月11日已突破600万)
1、订单架构
随着订单量的增长、业务复杂度的提升,外卖订单系统也在不断演变进化,从早期一个订单业务模块到现在分布式可扩展的高性能、高可用、高稳定订单系统。整个发展过程中,订单系统经历了几个明显的阶段,重点关注各阶段的业务特征、挑战及应对之道。
外卖订单业务是一个需要即时送的业务,对实时性要求很高。从用户订餐到最终送达用户,一般在1小时内。如果最终送达用户时间变长,会带来槽糕的用户体验。
在1小时内,订单会快速经过多个阶段,直到最终送达用户。各个阶段需要紧密配合,确保订单顺利完成。下图是一个用户视角的订单流程图。
从普通用户的角度来看,一个外卖订单从下单后,会经历支付、商家接单、配送、用户收货、售后及订单完成多个阶段。
以技术的视角来分解的话,每个阶段依赖于多个子服务来共同完成,比如下单会依赖于购物车、订单预览、确认订单服务,这些子服务又会依赖于底层基础系统来完成其功能。
第一阶段:早期,外卖整体架构简单、灵活,公共业务逻辑通过jar包实现后集成到各端应用,应用开发部署相对简单。比较适合业务早期逻辑简单、业务量较小、需要快速迭代的情况。
随着业务的发展以及业务的逐步成熟,业务大框架基本成型,业务在大框架基础上快速迭代。大家共用一个大项目进行开发部署,相互影响,协调成本变高;多个业务部署于同一VM,相互影响的情况也在增多。
为解决开发、部署、运行时相互影响的问题。我们将订单系统进行独立拆分,从而独立开发、部署、运行,避免受其它业务影响。系统拆分主要有如下几个原则:
- 相关业务拆分独立系统;
- 优先级一致的业务拆分独立系统;
- 拆分系统包括业务服务和数据。
基于以上原则,我们将订单系统进行独立拆分,所有订单服务通过RPC接口提供给外部使用。订单系统内部,我们将功能按优先级拆分为不同子系统,避免相互影响。订单系统通过MQ(队列)消息,通知外部订单状态变更。
第二阶段:独立拆分后的订单系统架构如下所示:
其中,最底层是数据存储层,订单相关数据独立存储。订单服务层,我们按照优先级将订单服务划分为三个系统,分别为交易系统、查询系统、异步处理系统。
订单系统经过上述独立拆分后,有效地避免了业务间的相互干扰,保障迭代速度的同时,保证了系统稳定性。
这时,我们的订单量突破百万,而且还在持续增长。之前的一些小问题,在订单量增加后,被放大,进而影响用户体验。
比如,用户支付成功后,极端情况下(比如网络、数据库问题)会导致支付成功消息处理失败,用户支付成功后依然显示未支付。
订单量变大后,问题订单相应增多。我们需要提高系统的可靠性,保证订单功能稳定可用。
为了提供更加稳定、可靠的订单服务,我们对拆分后的订单系统进行进一步升级。下面将分别介绍升级涉及的主要内容
1)、性能优化
- 异步化:1、线程或线程池:将异步操作放在单独线程中处理,避免阻塞服务线程;2、消息异步:异步操作通过接收消息完成。
- 并行化:对所有订单服务进行分析,将其中非相互依赖的操作并行化,从而提升整体的响应时间
- 缓存:通过将统计信息进行提前计算后缓存,避免获取数据时进行实时计算,从而提升获取统计数据的服务性能。
2)、一致性优化:订单系统是一个复杂的分布式系统,比如支付涉及订单系统、支付平台、支付宝/网银等第三方。仅通过传统的数据库事务来保障不太可行。对于订单交易系统的事务性,并不要求严格满足传统数据库事务的ACID性质,只需要最终结果一致即可。
- 重试/幂等:通过延时重试,保证操作最终会最执行。比如退款操作,如退款时遇到网络或支付平台故障等问题,会延时进行重试,保证退款最终会被完成。重试又会带来另一个问题,即部分操作重复进行,需要对操作进行幂等处理,保证重试的正确性。
- 2PC:指分布式事务的两阶段提交,通过2PC来保证多个系统的数据一致性。比如下单过程中,涉及库存、优惠资格等多个资源,下单时会首先预占资源(对应2PC的第一阶段),下单失败后会释放资源(对应2PC的回滚阶段),成功后会使用资源(对应2PC的提交阶段)
3)、高可用: 针对订单系统而言,其主要组成组件包括三类:存储层、中间件层、服务层
- 存储层:存储层的组件如MySQL、ES等本身已经实现了高可用,比如MySQL通过主从集群、ES通过分片复制来实现高可用。存储层的高可用依赖各个存储组件即可。
- 中间件层:分布式系统会大量用到各类中间件,比如服务调用框架等,这类中间件一般使用开源产品或由公司基础平台提供,本身已具备高可用。
- 服务层:在分布式系统中,服务间通过相互调用来完成业务功能,一旦某个服务出现问题,会级联影响调用方服务,进而导致系统崩溃。分布式系统中的依赖容灾主要有如下几个思路:
- 依赖超时设置;
- 依赖灾备;
- 依赖降级;
- 限制依赖使用资源;
- 订单系统服务层都是无状态服务,通过集群+多机房部署,可以避免单点问题及机房故障,实现高可用。
第三阶段:存储层的可扩展性改造,主要是对MySQL扩展性改造
针对订单表超过单库容量的问题,需要进行分表操作,即将订单表数据进行拆分。单表数据拆分后,解决了写的问题,但是如果查询数据不在同一个分片,会带来查询效率的问题(需要聚合多张表)。
具体来说,外卖主要涉及三个查询维度:订单ID、用户ID、门店ID。对订单表分表时,对于一个订单,我们存三份,分别按照订单ID、用户ID、 门店ID以一定规则存储在每个维度不同分片中。
这样,可以分散写压力,同时,按照订单ID、用户ID、门店ID三个维度查询时,数据均在一个分片,保证较高的查询效率。
订单表分表后,订单表的存储架构如下所示:
可以看到,分表后,每个维度共有100张表,分别放在4个库上面。对于同一个订单,冗余存储了三份。未来,随着业务发展,还可以继续通过将表分到不同机器上来持续获得容量的提升。
分库分表后,订单数据存储到多个库多个表中,为应用层查询带来一定麻烦,解决分库分表后的查询主要有三种方案:MySQL服务器端、中间件、应用层
- MySQL服务器端不能支持,我们只剩下中间件和应用层两个方案
- 中间件方案对应用透明,但是开发难度相对较大,当时这块没有资源去支持
- 我们采用应用层方案来快速支持。实现了一个轻量级的分库分表访问插件,避免将分库分表逻辑嵌入到业务代码
通过分库分表,解决了写容量扩展问题。但是分表后,会给查询带来一定的限制,只能支持主要维度的查询,其它维度的查询效率存在问题。
2、ES搜索
订单表分表之后,对于ID、用户ID、门店ID外的查询(比如按照手机号前缀查询)存在效率问题。这部分通常是复杂查询,可以通过全文搜索来支持。
在订单系统中,我们通过ES来解决分表后非分表维度的复杂查询效率问题。具体来说,使用ES,主要涉及如下几点
- 通过databus将订单数据同步到ES
- 同步数据时,通过批量写入来降低ES写入压力
- 通过ES的分片机制来支持扩展性
vivo Elasticsearch集群应用实践参见:https://elasticsearch.cn/slides/287#page=5
集群业务规模及集群配置:
1、订单系统架构
随着用户量级的快速增长,将订单模块从商城拆分出来,独立为订单系统,使用独立的数据库,为商城相关系统提供订单、支付、物流、售后等标准化服务。
2、技术挑战
1)、数据量和高并发问题
- 数据量问题:随着历史订单不断累积,MySQL中订单表数据量已达千万级。
我们知道InnoDB存储引擎的存储结构是B+树,查找时间复杂度是O(log n),因此当数据总量n变大时,检索速度必然会变慢, 不论如何加索引或者优化都无法解决,只能想办法减小单表数据量。
数据量大的解决方案有:数据归档、分表
- 高并发问题:订单量屡创新高,业务复杂度也在提升,应用程序对MySQL的访问量越来越高
单机MySQL的处理能力是有限的,当压力过大时,所有请求的访问速度都会下降,甚至有可能使数据库宕机。
并发量高的解决方案有:使用缓存、读写分离、分库
方案进行简单描述:
- 读写分离:主库负责执行数据更新请求,然后将数据变更实时同步到所有从库,用多个从库来分担查询请求。
订单数据的更新操作较多,下单高峰时主库的压力依然没有得到解决。且存在主从同步延迟,正常情况下延迟非常小,不超过1ms,但也会导致在某一个时刻的主从数据不一致。那就需要对所有受影响的业务场景进行兼容处理,可能会做一些妥协;
- 分库:分库又包含垂直分库和水平分库
- 水平分库:把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。
- 垂直分库:按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。
- 分表:分表又包含垂直分表和水平分表
- 水平分表:在同一个数据库内,把一个表的数据按一定规则拆到多个表中
- 垂直分表:将一个表按照字段分成多表,每个表存储其中一部分字段
3、分库分表技术选型
参考之前项目经验,并与公司中间件团队沟通后,采用了开源的 Sharding-JDBC 方案。现已更名为Sharding-Sphere
- Github:https://github.com/apache/shardingsphere
- 文档:https://shardingsphere.apache.org/document/current/cn/overview/
-
-
社区活跃
-
4、分库分表策略
结合业务特性,选取用户标识作为分片键,通过计算用户标识的哈希值再取模来得到用户订单数据的库表编号.
假设共有n个库,每个库有m张表, 则库表编号的计算方式为:
- 库序号:Hash(userId) / m % n
- 表序号:Hash(userId) % m
路由过程如下图所示:
5、分库分表的局限性和应对方案
- 分库分表解决了数据量和并发问题,但它会极大限制数据库的查询能力,有一些之前很简单的关联查询,在分库分表之后可能就没法实现了,那就需要单独对这些Sharding-JDBC不支持的SQL进行改写。
- 全局唯一ID设计:分库分表后,数据库自增主键不再全局唯一,不能作为订单号来使用,但很多内部系统间的交互接口只有订单号,没有用户标识这个分片键,如何用订单号来找到对应的库表呢?
- 我们在生成订单号时,就将库表编号隐含在其中了。这样就能在没有用户标识的场景下,从订单号中获取库表编号。
- 历史订单号没有隐含库表信息:用一张表单独存储历史订单号和用户标识的映射关系,随着时间推移,这些订单逐渐不在系统间交互,就慢慢不再被用到
- 管理后台需要根据各种筛选条件,分页查询所有满足条件的订单
- 将订单数据冗余存储在搜索引擎Elasticsearch中,仅用于后台查询
6、怎么做 MySQL 到 ES 的数据同步
上面说到为了便于管理后台的查询,我们将订单数据冗余存储在Elasticsearch中,那么,如何在MySQL的订单数据变更后,同步到ES中呢?
这里要考虑的是数据同步的时效性和一致性、对业务代码侵入小、不影响服务本身的性能等。
- MQ方案:ES更新服务作为消费者,接收订单变更MQ消息后对ES进行更新
-
-
Binlog方案:ES更新服务借助canal等开源项目,把自己伪装成MySQL的从节点,接收Binlog并解析得到实时的数据变更信息,然后根据这个变更信息去更新ES
-
其中BinLog方案比较通用,但实现起来也较为复杂,我们最终选用的是MQ方案
因为ES数据只在管理后台使用,对数据可靠性和同步实时性的要求不是特别高
考虑到宕机和消息丢失等极端情况,在后台增加了按某些条件手动同步ES数据的功能来进行补偿
滴滴ES规模参见:https://elasticsearch.cn/slides/255#page=5
ES服务架构
滴滴 ES 发展至今,承接了公司绝大部分端上文本检索、少部分日志场景和向量检索场景,包括地图 POI 检索、订单检索、客服、内搜及把脉日志 ELK 场景等。
滴滴 ES 在2020年由2.X升级到7.6.0,近几年围绕保稳定、控成本、提效能和优生态这几个方向持续探索和改进
1、架构
2、业务场景
- 在线全文检索服务,如地图 POI 起终点检索
- MySQL 实时数据快照,在线业务如订单查询
- 一站式日志检索服务,通过 Kibana 查询,如 trace 日志
- 时序数据分析,如安全数据监控
- 简单 OLAP 场景,如内部数据看板
- 向量检索,如客服 RAG
3、部署模式
- 物理机+小集群部署方式,最大集群机器规模100台物理机左右
4、数据实时类同步方式
如上图所示,实时类同步方式有2种
- 一种是日志和 MySQL Binlog 通过采集工具采集到 MQ,之后通过统一封装的 DSINK 工具,通过 Flink 写入到 ES
- 另一种是 MySQL 全量数据,其基于开源的 DataX 进行全量数据同步
5、成本优化
成本优化主要包括降低机器成本和降低用户成本
- 降低机器成本核心是降低存储规模和降低 CPU 使用率,即降低机器数;
- 降低用户成本的核心逻辑是降低业务用量
所以 ES 整体成本优化策略如下:
- 索引 Mapping 优化,禁止部分字段倒排、正排
- 新增 ZSTD 压缩算法,CPU 降低15%
- 接入大数据资产管理平台,梳理无用分区和索引,协助业务下线
使用的 ES 版本是 7.6,而社区的最新版本已经更新至 8.13,两者之间存在约 4 年的版本差距。因此,我们今年的重点工作是将 ES 平滑升级至 8.13 版本,以解决以下问题:
- 新版本的 ES Master 性能更优
- 能够根据负载自动平衡磁盘使用
- 减少 segment 对内存的占用
- 支持向量检索的 ANN 等新特性
小结:
- 京东到家:关注个人订单DB;在引入数据库时,用数据冗余来保证一些数据应用可互备互降;对于复杂的查询,MySQL支持得不够友好,所以订单中心系统使用了Elasticsearch来承载订单查询的主要压力 ;
- 美团外卖:关注外卖主要涉及三个查询维度:订单ID、用户ID、门店ID。对订单表分表时,对于一个订单,我们存三份;对于ID、用户ID、门店ID外的查询(比如按照手机号前缀查询)存在效率问题。这部分通常是复杂查询,可以通过ES来支持。
- VIVO: 可关注分库分表的可选组件shardingsphere;管理后台需要根据各种筛选条件,分页查询所有满足条件的订单; 将订单数据冗余存储在搜索引擎Elasticsearch中,仅用于后台查询
- 滴滴:关注成本优化
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略