林仕鼎:火车票系统后面的架构设计

林仕鼎:火车票系统后面的架构设计

林仕鼎

简单讨论火车票系统后面的架构设计

[有点晚了,简单写写,找时间再polish。另外,我们只谈技术。]

简单说,在线服务scalability有两种方式,scale-up和scale-out。Scale-up追求单机性能,如高级硬件、异步架构等,而scale-out则用加机器的方法。Scale-out也是最简单的方法,在规模不是非常大时很好用,也很容易解决问题,普通工程师就足以胜任。有很多现成的方法或模块可以使用。
也就是说,很多时候通过加机器就能解决大多数问题。只要规模在一定量级下(通常的在线系统规模都不会特别大),我们可以先不考虑硬件故障以及自动运维。
但为什么很多时候系统还是崩溃呢?罪魁祸首就是请求的尖峰,10倍于平常的压力是很正常的。普通模型到达性能瓶颈后,开始堆积请求(可能在web server,也可能在请求队列,不过通常不会在CDN),吞吐急剧下降,延迟急剧上升,而随着堆积请求越多,情况越糟,引起雪崩效应。而这样的压力通常不会持续很久,如果性能不急剧下降的话,一段时间后其实也就能把请求都响应了。
为10倍压力而准备机器是不合适的,我们需要有办法能扛住瞬间压力。这时候架构设计主要考虑的问题,也就是我上个weibo所说的,如何保证极限情况下的稳定吞吐。有很多种办法,分级队列、请求调度、延迟截断、主动拒绝等等,这些都有助于帮系统度过艰难时刻。具体的做法,就不在这里讨论了。
当然,我也并不是说这个系统是非常难做的,因为可以有很多种做法,如普通文艺、高级文艺等,:)。如何用软件架构的方法来实现scale-up就很困难,做得好与不好可能性能差异能达几倍到一个量级。但对于普通公司来说,直接scale-out是最合适的,规模不大时多一些机器也不会对成本产生太大影响。但关键的是,要注意如何处理峰值压力,提供极限情况下的稳定输出。也就是说对于普通在线系统,即那些transaction-based systems,规模不太大,只是可能有海量的用户请求。只要在设计时注意极限情况下的稳定输出就可以了,实现的工艺水平高不高,差别只是机器多少而已,通常看不出来。
对于大数据量的系统,无论在线或离线(其典型代表就是搜索引擎),则又是另外的故事了。
再谈谈火车票系统

