首发于自己的网站:Ruby Web API Server 小评测
前几天看到 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 并发和多线程并发的原理其实差不多,都是当前执行线程 (纤程) 在执行到外部 IO 调用的时候,放弃 CPU 控制权,让另一个线程 (纤程) 来获取 CPU。主要差异在于 fiber 并发只占用 1 个操作系统线程,由应用程序来调度纤程;而多线程并发占用 n 个操作系统线程,由 Ruby VM 来调度线程。
因此两者的性能差异主要是调度方式带来的:纤程的场景切换非常轻量级,而多线程的场景切换代价高于纤程,因此理论上来说 fiber 并发性能会更好,实际测试结果也表明了这一点:
运行 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 和 grape 都是 Ruby 的轻量级框架,在同样跑 Rainbows 多线程的评测对比下,也没有表现出明显的性能差异,两者主要区别还是在功能方面:
所以如果写纯 json/xml API 的话,用 Grape 更方便;如果需要模板渲染,那么就用 Sinatra。
用 Sinatra 写 Web 项目,如果应用的 IO 并发请求非常高,那么用 Thin 跑 fiber 并发,无疑是一个非常好的选择,我是强烈推荐用 sinatra_synchrony 的。但是 fiber 并发对驱动和 IO 库的兼容性要求非常高,目前只有很少的驱动和库能够良好的支持 fiber 并发,em-synchrony提供了常用的 fiber 并发 IO 支持库,我们开发 web 项目,可能涉及到的有:
我写了一个简单的示例: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 的方式,可以配置和控制多个进程。