库存中心的高并发实践

库存中心成立于2012年年初,目的是为了解决天猫商品的防超卖而引入了预扣库存的逻辑(在付款减库存逻辑中进行扩展),到了2012年年底又开拓了基于后端仓储sku的库存账户体系,从而实现了天猫大商家在淘宝上的库存管理和其线下erp系统的真正打通,完善了整个淘宝的库存管理体系。到了2013年,库存中心将物流宝库存、ic的商品库存和分销库存都接管过来,还有酒店门票的日历库存也落地在库存中心,至此,全网的库存都统一由库存中心存储并提供各种业务接口。最新统计,库存中心的ic商品有效库存记录有98亿条,而基于后端仓储sku的库存账户体系也有超过8000万条记录。库存中心不仅承接了海量数据,更加重要的是,库存中心是淘宝交易链路中的重要一环,每笔淘宝交易都要经过库存中心进行库存扣减。特别像双十一,很多商品都是5折甚至更低,大家都想抢到便宜货,当零点到来,几千万的用户蜂拥而至,在这么高的并发下,库存中心是如何应对的呢?下面从几个方面来介绍下:

一、 库存中心的整体架构:

主体是三层结构:tair+应用服务器+mysql,其中tair使用双机房单数据的部署模式,db数据变更后应用直接put来更新tair数据,命中率基本保持在100%,双十一最低99%,双十一tair集群的峰值qps达到1900万+,tair单机峰值qps达到33万+。要知道在8月初的时候tair单机还只有13w+的包吞吐量,tair团队的同学根据库存业务的特性做了针对性的优化,包括裁剪meta信息,优化内存管理,减少线程竞争,tair线程和cpu绑定等(详细信息可咨询@叶翔、@宗岱)。正是因为tair的高命中率,所以库存中心读组的机器双十一期间都很悠闲,峰值的qps只有3w+,load最高到1.02。而写组的压力就要大很多:峰值的tps达到5w+,rt基本在15ms以下,单机最大峰值661,小米手机大秒峰值tps约为1900/s,单品峰值tps为1381/s,写组的load最高也只有1。应用端具体做了哪些优化后面再细讲,先看看双十一整个数据库集群的数据:db峰值库存核心库集群最大tps: 13.8w;qps:14.9w;单实例最大tps:1949。今年库存中心的db做了很多优化,包括但不限于:mysql的patch的使用(并发控制、queue_on_pk、commit_on_success和限流等),对卖家编辑库存的sql做了多种优化尝试并最终使用了case when的语法,交易减库存的sql使用了id in的语法来同时更新sku和item的库存而不是分两条sql来执行,所有的交易减库存sql都走主键索引而不是二级索引等等。

二、 大秒

双十一00:30小米三款手机:小米3、小米2s和红米各11万台开卖,走的大秒系统,3分钟后,小米官方旗舰店销售额破亿,成为双十一第一家破亿的店,相信也是迄今最快破亿的店。经过日志统计,当时最高下单减库存tps是红米创造,达到1381/s(其实也不算太高)。那么库存中心是如何应对小米这样的秒杀活动的呢?首先从“读”开始,双十一前小米官方给到我们的峰值qps有100万,要知道小米一共就三款手机,平均下来一个商品就有33万的qps,这个数据着实吓到了小伙伴们,虽然双十一库存中心的tair单机也达到了33万的qps,但小米只有一个商品就可能达到33万,难不成给小米搭个单独的tair集群?答案是:yes,小米走大秒,而大秒的库存tair是单独的集群,这是ic以前就做了的,但就算是单独集群,也是有风险的,因为这个大秒集群是没有经过库存中心tair集群那样的优化的,吞吐量是远远不够的,so,如何解?答案是:使用localcache,即在应用服务器(秒杀detail集群)上单机缓存库存数据,然后按照一定时间来失效(比如1s),并且在本地失效以后只会有一个线程去到tair里获取最新数据,通过这种在数据的实时准确性和数据的高并发可读之间的平衡,解决了高并发读的问题。再来看一下“写”,最早的大秒减库存也是在tair里减的,但发展到后来,很多秒杀商品都带有sku,而sku库存和item库存是有联动关系的,tair又不支持事务,所以带有sku的商品的秒杀都是走db减库存的,这次的小米就是这种情况,另外一个挑战是,以前的大秒杀商品库存最多也就几千件,而这次的小米单品库存达到11万件,那库存中心又是如何来迎接挑战的呢?这里要先说明一下:这次的小米大秒活动首先从时间上就进行了错峰,从0点延后到0:30,避开了交易高峰;同时大秒设置了答题,增加了单独,使得下单减库存的并发度降级了不少;还有在buy系统对大秒下单做了保护性的tmd限流,大秒活动总的tps被控制在4k,不少请求没到下单页面就被拒绝了,呵呵。回到正题,看下库存中心在大秒活动中应对措施:

首先,大秒商品基本上要求走拍减的减库存模式,而拍减模式在整个交易过程中只有一次扣减交互,所以是不需要付款减库存那样的判重逻辑,就是说,拍减的减库存sql只有一条update语句就搞定了(而付减有两条,一条insert判重+一条update减库存)(数据说话:双十一拍减接口在高峰的rt约为8ms,而付减接口在高峰的rt约为15ms);