昨天那篇weibo发后,收到很多反馈。有很多朋友给了正面评价,非常感谢。也有些亲给了差评,这本无可厚非,只是其中有些人我感觉都没仔细看文章,时有露怯。也有人怀疑我只会说,没写过程序。我不想自吹,在十几年的程序和研究生涯里,几乎所有名字中含system字样的系统我都亲手做过。到百度做了几个实际系统后,负责了更多事情,这才转为设计规划,不再事必躬亲。
令人反感的是,有人说我拿了黑钱,这纯属无稽之谈。
还有些朋友给了一些非技术层面的建议,也非常感谢。我也知道这些问题所在,也考虑过用产品设计的方法来避免。之所以还来讨论,只不过是想借这个系统谈谈架构设计的一些看法。本人一向喜欢风花雪月,不想凑热闹不好为人师。只是恰好碰上自己的专业领域,这才斗胆来碰这么热门的事件。
我再阐述一下我的观点。这个系统的复杂度在于海量的并发请求,并发性可以通过scale-out(简单来说,就是堆机器)加以解决,但最难的却是保证系统的稳定吞吐。我想说明的是,在线系统应以保证极限情况下的稳定输出(sustained throughput)为首要设计目标,而这是不容易实现的。至于如何切分数据,如何scale-out,这和具体业务特点关系密切,在不知道火车售票具体业务模型的情况下,不好进行讨论。
对于一个理想的在线服务系统,应该包括几方面的能力。1) 单机高性能,也就是所谓的c10k能力。2) 良好的scalability。简单来说,就是加机器可以提升系统性能。3) 稳定输出,特别在极限情况下。4) 良好的自管理自运维能力,在故障、升级或扩容时尽量减少人工介入。
但是,并不是所有的服务都*需要*在以上4个方面做得很好,也不是所有的互联网公司都*有能力*在这4个方面做得很好,甚至很多工程师或架构师都没有*意识*到需要考虑这些方面。
所以,前一篇文章中我的建议是:
a) 可以先不考虑1,因为要提高单机性能难度很大,而效果却并不明显,通过加点机器即可取得同样效果。当然,作为一个old-fashioned engineer,我个人很喜欢做这样的事情。百度的快照库是我到百度后做的第一个系统,单机性能比旧系统提升了10倍以上。
b) 在业务不太复杂的情况下,加机器是最容易提升scalability的方法。我不了解具体的火车票业务模型,不太好讨论,但通常只要能把锁和正常的业务逻辑decouple,问题就不大了。谈到锁,多说两句。用它来保证的同步互斥,是并行系统中最基本的问题。有很多种做法,难度迥异。比如一些lock-free patterns,如果工程师的志向不在于此,不建议尝试。在实际中,按照我的review经验,只要能分解掉那把全局大锁,效果就很好了。
c) 最重要的是sustained throughput。在峰值压力情况下,平滑过渡,可以有效避免雪崩效应,大幅提升用户体验。我提了一些做法,很多朋友说这在通信系统中也是常用的。有兴趣的朋友也可以去研究一下queueing theory,看看latency和throughput是怎样的关系。
d) 在小规模时可以先不考虑自管理自运维能力。随着机群的扩展,故障处理、扩容减容以及服务调度等等逐渐成为最头疼的问题,而这正是分布式系统所要解决的最难的问题。对于大互联网公司来说,应该深有体会。
相对于普通的在线服务,搜索引擎的规模和复杂度要高出一到两个量级,很多考虑问题的出发点也不一样。习惯了这样的思考方式后,我也不太敢直接来设计一个火车票售票系统,over-design就麻烦了,呵呵。所以,以上根据系统的设计原则给出一些建议,希望有用。
三谈火车票系统

[文章有点长,比较沉闷,有违轻松技术的宗旨,先将就看吧]
有点标题党,这回还是没想谈具体的火车票系统方案。我的观点是在没有详细数据、业务流程还有内部系统模型的情况下,直接设计方案容易水土不服。当然,有几个朋友比较有经验,已经给出了方案。方案都不错,挺实用的,想直接解决类似问题的朋友不妨直接参考那些设计。
我希望从普遍意义上说明一个在线系统设计时需要考虑的问题,可能这对有过一定经验的朋友才有用。如果你只是想看看如何写代码解决具体问题,则必然会觉得言之无物,不妨以后再读。如果在技术一途发展,慢慢地你终会感觉到,写些代码或解决某个具体问题并不是最难的。另外,列出一些专业词汇,不是为了卖弄,目的是更清晰定义和准确说明。
开发一个能支持一定用户规模的在线服务并不难,但要做好,必须在业务逻辑和系统架构两方面下功夫。业务逻辑方面需增强的主要是快速开发与功能迭代的支持。根据需求(来自客户或产品经理)实现功能只完成了基础部分,在线服务必然会有大量的功能升级,如何以最小代价支持千奇百怪的升级要求将成为最主要的难题。解决方法则是通过合理的功能抽象,提取出公共组件和通用流程,进行最大化的复用。我们也可以称其为软件架构的可维护性问题,其实这也是传统的企业级开发最重要的问题。
与企业级应用不同的是,互联网应用直接面向大量的用户,需求变化快,开发周期短,而且可以利用用户反馈帮助系统演进。这也引出了互联网应用对技术的要求:软件架构和开发过程支持快速迭代,系统架构支持大规模用户,数据分析驱动迭代。具体而言,
A) 软件架构的可维护性是传统问题,不再赘述。开发过程需要考虑建模、原型开发、测试、部署以及基于数据的持续运营等各个阶段,也就是全流程的支持。现在市面上的App Engine平台主要对原型开发和部署进行支持,其他方面尚待增强。
B) 系统架构,上篇文章说过了,应该包括几方面的能力。1) 单机高性能,也就是所谓的c10k能力。2) 良好的scalability。简单来说,就是加机器可以提升系统性能。3) 稳定输出,特别在极限情况下。4) 良好的自管理自运维能力,在故障、升级或扩容时尽量减少人工介入。
C) 数据分析使得世界变得不同了。相信大的互联网公司们都有体会,当直接服务于用户在线请求的机器数量低于一半时,公司的技术特点变得不同。数据的海量以及实时处理的要求,使得机群规模急剧增大,对系统架构能力的要求也严重提升。其实这也就是云计算的相关技术,按技术特点可以再分为big data和datacenter computing。我在今年2月份《程序员》杂志上有篇文章谈datacenter computing,以后可以再讨论。
对于B中所说的4点,在线系统更看重1和3,而离线部分则是2和4。让我们继续讨论在线部分的1和3。
对于单机的高性能,我的定义是达到硬件瓶颈。比如对于随机读操作,必须达到磁盘的IOPS上限。对于网络IO,如果看throughput,则需要达到带宽极限而与单请求数据大小无关,如果看lantency,则需接近一次RTT。这些极限是很难达到的,不过通常的系统差距尚远。
考虑性能的话,除了正常的代码优化,还需要从multi-threading改为event-driven,换同步思维为异步思维。这里有很多种做法,也有很多概念的细微不同。有兴趣的可以看http://wenku.baidu.com/view/6fa262f67c1cfad6195fa74e.html,这是我以前在公司内部的一个讲座,不知哪位放出去了。PPT比较简单,会引发更多疑问,我们可以再讨论。
 
