Ruby Web 服务器:这十五年
坦率的说,作为一门年轻的计算机语言,Ruby 在最近二十年里的发展并不算慢。但如果与坐拥豪门的明星语言们相比,Ruby 就颇显平民范儿,表现始终不温不火,批评胜于褒奖,下行多过上扬。但总有一些至少曾经自称过 Rubyist 的程序员们,愉快地实践了这门语言,他们没有丝毫的歧视习惯,总是努力尝试各家之长,以语言表达思想,用基准评判高下,一不小心就影响了整个技术发展的进程。本文谨以 Ruby Web 服务器技术的发展为线索,回顾 Ruby 截至目前最为人所知的 Web 领域中,重要性数一数二的服务器技术的发展历程,试图帮助我们了解过去,预见未来。timeline Ruby Web 服务器发展时间轴
一、随波逐流
长久以来,任何 Web 服务器都具备的两项最重要的功能:一是根据 RFC2616 解析 HTTP/1.1 协议,二是接收、处理并响应客户端的 HTTP 请求。幸运的是 Web 技术的发展并不算太早,使得 Ruby 恰好能赶上这趟顺风车,但在前期也基本上受限于整个业界的进展。像 Apache HTTP Server、Lighttpd 和 Nginx 这些通用型 Web 服务器+合适的 Web 服务器接口即可完成大部分工作,而当时开发者的重心则是放在接口实现上。
cgi.rb
作为 Web 服务器接口的早期标准,CGI 程序在调用过程中,通过环境变量(GET)或 $stdin(POST)传递参数,然后将结果返回至 $stdout,从而完成 Web 服务器和应用程序之间的通信。cgi.rb 是 Ruby 官方的 CGI 协议标准库,发布于 2000 年的 cgi.rb 包含 HTTP 参数获取、Cookie/Session 管理、以及生成 HTML 内容等基本功能。
Web 服务器和 CGI
Web 服务器和 CGI
当支持 CGI 应用的 Web 服务器接到 HTTP 请求时,需要先创建一个 CGI 应用进程,并传入相应的参数,当该请求被返回时再销毁该进程。因此 CGI 原生是单一进程/请求的,特别是每次请求时产生的进程创建/销毁操作消耗了大量系统资源,根本无法满足较高负载的 HTTP 请求。此外,CGI 进程模型还限制了数据库连接池、内存缓存等资源的复用。
对于标准 CGI 应用存在的单一进程问题,各大厂商分别提出了兼容 CGI 协议的解决方案,包括网景的 NSAPI、微软的 ISAPI 和后来的 Apache API(ASAPI)。上述服务器 API 的特点是既支持在服务器进程内运行 CGI 程序,也支持在独立进程中运行 CGI 程序,但通常需要在服务器进程中嵌入一个插件以支持该 API。
Webrick
作为最古老的 Ruby Web 服务器而不仅仅是一个接口,诞生于 2000 年的 Webrick 从 Ruby 1.9.3(2011 年 10 月正式发布)起被正式纳入标准库,成为 Ruby 的默认 Web 服务器 API。Webrick 支持 HTTP/HTTPS、代理服务器、虚拟主机服务器,以及 HTTP 基础认证等 RFC2617 及以外的其它认证算法。同时,一个 Webrick 服务器还能由多个 Webrick 服务器或服务器小程序组合,提供类似虚拟主机或路由等功能:例如处理 CGI 脚本、ERb 页面、Ruby 块以及目录服务等。
Webrick 曾被用于 Rails 核心团队的开发和测试中。但是,Webrick 内置的 HTTP Parser 非常古老,文档缺失,性能低下且不易维护,功能单一且默认只支持单进程模式(但支持多线程,不过在 Rails 中默认关闭了对 Webrick 的多线程支持),根本无法满足产品环境中的并发和日常维护需求。目前一般只用于 Web 应用的本地开发和基准测试。
fcgi.rb
fcgi.rb 是 FastCGI 协议的 Ruby 封装(latest 版底层依赖 libfcgi)。为了与当时的 NSAPI 竞争,FastCGI 协议最初由 Open Market 提出和开发、并应用于自家 Web 服务器,延续了前者采用独立进程处理请求的做法:即维持一个 FastCGI 服务器。当 Web 服务器接收到 HTTP 请求时,请求内容和环境信息被通过 Socket(本地)或 TCP 连接(远程)的方式传递至 FastCGI 服务器进行处理,再通过相反路径返回响应信息。分离进程的好处是 Web 服务器进程和 FastCGI 进程是永远保持的,只有相互之间的连接会被断开,避免了进程管理的开销。
Web 服务器和 FastCGI/SCGI 服务器
进一步,FastCGI 还支持同时响应多个请求。为了尽量减少资源浪费,若干请求可以复用同一个与 Web 服务器之间的连接,且支持扩展至多个 FastCGI 服务器进程。FastCGI 降低了 Web 服务器和应用程序之间的耦合度,进而为解决安全、性能、管理等各方面问题提供新的思路,相比一些嵌入式方案如 mod_perl 和 mod_php 更具灵活性。
由于 FastCGI 协议的开放性,主流 Web 服务器产品基本都实现了各自的 FastCGI 插件,从而导致 FastCGI 方案被广泛使用。fcgi.rb 最早开发于 1998 年,底层包含 C 和 Ruby 两种实现方式,早期曾被广泛应用于 Rails 应用的产品环境。
mod_ruby
mod_ruby 是专门针对 Apache HTTP Server 的 Ruby 扩展插件,支持在 Web 服务器中直接运行 Ruby CGI 代码。由于 mod_ruby 在多个 Apache 进程中只能共享同一个 Ruby 解释器,意味着当同时运行多个 Web 应用(如 Rails)时会发生冲突,存在安全隐患。因此只在一些简单部署环境下被采用,实际上并没有普及。
LiteSpeed API/RubyRunner
LiteSpeed 是由 LiteSpeed Tech 公司最初于 2002 年发布的商用 Web 服务器,特点是与被广泛采用的 Apache Web 服务器的配置文件兼容,但因为采用了事件驱动架构而具有更好的性能。
LiteSpeed API(LSAPI)是 LiteSpeed 专有的服务器 API,LSAPI 具备深度优化的 IPC 协议以提升通信性能。类似其它 Web 服务器,LiteSpeed 支持运行 CGI、FastCGI、以及后来的 Mongrel。同时在 LSAPI 的基础上开发了 Ruby 接口模块,支持运行基于 Ruby 的 Web 应用。此外,LiteSpeed 还提供 RubyRunner 插件,允许采用第三方 Ruby 解释器运行 Ruby 应用,但综合性能不如直接基于 LSAPI Ruby。
由于 LiteSpeed 是收费产品,其普及率并不高,一般会考虑采用 LiteSpeed 作为 Web 服务器的业务场景包括虚拟主机/VPS 提供商、以及相关业务的 cPanel 产品。同时,LiteSpeed 也会被用于一些业务需求比较特殊的场合,例如对 Web 服务器性能要求高,且应用程序及其部署需要兼容 Apache 服务器。LiteSpeed 于 2013 年发布了开源的轻量 Web 服务器——OpenLiteSpeed(GPL v3),移除了商业版本中偏具体业务的功能如 cPanel 等,更倾向于成为通用 Web 服务器。
scgi.rb
scgi.rb 是对 SCGI 协议的纯 Ruby 实现。从原理上来看,SCGI 和 FastCGI 类似,二者的性能并无多大差别。但比起后者复杂的协议内容来说,SCGI 移除了许多非必要的功能,看起来十分简洁,且实现复杂度更低。
Web 服务器和多 FastCGI/SCGI 服务器
与 FastCGI 类似,一个 SCGI 服务器可以动态创建服务器子进程用于处理更多请求(处理完毕将转入睡眠),直至达到配置的子进程上限。当获得 Web 服务器请求时,SCGI 服务器进程会将其转发至子进程,并由子进程运行 CGI 程序处理该请求。此外,SCGI 还能自动销毁退出和崩溃的子进程,具有良好的稳定性。
二、闻名天下
2005 年,David Heinemeier Hansson(DHH)发布了基于 Ruby 的开发框架 Ruby on Rails(Rails),聚光灯第一次聚焦在 Ruby 身上。但是业内普遍对 Web 服务器的方案感到棘手,本地环境 Webrick/产品环境 FastCGI+通用 Web 服务器几乎成了标配,无论是开发、部署或维护都遇到不少困难,一些吃螃蟹的人遂把此视为 Rails 不如 J2EE、PHP 方案的证据。
Mongrel
2006 年,Zed Shaw 发布了划时代的 Mongrel。Mongrel 把自己定位成一个 “应用服务器”,因为其不仅可以运行 Ruby Web 应用,也提供标准的 HTTP 接口,从而使 Mongrel 可以被放置在 Web 代理、Load Balancer 等任意类型的转发器后面,而非像 FastCGI、SCGI 一样通过调用脚本实现 Web 服务器和 CGI 程序的通信。
Mongrel 采用 Ragel 开发 HTTP/1.1 协议的 Ruby parser,而后者是一个高性能有限自动机编译器,支持开发协议/数据 parser、词法分析器和用户输入验证,支持编译成多种主流语言(包括 Ruby)。采用 Regel 也使 parser 具有更好的可移植性。但是,Mongrel 本身不支持任何应用程序框架,而需要由框架自身提供这种支持。
Mongrel Web 服务器
Mongrel 支持多线程运行(但对于当时非线程安全的 Rails 来说,仍然只能采用多进程的方式提高一部分并发能力),曾被 Twitter 作为其第一代 Web 服务器,还启发了 Ryan Dahl 发布于 2009 年的 Node.JS。
但是当 Mongrel 发布后没过多久,Shaw 就与 Rails 社区的核心成员不和(实际上 Shaw 对业界的许多技术和公司都表达过不满),随后就终止了 Mongrel 的开发。进而在其 Parser 的基础上开发了其后续——语言无关的 Web 服务器 Mongrel2(与前续毫无关系)。
尽管 Mongrel 迅速衰落,却成功启发了随后更多优秀 Ruby 应用服务器的诞生,例如后文将介绍的 Thin、Unicorn 和 Puma。
Rack
随着 Web 服务器接口技术的发展,从开始时作为一个 module 嵌入 Web 服务器,到维护独立的应用服务器进程,越来越多的应用服务器产品开始涌现,同时相互之间还产生了差异化以便适应不同的应用场景。但是,由于底层协议和 API 的差别,基于不同的应用服务器开发 Web 产品时,意味着要实现各自的通信接口,从而为 Web 应用开发带来更多工作量。特别是对于类似 Django、Rails 这些被广泛使用的 Web 框架来说,兼容主流应用服务器几乎是必须的。
2003 年,Python 界权威 Phillip J. Eby 发表了 PEP 0333(Python Web Server Gateway Interface v1.0,即 WSGI),提出一种 Web 服务器和应用程序之间的统一接口,该接口封装了包括 CGI、FastCGI、mod_python 等主流方案的 API,使遵循 WSGI 的 Python Web 应用能够直接部署在各类 Web 服务器上。与 Python 的发展轨迹相似,Ruby 界也遇到了类似的挑战,并最终在 2007 年出现了与 WSGI 类似的 Rack。
与 WSGI 最初只作为一份建议不同,Rack 直接提供了模块化的框架实现,并由于良好的设计架构迅速统一了 Ruby Web 服务器和应用程序框架接口。
Rack 被设计成一种中间件 “框架”,接收到的 HTTP 请求会被 rack 放入不同的管线(中间件)进行处理,直到从应用程序获取响应。这种设计通过统一接口,把一般 Web 应用所需的底层依赖,包括 Session 处理、数据库操作、请求处理、渲染视图、路由/调度、以及表单处理等组件以中间件的形式 “放入” rack 的中间件管线中,并在 HTTP 请求/响应发生时依次通过上述管线传递至应用程序,从而实现 Web 应用程序对底层通信依赖的解绑。
Rack 中间件
Rack 中间件
Rack 接口部分包含两类组件:Handler,用于和 Web 服务器通信;Adapter,用于和应用程序通信。截至 Rack 1.6,Rack 内置的 handlers 包括 WEBrick、FCGI、CGI、SCGI、LiteSpeed 以及 Thin,上述 handlers 用以兼容已有的常见应用服务器。而 2008 年后,随着 rack 逐渐成为事实标准,更新的 Ruby Web 服务器几乎都包含 Rack 提供的 handler。包括 Rails、Sinatra、Merb 等等几乎所有主流框架都引入了 Rack Adapters 的支持。
三、百花齐放
Mongrel 和 Rack 的相继诞生,使 Ruby Web 服务器、乃至应用程序框架的发展有了一定意义上可以遵循的标准。Mongrel 后相继派生出 Thin、Unicorn 和 Puma;而 Rack 统一了 Ruby Web 服务器和应用程序框架接口,使应用开发不再需要考虑特定的部署平台。Ruby Web 服务器开始依据特定需求深入发展。
Thin/Goliath
发布于 2009 年的 Thin 沿用了 Mongrel 的 Parser,基于 Rack 和 EventMachine 开发,前者上文已有介绍,EventMachine 是一个 Ruby 编写的、基于 Reactor 模式的轻量级事件驱动 I/O(类似 JBoss Netty、Apache MINA、Python Twisted、Node.js、libevent 和 libev 等)和并发库,使 Thin 能够在面对慢客户端的同时支持高并发请求。
发表自 1995 年的 Reactor 模型的基本原理是采用一个单线程事件循环缓存所有系统事件,当事件发生时,以同步方式将该事件发送至处理模块,处理完成后返回结果。基于 Reactor 模型的 EventMachine 具备异步(非阻塞)I/O 的能力,被广泛用于大部分基于 Ruby 的事件驱动服务器、异步客户端、网络代理以及监控工具中。
Reactor 模型
Reactor 模型
2011 年,社交网络分析商 PostRank 开源了其 Web 服务器 Goliath,与 Thin 相似(都采用了 EventMachine)但又有很大不同,采用新的 HTTP Parser,同时针对异步事件编程中的高复杂度回调函数问题,借助 Ruby1.9+ 的纤程技术实现了线性编码,使程序具备更好的可维护性。Goliath 支持 MRI、JRuby 和 Rubinius 等多平台。在附加功能方面,Goliath 的目标不仅是作为 Web 服务器,更是一个快速构建 WebServices/APIs 的开发框架,但是随着之后 PostRank 被 Google 收购,Goliath 项目也就不再活跃在开源界了。
Unicorn
2009 年,Eric Wong 在 Mongrel 1.1.5 版本的基础上开发了 Unicorn。Unicorn 是一个基于 Unix/类 Unix 操作系统的、面向快客户端、低延迟和高带宽场景的 Rack 服务器,基于上述限制,任何情况下几乎都需要在 Unicorn 和客户端之间设置一个反向代理缓存请求和响应数据,这是 Unicorn 的设计特点所决定的,但也使得 Unicorn 的内部实现相对简洁、可靠。
尽管来源于 Mongrel,但 Unicorn 只在进程级运行,且吸收和利用了一些 Unix/类 Unix 系统内核的特性,如 Prefork 模型。
Unicorn 由 1 个 master 进程和 n 个 fork(2) 子进程组成,子进程分别调用 select(2) 阻塞自己,直到出错或者超时时,才做一些写日志、处理信号以及维护与 master 的心跳链接等内置任务。子进程和 master 间通过一个共享 socket 实现通信,而由 Unix/类 Unix 系统内核自身处理资源调度。
Unicorn 的多进程模型
Unicorn 的多进程模型
Unicorn 的设计理念是 “只专注一件事”:多进程阻塞 I/O 的方式令其无从接受慢客户端——但前置反向代理能解决这一问题;workers 的负载均衡就直接交给操作系统处理。这种理念大大降低了实现复杂度,从而提高了自身可靠性。此外,类似 Nginx 的重加载机制,Unicorn 也支持零宕机重新加载配置文件,使其允许在线部署 Web 应用而不用产生离线成本。
Phusion Passenger(mod_rails/mod_rack)
2008 年初,一位叫赖洪礼的 Ruby 神童发布了 mod_rails。尽管 Mongrel 在当时已经席卷 Rails 的 Web 服务器市场,但是面对部署共享主机或是集群的情况时还是缺少统一有效的解决方案,引起业内一些抱怨,包括 DHH(也许 Shaw 就不认为这是个事儿)。
mod_rails 最初被设计成一个 Apache 的 module,与 FastCGI 的原理类似,但设置起来异常简单——只需要设置一个 RailsBaseURI 匹配转发至 Rails 服务器的 URI 串。mod_rails 服务器会在启动时自动加载 Web 应用程序,然后按需创建子进程,并协调 Web 服务器和 Rails 服务器的通信,从而支持单一服务器同时部署多个应用,还允许按需自动重启应用服务器。
mod_rails 遵循了 Rails 的设计原则,包括 Convention over Configuration、Don’t Repeat Yourself,使其面向部署非常友好,很快得到了业界青睐,并在正式 release 时改名 Passenger。
在随后的发展中,Passenger 逐渐成为独立的 Ruby 应用服务器、支持多平台的 Web 服务器。截至 2015 年 6 月,Phusion Passenger 的版本号已经达到 5.0.10(Raptor),核心采用 C++ 编写,同时支持 Ruby、Python 和 Node.js 应用。支持 Apache、Nginx 和独立 HTTP 模式(推荐采用独立模式),支持 Unix/类 Unix 系统,在统计网站 Builtwith 上排名 Ruby Web 服务器使用率第一。
值得一提的是,Phusion Passenger 的开源版本支持多进程模式,但是其企业版同样支持多线程运行。本文撰写时,Phusion Passenger 是最近一个号称 “史上最快” 的 Ruby Web 服务器(本文最后将进一步介绍 Raptor)。
Trinidad/TorqueBox
Trinidad 发布于 2009 年,基于 JRuby::Rack 和 Apache Tomcat,使 Rails 的部署和世界上最流行的 Web 服务器之一 Tomcat 结合,支持集成 Java 代码、支持多线程的 Resque 和 Delayed::Job 等 Worker,也支持除 Tomcat 以外的其它 Servlet 容器。
与 Trinidad 相比,同样发布于 2009 年的 TorqueBox 不仅仅是一个 Web 服务器,而且被设计成一个可移植的 Ruby 平台。基于 JRuby::Rack 和 WildFly(JBoss AS),支持多线程阻塞 I/O,内置对消息、调度、缓存和后台进程的支持。同时具有集群、负载均衡、高可用等多种附加功能。
Puma
Puma——Mongrel 最年轻的后代于 2011 年发布,作者是 Evan Phoenix。
由于 Mongrel 诞生于前 Rack 时期,而随着 Rack 统一了 Web 服务器接口,任何基于 Rack 的应用再与 Mongrel 配合就有许多不便。Puma 继承了前者的 Parser,并且基于 Rack 重写了底层通信部分。更重要的是,Puma 部分依赖 Ruby 的其它两个流行实现:Rubinius 和 JRuby,与 TorqueBox 类似拥有多线程阻塞 I/O 的能力(MRI 平台不支持真正意义上的多线程,但 Puma 依然具有良好并发能力),支持高并发。同时 Puma 还包含了一个事件 I/O 模块以缓冲 HTTP 请求,以降低慢客户端的影响。但是,从获得更高吞吐量的角度来说,Puma 目前仍然需要采用 Rubinius 和 JRuby 这两个平台。
Reel
Reel 是最初由 Tony Arcieri 发布于 2012 年的采用事件 I/O 的 Web 服务器。采用了不同于 Eventmachine 的 Celluloid::IO,后者基于 Celluloid——Actor 并发模型的 Ruby 实现库,解决了 EM 只能在单一线程中运行事件循环程序的问题,从而同时支持多线程+事件 I/O,在非阻塞 I/O 和多线程方案间实现了良好的融合。
与其它现代 Ruby Web 服务器不同的是,Reel 并不是基于 Rack 创建,但通过 Reel::Rack 提供支持 Rack 的 Adapter。尽管支持 Rails,与 Puma 也有一定的相似性,但与 Unicorn、Puma 和 Raptor 相比,Reel 在部署 Rails/Rack 应用方面缺少易用性。实际上基于 Celluloid 本身的普及程度和擅长领域,相比其它 Web 服务器而言,Reel 更适合部署 WebSocket/Stream 应用。
Yahns
2013 年,Eric Wong 等人受 Kqueue(源自 FreeBSD,同时被 Node.js 作为基础事件 I/O 库)的启发启动了 Yahns 项目。其目标与 Reel 类似,同样是在非阻塞 I/O 设计中引入多线程。与 Reel 不同的是,Yahns 原生支持 Rack/HTTP 应用。
Yahns 被设计成具有良好的伸缩性和轻量化特性,当系统应用访问量较低或为零时,Yahns 本身的资源消耗也会保持在较低水平。此外,yahns 只支持 GNU/Linux(并通过 kqueue 支持 FreeBSD),并声称永远不会支持类似 Unicorn 或 Passenger 里的 Watchdog 技术,不会因为应用崩溃而自动销毁和创建进程/线程,因此对应用程序本身的可靠性有一定要求。
四、迈向未来
回顾过去,Ruby Web 服务器在发展中先后解决了缺少部署方案、与 Web 应用程序不兼容、运维管理困难等问题,基础架构趋于成熟且稳定。而随着更多基准测试结果的出现,业界逐渐开始朝着更高性能和并发量发展,同时针对 HTTP 协议本身的优化和扩展引入的 HTTP/2,以及 HTML5 的 WebSocket/Stream 等需求均成为未来 Ruby Web 服务器发展的方向。
高吞吐量
以最新的 Raptor(上文提到的 Phusion Passenger 5)为例,其在网络 I/O 模型的选择上融合了现有其它优秀产品的方案,包括 Unicorn 的多进程模型、内置基于多线程和事件 I/O 模型的反向代理缓冲(类似 Nginx 的功能,但对 Raptor 自身做了大量裁减和优化)、以及企业版具有的多线程模型(类似 Puma 和 TorqueBox);此外,Raptor 采用的 Node.js HTTP Parser(基于 Nginx 的 Parser)的性能超过了 Mongrel;更进一步,Raptor 甚至实现了 Zero-copy 和一般在大型游戏中使用的区域内存管理技术,使其对 CPU 和内存访问的优化达到了极致(感兴趣的话可以进一步查阅这里)。
Raptor 的优化模型
另外也需要看到,当引入多线程运行方式,现有 Web 应用将不得不仔细检查自身及其依赖,是否是线程安全的,同时这也给构建 Ruby Web 应用带来更多新的挑战。这也是为什么更多人宁愿选择进程级应用服务器的方式——毕竟对大多数应用来说不需要用到太多横向扩展,引入反向代理即可解决慢客户端的问题,而采用 Raptor 甚至在独立模式能工作的更好(这样就不用花时间去学习 Nginx)。
除非你已经开始考虑向支持大规模并发的架构迁移,并希望节省接下来的一大笔花费了。
HTTP/2
2015 年 5 月,HTTP/2 随着 RFC7540 正式发布。如今各主流服务器/浏览器厂商正在逐渐完成从 HTTP/2 测试模块到正式版本的过渡。而截至目前,主流 Ruby Web 服务器都还没有公开 HTTP/2 的开发信息。HTTP-2 是在 2013 年由 Ilya Grigorik 发布的纯 Ruby 的 HTTP/2 协议实现,包括二进制帧的解析与编码、流传输的多路复用和优先级制定、连接和流传输的流量控制、报头压缩与服务器推送、连接和流传输管理等功能。随着 HTTP/2 的发布和普及,主流 Ruby Web 服务器将不可避免的引入对 HTTP/2 的支持。
WebSocket/流(Stream)/服务器推送事件(Server Sent Events,SSE)
2011 年,RFC6455 正式公布了 WebSocket 协议。WebSocket 用于在一个 TCP 链接上实现全双工通信,其目的是实现客户端与服务器之间更高频次的交互,以完成实时互动业务。鉴于该特点,仅支持慢客户端的 Web 服务器就无法有效支撑 WebSocket 的并发需求,更何况后者对并发量更加严苛的要求了。而对于同样需要长连接的流服务器和服务器推送事件服务(SSE),都避免不了对长连接和高并发量的需求。尽管高性能的 Ruby Web 服务器都有足够的潜力完成这些任务,但是从原生设计的角度来看,更加年轻的 Reel 和 Yahns 无疑具有优势。
最近 Planet ruby 在 Ruby 邮件组发布了 Awesome Webservers,该 Github Repo 旨在对主流 Ruby Web 服务器进行总结和对比,并且保持持续更新,推荐开发者关注。