淘宝“超卖”背后的技术猜想

今年2012.11.11淘宝脱光大促销,是电子商务一个重要的里程碑,按照马云的说法,这次打响了对传统商业贸易购物的第一枪,宣告了多年来对传统行业的商务行为的革命。
按照历史逻辑分析,每一次革命必然给世人带来崭新的时代,但同时在某些方面也会付出一定的“血”代价,除了某些行业进入衰退期,支撑这场革命运动的背后力量的弱点也会一一暴露出来,而这股背后力量就是来自于淘宝(天猫)的技术系统平台。

根据淘宝官方提供的线索,超卖的原因是前后台商品数据的不一致,导致了这次系统的漏洞。但是如何导致数据不一致,却没有相应的解释。如此留下的悬念,让技术屌丝的我展开了“哥德巴赫”的猜想。

一般来讲,成熟的电子商务平台,除了提供给外部会员访问的前台系统(后续称为买家系统),必然会有配套的提供给内部运营管理的后台系统(后续称为信运系统),对于淘宝的商业模式来说,里面还有一个提供给商户管理的商务前台(后续称为卖家系统),如果三个系统分离的话,往往就是造成数据的不一致的罪魁祸首。

从操作流程上来讲,在活动之前,信运系统除了审核会员,商家,商品等基本功能外,会针对预定活动设定时间,制定规则;卖家系统上架活动商品,设定价格,数量等一系列的活动参数;活动当天,买家系统负责展示活动商品,订单交易等一系列最大流量的请求操作。当然在这个过程中,卖家系统也会同时根据销售情况更新数据。

从系统架构层面,想必淘宝基本上离不开MVC的设计模式,第一层展示层(Viewer), 第二层业务逻辑层(Controller),第三层数据存储层(Model Data),我这里自底向上分析导致这场故障的可疑点。

直观来讲,所有数据必然都是在数据库里面,那不一致最有可能是数据库不一致,首先淘宝可能采用的是Master-Slave读写分离的模式,那必然是有多台Master、Slave的数据库服务器,写操作指向master节点,读操作分流到slave节点,这样主从数据库就需要进行及时的同步保证一致性,往往在主库或从库压力巨大的时候,master的更新很难快速到slave。主从复制一般是先把master含有更新操作binlog复制到slave,slave通过I/O_thread线程写入replay_log,随后slave的sql_thread单线程解析replay_log重放里面的更新操作。如果暂不考虑网络传输binglog延迟的话,对于sql_thread可以进行调优,在可控的场景下,采用多线程sql_thread并发进行binlog追加,缩短延迟时间。也听过一种优化方式就是,开发mysql插件可以在sql_thread读取replay_log之前,把update所涉及的数据在内存中预热,从而帮助sql_thread缩短sql执行的时间。当然对于这些优化的前提,还是先建议把binlog记录格式从statement或mixed改成row的形式,尽管这个会造成日志大量增加,但是对于上述优化,row记录更具有可控性。
以前在电信行业,接触过mysql cluster的架构,其中所有mysql服务器都是master active的状态,都支持并发的读写,在每次写操作的时候,mysql永远保证所有节点数据的一致性,其中原理肯定相比之前的M-M模式更加复杂,当然性能也不错,可能从运维成本和可控性角度来讲,互联网公司很少采用这种架构。

说完mysql主从复制本身的局限性,我们可以提一提,在业务开发过程中,设计的冗余字段。譬如一家淘宝店铺的商品种类数,其中每件商品的库存,还有每件商品的点评数等等统计字段,我认为按照淘宝的设计,对于这些统计字段不可能每次执行这种sql语句(select count(1) from * group by *)来获得的,否则按照当天流量,数据库早就挂了。那么有人可能会说那就把它缓存起来,当然这是一个提高性能的办法,但是对于这些数据必然需要进行持久化到磁盘,以防止突然宕机或重启导致的数据丢失,那么这些冗余字段的一致性维护就需要可靠的机制来保证,目前业界通用做法采用消息系统来完成,复制更新来源系统发出通知消息,订阅此类消息的关系系统就会自动更新对所拥有的冗余字段,如此一来,消息系统的收发性能,响应时间和Qos质量保证就尤为重要,如果发生的长时间的延迟,连续重发,甚至丢包等现象,就会导致冗余字段数据不一致,从而引发业务上的错乱。所以消息系统可以选择一些高性能高可用性的成熟中间件,并且能够遵循JMS(JSR-914)规范,除此之外,通过执行一些脚本作业进行一致性确认及补偿,对于非敏感型的业务数据,并不一定需要实时一致,但可以保证最终一致性。