一般而言,在性能优化上做得好不好,带来的差别是图中普通模型那根线的高点不同,形状差不多,即在超出极限输入后输出急剧下降。如上篇文章所说,不建议普通服务在此花太多功夫,投入产出比不高。我们要追求的是文艺模型的输出曲线,也就是我最强调的sustained throughput能力。而第三种模型是什么,大家不妨想想,:)
由于要scale-out,必然要做数据拆分、功能解耦,一次请求的响应必然由多机完成,此时最大的问题是如何平衡各模块的能力。第一步可做的是,按模块峰值输出调整机器数量,使得全系统的平均吞吐达到机器数量带来的理论值。但是有两个问题:
a) 各模块的峰值输出却是与请求特点相关的,有些请求比其他请求慢数十倍。几个慢请求就将使某些模块输出变低,拖垮全系统。
b) 请求的尖峰也将急剧影响某些模块。综合a的作用,在雪崩效应下,全系统经常在远低于理论性能的情况下垮掉。
而这些问题的产生通常跟简单的multi-threading实现有关,现在的thread都是抢占式调度,保证了公平性,在小规模时还好,大规模时调度效率极低,而且还要预留栈消耗大量内存。解决这个问题,有threadpool或event-driven两种做法。这两种做法都需要自己维护请求队列,也带来了提高稳定性的可能性。简单来说,
c) 增加flow control机制,上游根据下游的负载做throttling,反馈可以有ack/poll等多种做法,有时需要由最下游一路反馈到最前端。可能有人会问,反正请求都在队列中,在上游或下游不都一样吗?这里的问题在于资源使用,在请求响应的不同阶段占用的资源是不同的。我们需要根据反馈,让任务在占用最少资源的阶段挂起。
d) 延迟截断,对于太老的请求,直接给出拒绝响应,让它在应用层重试。
e) 分级队列,某些高优先级任务优先响应。
可能很多人也听说过SEDA结构,这是一种方案,还有很多种做法。上面ppt中的kylin框架也是一种,以后找机会介绍。但最关键的是思维的转变,由同步思维换成异步思维。而当你习惯了异步思维后,其实慢慢也会发现单机系统和多机系统并没有太大差别,从而也将跨入分布式系统的阶段。

发表评论

电子邮件地址不会被公开。 必填项已用*标注