草庐IT

ruby-on-rails - 与 MRI Ruby 的并发请求

coder 2025-04-23 原文

我整理了一个简单的例子,试图使用一个基本的例子来证明 Rails 中的并发请求。请注意,我使用的是 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 配置文件中定义工作人员数量之后),但它没有。我可以看到 4 个 worker 开始,但一切都在串联。我正在使用 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 是多线程的(尽管 Puma 至少有一个“clustered”模式,您可以在其中使用 -w 参数启动工作程序)并且无论如何都不应该(在多线程模式下)使用 MRI Ruby2.0,因为“有一个全局确保一次只能运行一个线程的解释器锁 (GIL)”。

  • 所以,
  • 我有一个有效的例子(或者使用 sleep 是错误的)?
  • 我上面关于多进程和多线程(关于 MRI Rails 2)的陈述是否正确?
  • 关于为什么我不能让它与 Unicorn(或任何服务器)一起工作的任何想法?

  • 有一个很similar question to mine但我无法让它按答案工作,并且它没有回答我关于使用 MRI Ruby 的并发请求的所有问题。

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

    最佳答案

    我邀请您阅读 Jesse Storimer 的系列 Nobody understands the GIL
    它可能会帮助您更好地了解一些 MRI 内部结构。

    我也找到了Pragmatic Concurrency with Ruby ,读起来很有趣。它有一些同时测试的例子。

    编辑:
    另外我可以推荐文章Removing config.threadsafe!
    可能与 Rails 4 无关,但它解释了配置选项,您可以使用其中之一来允许并发。

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

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

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

    关于你问题的配置和你分享的GitHub repo,我认为一个合适的配置(我用的是Puma)是设置4个worker和1到40个线程。这个想法是一名 worker 提供一张标签。每个选项卡最多发送 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 个线程启动。您可以使用环境变量来修改这些参数。我这样做了:
    export MAX_THREADS=40
    export WEB_CONCURRENCY=4
    

    要使用此配置启动 Puma,我输入
    bundle exec puma -C config/puma.rb
    

    在 Rails 应用程序目录中。

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

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

    GitHub 项目中的每个 API 调用都会休眠 15 秒。我们有四个 4 个选项卡,每个选项卡有 10 个 API 调用。这使得最长耗时为 600 秒,即 10 分钟(在严格的串行模式下)。

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

    EDIT I read further about the Rack::Lock that wraps a mutex around each request (Third article above). I found the option config.allow_concurrency = true to be a time saver. A little caveat was to increase the connection pool (though the request do no query the database had to be set accordingly); the number of maximum threads is a good default. 40 in this case.

    I tested the app with jRuby and the actual elapsed time was 2mins, with allow_concurrency=true.

    I tested the app with MRI and the actual elapsed time was 1min47s, with allow_concurrency=true. This was a big surprise to me. This really surprised me, because I expected MRI to be slower than JRuby. It was not. This makes me questioning the widespread discussion about the speed differences between MRI and JRuby.

    Watching the responses on the different tabs are "more random" now. It happens that tab 3 or 4 completes before tab 1, which I requested first.

    I think because you don't have race conditions the test seems to be OK. However, I am not sure about the application wide consequences if you set config.allow_concurrency=true in a real world application.



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

    要按顺序回答您的问题:
  • 我认为你的例子是有效的结果。然而,对于并发,最好使用共享资源进行测试(例如在第二篇文章中)。
  • 关于你的陈述,如本文开头所述
    回答,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
    

    关于ruby-on-rails - 与 MRI Ruby 的并发请求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29955290/

    有关ruby-on-rails - 与 MRI Ruby 的并发请求的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. Ruby 解析字符串 - 2

      我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?

    4. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

      我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

    5. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    6. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

      很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

    7. ruby-on-rails - rails : keeping DRY with ActiveRecord models that share similar complex attributes - 2

      这似乎应该有一个直截了当的答案,但在Google上花了很多时间,所以我找不到它。这可能是缺少正确关键字的情况。在我的RoR应用程序中,我有几个模型共享一种特定类型的字符串属性,该属性具有特殊验证和其他功能。我能想到的最接近的类似示例是表示URL的字符串。这会导致模型中出现大量重复(甚至单元测试中会出现更多重复),但我不确定如何让它更DRY。我能想到几个可能的方向...按照“validates_url_format_of”插件,但这只会让验证干给这个特殊的字符串它自己的模型,但这看起来很像重溶液为这个特殊的字符串创建一个ruby​​类,但是我如何得到ActiveRecord关联这个类模型

    8. ruby - 在 Ruby 中使用匿名模块 - 2

      假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

    9. ruby - 其他文件中的 Rake 任务 - 2

      我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

    10. ruby - 如何在 Ruby 中顺序创建 PI - 2

      出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

    随机推荐