MRI Ruby的并发请求

我把一个简单的例子放在一起,尝试使用一个基本的例子来certificateRails中的并发请求。 请注意,我使用的是MRI Ruby2和Rails 4.2。

def api_call sleep(10) render :json => "done" end 

然后我在我的Mac(I7 / 4 Core)上的Chrome中找到4个不同的选项卡,看看它们是以串行还是并行方式运行(真正并发的是关闭但不是同一个东西)。 即, http:// localhost:3000 / api_call

我不能使用Puma,Thin或Unicorn来使用它。 每个请求都是串联的。 10秒后的第一个标签,20个之后的第二个标签(因为它必须等待第一个完成),之后是第三个….

从我所读到的,我相信以下是真实的(请纠正我)并且是我的结果:

  • Unicorn是多进程的,我的例子应该有效(在定义unicorn.rb配置文件中的worker数量之后),但事实并非如此。 我可以看到4名工人开始,但一切都在串联。 我正在使用unicorn-rails gem,使用unicorn -c config / unicorn.rb启动rails,在我的unicorn.rb中我有:

– unicorn.rb

 worker_processes 4 preload_app true timeout 30 listen 3000 after_fork do |server, worker| ActiveRecord::Base.establish_connection end 
  • Thin和Puma是multithreading的(虽然Puma至少有一个’ 集群 ‘模式,你可以使用-w参数启动worker)并且不应该使用MRI Ruby2.0无论如何(在multithreading模式下),因为“有一个全局解释器锁(GIL)确保一次只能运行一个线程“。

所以,

  • 我有一个有效的例子(或使用睡眠错误)?
  • 我上面关于多进程和multithreading(关于MRI Rails 2)的陈述是否正确?
  • 有关为何我无法使用Unicorn(或任何服务器)的任何想法?

我有一个非常类似的问题,但是我不能解决这个问题并且它没有回答我关于使用MRI Ruby的并发请求的所有问题。

Github项目: https : //github.com/afrankel/limitedBandwidth (注意:项目正在考虑的不仅仅是服务器上的多进程/线程问题)

我邀请你阅读Jesse Storimer的系列文章没有人理解GIL这可能会帮助你更好地理解一些MRI内部结构。

我也发现了Ruby的Pragmatic Concurrency ,它有趣。 它有一些同时测试的例子。

编辑:此外我可以推荐文章删除config.threadsafe! 可能与Rails 4无关,但它解释了配置选项,其中一个可用于允许并发。


我们来讨论你的问题的答案。

即使使用Puma,您也可以使用多个线程(使用MRI)。 GIL确保一次只有一个线程是活动的,这是开发人员称为限制性的约束(因为没有真正的并行执行)。 请记住,GIL不保证线程安全。 这并不意味着其他线程没有运行,他们正在等待轮到他们。 它们可以交错(文章可以帮助更好地理解)。

让我澄清一些术语:工人流程,线程。 进程在单独的内存空间中运行,可以为多个线程提供服务。 同一进程的线程在共享内存空间中运行,这是其进程的空间。 对于线程,我们指的是此上下文中的Ruby线程,而不是CPU线程。

关于你的问题的配置和你分享的GitHub仓库,我认为一个合适的配置(我使用Puma)是设置4个工作线程和1到40个线程。 这个想法是一个工人服务一个标签。 每个标签最多可发送10个请求。

让我们开始吧:

我在虚拟机上使用Ubuntu。 首先,我在我的虚拟机设置中启用了4个核心(以及其他一些我认为可能有用的设置)。 我可以在我的机器上validation这一点。 所以我顺其自然。

 Linux command --> lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 69 Stepping: 1 CPU MHz: 2306.141 BogoMIPS: 4612.28 L1d cache: 32K L1d cache: 32K L2d cache: 6144K NUMA node0 CPU(s): 0-3 

我使用了你的共享GitHub项目并略微修改了它。 我创建了一个名为puma.rb的Puma配置文件(将其放在config目录中),其中包含以下内容:

 workers Integer(ENV['WEB_CONCURRENCY'] || 1) threads_count = Integer(ENV['MAX_THREADS'] || 1) threads 1, threads_count preload_app! rackup DefaultRackup port ENV['PORT'] || 3000 environment ENV['RACK_ENV'] || 'development' on_worker_boot do # Worker specific setup for Rails 4.1+ # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot #ActiveRecord::Base.establish_connection end 

