应对618,京东到家订单系统高可用架构的迭代实战
大家好,我是京东到家后台研发部的架构师闫文广,今天将给大家分享京东到家订单系统的高可用架构及演变过程。
京东到家是达达集团旗下中国最大的本地即时零售平台之一,目标就是实现一个小时配送到家的业务。一直到 2019 年京东到家覆盖 700 个县区市,合作门店近 10 万家,服务数千万消费者。随着订单量的增长、业务复杂度的提升,订单系统也在不断演变进化,从早期一个订单业务模块到现在分布式可扩展的高并发、高性能、高可用订单系统。整个发展过程中,订单系统经历了几个明显的阶段,通过不同的技术优化方案解决业务上遇到的问题。
下面我将为大家逐一介绍我们遇到了哪些问题及如何解决,主要分为以下三部分:
京东到家系统架构
订单系统架构演进
订单系统稳定性保障实践
一、京东到家系统架构
业务架构
首先来看以下这张流程图,这个系统架构主要由几个部分构成:用户端分别是 C 端用户和 B 端用户。B 端用户针对的是像沃尔玛、永辉超市等的一些商家,商家生产需要用到我们的一些拣货 APP 和拣货助手,后面商家履约完成会用到配送端,配送端就是给骑手接单抢单,最后是结算部分,分别给骑手和商家结算。
C 端针对的是用户,用户进来浏览、下单到支付,整个过程是用户的操作行为。基于用户的操作行为,我们有几大模块来支撑,首先是京东到家 APP 的后端业务支撑的基础服务,另外就是营销系统、业务系统等等。基于上面这些,我们需要有很多系统来支撑,比如运营支撑系统、管理后台的支撑系统、对商家的履约支撑系统。这些业务系统的底层大概有三块的持久化,分别是缓存(LocalCache、Redis 等)、DB(MySQL、MongoDB 等数据库)、ES。这就是京东到家简版的业务架构图。
运营支撑业务架构
京东到家的运营支撑业务架构主要分为商家管理、CMS 管理、营销管理、财务管理、运营数据这五大模块,每块包含的内容具体如下图所示:
后端架构
接下来是我们 C 端 APP 的一些网关后端的接口及基础服务的支撑。首先所有的请求都会经过网关协议,网关下面分为业务系统,包括首页、门店页、购物车、结算页以及提单系统、支付系统和个人订单系统,这些系统的支撑都离不开我们的基础服务的支撑,比如库存、商品、门店、价格等等,这些是一些重要的基础服务支撑,来保证用户可以流畅的下单以及到结算。
业务支撑包含了很多业务系统,比如用户、定位、地址库、运费、promise、推荐、搜索、收银台、风控等。
上面说到了营销的一些管理系统,其实营销还有一些后端的基础服务系统,比如优惠券、满减、秒杀、首单等。
订单数据入库流程
用户提单以后数据怎么流转?提单其实是一个把用户下单数据存储到数据库,提单系统做了一些分库分表。那么提完单的数据怎么下发到订单系统生产?首先我们会有一个管道,提单通过一个分布式异步任务来下发订单管道里。所有的订单下来都会放到管道里,我们通过一个异步的任务,按照一定的速率,均匀地把订单下发到订单生产系统。这样设计有一个好处,比如像大促时可能会有大量数据一下子下发到订单生产系统,对订单生产库有很大压力,所以我们中间设计出一个管道,通过异步任务来分发生产订单。
看了图可能有人会问为什么要有一个个人订单 DB?其实个人订单 DB 跟订单系统是不同维度的数据,因为个人订单其实是基于用户去做了一个分库分表,它每一个查询的订单都是基于这种个人,跟订单生产是不一样的,所以每个维度的数据都是单独的存储,来提高系统的稳定性,以及适合它自身业务特性的设计。
那么订单系统跟个人中心是怎么交互的?首先异步,我们是通过 MQ 来交互这些订单状态的变更。另外 C 端的订单取消,是怎么同步到订单生产系统的?我们是通过 RPC 的调用来保证订单实时取消,有一个返回结果。
二、订单系统架构演进
订单系统业务流程
我们的订单履约分为两大块,商家生产和配送履约,具体步骤如下图所示:
整个订单履约的流程是怎么样的?在用户支付完成后,订单会有一个补全的过程,补全完后,我们会根据一些门店的设置,把订单下发到商家。下发商家后,有几种对接模式:有开发能力的商家可以通过开放平台,一些小商家可以通过商家中心以及我们的京明管家来完成订单的生产履约。在商家拿到新订单后,通过打印出小票进行拣货。拣货会分为几个业务场景,因为有可能商家有货也有可能没货,如果缺货的话,我们有一个调整的功能,让商家通过订单调整来保证有商品的订单可以继续履约。
在商家完成拣货时,我们订单会分为分区拣货、合单拣货、前置仓拣货这几块业务上的操作,其实在系统里我们有一个拣货的池子,会通过不同维度的数据来完成高效的拣货。拣货完成后,我们的配送主要分为两个模块,一种是单个订单的配送,另一种是集合单的配送。集合单就是把发单地址和配送地址在两个相近的格子里的订单合并起来,基本上都是基于将同一个门店的配送目的是同一个相近格子里的订单,进行合单后让一个骑士完成配送。配送会下发到一些配送系统,分为两种模式,集合单和单个订单的配送,以及和配送系统的整个运单交互的一个流程。
RPC 微服务化集群
说完业务后,接下来介绍一下我们应用的一些微服务的拆分过程。先讲一下微服务理论方面的知识,比如为什么要拆分微服务?微服务拆分后可以解决哪些问题?这是接下来一个重点内容,大家可以思考一下,系统架构必须具备哪些条件才能达到高可用?
简单总结来说,微服务可以降低系统的复杂度,可以独立部署,并且有很好的扩展性:
降低复杂度:把原来耦合在一起的业务,按领域拆分为不同的微服务,拆分原有的复杂逻辑,每个微服务专注于单一业务逻辑。明确定义领域职责与边界;
独立部署:由于微服务具备独立部署运行能力,当业务发生变更升级时,微服务可以单独开发、测试、部署升级。提高了迭代效率,降低了研发风险;
扩展性:拆分后的微服务架构独立部署,可以根据流量预估或压测评估独立进行扩容升级。
我们订单系统的架构演进如上图所示,最左边是最初的一个模型,所有的业务都耦合在一个应用里面,这个应用可能就有一个 service 来支撑,数据库也是一个单点的数据库。随着这些年的迭代升级变更,逐步演进到一个应用有多个服务支撑,数据库也在不断升级变更,以及到后面把应用按微服务拆分成多个模块,拆成多个领域的支撑,按不同的系统边界去拆分。并且拆完后,随着业务量越来越大,其实我们也在做一些升级,比如 Redis 的接入。
Redis 的接入解决了什么问题?数据库为什么要分库?ES 为什么在接下来一些系统架构升级里会被引入进来?为什么 DB 要拆成多个集群?这背后的一些根本问题,以及解决业务系统的一些背景,接下来我们逐一探讨。
在最初搭建项目时,其实我们是要保证业务的快速试错。这个模型会有什么问题?就是系统会有一些单点风险,以及系统发布是一个短暂停,所有请求都是一个主观的操作,并发能力很差,稍微有一些业务量时,系统接口就会超时比较严重。这是最初 2015-2016 年的情况。
接下来 2016-2017 年,随着业务的快速迭代,系统复杂度也慢慢高了起来,系统逻辑耦合会比较严重,改动一块的逻辑影响就会比较大,导致线上问题频发,因为所有的逻辑都耦合在一起,一次发布可能就会影响范围比较大。
按微服务拆分成多个系统,如果发布有问题也只会影响其中的一些很小的部分。在后面随着业务量越来越大,RPC 这种框架的引入,解决故障的自动下线,保证高可用,比如单台服务器有问题时,能做到自动下线来保证不影响业务。
Redis 集群
2017-2018 年,我们根据 2016 年遇到的问题做了一些拆分,比如按领域拆分不同的 APP 应用。这样拆分做到的就是系统没有单点,负载均衡可以横向扩展,多点部署。包括引入 Redis,其实我们用到了 Redis 的分布式锁、缓存、有序队列、定时任务。
我们数据库为什么升级?因为数据库的数据量越来越大,比如添加一些字段,它其实会做一些锁表操作,随着数据量越大,单表的数据越来越多,数据主从延迟以及一些锁表的时间会越来越长,所以在加字段的时候对生产影响特别大,我们就会对数据做一个分离,把一些冷的数据单独做一个历史库,剩下的生产库只留最近几天的一些生产需要的数据,这样生产库的订单数据量就会很小,每次修改表的时间是可控的,所以我们会把数据按照冷备进行拆分。
至于为什么引入 ES,是因为订单在生产方面会有一些很复杂的查询,复杂查询对数据库的性能影响非常大,引入 ES 就可以很好地解决这个问题。
2018-2019 年,我们发现之前在引入数据库时,用数据冗余来保证一些数据应用可互备互降。比如我们之前在用 ES 低版本 1.7 的时候,其实就是一个单点,当集群有问题时是会影响生产的。我们后来引入了一个双集群,双 ES 集群互备,当一个集群有问题时,另一个集群可以直接顶上来,确保了应用的高可用和生产没有问题。
另外,引入 Redis 双集群,Redis 其实有一些大 key 的问题,当一些核心业务和非核心业务都用到 Redis 的时候,我们会把一些核心业务拆到一个集群,非核心业务拆到另一个集群,来保证 Redis 集群的稳定性,能稳定支撑订单生产。
注:大 key(线上看到过 list 的 elements 超过百万的)删除时会阻塞比较长的时间,大 key 的危害:
OPS低也会导致流量大,比如一次取走100K的数据,当OPS为1000时,就会产生100M/s的流量;
如果为list,hash等数据结构,大量的elements需要多次遍历,多次系统调用拷贝数据消耗时间;
主动删除、被动过期删除、数据迁移等,由于处理这一个key时间长,而发生阻塞。
单个应用怎么保证高可用?其实我们这边从最初的一个单机房单实例单 IP,一步步演进为单机房的单服务、单机房的多服务,最终是多机房的多服务,比如某个机房某些 IP 有问题,我们能确保应用可以正常请求响应,来保证系统的高可用。
MySQL 数据库
下面来介绍一下我们主从架构的演进。最初我们就是一些主从的架构,并且主读和写都会请求到一个主库,这样就会给主库带来非常大的压力。而像订单生产这种天然性就是读多写少,主库的压力会比较大。所以基于这个问题,我们就做了数据库架构的升级。
我们做了读写分离,就能很好地解决了这个问题。比如很多查询,我们就会直接查询到从库的,主库只是用来承载一些写的流量,这样主库就减少了很多压力。但这样就会遇到上面说到的问题,因为我们是一个生产表和历史表的结构,在每次加字段时,数据量很多的话,锁表时间就会很长,导致在这种读写分离的架构下数据延迟就会比较大。
基于这种场景,我们又做了一些升级和改造。我们把数据拆出来了,拆成历史库,当所有的生产数据都很小的时候,对于提高性能是非常有帮助的。我们把生产完的一些数据全部都归档到历史中,减轻主库的整个压力,以及在添加表字段修改的时候,其实就没有太大影响了,基本上都很稳定。
数据的演进最终结构如上图,当这是基于目前业务的一个支撑,在未来业务不断发展的情况下,这个数据库架构是原因不够的。基于以上架构,我们主要是做到了一主多从的主备实时切换,同时确保主从在不同机房来保证数据库的容灾能力。同时通过业务隔离查询不同的从库给主库减轻压力,以及冷备数据的隔离的一个思路来保证订单数据库的稳定性。
ElasticSearch 集群
最开始我们是单 ES 集群,DB 会通过一个同步写写到 ES 集群。这个时候我们的 ES 是一个单机群,如果写失败的话,我们会起一个异步任务来保证数据的最终一致性,是这样的一个架构。在 ES 集群没问题的情况下,这个架构也是没问题的,但当集群有问题时,其实就没有可降级的了。
为了解决这个问题,我们引入了 ES 的冷备两个集群,热集群只保存跟数据库一样的生产库的数据,比如说我们现在保证的就是 5 天的生产数据,其它所有数据都归档在一个 ES 的冷集群里。通过这种异步跟同步写,通过异步任务来保证最终的集群的数据一致性。这就是 ES 的架构升级。
其实 ES 这样写的话会带来一些很大的侵入,每次我们数据库的一个变更都会带来一些要 RPC 调用去同步 ES 的数据。这种瓶颈以及侵入式的问题怎么解决?我们接入了开源的 Canal,通过监听数据库变更了 binlog,来通过 Canal、kafka,然后异步通过消息来同步到 ES 集群。这个集群目前已经应用到线上的一些业务,经过灰度发布、后期验证没有问题后,会逐步接入生产系统。
如上图所示,是我们整个订单系统的结构。整个过程我们是通过业务网关、RPC 高可用、业务聚合、DB 冗余、多机房部署,来保证整个订单应用的一些系统架构高可用。上述就是整体的订单架构演进过程。
三、订单系统稳定性保障实践
大家思考一下,如果你要负责一些核心系统,你怎么保证稳定性?接下来我会从订单系统可用性建设、系统容灾能力、系统容量能力、系统预警能力分享一下我们在稳定性保障上的实践。
订单系统可用性建设
业务的快速发展对可用性保证要求越来越高,在方法论层面,我们按照系统故障的时间顺序提出了事前、事中、事后三个阶段,同时提出了四方面的能力建设——预防能力、诊断能力、解决能力、规避能力。
具体在工作上,我们会划分为流程和系统建设两个方面。其实最开始我们是为了完成工作,保证的是结果,最后发现每一个过程我们会把它平台化,来提升人效。以上是一个大概的框架,下面我们一项一项详细分析一下。
系统容灾能力
容灾能力这块,我们从 ES 冷热集群互备、Redis 缓存集群业务隔离,到业务接口的可降级和可异步,再到多维度的灰度发布。
就像上面提到的,我们对开放平台、商家中心、京明管家等业务系统的支撑怎么做到互备?其实就是通过 ES 的冷热集群,冷集群存全量的数据,热集群存最近几天的生产数据。而 Redis 是做业务隔离,Redis 存储有一些大 key 会影响核心业务,我们就会把非核心的业务拆出来,拆到另外一个 Redis 集群。这就是我们系统的业务隔离和集群的互备。
业务接口可降级,其实是在一些复杂的业务操作接口里,我们可以通过一些异步处理,比如在订单状态变更的操作接口、除了更新 DB 和 ES、发送 MQ,订单状态的变更通过消息去同步发送。那么我们哪些可降低?比如说我们在业务核心操作接口里有一些 push 消息和发送短信,像这样的非核心操作就可以用异步可降级的方案来解决。
灰度发布其实很重要,线上如果有一些新业务或系统的架构升级、技术的迭代,我们这边都会通过灰度发布,比如可以通过一些门店先做门店汇总,如果单个门店没问题,再通过一些商家,如果商家没问题,就会灰度到整个城市,如果城市也没问题,我们就会通过全量。
另一个维度来看,我们也会先灰度单台机器,再到单机房、多机房。当然这个很局限,因为跟你灰度的一些功能有关系,大家要酌情根据自己的业务借鉴这种方案思路。
系统容量能力
至于系统容量的能力,主要分为评估和扩容两个方面。容量评估可以借助一些辅助的工具,然后进行场景的压测和全链路的压测;而扩容方面,可以分阶段依次实施冗余备份(主从分离)、垂直拆分(拆分核心属性与非核心属性)、自动归档。
系统预警能力
最后是预警能力,我们这边用的是京东自研的 UMP 监控。
在接口层面,我们可以监控到:
可用率;
响应时间;
调用量:当别人调用你的接口,你设置调用的一个量值,当超过阀值时就是进来了一些非正常的流量,当监控到这些异常流量,就可以做限流等相应操作;
自定义:自定义一些报警;
关键词:当系统出现某种问题,需要关键字报出来然后进行人工介入;
调用链:一个接口操作下来,谁调用了你?你调用了谁?哪个环节有问题?
在应用层面,我们可以监控到:
Young GC;
Full GC;
堆内存;
非堆内存;
CPU使用率;
线程数。
下面是关于 DB、ES、Redis 的集群监控,包括:
CPU使用率;
系统负载;
内存;
线程数;
读写IO;
磁盘使用率;
TCP连接数。
如果大家有排查过线上的问题,就应该能感受到比如像 IO 高、TCP 连接、重传等,都会影响到线上一些核心接口的响应时间,包括你 CPU 高的时候,线程数是否飙高?系统负载是否飙高?当这些指标都发生异常变化时,对于接口的响应时间都会有很大影响。
另外,我们还要做一些积压监控,比如一些异步任务正常来说一分钟最多积压 1000,就需要添加对应的监控,否则数据异常了都不知道;再比如订单支付的状态,当积压到一定数量,可能是系统出了问题,就需要人工介入。
四、总结
一个企业的架构师团队,需要长期关注技术驱动业务、明确领域职责与边界等关键问题,同时架构的演进过程也是不断考虑 ROI 的权衡取舍过程。技术的持续发展,有助于不断提升用户体验、业务规模,降低运营成本,而架构在其中需要解决的问题就是化繁为简,将复杂问题拆解为简单的问题逐个攻破。随着企业规模的持续增长、业务的持续创新,会给系统架构提出越来越高的要求,所以系统架构设计将是我们长期研究的课题。在这条架构演进之路上,希望能与大家共勉共进。
Q&A
Q1:集群规模大概是什么样的?各集群节点规模如何?
A: 京东到家订单中心 ES 集群目前大约有将近 30 亿文档数,数据大小约 1.3TB,集群结构是 8 个主分片,每个主分片有两个副本,共 24 个分片。每个机器上分布 1-2 个分片,如果企业不差钱最好的状态就是每个分片独占一台机器。这些集群规模和架构设计不应该是固定的,每一个业务系统应该根据自身实际业务去规划设计。
这样做确定分片数:
ES是静态分片,一旦分片数在创建索引时确定那么后继不能修改;
数据量在亿级别,8或者16分片够用, 分片数最好是2的n次方;
如果后继数据量的增长超过创建索引的预期,那么需要创建新索引并重灌数据;
创建mapping是请自行制定分片数,否则创建的索引的分片数是ES的默认值。这其实并不符合需求;
副本数:一般设置为1,特色要求除外。
Q2:ES 优化做过哪些措施?
A:ES 使用最佳实践: 写入的数据考虑清楚是否会过期,ES 擅长的不是存储而是搜索,所以一般存入 ES 的数据难免会随着时间删除旧数据。删除方法有两种: ①按记录(不推荐)②按索引。 推荐使用后者,所以需要业务根据数据特点,按天、月、季度等创建索引。分片数够用就好,不要过多不要过少。ES 不是数据库,不建议做频繁的更新。
Q3:集群可承受的 TPS 是多少?
A: 这个没有具体的数字,根据不同规模集群、不同的索引结构等不同,建议根据业务评估自己的流量,压测双倍流量,若 ES 无法承受或满足,可以考虑扩容集群,不要流量暴增于平时的 3 倍、4 倍,甚至更多的规模。
Q4:ES 主要是用于明细单查询,还是聚合统计?Join 对资源耗用大吗?如何控制内存及优化?
A: ES 在订单系统中的实践主要是解决复杂查询的问题,ES 不建议使用聚合统计,如果非要使用那我也拦不住你,哈哈哈。
深分页的问题【内存】
ES 处理查询的流程如下:
Client需要第N到N+m条结果;
接到这个请求的ES server(后继称之为协调者)向每一个数据分片所在的数据节点发送请求;
每一个数据节点都需要向协调者返回(N+m)个结果;
如果有n个数据分片,那么协调者拿到n * (N+m)个结果,排序,扔掉(n-1) * (N+m)个结果,返回给client N+m个结果;
如果N是10W,100W,那么协调者的内存压力会非常大;
在我们目前维护的2.1版本中,ES已经不容许N>10000了。
深分页的危害:导致打爆节点内存引起集群整体不可用。
Q5:应用 canal 同步数据,会有延迟吗?
A: 首先来说下 ES 特点:Elasticsearch 是一个接近实时的搜索平台。这意味着,从索引一个文档直到这个文档能够被搜索到有一个轻微的延迟(通常是 1 秒),这个参数也是可以调整的,根据业务场景调整。
可以肯定的是延迟是肯定的。其实延迟大小完全取决你整个同步流程中是否有瓶颈点,如果业务要求实时的,其实不建议在这种场景下使用 ES。就好比数据库查询从库不能接受延迟,那就不要做读写分离,或者都查询主库。
Q6:sqlproxy 具体用的哪个?
A: sqlproxy 这个是指 MySQL 读写分离的实现,大家可以网上查询下有很多资料。官网地址:https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-master-slave-replication-connection.html
<mvn.jdbc.driver>com.mysql.jdbc.ReplicationDriver</mvn.jdbc.driver>
<mvn.jdbc.url>jdbc:mysql:replication://m.xxx.com:3306,s.xxx.com:3306/dbName</mvn.jdbc.url>
Q7:Redis 用于查询缓存、分发任务缓存?
A: Redis 在项目中的使用场景,缓存查询,分布式锁使用。其中还有一个异步任务是通过 redis zset + tbschedule 定时或实时的去执行一些业务逻辑。
添加队列数据方法:
public Boolean zAdd(String key, final double score, String value) ;
查询获取队列数据:
public Set<String> zRangeByScore(String key, final double min, final double max) ;
Q8:容量评估可以讲一些细节嘛?
A: 基本有两种场景:
日常业务流程是否有瓶颈 ;
大促期间根据流量预估系统是否有瓶颈。
京东到家内部系统是有一套完整的监控系统,基于接口,应用机器,集群的多维度监控。
接口:
响应时间,tp99,tp999,tp9999 等;
接口调用量,次数/分钟;
可用率。
应用机器
根据监控可以查看单机器相关指标数据是否正常,比如:
CPU使用率;
系统负载;
网络IO;
TCP连接数,线程数;
磁盘使用率。
集群
Redis集群;
ES集群;
MySQL集群。
对于集群来说是根据集群下机器指标是否正常来评估整个集群是否正常。需要看集群可以承载业务流量的 TPS、QPS 等指标是否满足业务需求,同时需要评估大促场景下是否可以满足要求。这种情况就需要根据大促流量评估压测,看集群以及应用,接口是否可以满足需求。
每个公司可以根据自身规则进行扩容,及架构升级。比如日常 CPU 超过 60%考虑应用扩容,负载远大于机器核数等等。
Q9:异步定时任务用的是什么中间件?
A: tbschedule 是一个支持分布式的调度框架,让批量任务或者不断变化的任务能够被动态的分配到多个主机的 JVM 中, 在不同的线程组中并行执行,所有的任务能够被不重复,不遗漏的快速处理。基于 ZooKeeper 的纯 Java 实现,由 Alibaba 开源。
Q10:在云上部署还是物理服务器?
A: 应用都部署在云服务器上,首先即时,几分钟即可完成,可一键部署、也可自主安装操作系统。安全性方面因为服务分布在多台服务器、甚至多个机房,所以不容易彻底宕机,抗灾容错能力强,可以保证长时间在线。弹性以及可扩展性方面云主机基本特点就是分布式架构,所以可以轻而易举地增加服务器,成倍扩展服务能力。
Q11:RPC 高可用怎么实现?
A: RPC 高可用基本都是借助于分布式框架,阿里开源 dubbo,Spring 全家桶的 SpringCloud,包括我们使用的京东自研的 JSF。其工作原理,感兴趣的同学可以网上搜下,很多资料。在这儿就不一一解答了。
作者介绍:
闫文广,京东到家后台研发部架构师
从事支付系统、计费系统和订单履约系统等后台领域的研发,现专注于订单中心架构优化和研发相关的工作。