Sinatra Ruby Web API Server 小评测

robbin · 2013年05月27日 · 最后由 dingyiming 回复于 2015年12月10日 · 8443 次阅读
本帖已被设为精华帖!

首发于自己的网站: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的方式,可以配置和控制多个进程。

共收到 15 条回复

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

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

@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防火墙的方式避免出现进程被阻塞的情况。用多线程,这方面的问题就好很多了。

谢谢学习了
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册