默认情况下,Puma以1个worker和1个线程启动。 您可以使用环境变量来修改这些参数。 我这样做了:

 export MAX_THREADS=40 export WEB_CONCURRENCY=4 

要使用此配置启动Puma,我输入了

 bundle exec puma -C config/puma.rb 

在Rails应用程序目录中。

我用四个选项卡打开浏览器来调用应用程序的URL。

第一个请求在15:45:05左右开始,最后一个请求是在15h49:44左右。 这是4分39秒的经过时间。 您还可以在日志文件中以非排序顺序查看请求的ID。 (见下文)

GitHub项目中的每个API调用都会hibernate15秒。 我们有4个4个选项卡,每个选项卡有10个API调用。 这使得最大经过时间为600秒,即10分钟(在严格的串行模式下)。

理论上的理想结果将是并行的,并且经过的时间不会超过15秒,但我根本没想到。 我不确定结果究竟是什么,但我仍然感到非常惊讶(考虑到我在虚拟机上运行并且MRI受到GIL和其他一些因素的限制)。 该测试的经过时间不到最大经过时间的一半(在严格的串行模式下),我们将结果减少到不到一半。

编辑我进一步阅读了围绕每个请求包装互斥锁的Rack :: Lock(上面的第三篇文章)。 我发现选项config.allow_concurrency = true可以节省时间。 一点需要注意的是增加连接池(尽管请求不进行查询,因此必须相应地设置数据库); 最大线程数是一个很好的默认值。 在这种情况下为40。

我用jRuby测试了应用程序,实际经过的时间是2分钟,allow_concurrency = true。

我用MRI测试了应用程序,实际经过的时间是1分47秒,其中allow_concurrency = true。 这对我来说是个大惊喜。 这真让我感到惊讶,因为我预计MRI会慢于JRuby。 它不是。 这使我质疑关于MRI和JRuby之间速度差异的广泛讨论。

现在,观察不同选项卡上的响应“更随机”。 在选项卡1之前完成选项卡3或4,我首先请求。

我想因为你没有竞争条件,测试似乎没问题。 但是,如果在实际应用程序中设置config.allow_concurrency = true,我不确定应用程序的广泛后果。

请随时查看,并让我知道您的读者可能有的任何反馈。 我的机器上仍然有克隆。 如果您有兴趣,请告诉我。

按顺序回答您的问题:

  • 我认为你的例子是有效的结果。 但是,对于并发性,最好使用共享资源进行测试(例如,在第二篇文章中)。
  • 关于你的陈述,如本回答开头所述,MRI是multithreading的,但一次只能由GIL限制为一个活动线程。 这提出了一个问题:使用MRI不是更好的测试更多的进程和更少的线程? 我真的不知道,第一个猜测是没有或没有多大区别。 也许有人可以阐明这一点。
  • 我想你的例子很好。 只是需要一些细微的修改。

附录