说到数据库这种磁盘持久化,不得不说一下另外一种存储形式-缓存,顾名思义,缓存就是缓冲区的存储,它与数据库存储不同之处,它是放在内存里面的,当然如果一旦重启,缓存数据就会丢失,而它的优点就是业务逻辑的程序可以直接在从内存里面读取它,避免了诸如读取磁盘的耗时I/O操作,可以大大的提高系统性能。许多业务中的计数器,类似上面的统计字段,往往适合放在缓存里面,提供高并发的读取,既然这样,大家就自然而然想到,这个缓存数据又是如何保证一致性或实时性,更新数据之后,是否可以保证能够成功的刷新缓存,这些都是值得怀疑的地方。我的建议是,虽然有时无法保证一定成功刷新缓存,但是可以在某些数据的敏感区间内做二阶段确认,比如当某衣服实际还剩下不到10件的时候,在提交提单的时候,除了从缓存中读取到库存数并且发现已经小于10时,可以从数据库读取真实的库存,做一次数据对比,以此判断是否还可以进一步交易。

在业务逻辑层面,我想更多谈的就是业务操作的同步,当大量会员交易的时候,库存的数据就要很迅速的更新,前一个交易的结果就会影响到下一个交易行为的可行性,落地到技术细节,就演变成了多线程读与写的同步问题和事务控制,这个也算是并发操作的经典问题,同时放在多台业务系统组成的集群环境下面,就变得更加复杂。根据不同的部署来看,一般分为两种情况,一种是所有持久化数据存在中心化数据库服务器,缓存数据存在独立的分布式缓存集群服务器,那对于业务系统来讲,不管有几台,其实他们是无状态的,除了增加事务控制外,数据一致性大部分由存储服务器来保证。另外一种就是一部分缓存数据放在了业务系统本身,通过集群技术,分布式存储这些数据,这样一来,就从单进程的多线程同步演变成跨JVM的分布式同步,甚至引入了分布式事务控制。根据经验,笔者建议尽可能采用第一种保证业务服务器的无状态性,避免第二种分布式情况带来的复杂性,降低运维成本。但是在某些场景下,比如复杂结构的大数据对象,就不大建议存放在外部缓存系统,否则带来了网络I/O反而增加了延时,更适合放在业务系统本身,那么到底是replicated-ALL存储,还是distributed存储,这个还要结合考虑系统所需的伸缩性和稳定性。笔者正在研究几款内存数据网格的中间件,适用于集群环境下面使用,待觉得成熟之后,可以推荐给大家,一起讨论研究。

在某些时刻,极端高峰的时候,数据一致性问题从技术上已经不大可能避免的话,还可以采用一些特殊的非正常手段进行保护,比如服务降级,流控,可以把超过阀值一些交易操作请求放到排队系统,通过时间换空间的做法,保证系统的稳定性,从而保证数据的一致性。

到这里,其实从底向上分析,已经有许多在系统架构上面可能导致问题的关键点,尽管笔者目前是局外人,但是这些也是曾经经历过的种种,姑且在这里抛砖引玉,欢迎有缘人或当事人一起吐槽。

有时候可能事实喜欢开个冷笑话,说不定就像狂欢当天,某位老板手贱把一款原价118的商品标错了11.8元,所以可能就是淘宝压根忘记了更新某个字段。呵呵,不过我想应该不会发生这种低级问题的。

最后淘宝对于超卖事件,也对买家做出了经济补偿,就像上面提到的数据作业补偿一样,一旦一时发生了不一致或不平衡,最后还是需要保证一致性或平衡性, 今年最后奋力一搏的淘宝,以一天进账4亿的收入非常出色的完成收官之作,奠定了电商领域的霸主地位。

相关推荐