(读者可以在 http://stomperl.googlecode.com/ 看到本文介绍的名为“Stomperl”的开源项目。)
我对 Erlang 的兴趣是从去年就已经开始了。但是在断断续续的学习过程中,一个问题一直困扰着我:Erlang 究竟能干什么。就像我在 6 月的一篇 blog 里所写的:
什么时候才需要这样“rocks”的进程管理能力?什么时候才需要如此清晰的进程建模?当然高性能服务器需要。但高性能服务器不是谁都会拿着玩玩的。Ruby on Rails 会走红,因为谁都可以拿它做个网站然后做创业梦。那么 Erlang 呢?它带来的联想是什么?
有必要向对 Erlang 感到陌生的读者做一点背景介绍。作为爱立信在多年前发明的编程语言,Erlang 一直主要被应用在电信领域,并且因为它强大的并行程序设计能力而为人称道。但在很长的时间里,Erlang 一直被视为电信行业的专用工具(它的基础库名叫 OTP,那是 Open Telecom Platform 的缩写,由此可见 Erlang 与电信领域的渊源)。直到前两年,像我这样从事企业应用开发的程序员还是只能通过一篇篇零散而枯燥的论文来了解 Erlang。但随着多核时代的到来,并行计算突然就成了热门话题,Erlang 于是逐渐步入了大众的视野。先是一些函数式编程的爱好者越来越频繁地在各种场合介绍Erlang,然后以“实用主义”著称的Pragmatic Programmers 出版了《 Programming Erlang 》一书。今年这阵风潮也影响到了国内, Erlang 中文社区的成立和两届CN Erlounge 的召开让我们越来越清晰地感觉到这种被昵称为“二郎”的编程语言发出的热度。我之所以投入时间来学习Erlang,主要就是想知道企业软件开发的社区为何对它产生了如此浓厚的兴趣。
尽管买了那本《 Programming Erlang 》,但因为找不到一个适当的项目来练习,学 Erlang 的进展还是不大。在随后的几个月里,我一直努力保持着对 Erlang 社区的关注度。我尝试了 ErlyWeb,也想过在 Ruby 或者 Python 之类的脚本语言里借鉴Erlang 的好处,还在 Erlang 文档计划翻译了两篇论文。直到最近,偶然在一个讨论中听说 Twitter 为提高性能而用到了 Erlang 实现的消息中间件,于是就萌发出动手做一个消息中间件的念头。把几种主要的协议看过一遍之后,我把目标锁定在 Stomp 上,原因就是它足够简单。正如 Stomp 网站上介绍的:
Stomp 是一个非常简单而容易实现的协议……服务器端要做得好可能还不太容易,但要做一个客户端是非常轻松的,你甚至可以用 Telnet 登录到 Stomp broker 并与其交互!
目标确定了,然后就要一步步向前走。我搜索到了 erlstomp 这个项目,但其中的参与者们讨论了好几个月却还没开始写一行代码。不过 Kurt 的一个计划给我指出了方向。和别的通信协议一样,Stomp 无非是规定了客户端和服务器之间对话的语义。(下图描绘了一个经过简化的、典型的 Stomp 通信序列。)先让双方能对话,然后逐渐完善语义,这就是我要做的两件事。
又经过半天的搜索,我发现了一篇题为“采用OTP 原则构造非阻塞的TCP 服务器”的文章和另一篇日语文章,它们都描述了如何搭建一个基本的TCP 服务器架构,并实现了一个最简单的通信协议:echo(客户端发送什么,服务器端就原样发回什么)。抄袭了这个基础,剩下的事情就相对清晰了,无非是一点点把我想要的协议实现出来。同时我还加上了 EUnit 单元测试,用 Gozirra (一个 Java 实现的 Stomp 客户端)创建了验收测试。一个星期业余时间的忙碌之后,这个 broker 已经可以跟验收测试的客户端通畅对话了。于是我给它起了个名字叫“ Stomperl ”(看得出来这是个丝毫不费脑子的名字),发布了0.0.1 版本。可以看到,此时Stomperl 的架构基本上还是第一天抄袭来的那个样子:
+----------------+<br></br> | tcp_server_sup |<br></br> +--------+-------+<br></br> | (one_for_one)<br></br> +----------------+---------+<br></br> | |<br></br> +-------+------+ +-------+--------+<br></br> | tcp_acceptor | + tcp_client_sup |<br></br> +--------------+ +-------+--------+<br></br> | (simple_one_for_one)<br></br> +-----|---------+<br></br> +-------|--------+|<br></br> +--------+-------+|+<br></br> | tcp_stomp |+<br></br> +--------+-------+<br></br> | (one for each)<br></br> +-------+--------+<br></br> | mailer |<br></br> +-------+--------+
实现 Stomp 协议的过程轻松得出人意料。一旦习惯了用 Erlang 的方式来看待手中的多个进程,一些在 Java 或者 Ruby 里会相当棘手的并发编程问题就成了小菜一碟,我能够像操作对象一样轻松地操作多个进程,而且还能够清晰地描述它们(前面的架构图中所有的矩形框都是进程)。而且 OTP 的 supervisor 机制实在是一大惊喜:负责处理的tcp_stomp
进程如果发生任何异常而崩溃,只会导致与其对应的一个客户端通信失败,作为 supervisor 的tcp_client_sup
进程会帮它处理好一切后事,不会让影响波及整个服务器。让我最费脑筋的反而是另一个看似简单的问题:信息放在什么地方最合理。在一篇 blog 里我把保存信息的方式分为参数传递、进程字典、ETS、DETS 和 Mnesia 五种,并在最后总结道:
应该首先考虑靠前的手段,如果有明确的理由表明一种手段不能满足需要时才可以考虑比较靠后的手段。这很费脑子,有时让人沮丧。但经过深思熟虑的程序好过不假思索的程序,发现自己犯错好过犯错而不自知。
在 Stomperl 里,每个 tcp_stomp 进程(以及与之对应的 mailer 进程)负责处理一个客户端的 socket 连接。在这些进程之间有两项信息需要共享:
- 订阅信息,所有进程都需要知道哪个客户端订阅了哪个消息主题(或者队列);
- 消息本身需要从接收端传递到发送端。
一开始我曾经考虑用 DETS 来保存订阅信息,但经过认真考虑之后发现,在这种情况下使用 DETS 会造成较大的开销(每次消息传递都需要读写文件),而目前 Stomperl 并不支持多节点分布式的部署,因此 DETS 不带来额外的好处。于是我转而使用了 ETS:在整个系统的“顶端”(即 tcp_server_sup 进程启动时)创建一个 ETS 表,逐次传递给所有子进程,在这张表里存放所有需要在进程间共享的信息。做这个重构花了一些时间,因为几乎所有的函数都需要加上一个参数来传递 ETS 表。但重构完成以后,“消息队列如何实现”的问题也迎刃而解了:直接用这个 ETS 表来实现消息队列,所有进程都能到其中查找自己需要的消息。
刚开始做 Stomperl 的时候我对消息中间件几乎一无所知,“消息发送失败时该做什么”这个问题曾经让我困扰了两三天:如果一条消息没有人接收,它应该被保存起来吗?如果一个订阅者接收成功而另一个接收失败,还应该重发吗?直到与 Stomp 协议的维护者们进行一次讨论之后,才澄清了我的疑惑:
你究竟是采用“消息队列”模式还是“发布 / 订阅”模式?或者说,消息的目标是队列(queue)还是主题(topic)?如果是队列,那么 broker 只把消息发给第一个合适的订阅者,一旦成功就不会尝试发给第二个订阅者。队列里的消息只发送一次、给一个订阅者。如果是主题,那么 broker 会把消息发布给两个订阅者,如果其中之一失败了,它也不会尝试重发,因为按照定义主题就只是把消息发布给所有订阅者,不管这些订阅者是否真的使用(甚至收到)这些消息。
我在 0.0.1 版本中的实现方式实际上是一个“发布 / 订阅”模式。经过一番研究以后我发现大多数消息中间件至少支持消息队列,其中一些同时也支持“发布 / 订阅”模式,于是我又花了一些时间来实现消息队列。还好,得益于 Erlang 强大的进程建模能力,这一步的难度并不大。
除了可以用来练习 Erlang 编程之外,Stomperl 还有什么真实的用途呢?实际上这个问题主要归结为“Stomp 有什么真实的用途”。Stomp 的主要优势还是简单。由于简单,互联网应用、甚至是某些特殊的桌面应用都可以考虑在其中引入这样一个消息总线,从而给应用的各个部分解耦。譬如说 Selenium 或者 Eft 之类的功能测试工具都可能把 TestRunner 和 TestEngine 两部分拆开,后者把用户的输入(可能是某种 DSL)发送给前者去执行,这时就可以考虑引入一个 Stomp broker 来给两部分牵线搭桥。而且借助 StompConnect 可以把任何 JMS broker 包装成 Stomp broker,这也就使得编写 Stomp 客户端程序的工作不大可能被浪费——因为几乎所有重要的消息中间件都支持 JMS。
那么 Stomperl 呢?正如我在项目主页上写的,既然用了 Erlang,那么高效率、可伸缩性、可靠性和并行程序设计的优雅都是题中应有之义。截至今天,我已经准备好发布 0.0.2 版本,这个版本同时支持消息队列和“发布 / 订阅”两种消息模式,并且完整支持了 Stomp 协议中描述的所有消息格式,而代码规模还不到 1000 行。随后我还会为它做更多的兼容性测试和性能测试,并努力为它找到一个实际应用的场景。如果你恰好有一个项目需要一个简单的消息中间件,不妨考虑一下 Stomperl,或许它也会给你一个惊喜。
评论