今年的双十一有幸参与自己的项目并作出贡献,从周五通宵熬夜忙到周一,终于可以休息下,现抽点时间进行复盘,为后续学习方向进一步开阔视野:
情景再现:
1、12:00~12:01
10号晚上安排我在值班,12点一过,自己在淘宝手机端下了两单,并且付款成功,中间跳出了 系统拥挤,库存不足提示,但是重试两次后就付款成功
2、12:02~12:30
同事监控到订单和去年不一样,每10分钟只有2000左右的订单进来,去年一个小时我们接到了10万个订单,按照我们的预期,每分钟今年应该接入2000单
我直接查看聚石塔中的mysql 监控,CPU下降,每秒执行的SQL下降明显,QPS也在下降,总体来看,所有指数都在降低,说明服务器卡主了,没有在处理任务,或者任务处理时间很长,前面的job没有结束,后面的job起不来
我直接去查每个域是否正常在抓单,查看后发现没问题,但是很快,域的抓单时间不走了,查看catlina.out未进行报错,但是发现抓单的job只有开始,但是结束要等很久,而且有看不到结束的趋势
3、12:30~1:00
开始和组长讨论,我分析是因为单量太大,导致线程job要执行很久,然后想去查域当前代抓的订单数,但很卡,sql执行不下去,我们迅速做出决定,加大job,(前面我已经看到线程只有3个线程抓单),然后我直接加到10个job,部署后,指数上去了,但是效果不明显,只是有一点点感觉,杯水车薪
4、1:00~2:00
技术总监和研发总裁过来监督,压力山大,汗死。。。
开发组通过以前出现过的场景,开始关掉库存同步功能,全链路功能,减少聚石塔资源占用,占用数据库过长,因为这两个要调用淘宝接口,耗时较长
5、2:00~3:00
技术总监和总监直接讨论的是Mysql堵住的问题,老大的意见是直接打日志,看哪里堵住了,技术总监拉来DBA的同事,帮忙监控我们的mysql是否正常,在讨论的时候,我们已经确定要将线程加大,加大到37个线程抓单,37个线程下单,效果好点了,但是发现大客户(单量很大的客户)抓单不及时,延时1个小时到2个小时都有
6、3:00-4:00
组长和一个同事被叫回来公司了,同事帮忙把订单量大的客户进行关停,有些店铺是一直不活动的,还有就是将他们均匀hash,和我分析出的一致,我们的hash不均匀,导致有些单量大的客户集中在某一个线程抓单
在中间,我们也进行了日志打印,我的目标一直在盯着某个客户,为什么某个客户时间不走,我的想法是对抓单时间较长的客户进行打印,看他执行了多长时间
老大的意思是对单个job打印,查看哪一步时间耗时较长,也是我一直不理解的地方,导致错过时机
7、4:00-5:00
经过上面两步,加线程,均匀hash,已经做到了延时只有半小时了,
加日志后,发现有些地方执行有点久,通过DBA同事分析,虽然慢一点,然后进行优化,还是不是很好用,
我很迷茫
8、5:00-6:30
随着单量的下降,抓单也不存在延时了,单量下降了,延时就不再出现
我想说,天亮了
9、6:30-18:30
白天同事们对中间的过程进行了优化,但是效果不理想,并未有收获,单量大的还是延时,之后我回去补觉时,同事们打印了堆日志,发现很多mysql的请求在等待
后续用了一个新的mysql驱动包,并且加大了数据库连接池的连接数,减少了链接的等待时间,一开始是等待1分钟, 改成了5秒
10、后面就好了
上面是处理过程,下面用流程图来还原整个抓单和下单的过程:
抓单流程:
场景分析:
并发问题:
为了不会让一个域出现并发,因为如果一个域调用多个线程去跑任务,会造成下单到erp_orders这个mysql表时,因为要先进行扣库存,库存的唯一性,会出现锁等待耗时较长的情况,所以我们只是对不同域进行并发,单个域的抓单和下单流程其实是一个单线程工作
hash不均匀问题:
我们hash函数直接使用域id的余数。我们抓单和下单都采用了37个线程,从日志看出,每个线程负责大约120个域的抓单和下单任务,虽然在域hash时,使每个job都获得了相对平均的域,但是,如果某个域订单量较多,如抓单中,job1种id=1有2000个订单,其他id每个域只有10个订单,那么明显job1需要处理的订单任务数不是均匀的。查询数据库可知道当晚12:00~12:30直接就进来了38万个订单,订单集中在某些域内,如图:
我查到的数据是当晚有60万个订单进来,但是38万是00:00~00:30,45万是00:30-01:00,剩下的是01:00-06:00的订单
当晚为什么订单抓下不来(很慢)?
当晚我们采取的措施,是直接加大抓单和下单的线程,并且将量大的客户通过修改id或者job总数的方法,将这些客户分布到不同的job中,最后在早上6点钟左右把订单下发到ERP
我们在抓单时,没抓完一次,就会进行时间标记,如抓到00:05,下次抓单就会从00:05开始抓取。
但是中间,我们在job未结束,意味着,如果当前job正在处理id=1的2000个订单,假设当时处理到订单第1900个时,虽然我们通过事务控制已经将前面1900个订单写入了我们的proxy_orders,但是我们没有标记好抓单时间值00:05,因为还有100个订单没有抓完,导致重启后,又从00:00开始抓单
当晚我们的订单为什么下发很慢?
下单时,每次遍历一个域的所有处于Request状态的订单,然后一个一个发生Http请求到tomcat,下发到我们erp_orders.然后下发好的订单,将改写状态为succsess,这样就规避了重启后又重新下发相同订单的状况。但是经过查看,某个域有1万个订单处于Request状态,每次job需要3分钟,每次只能下发400个订单,计算出来,1万条订单需要75分钟。对于当晚的60万订单,即使分布在37个下单线程很均匀,大约每隔job需要执行2万个订单,那么就需要150分钟,大约3个小时。当时的情况是,每次只能下发200个订单,下发的线程也只有12个,还不考虑分布均匀问题
为什么下发400个订单,我们需要3分钟?
因为下发订单发送Http请求,tomcat接收到请求后,执行一个事务,这个事务需要处理的业务有:
1.对应到商品2、创建原始订单3、创建ERP订单4、如果开了自动审核还要扣除库存
整个事务繁杂,只有等事务完成后,下发job才能收到成功或失败的信息,导致下发job执行很长时间,当然中间还有sql需要优化的问题
疑惑的地方:当时下午6点,将proxy_ordes的mysql驱动包换了,主要是一开始发现proxy_orders时快时慢的操作,然后打堆栈,发现很多线程在等待,采取的措施1.加大数据库连接池,减小等待时间,但是效果不明显,2.、替换驱动包tomcat/lib
经过上面的步骤,整个业务大家都觉得回复了正常,这一个步骤我未参加,不太清楚
解决方案分析:
整个过程,涉及到地方很多,从架构师的角度来讲,核心是
1、提高并发数
2、保障并发一致性
从提高并发数来说:
a.硬件来讲:带宽,CPU,内存,确保这三个能满足我们的需求
b.从软件来讲:
增加线程:
1、tomcat线程池配置,bio,nio配置
2、mysql线程池:驱动包,线程数等
3、分布式集群处理:多个tomcat,分表等
减少事务的执行时间:
1、用缓存redis,提高读写能力,
2、sql优化,减少执行sqL时间
3、同步改异步:kafka
如何保障并发一致性:
zookeeper协调
redis分布式锁
队列
针对抓单,下单各个流程,我想到的解决方案:
1、获取订单时,用redis保存,第一次job读取到2000个订单,因为重启,又需要重新读取,耗时巨大,可以考虑将第一次的数据保存在redis,这样重启后,读取速度将更快
2、获取订单做到真正的并发:如果发现超过2000,那么将时间拆分,如发现00:00-00:30中间单量超过2000,那么就另起一个线程,将时间拆分00-05和05-30,并且用redis分布式锁,当两个时间,在不同的线程都跑完后,记录时间点30,这样就用线程换取了时间
3、sql优化:对于优化也写了很多,主要是走索引,打印日志,将耗时的操作找出来,看能不能优化
在打印日志中,我们发现抓单后还要写一个日志,这个日志可用可不用,所以临时关闭了该功能
4、优化tomcat线程池:
以前打过堆栈,我们的tomcat还在使用bio的Http请求,具体看http://mp.weixin.qq.com/s/YoQJOhFBWzUqFkBtHevldA
关键问题在于:如果监控有多少Http请求被拒绝,多少被处理,这样才能具体分析具体配置多少个线程
5、mysql线程池优化:
具体配置请看
6、同步改异步:接单,使用kafka消息队列,不在进行单子处理,直接返回信息,加快下发速度
8、事务:长事务改拆分成多个短事务
尽量拆分事务,长事务不但会影响性能,还会对表锁住很长时间,造成等待超时,表死锁,还会占用socket,导致后续的连接请求被丢弃
造成的问题:表等待超时
表死锁
socket占用过长
9、mybatis开发:读取和更新有序进行
updateList先按id排序再操作,避免死锁
保证一致性:
redis分布式锁:http://blog.csdn.net/zhangyufeijiangxi/article/details/78273864
zookeeper:一致性算法,从poxos到zab http://blog.csdn.net/zhangyufeijiangxi/article/details/78486064