解剖 Twitter【7】作为一种进步的不彻底
不彻底的工作方式,对于架构设计是一种进步。
当一个来自浏览器的用户请求到达 Twitter 后台系统的时候,第一个迎接它的,是 Apache Web Server。第二个出场的,是 Mongrel Rails Server。Mongrel 既负责处理上传的请求,也负责处理下载的请求。Mongrel 处理上传和下载的业务逻辑非常简洁,但是简洁的表象之下,却蕴含着反常规的设计。这种反常规的设计,当然不是疏忽的结果,事实上,这正是 Twitter 架构中,最值得注意的亮点。
Figure 9. Twitter internal flows
Courtesy http://farm3.static.flickr.com/2766/4095392354_66bd4bcc30_o.png
所谓上传,是指用户写了一个新短信,上传给 Twitter 以便发表。而下载,是指 Twitter 更新读者的主页,添加最新发表的短信。 Twitter 下载的方式,不是读者主动发出请求的 pull 的方式,而是 Twitter 服务器主动把新内容 push 给读者的方式。先看上传,Mongrel 处理上传的逻辑很简洁,分两步。
1. 当 Mongrel 收到新短信后,分配一个新的短信 ID。然后把新短信的 ID,连同作者 ID,缓存进 Vector MemCached 服务器。接着,把短信 ID 以及正文,缓存进 Row MemCached 服务器。这两个缓存的内容,由 Vector MemCached 与 Row MemCached 在适当的时候,自动存放进 MySQL 数据库中去。
2. Mongrel 在 Kestrel 消息队列服务器中,寻找每一个读者及作者的消息队列,如果没有,就创建新的队列。接着,Mongrel 把新短信的 ID,逐个放进 “追” 这位作者的所有在线读者的队列,以及作者本人的队列。
品味一下这两个步骤,感觉是 Mongrel 的工作不彻底。一,把短信及其相关 IDs,缓存进 Vector MemCached 和 Row Cached 就万事大吉,而不直接负责把这些内容存入 MySQL 数据库。二,把短信 ID 扔进 Kestrel 消息队列,就宣告上传任务结束。Mongrel 没有用任何方式去通知作者,他的短信已经被上传。也不管读者是否能读到新发表的短信。
为什么 Twitter 采取了这种反常规的不彻底的工作方式?回答这个问题以前,不妨先看一看 Mongrel 处理下载的逻辑。把上传与下载两段逻辑联系起来,对比一下,有助于理解。Mongrel 下载的逻辑也很简单,也分两步。
1. 分别从作者和读者的 Kestrel 消息队列中,获得新短信的 ID。
2. 从 Row MemCached 缓存器那里获得短信正文。以及从 Page MemCached 那里获得读者以及作者的主页,更新这些主页,也就是添加上新的短信的正文。然后通过 Apache,push 给读者和作者。
对照 Mongrel 处理上传和下载的两段逻辑,不难发现每段逻辑都 “不彻底”,合在一起才形成一个完整的流程。所谓不彻底的工作方式,反映了 Twitter 架构设计的两个 “分” 的理念。一,把一个完整的业务流程,分割成几段相对独立的工作,每一个工作由同一台机器中不同的进程负责,甚至由不同的机器负责。二,把多个机器之间的协作,细化为数据与控制指令的传递,强调数据流与控制流的分离。
分割业务流程的做法,并不是 Twitter 的首创。事实上,三段论的架构,宗旨也是分割流程。Web Server 负责 HTTP 的解析,Application Server 负责业务逻辑,Database 负责数据存储。遵从这一宗旨,Application Server 的业务逻辑也可以进一步分割。
1996 年,发明 TCL 语言的前伯克利大学教授 John Ousterhout,在 Usenix 大会上做了一个主题演讲,题目是 “为什么在多数情况下,多线程是一个糟糕的设计 [36]”。2003 年,同为伯克利大学教授的 Eric Brewer 及其学生们,发表了一篇题为 “为什么对于高并发服务器来说,事件驱动是一个糟糕的设计 [37]”。这两个伯克利大学的同事,同室操戈,他们在争论什么?
所谓多线程,简单讲就是由一根线程,从头到尾地负责一个完整的业务流程。打个比方,就像修车行的师傅每个人负责修理一辆车。而所谓事件驱动,指的是把一个完整的业务流程,分割成几个独立工作,每个工作由一个或者几个线程负责。打个比方,就像汽车制造厂里的流水线,有多个工位组成,每个工位由一位或者几位工人负责。
很显然,Twitter 的做法,属于事件驱动一派。事件驱动的好处在于动态调用资源。当某一个工作的负担繁重,成为整个流程中的瓶颈的时候,事件驱动的架构可以很方便地调集更多资源,来化解压力。对于单个机器而言,多线程和事件驱动的两类设计,在性能方面的差异,并不是非常明显。但是对于分布式系统而言,事件驱动的优势发挥得更为淋漓尽致。
Twitter 把业务流程做了两次分割。一,分离了 Mongrel 与 MySQL 数据库,Mongrel 不直接插手 MySQL 数据库的操作,而是委托 MemCached 全权负责。二,分离了上传和下载两段逻辑,两段逻辑之间通过 Kestrel 队列来传递控制指令。
在 John Ousterhout 和 Eric Brewer 两位教授的争论中,并没有明确提出数据流与控制流分离的问题。所谓事件,既包括控制信号,也包括数据本身。考虑到通常数据的尺寸大,传输成本高,而控制信号的尺寸小,传输简便。把数据流与控制流分离,可以进一步提高系统效率。
在 Twitter 系统中,Kestrel 消息队列专门用来传输控制信号,所谓控制信号,实际上就是 IDs。而数据是短信正文,存放在 Row MemCached 中。谁去处理这则短信正文,由 Kestrel 去通知。
Twitter 完成整个业务流程的平均时间是 500ms,甚至能够提高到 200-300ms,说明在 Twitter 分布式系统中,事件驱动的设计是成功。
Kestrel 消息队列,是 Twitter 自行开发的。消息队列的开源实现很多,Twitter 为什么不用现成的免费工具,而去费神自己研发呢?
Reference,
[36] Why threads are a bad idea (for most purposes), 1996.
(http://www.stanford.edu/class/cs240/readings/threads-bad-usenix96.pdf)
[37] Why events are a bad idea (for high-concurrency servers), 2003.
(http://www.cs.berkeley.edu/~brewer/papers/threads-hotos-2003.pdf)