InfoQ 中文站报道过一些公司从 Ruby 转移到其他语言的新闻,包括 Iron.io 从 Ruby 迁移到 Go:减少了 28 台服务器并避免了连锁故障、从Ruby 向Java 的迁移帮助Twitter 挺过了美国大选和在LinkedIn 的Ruby on Rails 和Node.js 对决。最近,IT 元老范凯在博客中详细地分析了Rails 目前在Web 服务方面的局限和原因,针对“继续沿用Ruby on rails 重写或者重构应用,性能可能会有一两倍的提升,但无法弥合10 倍以上的性能差距,难道说Ruby 真的如此不堪吗?注定要被Node.js 或者Go 所取代吗?”这个问题给出了自己的观点,即:使用其他Ruby 轻量级框架取代Rails,而不是使用Node.js 和Go 取代Ruby。
针对Ruby 的性能问题,范凯首先做了澄清,存在性能问题的是Rails,而不是Ruby 语言本身,他引用了Ruby 的纤程框架 Goliath 的实际应用数据:
- VPS 上总共使用了 16 个 CPU 内核,跑了 16 个单进程实例
- 每个进程实例稳定消耗 50MB 内存
- Web 框架使用 Goliath, URL 分发是 grape,数据库访问使用 ActiveRecord,缓存使用 Redis
- 应用吞吐量达到了 1800 request/s
这个数据意味着一台配备了 4 颗 4 核 CPU,2G 内存的服务器,每天可以处理 1.5 亿次 web 请求。由此可见,Ruby 完全可以做到高并发 IO 的应用。问题主要不在 Ruby 解释器上,而在 Rails 框架上。更准确的说就是, Ruby on R****ails 作为一个 full-stack 的 web 开发框架,并不适合用来开发 Linkedin 和 Iron.io 的后台 web 服务,从某种意义上来说,属于 Rails 的时代已经过去了。
范凯表示:移动时代,Web 服务将取代 Web 网站。为什么 Rails 不适合 Web 服务呢,他认为主要原因包括:
- Rails 调用堆栈过深,URL 请求处理性能很差。Rails 的设计目标是提供 Web 开发的 最佳实践 ,所以无论你需要不需要,Rails 默认提供了开发 Website 所有可能的组件,但其中绝大部分你可能一辈子都用不上。其中最夸张的是
ActionDispatch::RequestId
middleware,只有在大型应用部署在群集环境下进行线上调试才可能用到的功能,有什么必要做成默认的功能呢? Rails 的哲学是:提供最全的功能集给你,如果你用不到,你自己手工一个一个关闭掉 ,但是这样带来的结果就是默认带了太多不必要的冗余功能,造成性能损耗极大。 - Rails 加载的框架和依赖库过多,内存消耗过度。Rails 自身依赖库非常多,造成的结果就是 Rails 应用持续运行以后内存消耗非常高。举个例子:如果你用到了 Rails 的 asset pipeline 功能,那么项目需要依赖一个 JS 引擎来编译 JS 和 CSS,默认会使用 libv8 这个库。尽管只是编译阶段使用 libv8,运行期并不需要它,但是仍然会加载 libv8,这意味着你的每个 ruby 进程会多占 20MB 内存。在我们其中一个大项目上,总共开了 40 个 Ruby 进程,直接浪费了 800MB 内存。于是我们不得不在生产服务器上安装了 Node.js,替换了 libv8。此外,一旦其中某个依赖库有内存泄露,整个应用也可能出现内存泄露,这种内存泄露是很讨厌的事情,Rails 如此肆无忌惮不加限制的使用第三方依赖库也是一个潜在的隐患。
- Rails 传统多进程模型的 IO 并发能力很低。Rails 的多进程并发模型的 IO 并发能力很低,开多少个进程,就只能同时响应多少个并发请求,但 Ruby 进程的内存消耗是很大的,多进程调度的 CPU 开销也很高,这决定了单台服务器上能开的进程数是非常有限的,一般不会超过 30 个。但是对于 Web Service 类型的应用,需要很高的 IO 并发处理能力,传统 Rails 多进程很容易就会出现负载的瓶颈。从 Rails4.0 开始,默认也开启了多线程模式,也可以支持多线程方式运行 Rails 应用。但就目前来说,Rails 使用多线程,还面临一些兼容性问题:大量的 Rails 插件和代码不是线程安全的,在多线程模型下运行,会出现意想不到的 bug;另外 Rails 的多线程应用尚未得到广泛应用,可能会有潜在的 bug。
不用 Rails,Ruby 社区又能用什么呢?范凯提供了自己的建议:
- Sinatra ——Sinatra 本身也是 Ruby 社区非常流行和著名的轻量级 Web 框架,核心源代码不超过 1000 行,文档只有 1 页。对于 Rails 开发者来说,花了几个小时,就可以快速使用 Sinatra 开发 Web Service 了。Sinatra 对多线程支持的非常好,可以用 rainbows 来跑多线程 Sinatra,IO 并发处理能力很好。Github 也是用它来提供开放 API 服务的。我自己写了一个 Sinatra 的项目模版,如果你用 Sinatra 开发 Web Service,可以参考。
- Padrino ——Padrino 是一个基于 Sinatra 之上的轻量级 Web 框架,在 Sinatra 基础之上提供了命名路由,模块化项目组织,页面 helpers 和 generators 等等。Padrino 是一个高度模仿 Rails 的框架,API 的命名和 Rails 很像,Rails 开发者花 1-2 天看看文档就可以快速上手开发了。Padrino 相比 Rails 易学易用,多线程支持良好,性能比 Rails 好很多,开发 Website 推荐使用。我自己的网站也是用 Padrino 开发的,源代码在: robbin_site
- Goliath ——Goliath 是一个 Ruby 的纤程开发框架,性能非常好,作者本身是在开发 PostRank 产品过程中开发的 Goliath。PostRank 是一个用户社交行为实时跟踪工具,需要很高的性能来支撑,PostRank 被 Google 收购了,作者现在在 Google 工作。Goliath 适合用来开发对性能非常敏感的 Web Service 或者 real-time 的应用,但使用 Goliath 有一些门槛,你不能使用普通的阻塞 IO 库,必须使用作者封装的一些纤程的库。
所谓的“去 Rails 化”,范凯认为不必大惊小怪:
学习 Rails 无非意味着你花了时间熟悉 ActiveRecord 和 ActionPack 以及相关库的功能而已,所谓去 Rails 化也仅仅只是放弃使用 ActionPack,换一个更轻量级更简单的 URL 路由处理器,例如换成 Grape,Sinatra,Padrino 或者 Camping 而已。这对一个长期使用 Rails 的 Ruby 开发者来说,应该是举手之劳的事情。所以自己动手,根据实际应用场景挑选最合适的组件。例如 ActionPack 不太适合写 Web Service,那我换成 Sinatra 就行了,但是 ActiveRecord 照常用,这并不需要你付出多少学习成本,更不需要你放弃什么。
目前有一些公司从 Ruby/Rails 转向 Node.js 和 Go,范凯经过调研,认为 Ruby 更适合自己:
- 用 Sinatra 或者 Goliath 这样的轻量级框架写 Web Service,性能已经足够好了,特别是 @黄志敏的案例证明,16 核已经可以支撑每天 1.5 亿次请求了,对我来说已经不太可能遇到超过这个负载量的应用了。而 Ruby 的开发效率,代码表达能力和可维护性对我来说还是很重要的。
- Node.js 的 Event IO 编程风格在我看来是“反人类”的,极其变态的。用来写代码上规模的应用,代码的可读性和可维护性都很差。Event IO 是很底层的技术,我很难理解为何不封装成 coroutine 来使用。Node.js 只适合用来开发 real-time 类型的应用。
- Go 的主要问题在于现阶段还不成熟:一方面 Go 自身还在演进当中;另一方面 Go 的类库还是过于贫瘠了,用来开发项目还是需要自己写很多东西的,感觉很不方便。
有关范凯完整的论述,可以查看博客原文。欢迎 InfoQ 读者发表自己的看法。
评论