其次,当大量请求(线程)落到mysql的同一条记录上进行减库存时,线程之间会存在竞争关系,因为要争夺InnoDB的行锁,当一个线程获得了行锁,其他并发线程就只能等待(InnoDB内部还有死锁检测等机制会严重影响性能),当并发度越高时,等待的线程就越多,此时tps会急剧下降,rt会飙升,性能就不能满足要求了。那如何减少锁竞争?答案是:排队!库存中心从几个层面做了排队策略。首先,在应用端进行排队,因为很多商品都是有sku的,当sku库存变化时item的库存也要做相应变化,所以需要根据itemId来进行排队,相同itemId的减库存操作会进入串行化排队处理逻辑,不过应用端的排队只能做到单机内存排队,当应用服务器数量过多时,落到db的并发请求仍然很多,所以最好的办法是在db端也加上排队策略,今年库存中心的排队patch有两个,一个叫“并发控制”,是做在InnoDB层的,另一个叫“queue on pk”,是做在mysql的server层的,两个patch各有优缺点,前者不需要应用修改代码,db自动判断,后者需要应该写特殊的sql hint,前者控制的全局的sql,后者是根据hint来控制指定sql,两个patch的本质和应用端的排队逻辑是一致的,具体实现不同。双十一库存中心使用的是“并发控制”的patch。看下@秀卿的压测总结:

这里还要说明一点:应用端的排队不仅控制了应用端到db的并发度,同时也控制了数据库链接的占用率,不会造成热卖商品把全部链接都占满的情况!

如果对mysql的patch有兴趣,可以找库存中心的dba@凌洛、@张瑞,也可以找“并发控制”的作者@希羽和“queue_on_pk(原名ic_reduce)”的作者@丁奇。

三、 热点库

当应用端的热点商品排队减库存还未经过全链路压测的洗礼,当mysql的并发控制patch及queue_on_pk的patch还未上线验证之前,应用和db对于热点商品减库存的控制力是很弱的,一个热点商品可能影响到整个库的其他商品库存,@黄忠同学来到天猫后提出了热点库的想法,一来可以将热点商品从主库里前一出来,做到物理隔离,从而避免对其他大量非热点商品的影响;二来热点库相当于一个小备库,性能基本上和主库一样,而热点库的商品总量很少,可用相对大的db资源来支持相对少的减库存请求。当然,凡事都有另外一面,热点商品库存的迁移还是比较麻烦的,首先需要将原主库的数据进行停写,然后迁移,迁移完毕后再重新路由并开启写操作;由于进入热点库的商品都是热卖商品,所以热点库的并发风险相对更高,因此必须控制进入热点库的商品数量,不能太多,而且最好同一个库的热卖商品的售卖时间能错开,双十一共有近1000件商品进入了热点库,热点库集群的tps峰值为3600。

四、 Commit_on_success & Rollback_on_fail的patch

前面说到了mysql的InnoDB行锁,双十一天猫大部分商品是付款减库存,在拍下订单时会先进行预扣,预扣的sql逻辑是先insert一条判重记录,然后update进行库存预扣,两个sql在一个事务里,但是是分别两次通过应用服务器向mysql server发出sql请求,当update执行完毕后,应用端接收到mysql返回的affectrows来判断是继续commit还是rollback,这里又是一次网络远程调用,网络上的消耗加长了行锁的占用时间,极端情况下如果应用端迟迟不能给mysql server发出commit/rollback指令,那行锁会一直被占用而得不到释放,等待这行锁的线程会越来越多,mysql的资源会耗光,应用端的rt会暴涨,进而导致hsf线程池满,应用端渐渐就拒绝服务了!如何避免这种悲剧的上演?如何解决因为事务而拉长行锁的时间?试想下,如果在update执行完毕后mysql server直接根据affectrows来进行commit或rollback,行锁就可被立刻释放,问题就迎刃而解了。基于这样的想法,dba和核心系统的同学开发出了commit_on_success&rollback_on_fail的mysql patch,应用端在事务的最后一个“写”语句上加上hint就行(这里还有一个优化,事务里的sql顺序很重要,大家可以想想看)

五、 Tair数据的准确性保证

前面讲到,库存中心的tair集群是双机房单数据模式部署,采用put来更新而不是invalid来失效,那么put到tair的数据的准确性是如何保证的呢?最早的时候,减库存的代码逻辑是,先select库存记录,程序里判断是否能减和够减,可以的话就进行减库存的update操作,最后失效tair。经过分析,其实第一个select是没有必要的,因为detail和buy都会先查一遍库存来判断是否够减,所以到库存中心服务端的这个select百分之95以上都是重复无谓的一次判断,因此可以省掉这个select,直接进行减库存update操作。原来有个patch方案是:select after update,即一个sql先执行update再将结果select出来返回,这样的性能是最好的,但时间比较紧就没落地了。今年双十一我们采用的是(commit_on_success)update以后在事务里select一次取到最新的库存数据,然后put到tair里,这样基本能保证tair和db的数据一致性。不过还是有一种极端情况,就是在高并发的时候,最后两件库存分别被T1线程和T2线程减掉,T1线程最终从db里select出来是1件库存,T2减完库存后select出来是0件库存,两个线程同时put到tair,由于网络或其他什么原因T2的put先到,tair先变为0,然后T1的put到了,将tair里的库存置为了1,这样tair和db的数据就不一致了。产生的后果是,detail和buy看到还有1件库存(因为库存的读取都是走tair),然后到库存中心进行db减库存时就失败了,因为db里是0,不够减。解决方案是做一个容错机制,当发现db里为0时,再去更新一次tair,让数据最终一致。

六、 其他

库存中心在2013年做了很多新技术和方案的尝试,比如双层tair、比如精卫更新item总库存、精卫更新tair、ob的compoundsql等等,虽然最后由于成本或性能等原因都没上线,但在新技术的探索和创新的过程中,团队成员收获了很多,如果你喜欢大数据高并发的挑战,欢迎来库存中心看看!

Home - Wiki
Copyright © 2011-2024 iteam. Current version is 2.134.0. UTC+08:00, 2024-09-29 00:19
浙ICP备14020137号-1 $Map of visitor$