Ruby Web API Server小评测
by robbin
at 2013-05-27 00:44:45
original http://www.udpwork.com/item/9922.html
前几天看到5月份杭州Ruby活动上黄志敏的Topic构建异步的API服务,挺有收获的,对Fiber方式运行Web API Server比较有兴趣。可惜的是作者测试的是单进程并发对比Fiber并发的数据,没有测试多线程对比Fiber并发,所以周末我写了个简单的测试案例,做了一下评测,评测方案有4个,分别是:
-
Goliath有点类似于Rack,但是提供了fiber并发的封装,Grape是类似于Sinatra的Ruby轻量级框架,专门为写API设计的框架。
-
Rainbows是Ruby的多线程服务器,Grape也可以以多线程的方式运行,用来和fiber并发做对比。
-
Sinatra以多线程方式运行在Rainbows上,可以和Grape多线程模式做下对比。
-
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并发性能会更好,实际测试结果也表明了这一点:
- Running状态的并发线程/纤程不太多的情况下,多线程和fiber并发的性能差异很小,不明显。
- Running状态的并发线程/纤程很高的情况下,比方说超过50个running的线程和纤程,性能差异可以明显的看出来,fiber并发CPU的消耗明显低于线程并发10-20%。并发越高,性能差异越大。
运行fiber并发有两个常见的方案:
-
这是Kyle Drake写的一个Sinatra扩展,在支持EventMachine的Ruby Web Server(例如Thin)运行。当Web请求到达的时候,调用rack fiber_pool中间件,创建一个fiber,封装当前执行场景,请求执行完毕,释放fiber。sinatra_synchrony还hack了ruby内置的TCPSocket,所以向外发起HTTP请求,也不会被阻塞,也会让当前fiber释放CPU控制权。
-
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项目,可能涉及到的有:
- 数据库,例如mysql,postgresql,mongodb等等,目前em-synchrony可以良好的支持mysql和mongodb,其他数据库尚不支持。
- 缓存服务器,例如redis和memcached,目前redis-rb可以提供良好的支持,memcached也可以用。
- 向外发起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的方式,可以配置和控制多个进程。
<div style="margin-top:8px;padding:6px 0;border-top:1px solid #3cf">
<div style="text-align:center;margin:16px 0;padding:6px;border:0px dashed #999;font-family:arial;font-size:26px;font-weight:bold">
<a href="http://www.udpwork.com/item/9922.html#review_form" title="不喜欢" style="text-decoration:none">
<img src="http://www.udpwork.com//images/thumb_down24.gif" alt="">
<span style="color:#f33">0</span>
</a>
<a href="http://www.udpwork.com/item/9922.html#review_form" title="喜欢" style="text-decoration:none">
<img src="http://www.udpwork.com//images/thumb_up24.gif" alt="">
<span style="color:#3c3">0</span>
</a>