订单调度系统
履约是一个承上启下的系统,上承前端交易,往下对接各种仓配系统。提供定仓、建单、接单、拆单、合单、截单等等业务功能,这些功能以发货单为载体,订单调度系统就是如何调度发货单进行业务流转的系统
v1版本
发货单的状态流转为单据到履约系统后会被停30分钟进行Hold单,因为用户下单后有很大概率会在半小时内退款,Hold单30分钟可以很有效的减少仓库的实操成本。当时的调度处理如下:
- 依赖了一个分布式的任务调度中间件
- Hold单任务3分钟执行一次,下发任务1分钟执行一次。两者调度流程是一样的
- 因为数据分了512张表,所以第一次调度时随机选中一台服务器,然后生成32个任务,每个任务扫描16张表,然后将这些任务投递给集群内所有机器。这个功能依赖于任务调度中间件
v2版本
很快的在一次大促中,仓库系统被打挂了,因为外包仓库的WMS系统性能太差,需要对下发做流控。当时做流控碰到两个问题:
1、下发任务一分钟一次,需要改成秒级
2、数据分了512张表,按指定流量捞取很不方便
所以,我们将待下发的订单单独做了一张单表保存做为待下发的订单池(这个没有分表,因为当时单量不大,而且下发成功后会立刻删除,所以这个表数据量不大
)
没有了分表,所以去掉了分发的过程,下发任务每一次都只在一台服务器运行,从而控制了下发的流量
v3版本
v2版本在跑了时候,偶尔会出现下发流量翻倍或者连续好几秒没有任何下发流量,这个问题在大促的时候是个极大的风险点,究其原因是分布式任务中间件秒级调度并不能100%保证。所以我们打算在下发流程中去掉对该中间件的依赖。改起来也很容易:用zk选个master,在master上启动定时任务,监听机器变更消息,重新选主v4版本
业务发展比较快,单量越来越多,同时接入的仓库也从1家增加到多家,导致下发的流量倍增,master捞取订单流控变为:
for(store_i : stores)
qps_i = qps(store_i)
List orders_i = selectFromDB(store_i, qps_i)
process(orders_i)
同时,在1s内既要捞取订单同时还有处理下发流程,master
开始抗不住了。此时,我们决定将订单调度跟下发的业务逻辑彻底解耦,所以引入了消息中间件,调度流程变为
master
处理,扩展到集群内任何服务器都可以执行,大大的减轻了master
的负载
v5版本
当引入MQ后,发现有时候下发给仓库的流量会超过限定值,特别是下游系统出问题的时候。原因是在下发消息消费失败后,会踢回去依靠MQ重试,也即在异常情况下多了一个数据源。为了解决这个问题,一开始我们采取在消费端也加上限流:
1、使用Guava的RateLimiter,因为这个是单机限流,所以我们订阅zk消息获取集群机器数,计算单机的限流值。但是,问题是RateLimiter会阻塞线程,而当时我们好几个仓库同时使用一个线程池,导致下发的吞吐量抖降
2、使用redis做集群限流,但重试机制依赖于MQ,而MQ采用的指数退避算法,在很短一段时间内会连续重试。这些异常的订单不停的重试,挤压了正常的流量,导致一段时间内正常下发的流量明显小于流控值
所以,我们还是决定不在消费端限流,也就是去掉MQ的重试
producer(orderTask):
sendMQ(orderTask)
orderTask.retryScheTime = getRetryScheTime(orderTask)
persistToTB(orderTask)
consumer(orderTask):
processStatus = fasle
try{
...
processStatus = true
}catch(e){
processStatus = false
}
if(processStatus == true){
deleteDB(orderTask)
}
return true
v6版本
在这个版本我们将系统的能力开放给了第三方仓库,导致对接的仓库一下变成了几十家。因为对接了第三方仓库的接口,这些接口的稳定性不可保证,所以需要在消费端做线程池的隔离,避免一个仓库接口挂了,导致整个系统下发出现异常。v7版本
系统跑到这个版本,开始出现瓶颈,瓶颈主要体现在性能跟业务功能两个方面
性能
1、订单量越来越大,单表的订单池中订单数越来越多
2、自从开放能力给第三方仓库后,接入的合作仓库上升到了上百家,为了在生产端做流控,master每次调度都需要查订单池表上百次
业务功能
1、hold单类型的增加,有之前的半小时hold单变成了:
---- 1>、各个仓库对hold单时间要求不一致,有的甚至希望不hold直接下发,增加用户的退款成本从而减少退款数
----- 2>、有些hold单需要消息触发才能往下推进,比如拼团、负卖
------3>、有些商家希望订单经过他们审核才可以发货
------4>、hold类型是可以组合的,比如拼团的订单,用户拼团成功之后,还可以hold单固定时间,或者说一个订单即可以是负卖又可以是拼团
2、特殊订单拦截功能的增加
---- 1>、一些黄牛订单需要拦截,而规则必须是可配的。比如拦截指定用户、指定订单、指定商品、指定收货人等等
---- 2>、运营上需求可能说某一个商品类目的订单需要拦截下来,或者商品上有某一个标等也可以拦截下来
---- 3>、符合某些规则的订单需要优先下发,而这些规则是可配的
因为这些需求跟瓶颈,在v7版本我们对订单调度做了一次全新的架构升级
模型
首先抽象出调度策略这个模型,每个策略跟一个队列一一对应。每个策略可以配置入队的规则,出队的规则,这个队列调度的cron表达式以及队列的消费速度等等,同时总共有3大类型的队列:
1、 hold单队列
2、下发队列
3、重试队列
调度策略与服务器的关系
因为要对每个队列做流控,那么每个队列只在一台服务器进行调度是最容易控制的
整体流程
负载均衡
这里的负载是一个机器上的调度策略的权重值。每个调度策略都有一个权重,这个权重表示这个调度策略的计算消耗。比如,一个调度策略每隔1s从一个队列拉取一个订单的权重为1,那么每隔1s从一个队列拉取10个订单的权重可以简单设为10. 因为一个调度策略只在一台服务器上进行调度,才能有效的进行流量控制。所以这里的负载均衡就是根据策略权重,将策略分配到某个服务器,从而保证他们调度所消耗的计算资源大体是一样的。比如,有5个调度策略,3台服务器,负载策略可能为:
通过Zookeeper
监听机器上下线,同时调度策略发生变化会发送MQ消息通知负载均衡器重新计算负载
技术实现
1、 队列采用redis的zset
2、调度cron采用quartz
3、订单抓取采用zset的range,每次range的量不会很大