MRI Ruby下的并发请求

26

我制作了一个简单的示例,试图使用基本示例证明在Rails中可以使用并发请求。请注意,我正在使用MRI Ruby2和Rails 4.2。

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

我在我的mac电脑上使用Chrome浏览器打开了4个不同的标签页,并查看它们是否依次运行或并行(实际为并发,但并非完全相同)。例如:http://localhost:3000/api_call

我无法使用Puma、Thin或Unicorn使其起作用。每个请求都是按顺序执行的。第一个标签页要等待10秒后,第二个标签页才会开始(因为它必须等待第一个标签页完成),然后是第三个标签页...

根据我所读到的内容,我相信以下内容是正确的(请纠正我)并且这是我的结果:

  • Unicorn是多进程的,我的例子应该可以正常工作(在unicorn.rb配置文件中定义了工作进程数),但实际并没有。我可以看到4个进程正在启动,但所有内容都是按顺序进行的。我正在使用unicorn-rails gem,在unicorn.rb中用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是多线程的(尽管Puma至少有一个“集群”模式,您可以使用-w参数启动工作进程),因此在多线程模式下不应该与MRI Ruby2.0一起使用,因为“有全局解释器锁(GIL)确保只能同时运行一个线程”。

所以,

  • 我的示例是否有效(或仅使用sleep是错误的)?
  • 关于多处理和多线程(关于MRI Rails 2),我上面的陈述是否正确?
  • 为什么我不能将其与Unicorn(或任何服务器)一起工作?

有一个非常类似于我的问题,但我无法按答案中的方式使其工作,并且它没有回答所有关于使用MRI Ruby进行并发请求的问题。

Github项目:https://github.com/afrankel/limitedBandwidth(注意:该项目正在研究服务器上的多进程/线程问题之外的其他问题)


我无法通过独角兽来重现这个问题 - 一切都按预期工作。 - Anthony
@Anthony - 我编辑了我的帖子并附上了我的独角兽配置。你看到我列出的东西有什么问题吗? - Arthur Frankel
如果你有一个可访问的示例(GitHub?)也许我们可以自己检查它并提供进一步的输入。 - Ely
我创建了一个Github项目,其中包含我的示例:https://github.com/afrankel/limitedBandwith。请注意,我的主要目的是不同的(从AngularJS客户端测试有限带宽情况),但服务器部分是我在本文中讨论的重点。感谢您的关注。 - Arthur Frankel
@ArthurFrankel 我正在查看。我想用我已经找到的一些有用信息编辑我的答案。 在您的反馈后,我们可以进一步改进答案,最终分享我从您的GitHub克隆的修改版本。 - Ely
显示剩余3条评论
2个回答

25

我邀请您阅读Jesse Storimer的系列文章“没有人理解GIL”。这可能有助于更好地理解一些MRI内部机制。

我还发现Ruby并发编程实践很有趣。其中有一些测试并发性的示例。

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


让我们讨论一下您的问题的答案。

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

让我澄清一些术语:工作进程,线程。进程在单独的内存空间中运行,并且可以为多个线程提供服务。同一进程的线程在共享内存空间中运行,即其进程的内存空间。在线程中,我们指的是Ruby线程,而不是CPU线程。

关于您的问题的配置和您分享的GitHub repo,请注意适当的配置(我使用Puma)是设置4个工作进程和1到40个线程。想法是一个工作进程为一个选项卡服务。每个选项卡最多发送10个请求。

所以让我们开始吧:

我在虚拟机上使用Ubuntu。因此,首先我在我的虚拟机设置中启用了4个核心(还有其他设置,我认为它可能有帮助)。我可以在我的计算机上验证这一点。所以我采用了这个设置。

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个thread启动。您可以使用环境变量来修改这些参数。我就这样做了:

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调用休眠15秒。我们有四个选项卡,每个选项卡有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之间的速度差异的广泛讨论产生了疑问。 现在观察不同选项卡的响应更“随机”了。有时候,第3或第4个选项卡会在第1个选项卡之前完成,尽管我先请求了第1个选项卡。 我认为,由于没有竞争条件,测试似乎是正常的。然而,如果您在真实世界应用程序中将config.allow_concurrency=true设置为true,我不确定是否会产生整个应用程序的后果。 请随意查看并让我知道读者可能拥有的任何反馈。我仍然有我的克隆机。如果您有兴趣,请告诉我。 按顺序回答您的问题:
  • 我认为您的示例结果是有效的。然而,在并发方面,最好使用共享资源进行测试(例如第二篇文章所述)。
  • 关于您的声明,正如本答案开头提到的那样,MRI是多线程的,但受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

谢谢!让我好好思考一下,很快会回复您的。 - Arthur Frankel
顺便提一下,这超出了你的问题范围:我将你的示例移植到了jRuby环境中运行。测试用了2分钟(1秒)。 - Ely
@ArthurFrankel 我编辑了回复,并提供了一些新的见解,当使用JRuby和MRI设置allow_concurrency=true时。 - Ely
1
这是很棒的东西!作为建议,也许您可以添加一个表格,其中包含更改工人和线程数量以及启用/禁用allow_concurrency的结果。例如:1个线程/4个工人(因为您有4个核心);10个线程/1个工人;等等。然后,也许将15秒更改为60秒。我注意到在我的测试中,60秒允许工人启动并开始使用。所有建议。此外,如果您想搬到华盛顿特区(弗吉尼亚州北部),您有一份工作! - Arthur Frankel

4
对于 @Elyasin 和 @Arthur Frankel,我创建了这个 repo 来测试 Puma 在 MRI 和 JRuby 中的运行。在这个小项目中,我没有使用 sleep 来模拟长时间运行的请求。我发现在 MRI 中,GIL 处理这个的方式与常规处理不同,更类似于外部 I/O 请求。

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

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

我测试过 thin + MRI,Puma + MRI,然后是 Puma + JRuby。结果如下:

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

  2. 让我们先谈谈Puma + JRuby。当我快速重新加载2个浏览器时,它们似乎几乎同时开始,并在同一秒结束。两者都需要约6.9秒才能完成。Puma是一个多线程服务器,而JRuby支持多线程。

  3. 最后是Puma + MRI。当我快速重新加载2个浏览器时,对于两个浏览器来说,它们都需要22秒才能完成。它们几乎同时开始,也几乎同时结束。但是,对于两者来说,完成所需的时间是原来的两倍。这正是GIL的作用:在并发性的线程之间进行切换,但锁本身防止了并行性的发生。

关于我的设置:

  • 所有服务器都在Rails生产模式下启动。在生产模式下,config.cache_classes被设置为true,这意味着config.allow_concurrency = true
  • Puma以8个线程的最小值和8个线程的最大值启动。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接