日志文件Rails应用程序:

 **config.allow_concurrency = false (by default)** -> Ideally 1 worker per core, each worker servers up to 10 threads. [3045] Puma starting in cluster mode... [3045] * Version 2.11.2 (ruby 2.1.5-p273), codename: Intrepid Squirrel [3045] * Min threads: 1, max threads: 40 [3045] * Environment: development [3045] * Process workers: 4 [3045] * Preloading application [3045] * Listening on tcp://0.0.0.0:3000 [3045] Use Ctrl-C to stop [3045] - Worker 0 (pid: 3075) booted, phase: 0 [3045] - Worker 1 (pid: 3080) booted, phase: 0 [3045] - Worker 2 (pid: 3087) booted, phase: 0 [3045] - Worker 3 (pid: 3098) booted, phase: 0 Started GET "/assets/angular-ui-router/release/angular-ui-router.js?body=1" for 127.0.0.1 at 2015-05-11 15:45:05 +0800 ... ... ... Processing by ApplicationController#api_call as JSON Parameters: {"t"=>"15?id=9"} Completed 200 OK in 15002ms (Views: 0.2ms | ActiveRecord: 0.0ms) [3075] 127.0.0.1 - - [11/May/2015:15:49:44 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 60.0230 

 **config.allow_concurrency = true** -> Ideally 1 worker per core, each worker servers up to 10 threads. [22802] Puma starting in cluster mode... [22802] * Version 2.11.2 (ruby 2.2.0-p0), codename: Intrepid Squirrel [22802] * Min threads: 1, max threads: 40 [22802] * Environment: development [22802] * Process workers: 4 [22802] * Preloading application [22802] * Listening on tcp://0.0.0.0:3000 [22802] Use Ctrl-C to stop [22802] - Worker 0 (pid: 22832) booted, phase: 0 [22802] - Worker 1 (pid: 22835) booted, phase: 0 [22802] - Worker 3 (pid: 22852) booted, phase: 0 [22802] - Worker 2 (pid: 22843) booted, phase: 0 Started GET "/" for 127.0.0.1 at 2015-05-13 17:58:20 +0800 Processing by ApplicationController#index as HTML Rendered application/index.html.erb within layouts/application (3.6ms) Completed 200 OK in 216ms (Views: 200.0ms | ActiveRecord: 0.0ms) [22832] 127.0.0.1 - - [13/May/2015:17:58:20 +0800] "GET / HTTP/1.1" 200 - 0.8190 ... ... ... Completed 200 OK in 15003ms (Views: 0.1ms | ActiveRecord: 0.0ms) [22852] 127.0.0.1 - - [13/May/2015:18:00:07 +0800] "GET /api_call.json?t=15?id=10 HTTP/1.1" 304 - 15.0103 

 **config.allow_concurrency = true (by default)** -> Ideally each thread serves a request. Puma starting in single mode... * Version 2.11.2 (jruby 2.2.2), codename: Intrepid Squirrel * Min threads: 1, max threads: 40 * Environment: development NOTE: ActiveRecord 4.2 is not (yet) fully supported by AR-JDBC, please help us finish 4.2 support - check http://bit.ly/jruby-42 for starters * Listening on tcp://0.0.0.0:3000 Use Ctrl-C to stop Started GET "/" for 127.0.0.1 at 2015-05-13 18:23:04 +0800 Processing by ApplicationController#index as HTML Rendered application/index.html.erb within layouts/application (35.0ms) ... ... ... Completed 200 OK in 15020ms (Views: 0.7ms | ActiveRecord: 0.0ms) 127.0.0.1 - - [13/May/2015:18:25:19 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 15.0640 

对于@Elyasin和@Arthur Frankel,我创建了这个回购用于测试在MRI和JRuby中运行的Puma。 在这个小项目中,我没有sleep来模仿长时间运行的请求。 正如我在MRI中发现的那样,GIL似乎与常规处理不同,与外部I / O请求更为相似。

我把斐波纳契序列计算放在控制器中。 在我的机器上, fib(39)在JRuby中花了6.x秒,在MRI中花了11秒,这足以显示差异。

我打开了2个浏览器窗口。 我这样做是为了防止浏览器发送到同一域的并发请求的某些限制,而不是在同一浏览器中打开选项卡。 我现在确定细节,但2个不同的浏览器就足以防止这种情况发生。

我测试了瘦+ MRI和Puma + MRI,然后是Puma + JRuby。 结果是:

  1. 瘦+ MRI:当我快速重新加载2个浏览器时,第一个在11秒后完成。 然后第二个请求开始,又花了11秒钟完成。

  2. 我们先来谈谈Puma + JRuby。 当我快速重新加载2个浏览器时,它们似乎几乎在同一秒开始,并在同一秒完成。 两人花了大约6.9秒才完成。 Puma是一个multithreading服务器,JRuby支持multithreading。

  3. 最后Puma + MRI。 在我快速重新加载2个浏览器后,两个浏览器都花了22秒钟完成。 他们几乎在同一秒开始,几乎在同一秒完成。 但两次完成需要两倍的时间。 这正是GIL所做的:在并发线程之间切换,但锁本身可以防止并行发生。

关于我的设置:

  • 服务器全部在Rails生产模式下启动。 在生产模式下, config.cache_classes设置为true ,这意味着config.allow_concurrency = true
  • Puma以最少8个线程和最多8个线程开始。