12306.cn网站挂了,被全国人民骂了。我这两天也在思考这个事,我想以这个事来粗略地和你们讨论一下网站性能的问题。由于仓促,并且彻底基于本人有限的经验和了解,因此,若是有什么问题还请你们一块儿讨论和指正。(这又是一篇长文,只讨论性能问题,不讨论那些UI,用户体验,或是是否把支付和购票下单环节分开的功能性的东西)css
任何技术都离不开业务需求,因此,要说明性能问题,首先仍是想先说说业务问题。html
多说几句:前端
我说那么多,我只是想从业务上告诉你们,咱们须要从业务上真正了解春运铁路订票这样业务的变态之处。linux
要解决性能的问题,有不少种经常使用的方法,我在下面列举一下,我相信12306这个网站使用下面的这些技术会让其性能有质的飞跃。nginx
经过DNS的负载均衡器(通常在路由器上根据路由的负载重定向)能够把用户的访问均匀地分散在多个Web服务器上。这样能够减小Web服务器的请求负载。由于http的请求都是短做业,因此,能够经过很简单的负载均衡器来完成这一功能。最好是有CDN网络让用户链接与其最近的服务器(CDN一般伴随着分布式存储)。(关于负载均衡更为详细的说明见“后端的负载均衡”)算法
我看了一下12306.cn,打开主页须要建60多个HTTP链接,车票预订页面则有70多个HTTP请求,如今的浏览器都是并发请求的。因此,只要有100万个用户,就会有6000万个连接,太多了。一个登陆查询页面就行了。把js打成一个文件,把css也打成一个文件,把图标也打成一个文件,用css分块展现。把连接数减到最低。数据库
这个世界不是哪一个公司都敢作图片服务的,由于图片太耗带宽了。如今宽带时代很难有人能体会到当拨号时代作个图页都不敢用图片的情形(如今在手机端浏览也是这个情形)。我查看了一下12306首页的须要下载的总文件大小大约在900KB左右,若是你访问过了,浏览器会帮你缓存不少,只需下载10K左右的文件。可是咱们能够想像一个极端一点的案例,1百万用户同时访问,且都是第一次访问,每人下载量须要1M,若是须要在120秒内返回,那么就须要,1M * 1M /120 * 8 = 66Gbps的带宽。很惊人吧。因此,我估计在当天,12306的阻塞基本上应该是网络带宽,因此,你可能看到的是没有响应。后面随着浏览器的缓存帮助12306减小不少带宽占用,因而负载一下就到了后端,后端的数据处理瓶颈一下就出来。因而你会看到不少http 500之类的错误。这说明服务器垮了。后端
静态化一些不常变的页面和数据,并gzip一下。还有一个并态的方法是把这些静态页面放在/dev/shm下,这个目录就是内存,直接从内存中把文件读出来返回,这样能够减小昂贵的磁盘I/O。浏览器
不少人查询都是在查同样的,彻底能够用反向代理合并这些并发的相同的查询。这样的技术主要用查询结果缓存来实现,第一次查询走数据库得到数据,并把数据放到缓存,后面的查询通通直接访问高速缓存。为每一个查询作Hash,使用NoSQL的技术能够完成这个优化。(这个技术也能够用作静态页面)缓存
对于火车票量的查询,我的以为不要显示数字,就显示一个“有”或“无”就行了,这样能够大大简化系统复杂度,并提高性能。
缓存能够用来缓存动态页面,也能够用来缓存查询的数据。缓存一般有那么几个问题:
1)缓存的更新。也叫缓存和数据库的同步。有这么几种方法,一是缓存time out,让缓存失效,重查,二是,由后端通知更新,一量后端发生变化,通知前端更新。前者实现起来比较简单,但实时性不高,后者实现起来比较复杂 ,但实时性高。
2)缓存的换页。内存可能不够,因此,须要把一些不活跃的数据换出内存,这个和操做系统的内存换页和交换内存很类似。FIFO、LRU、LFU都是比较经典的换页算法。相关内容参看Wikipeida的缓存算法。
3)缓存的重建和持久化。缓存在内存,系统总要维护,因此,缓存就会丢失,若是缓存没了,就须要重建,若是数据量很大,缓存重建的过程会很慢,这会影响生产环境,因此,缓存的持久化也是须要考虑的。
诸多强大的NoSQL都很好支持了上述三大缓存的问题。
前面讨论了前端性能的优化技术,因而前端可能就不是瓶颈问题了。那么性能问题就会到后端数据上来了。下面说几个后端常见的性能优化技术。
关于数据冗余,也就是说,把咱们的数据库的数据冗余处理,也就是减小表链接这样的开销比较大的操做,但这样会牺牲数据的一致性。风险比较大。不少人把NoSQL用作数据,快是快了,由于数据冗余了,但这对数据一致性有大的风险。这须要根据不一样的业务进行分析和处理。(注意:用关系型数据库很容易移植到NoSQL上,可是反过来从NoSQL到关系型就难了)
几乎全部主流的数据库都支持镜像,也就是replication。数据库的镜像带来的好处就是能够作负载均衡。把一台数据库的负载均分到多台上,同时又保证了数据一致性(Oracle的SCN)。最重要的是,这样还能够有高可用性,一台废了,还有另外一台在服务。
数据镜像的数据一致性多是个复杂的问题,因此咱们要在单条数据上进行数据分区,也就是说,把一个畅销商品的库存均分到不一样的服务器上,如,一个畅销商品有1万的库存,咱们能够设置10台服务器,每台服务器上有1000个库存,这就好像B2C的仓库同样。
数据镜像不能解决的一个问题就是数据表里的记录太多,致使数据库操做太慢。因此,把数据分区。数据分区有不少种作法,通常来讲有下面这几种:
1)把数据把某种逻辑来分类。好比火车票的订票系统能够按各铁路局来分,可按各类车型分,能够按始发站分,能够按目的地分……,反正就是把一张表拆成多张有同样的字段可是不一样种类的表,这样,这些表就能够存在不一样的机器上以达到分担负载的目的。
2)把数据按字段分,也就是竖着分表。好比把一些不常常改的数据放在一个表里,常常改的数据放在另外多个表里。把一张表变成1对1的关系,这样,你能够减小表的字段个数,一样能够提高必定的性能。另外,字段多会形成一条记录的存储会被放到不一样的页表里,这对于读写性能都有问题。但这样一来会有不少复杂的控制。
3)平均分表。由于第一种方法是并不必定平均分均,可能某个种类的数据仍是不少。因此,也有采用平均分配的方式,经过主键ID的范围来分表。
4)同一数据分区。这个在上面数据镜像提过。也就是把同一商品的库存值分到不一样的服务器上,好比有10000个库存,能够分到10台服务器上,一台上有1000个库存。而后负载均衡。
这三种分区都有好有坏。最经常使用的仍是第一种。数据一旦分区,你就须要有一个或是多个调度来让你的前端程序知道去哪里找数据。把火车票的数据分区,并放在各个省市,会对12306这个系统有很是有意义的质的性能的提升。
前面说了数据分区,数据分区能够在必定程度上减轻负载,可是没法减轻热销商品的负载,对于火车票来讲,能够认为是大城市的某些主干线上的车票。这就须要使用数据镜像来减轻负载。使用数据镜像,你必然要使用负载均衡,在后端,咱们可能很难使用像路由器上的负载均衡器,由于那是均衡流量的,由于流量并不表明服务器的繁忙程度。所以,咱们须要一个任务分配系统,其还能监控各个服务器的负载状况。
任务分配服务器有一些难点:
我看到有不少系统都用静态的方式来分配,有的用hash,有的就简单地轮流分析。这些都不够好,一个是不能完美地负载均衡,另外一个静态的方法的致命缺陷是,若是有一台计算服务器死机了,或是咱们须要加入新的服务器,对于咱们的分配器来讲,都须要知道的。
还有一种方法是使用抢占式的方式进行负载均衡,由下游的计算服务器去任务服务器上拿任务。让这些计算服务器本身决定本身是否要任务。这样的好处是能够简化系统的复杂度,并且还能够任意实时地减小或增长计算服务器。可是惟一很差的就是,若是有一些任务只能在某种服务器上处理,这可能会引入一些复杂度。不过整体来讲,这种方法多是比较好的负载均衡。
异步、throttle(节流阀) 和批量处理都须要对并发请求数作队列处理的。
因此,只要是异步,通常都会有throttle机制,通常都会有队列来排队,有队列,就会有持久化,而系统通常都会使用批量的方式来处理。
云风同窗设计的“排队系统” 就是这个技术。这和电子商务的订单系统很类似,就是说,个人系统收到了你的购票下单请求,可是我尚未真正处理,个人系统会跟据我本身的处理能力来throttle住这些大量的请求,并一点一点地处理。一旦处理完成,我就能够发邮件或短信告诉用户你来能够真正购票了。
在这里,我想经过业务和用户需求方面讨论一下云风同窗的这个排队系统,由于其从技术上看似解决了这个问题,可是从业务和用户需求上来讲可能仍是有一些值得咱们去深刻思考的地方:
1)队列的DoS攻击。首先,咱们思考一下,这个队是个单纯地排队的吗?这样作还不够好,由于这样咱们不能杜绝黄牛,并且单纯的ticket_id很容易发生DoS攻击,好比,我发起N个 ticket_id,进入购票流程后,我不买,我就耗你半个小时,很容易我就可让想买票的人几天都买不到票。有人说,用户应该要用身份证来排队, 这样在购买里就必须要用这个身份证来买,但这也还不能杜绝黄牛排队或是号贩子。由于他们能够注册N个账号来排队,但就是不买。黄牛这些人这个时候只须要干一个事,把网站搞得正常人不能访问,让用户只能经过他们来买。
2)对列的一致性?对这个队列的操做是否是须要锁?只要有锁,性能必定上不去。试想,100万我的同时要求你来分配位置号,这个队列将会成为性能瓶颈。你必定没有数据库实现得性能好,因此,可能比如今还差
3)队列的等待时间。购票时间半小时够不够?多很少?要是那时用户正好不能上网呢?若是时间短了,用户不够时间操做也会抱怨,若是时间长了,后面在排队的那些人也会抱怨。这个方法可能在实际操做上会有不少问题。另外,半个小时太长了,这彻底不现实,咱们用15分钟来举例:有1千万用户,每个时刻只能放进去1万个,这1万个用户须要15分钟完成全部操做,那么,这1千万用户所有处理完,须要1000*15m = 250小时,10天半,火车早开了。(我并不是乱说,根据铁道部专家的说明:这几天,平均一天下单100万,因此,处理1000万的用户须要十天。这个计算可能有点简单了,我只是想说,在这样低负载的系统下用排队可能都不能解决问题)
4)队列的分布式。这个排队系统只有一个队列好吗?还不足够好。由于,若是你放进去的能够购票的人若是在买同一个车次的一样的类型的票(好比某动车卧铺),仍是等于在抢票,也就是说系统的负载仍是会有可能集中到其中某台服务器上。所以,最好的方法是根据用户的需求——提供出发地和目的地,来对用户进行排队。而这样一来,队列也就能够是多个,只要是多个队列,就能够水平扩展了。
我以为彻底能够向网上购物学习。在排队(下单)的时候,收集好用户的信息和想要买的票,并容许用户设置购票的优先级,好比,A车次卧铺买 不到就买 B车次的卧铺,若是还买不到就买硬座等等,而后用户把所需的钱先充值好,接下来就是系统彻底自动地异步处理订单。成功不成功都发短信或邮件通知用户。这样,系统不只能够省去那半个小时的用户交互时间,自动化加快处理,还能够合并相同购票请求的人,进行批处理(减小数据库的操做次数)。这种方法最妙的事是能够知道这些排队用户的需求,不但能够优化用户的队列,把用户分布到不一样的队列,还能够像亚马逊的心愿单同样,让铁道部作车次统筹安排和调整(最后,排队系统(下单系统)仍是要保存在数据库里的或作持久化,不能只放在内存中,否则机器一down,就等着被骂吧)。
写了那么多,我小结一下:
0)不管你怎么设计,你的系统必定要能容易地水平扩展。也就是说,你的整个数据流中,全部的环节都要可以水平扩展。这样,当你的系统有性能问题时,“加3倍的服务器”才不会被人讥笑。
1)上述的技术不是一朝一夕能搞定的,没有长期的积累,基本无望。咱们能够看到,不管你用哪一种都会引起一些复杂性。
2)集中式的卖票很难搞定,使用上述的技术可让订票系统能有几佰倍的性能提高。而在各个省市建分站,分开卖票,是能让现有系统性能有质的提高的最好方法。
3)春运前夕抢票且票量供远小于求这种业务模式是至关变态的,让几千万甚至上亿的人在某个早晨的8点钟同时登陆同时抢票的这种业务模式是变态中的变态。业务形态的变态决定了不管他们怎么办干必定会被骂。
4)为了那么一两个星期而搞那么大的系统,而其它时间都在闲着,有些惋惜了,这也就是铁路才干得出来这样的事了。