代码改变世界

铁道部的网上订票系统的几点思考

2012-01-16 09:05  hbqiao  阅读(1238)  评论(0编辑  收藏  举报

听到铁道部网上订票系统因为流量过大出了问题,周末就在思考这个问题,有点欲罢不能。 本人并没有亲手做过订票系统,所以也是即兴原创性的想法,主要是和大家讨论交流一下 (不过你可能还需要有一定的功底才能理解并参加讨论)。

首先, 做好一个售货系统并不容易。 比如, 前一阵美国HP公司宣布退出TouchPad市场, 把所有的库存以100美元出空, 引起网上的抢购。一时HP的网站连续数天濒临崩溃,刷新一次界面需要几分钟。更有甚者, 有人交了钱,购买成功, 但是其实HP的库存已经早就卖完了, HP网站的软件也犯了如此低级错误。而且不止HP一家。 本人在BarnsNoble网站上也购买成功并付了钱, 但是后来过了几天,  接到一个Email, 说是抱歉, 他们已经卖完了。 当时已经卖完了,网站还卖了大概一天多的时间。

HP和BarnsNoble网站在抢购TouchPad的时候, 其网站的压力要比铁道部的压力可能要小一个数量级以上,有着HP和美国电子商务的多年的经验, 但还是崩溃了。 铁道部的网上订票系统出点问题还是值得大家理解原谅一下的,

这样的一个网站大致分网站内容服务和事务处理两个部分。 网站内容服务要能保证用户能够打开页面,查询内容。事务处理部分是要能够查询车次的余票和实际订票。如果这样的网站要能支持100万同时用户, 其挑战还是很大的。

 

网站内容服务

 

本人对大型内容网站的建设没有一手经验, 所以就不展开谈了。大家应该有不少人对此有了不少经验了。 大型内容网站的建设无非是放上几百上千台机器, 辅以足够的带宽, 有些内容近地加以缓存。另外AJAX的出现, 有些动态的内容可以通过WebService/Restful/JSON 取得, 而不是机械的刷新HTML, 可以大大减少服务器的处理和网络带宽。 比如查询某一车次还有无余票,以前的做法是客户端送一Request到服务端, 服务端取车次数据以后, 产生一个包括整个页面的HTML文件, 送回Browser, 当然有些图像信息Browser是缓存的。用了AJAX以后, 服务端只要查询车次数据就行了, 这样带宽和服务器的处理会有量级的不同。

如果用现在的Intel 双CPU 共8-12 core的网站服务器,如果每个用户10-20秒刷新一次界面, 一个服务器应该可以支持5千到一万个同时用户。这样支持1百万的同时用户, 100-200 台机器就差不多了。 当然这里只是讨论的网站服务器。 这些服务器还可以分类, 如北京地区的用其中的一个集群, 上海用另一个集群。 这样可以增加cache的利用程度。

订票事务处理

对1百万人在线,1/3人实际购票, 购票人平均花100秒,每秒的出票率在3000左右。如果3000每秒, 每小时的出票就可以达到一千万张, 应该可以满足要求了吧。 这样的通量在经过仔细设计以后,用普通的数据库运行在通常的高端企业硬盘系统上就可以了。 以下对订票事务处理的一些问题进行一些讨论, 有时一个问题没有考虑好, 都会造成数据库通量1倍甚至数倍的降低。

 

下单以前票的锁定

有人可能认为有必要在用户下单以前可以为用户锁定一张票一个座位,给客户10-15分钟时间考虑, 然后再交钱完成交易。 这样在其他用户也在买票而余票不多的情况下,能保证这个用户肯定能拿到票。 不知铁道部是否是这样做的, 这个需求看上去容易,但是对系统的要求高出很多。可以说是得不偿失。因为锁定一个位置, 就意味着要增加数据库写的操作,增加数据库的性能负担, 增加数据库Lock和Deadlock的机会。而且, 还要考虑用户断线离开后要解开锁定的票, 程序的复杂性也会大大增加。

其实对于用户, 只要告诉他余票不多了, 要是下单没有“抢到”, 他也能理解的。所以强烈不建议在用户下单在以前可以为用户锁定票。要是为这个非根本性的(non-essential)需求 而造成系统的响应缓慢甚至不稳定,对用户的影响就更大了。

付款和出票的一致性问题

有人反映钱都扣了, 而票没有拿到。订票和银行的支付系统肯定不是一个地方。 通常涉及两个以上的事务处理系统, 要用XA two phase transaction, 可以避免钱扣了, 票没有, 或者票出了, 钱没有收的问题。 问题是本人不认为支付系统会支持XA transaction,所以这个问题必须用其他办法来解决。(BTW, 要是开发一个支付系统支持XA, 可能会有竞争优势的, 后面会提到相当于XA的功能)。

本人是这样设想的。 设计一个支付的数据库表, 记录支付的情况。 在订票完成, 写进订票数据表时, 在一个事务里同时写进支付表。同时内存中建立一个到银行支付系统取钱的request,在系统排队。 这时事务结束,请求银行支付异步执行。在银行支付结束拿到钱以后, 更新支付表说明支付结束,交易完成。要是支付完成以前系统出问题,系统恢复以后还可以从支付表读取数据,继续支付, 完成交易。

但是这样还是会有问题, 在银行支付结束, 更新支付表以前, 系统断电了, 这样还是会出现不一致的问题。 这个问题出现的几率实在不大, 因为是在中心机房。当然还有可能支付请求送到银行, 银行处理完后网络断了,铁道部不知道。这个问题的可能性就要比前一个问题的可能性大得多了。 问题可以解决:银行的支付两步走, 第一先请求付款, 拿到一个交易号,并将交易号写进支付表。 第二步才要求正式交易。这样若出现银行断线或本地死机,恢复后就可以到银行用交易号查询交易的实际执行情况。这其实已经是和XA 事务处理有点像了。 要是你设计一个支付系统, 可以考虑这个功能啊。

这里讨论一下为什么订票的事务处理先结束, 而银行的支付异步进行呢?  支付是远程执行,要花的时间是本地订票事务处理的10-100倍以上。 要是订票事务不结束,事务就会在数据库hold Lock, 数据的性能和通量会大大下降, 出现死锁的几率也会大大增加。这么一个小小的考虑对系统通量有很大的影响。

再讨论一下:现在有一张票给了张三, 但是因为异步支付, 我们要知道支付是否完成,才能正式出票。  票务的数据库表要不要写进支付完成状态? 答案是不需要。 每次去查询一下支付表就行了。 为什么, 你去想想。 

车次余票的查询

余票的查询需要很高的通量。有百万人在线, 每秒会有超过一万甚至百万的余票查询。要是总是去数据库, 系统马上就崩溃了。 所以这个问题要有很好的设计。 要注意到用户只关心有无票,余票还多不多, 并不关心余票的准确数字,所以我们要充分利用这点。

本人认为有必要有专门的服务器(集群)提供余票查询服务。服务器可以Partition, 每个服务器提供一段车次的余票情况 (这是NoSQL经常使用的招数)。 余票查询服务器cache所有车次的余票情况, 然后不时的查询数据库得到最新的正确信息。 因为其实用户只关心有无票,或余票多不多, 所以在余票还有很多的情况下,没有必要经常查询数据库, 可以1-10分钟查一查, 在余票不多的情况下, 可以查的勤一些, 以保证信息的实时。 时间间隔可以根据实际情况讨论确定。

对百台以上网站服务器,要是每个网站服务器RPC到余票信息服务器查询, RPC的数量也是惊人的。这时可以考虑在网站服务器进行Cache, 可以同样根据余票多少和Cache时间的情况决定是否用RPC去查询, 同时可以考虑采用IP multicast 把余票信息推送至网站服务器, 如果有100台网站服务器, 使用IP Multicast还是可以省下相当多的RPC的。RPC不考虑使用Webservice或JSON,应使用效率最高的native格式, 如RMI/IIOP, 甚至考虑使用非标准的Protocol。  

数据库的设计

数据库要能够分开, 可以有多个数据服务器, 每个服务器服务不同的一些车次。  比如上海始发的武汉用一个服务器, 北京四川用另外一个。 数据库服务器和Web服务器不一样, Web服务器用便宜的,2-3万元RMB就可以了, 而数据库服务器要用高可靠高性能的高端机器。另外也没有必要使用数据库集群, 不久以前只有Oracle有真正的分担负载的集群,偶尔还有Bug。 DB2和SQLServer的所谓集群只是热备而已。集群解决什么问题? CPU不够? 数据库的瓶颈通常在硬盘而不在CPU。 当今世界上最大型的数据库估计大部分也没有数据库集群, 也运行的很好。 有钱可以花在数据库服务器, 存储和机房网络 UPS的建设上。 当然是本人管见而已。

另外是把所有的车次写在一张表还是分开成很多张表, 每个表管理一个或好几个车次? 我倾向于后者,同样线路的车次也可以放在一张表。 有千张以上的表, 当然要和数据库厂商核实一下。 因为通常用户是买一张或两张转车票, 后者可以大大的降低查询的时间和复杂程度,降低数据库锁的复杂程度。

将车次分布到许多表对购票事务处理有帮助。 在此设计中,每个座位每张票就是一数据库行。  因为并不是每个人都是从起点到终点,一个座位可以卖出几张票,行数肯定会超过座位数,这就是为什么上座率可以得到超过100%的原因。 但是在这种设计下,要是有一个人想要从上海到徐州, 就不能用一个数据库查询找到所有车次的余票情况了。但是这并不是个问题,因为我们并不能每次依靠数据库查询来得到余票的情况。 如果每次查询都要用数据库,要一张有读有写的表支持每秒上万次的读操作, 是不行的。必须有其他的做法。 优化后的查询应该从两步走, 第一步:先找出从上海到徐州的车次, 因为这个车次信息的相对静态的,可以缓存,不会和读操作交互,可以有很大的通量。 当然应有一定的算法考虑到转车等等。第二步才是找出每个车次的余票情况, 如何支持余票高通量查询的问题在前面已有描述。

另外一个从上海到北京的车次,要是某人要从苏州到南京, 那么购买的苏州-南京的票以后,表会增加2行, 就是上海-苏州 和 南京-北京, 原来的变成苏州-南京。出发站和中途每个站有一个序列,查询时用序列号来, 如“起点 小于等于 苏州 终点大于等于南京”, 就可以把一张上海到北京的票查到并分开成三张票。 这个问题还是有点复杂的。 尤其在考虑到查询余票数量的情况下。 一个车次到最后会有很多种余票的组合。这个问题可以根据具体情况和需求进行一定的假定和约束, 使问题不会无限扩大。 本人并没有做过座位和车次的分配的算法, 这里只是一些初步的思考, 估计做这一行的同行已经有了很好的方案。

 

部署的考虑

能否用虚拟机云计算的方式部署?

虚拟机的优势在于软件安装不再和硬件挂钩, 虚拟机可以很容易的从一个机器移到另一个机器上。也可以很快的将虚拟机在很多机器上运行起来。但是虚拟机的性能大概是原机的70%。所以本人觉得数据库还是用Native机器。 对于网站服务器, 可以维持一个基本服务的一定数量Native机器, 如50台机器。 其他可以用虚拟机动态的增加处理能力。

可以考虑滚动开始售票

如果每天在一个固定时间开放所有车次的订票, 春运期间那一个时刻的流量实在是惊人。 可以考虑滚动开放, 先开放一些车次或一个路段, 半小时以后, 哪怕10分钟以后再开放另外一批, 在一个小时内全部开放。 这样尽管造成一点不便,但会大大降低瞬间的高峰流量对系统的要求。

能否考虑分布式的网站和数据库

 比如在北京上海广州武汉四川各设一个数据中心处理客户请求。 如果这样做的话,当然可以减轻负担, 但是有一些前提, 第一:数据中心相对独立, 服务的车次不交叉重叠, 不需要数据中心之间的数据同步。 第二, Web服务器和数据库服务器之间的RPC不能跨数据中心。否则会有很多问题。 要是这两个前提能得到满足, 多个数据中心是可以的。但是要是某人要转车, 而上海到武汉再到四川, 我们是难以保证两张票同时买到的一致性的。在一个数据中心, 不同的数据库, 可以很容易的使用XA事务处理来实现。所以分布数据中心还是有一定的挑战。

 

 结论

本文讨论了铁道部订票系统这种高网页流量和高事务处理通量的一些具体设计的考虑。按此框架应该可以在一般硬件环境下支持春运的高峰流量。当然很多细节问题还需要经过艰苦的设计推敲过程。 本文提到的设计要点是:第一, 实现网页静态内容和事务处理部分的分离;第二,数据库设计要充分考虑到partition(分区)和读写操作的极大优化, 本文提到了好几个细节, 如划款的异步处理;第三, 对高通量的操作要有特别的考虑, 如余票数量的查询; 第四, 对事务处理的一致性要有充分的考虑,