Sinatra Ruby Web API Server 小评测

robbin · 2013年05月27日 · 最后由 dingyiming 回复于 2015年12月10日 · 9731 次阅读
本帖已被管理员设置为精华贴

首发于自己的网站:Ruby Web API Server 小评测

前几天看到 5 月份杭州 Ruby 活动上黄志敏的 Topic 构建异步的 API 服务 ,挺有收获的,对 Fiber 方式运行 Web API Server 比较有兴趣。可惜的是作者测试的是单进程并发对比 Fiber 并发的数据,没有测试多线程对比 Fiber 并发,所以周末我写了个简单的测试案例,做了一下评测,评测方案有 4 个,分别是:

  • grape_on_goliath

    Goliath 有点类似于 Rack,但是提供了 fiber 并发的封装,Grape 是类似于 Sinatra 的 Ruby 轻量级框架,专门为写 API 设计的框架。

  • grape_on_rainbows

    Rainbows 是 Ruby 的多线程服务器,Grape 也可以以多线程的方式运行,用来和 fiber 并发做对比。

  • sinatra_on_rainbows

    Sinatra 以多线程方式运行在 Rainbows 上,可以和 Grape 多线程模式做下对比。

  • sinatra_on_thin

    sinatra_synchrony可以让 Sinatra 框架以 fiber 并发的方式运行在 Thin 上,这样测试可以用来对比多线程 Sinatra 并发性能,以及对比 Grape 的 fiber 并发。

对于 IO 并发来说,无论是多线程并发,还是 fiber 并发,只有当 IO 操作占比例比较高的时候,并发的优势才能体现出来,而测试案例访问的数据库非常简单,数据量又太少,所以我在测试里面设置一个 WAIT_TIME 参数,用来控制访问数据库的时长,最多设置到 500ms,模拟真实环境的数据库 IO 比例。

在我的 MacbookPro 上做的测试结果在:ruby_framework_bench ,大家有兴趣和耐心,可以自己测试一下。测试原始数据比较多,我也懒得一一整理了,直接上结论吧:

Fiber vs Multi-thread

fiber 并发和多线程并发的原理其实差不多,都是当前执行线程 (纤程) 在执行到外部 IO 调用的时候,放弃 CPU 控制权,让另一个线程 (纤程) 来获取 CPU。主要差异在于 fiber 并发只占用 1 个操作系统线程,由应用程序来调度纤程;而多线程并发占用 n 个操作系统线程,由 Ruby VM 来调度线程。

因此两者的性能差异主要是调度方式带来的:纤程的场景切换非常轻量级,而多线程的场景切换代价高于纤程,因此理论上来说 fiber 并发性能会更好,实际测试结果也表明了这一点:

  1. Running 状态的并发线程/纤程不太多的情况下,多线程和 fiber 并发的性能差异很小,不明显。
  2. Running 状态的并发线程/纤程很高的情况下,比方说超过 50 个 running 的线程和纤程,性能差异可以明显的看出来,fiber 并发 CPU 的消耗明显低于线程并发 10-20%。并发越高,性能差异越大。

运行 fiber 并发有两个常见的方案:

  1. sinatra_synchrony

    这是 Kyle Drake 写的一个 Sinatra 扩展,在支持 EventMachine 的 Ruby Web Server(例如 Thin) 运行。当 Web 请求到达的时候,调用 rack fiber_pool 中间件,创建一个 fiber,封装当前执行场景,请求执行完毕,释放 fiber。sinatra_synchrony 还 hack 了 ruby 内置的 TCPSocket,所以向外发起 HTTP 请求,也不会被阻塞,也会让当前 fiber 释放 CPU 控制权。

  2. goliath

    Goliath 是一个底层的框架,相当于实现了一个 fiber 并发版本的 Rack 框架,简单的项目,可以直接用 Goliath 自己的 API 写,也可以使用 Grape 在 Goliath 上面运行。Sinatra 做一些额外的处理也可以跑在 Goliath 上,但没有用 sinatra_synchrony 更加方便。

在我的测试当中,sinatra_on_thin 和 grape_on_goliath 没有表现出明显的性能差异,sinatra_on_thin 性能表现的稍微好一点点。

Sinatra vs Grape

sinatra 和 grape 都是 Ruby 的轻量级框架,在同样跑 Rainbows 多线程的评测对比下,也没有表现出明显的性能差异,两者主要区别还是在功能方面:

  • Grape 是一个纯 API 框架,提供了非常多写 json/xml API 很方便的设施,但不提供任何 view 模板功能
  • Sinatra 是一个通用的框架,提供了主流的各种 view 模板功能,但写纯 json/xml API,没有 Grape 方便

所以如果写纯 json/xml API 的话,用 Grape 更方便;如果需要模板渲染,那么就用 Sinatra。

Sinatra on Thin 的配置

