Rails是共享无状态的还是可以分离请求访问相同的运行时变量?

24

PHP运行在共享无状态环境中,这意味着每个网络请求都在一个干净的环境中运行。 除了通过单独的持久层(文件系统、数据库等)之外,您无法访问另一个请求的数据。

那么Ruby on Rails呢?我刚刚读到一篇博客文章说,不同的请求可能会访问相同的类变量。

我想这可能取决于Web服务器。 Mongrel的常见问题解答指出,Mongrel使用一个线程处理一个请求 - 这表明它是一个共享无状态环境。 FAQ继续说RoR不支持线程安全,这进一步表明RoR不会存在于共享环境中,除非新请求重用从上一次请求创建的内存对象。

显然,这对安全性有巨大影响。所以我有两个问题:

  1. RoR的环境是否为共享无状态?
  2. 如果RoR在某些情况下运行在共享环境中(或可能),我应该对哪些变量和其他数据存储感到担心?

更新:我将进一步澄清。在Java Servlet容器中,您可以拥有跨多个请求持续存在的对象。 这通常是为缓存多个用户都可以访问的数据、数据库连接等而做的。 在PHP中,无法在应用程序层面完成这一点,必须在单独的持久性层(如Memcached)中完成。 因此,这两个问题是:RoR类似于哪种情况(PHP或Java),如果像Java,哪些数据类型会持续存在于多个请求之间?

4个回答

36

简而言之:

  1. Rails永远不会在共享无关环境下运行。
  2. 对于类变量和类实例变量要保持警惕。

详解:

Rails进程通过加载框架和应用程序启动其生命周期。它们通常仅运行单个线程,在其生命周期内处理许多请求。因此,请求将严格以顺序方式分派。

然而,所有类都会跨请求持久化存在。这意味着从您的类和元类引用的任何对象(如类变量和类实例变量)都将在请求之间共享。如果你试图在你的类方法中记忆方法(@var ||= expensive_calculation),期望它只在当前请求期间持续存在,则可能会出现问题。实际上,计算仅在第一个请求中执行。

表面上,实现缓存或依赖于请求之间持久性的其他行为似乎很好。通常情况下并非如此。这是因为大多数部署策略将使用几个 Rails 进程来抵消其自身单线程的特性。阻塞所有请求等待慢速数据库查询显然不太友好,因此简单而快捷的方法是生成更多进程。当然,这些进程不共享任何东西(也许除了一些内存,您可能注意不到)。如果您在请求期间保存东西到类变量或类实例变量中,那么可能会出现问题。有时候东西似乎存在,有时候似乎不存在。(实际上,数据可能存在于某些进程中,而在其他进程中则不存在)。

一些部署配置(最显著地是JRuby + Glassfish)确实是多线程的。

Rails是线程安全的,因此它可以处理。但是您的应用程序可能不是线程安全的。所有控制器实例在每个请求后都被丢弃,但是我们知道类是共享的。如果您在类变量或类实例变量中传递信息,则这可能会让您感到困扰。如果您没有正确使用同步方法,很可能会陷入竞争条件的困境中。
顺便提一下:Rails通常在单线程进程中运行,因为Ruby的线程实现不完美。幸运的是,在Ruby 1.9中情况有所改善。在JRuby中情况更好。
随着这两种Ruby实现越来越受欢迎,多线程Rails部署策略似乎也将越来越受欢迎和普及。编写考虑多线程请求分发的应用程序已经是一个好主意。

2
感谢您的出色回复molf,非常有帮助。对于那些刚接触Ruby的人来说,类变量是静态类变量,例如@@bar。类实例变量是常规实例变量(@bar),但在静态类上下文中使用。例如:def foo{ p @bar }打印常规对象实例变量,而def self.foo{ p @bar }则打印类实例变量。这是因为在Ruby中,所有东西都是对象,包括静态类。因此,静态类对象可以具有实例变量。 我是Ruby的新手,如果我错了,请告诉我,我会发布一个新问题。 :) - Jonah Braun
感谢您的详细解释。这在2020年仍然正确吗? - Crashalot

6
这是一个相对简单的示例,说明如果您不小心修改共享对象会发生什么。
  1. Create a new Rails project: rails test

  2. Create a new file lib/misc.rb and put in it this:

    class Misc
      @xxx = 'Hello'
      def Misc.contents()
        return @xxx
      end
    end
    
  3. Create a new controller: ruby script/generate controller Posts index
  4. Change app/views/posts/index.html.erb to contain this code:

    <%
      require 'misc'; y = Misc.contents() ; y << ' (goodbye) '
    %>
    <pre><%= y %></pre>
    

    (This is where we modify the implicitly shared object.)

  5. Add RESTful routes to config/routes.rb.
  6. Start the server ruby script/server and load the page /posts several times. You will see the number of ( goodbye) strings increasing by one on each successive page reload.

如果生成了第二个单线程的Passenger进程,并加载了该页面,用户会看到哪一个页面? - Daniel Viglione
谢谢您的解释,但是为什么会出现错误还不太清楚。@xxxy 变量不是类变量,所以它们不应该持久存在,对吧? - Crashalot

3
在使用Passenger进行平均部署时,您可能有多个应用程序进程,它们之间没有共享任何东西,但每个进程内的类维护其(静态)状态从请求到请求。然而,每个请求都会创建控制器的新实例。
您可以将其称为不同共享状态环境的集群。
使用Java类比,您可以进行缓存并使其在请求之间工作,只是不能假设它将在每个请求上都可用。

2

“共享无所不有”有时是个好主意。但当你需要在每个请求中加载大型应用框架、大型领域模型和大量配置时,这就不再适用。

为了提高效率,Rails将一些数据保存在内存中,以便在应用程序的整个生命周期中与所有请求共享。其中大部分数据都是只读的,所以你不必担心。

在编写应用程序时,要避免向共享对象(例如数据库,它自带良好的并发控制)写入数据,这样就可以避免出现问题。


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