用 Sinatra 写 Web 项目,如果应用的 IO 并发请求非常高,那么用 Thin 跑 fiber 并发,无疑是一个非常好的选择,我是强烈推荐用 sinatra_synchrony 的。但是 fiber 并发对驱动和 IO 库的兼容性要求非常高,目前只有很少的驱动和库能够良好的支持 fiber 并发,em-synchrony提供了常用的 fiber 并发 IO 支持库,我们开发 web 项目,可能涉及到的有:

  1. 数据库,例如 mysql,postgresql,mongodb 等等,目前 em-synchrony 可以良好的支持 mysql 和 mongodb,其他数据库尚不支持。
  2. 缓存服务器,例如 redis 和 memcached,目前 redis-rb 可以提供良好的支持,memcached 也可以用。
  3. 向外发起 HTTP 请求,例如调用其他外部服务,目前 sinatra-synchrony 内置支持,faraday 也提供了良好的支持。

我写了一个简单的示例:sinatra_synchrony_template ,相比标准的 sinatra 项目,fiber 并发需要修改如下配置:

Gemfile里面相关配置

gem 'sinatra-synchrony', :require => 'sinatra/synchrony' gem 'em-synchrony', :require => ['em-synchrony', 'em-synchrony/mysql2', 'em-synchrony/activerecord']

数据库的配置文件database.yml需要如下修改:

adapter: em_mysql2

如果需要使用 redis 做缓存,配置如下application.rb

CACHE = EventMachine::Synchrony::ConnectionPool.new(size: 100) do ActiveSupport::Cache.lookup_store :redis_store, { :host => "localhost", :port => "6379", :driver => :synchrony, :expires_in => 1.week } end

每个 fiber 会分配一个 redis 连接。

如果需要使用 memcached 做缓存也可以,memcached 的 ruby client:dalli 并不原生支持 fiber,所以当一个 fiber 访问 memcached 的时候,fiber 并不会放弃 CPU 控制权。不过因为 memcached 访问速度非常快,一般只有 0.1ms 左右,所以并不会造成 fiber 堵塞的问题,可以直接配置:

CACHE = ActiveSupport::Cache::DalliStore.new("127.0.0.1")

如果使用 faraday 访问外部服务,配置如下:

conn = Faraday.new(:url => 'http://www.facebook.com') do |faraday| faraday.request :url_encoded # form-encode POST params faraday.response :logger # log requests to STDOUT faraday.adapter :em_synchrony # fiber aware http client end

当访问外部服务的时候,当前 fiber 就会放弃 CPU 控制权。

最后提醒一点:无论多线程还是 fiber 并发,都只能利用单颗 CPU 内核,在多核服务器上,应该启动多个进程,有几个 CPU 内核,就启动几个进程,这样可以充分利用服务器的 CPU 资源。目前无论是支持多线程的 Rainbows,还是支持 fiber 并发的 Thin,都内置了 cluster 的方式,可以配置和控制多个进程。

最后一段加黑加亮,太重要了:)

赞,用多线程的话基本不需要对代码进行改动,不过需要注意线程安全的问题。

@ashchan 最后一段,如果用 jruby 的话,多线程是可以利用多核的,所以一个 server 跑一个 java 进程就可以了,不过切换到 jruby 也是坑比较多。多进程的话我比较喜欢用外部的管理工具,比如 monit 或 god 来管理,比较灵活

还有一个问题,没有在 sinatra-synchrony 下跑起来 newrelic,等待解决

@robbin 如果一个 fiber 的运行很密集并且执行时间很长,对同一时间其他的请求会有多大影响呢?

#5 楼 @cgyy 和执行时间长短无关,只要有 IO 操作,就会放弃 CPU 控制权。如果是纯 CPU 运算的话,那就和单进程没区别了,IO 比例越高,fiber 并发性能越好。

不错,学习了,顺路 mark 一下。

还是那句话,在 ruby 这么小众的社区里,很所方面都走在最前沿。

@robbin 再请问一下,如果不用 fiber,因为程序是用 thin 部署的,所以直接在引用里面调 event_machine 的原生 API,效率会不会比用 fiber 更快呢? 比如这个 https://github.com/raggi/async_sinatra

#9 楼 @cgyy 用 em 写,会比 fiber 更快,但是 em 那种回调写法和 node.js 一样难用,而且库和第三方项目兼容性问题也很大。

@robbin 明白了,以前没接触过 fiber,粗浅的看了一下。我的理解是它只是一个语法糖,和 thread 不是一个层面上的东西啊

@cgyy 不是,Fiber 是 Native 实现的,不是语法糖,运行意义而言,和 Thread 是一个层面的

@robbin 我试了一下 rainbow 的线程模型,如果 db 的时间只占很少的话,跑起来比 unicorn 的单进程要慢许多呢

#13 楼 @flyerhzm IO 占的比例很低的话,我测试下来,对比 unicorn 的确会稍慢一点。

但是之前大量实际应用,多进程比较大的问题就是很容易遭遇高并发的 DOS 攻击,不得不使用 IP 防火墙的方式避免出现进程被阻塞的情况。用多线程,这方面的问题就好很多了。

谢谢学习了
lgn21st Unicorn fork 的多个进程之间数据是否可以共享 提及了此话题。 04月25日 09:19
需要 登录 后方可回复, 如果你还没有账号请 注册新账号