在 Rails 中分叉独角兽进程时,单例消失

Singleton disappears when forking unicorn processes in Rails

我有一个 rails 应用程序 运行 ruby 2.4.4 使用 Unicorn 作为 Web 服务器,它使用单例在后台线程中从 Kafka 读取数据。这个想法是让每个独角兽进程有一个单例实例。所以 4 个进程,4 个单例。

我在我的独角兽配置中的 after_fork 挂钩内启动了 kafka 消费。我可以成功等待历史消息的消费完成(通过立即撬动验证)。

然而,当我到达服务流量点时,单例实例是 a) 一个不同的实例,并且 b) 是空的 - 之前设置的 ivar 已经消失。

我已经确认我在同一个进程和同一个线程中。

设置如下:

# background_foo_consumer.rb
class BackgroundFooConsumer
  include Singleton

  attr_reader :background_consumer

  def add_background_consumer(consumer, topics, options: nil)
    @background_consumer ||= BackgroundKafkaConsumer.new(consumer, topics, options: options)
  end

  def processed_historical_messages?
    background_consumer&.consumer&.reached_head
  end
end


# config/unicorn.rb
after_worker_ready do |server, worker|
  BackgroundFooConsumer.instance.add_background_consumer(nil, ["foos"])
  BackgroundFooConsumer.instance.background_consumer.start

  BackgroundFooConsumer.instance.background_consumer.consumer.mutex.synchronize {
    BackgroundFooConsumer
    .instance.background_consumer.consumer.processed_historical_messages.wait(
      BackgroundFooConsumer.instance.background_consumer.consumer.mutex
    )
  }
  end
end

我确认我在同一个进程中,甚至是同一个线程中,因为我可以通过将 include Singleton 替换为自定义实现和线程局部变量来成功地将正确的对象传递给应用程序,如下所示:

# config/unicorn.rb
after_worker_ready do |server, worker|
  # ... same as above

  Thread.current[:background_foo_consumer] = BackgroundFooConsumer.instance
end


# background_foo_consumer.rb
class BackgroundFooConsumer
  attr_reader :background_consumer

  def self.instance
    @instance ||= begin
                    Thread.current[:background_foo_consumer] || self.new
                  ensure
                    Thread.current[:background_foo_consumer] = nil
                  end
  end
end

在这个实现中,当我为来自我的应用程序的流量提供服务时,BackgroundFooConsumer.instance 是在 after_fork 挂钩中创建的正确实例,并且每个独角兽进程都有一个独立的实例,通过检查确认对象 ID。

我不相信这是 GC,至少底层对象没有被清除,我已经通过在 after_fork 挂钩中设置 Thread 局部变量来确认这一点,然后使用 include Singleton 在我的消费者中 class。我仍然得到empty/new单例,但如果我直接查询它,线程局部变量仍然存在。

我目前的假设是,这与写时复制有关,通过设置线程局部变量,我以某种方式强制 ruby 为我创建一个仅用于该进程的单例并将其保存到该变量.

所以我的问题是单例实例如何在单个线程中像这样消失?我怎样才能阻止它发生?如果可以的话,我宁愿不使用这些线程局部变量。

这个问题的答案最终归因于 rails 配置的一个非常小众的位:cache_classes。我在本地 运行ning 我的独角兽服务器,所以 classes 没有被缓存。

Rails(当 运行 处于除生产模式以外的任何模式时,生产模式通常用于暂存和生产但不在本地使用)重新加载 class 关卡对象,如果它们改变了在生产中否则是静态的。

实际上 rails 看到了一些变化并重新加载 classes,因为这会阻止程序员重新启动服务器。

这是由一个名为 cache_classes 的配置控制的 - 我以前听说过,这就是为什么在 运行 生产迁移后需要重新启动服务器的原因任何更改都可以从 ActiveRecord 对象访问。我没有把两个和两个放在一起,因为我不知道 classes 会被重新加载。我仍然不确定为什么他们会被认为已更改并需要重新加载。

如果我不尝试在本地 运行 独角兽服务器,最终我不会看到这个问题,并且可以通过在 development.rb[=] 中设置 config.cache_classes = true 来防止15=]

文档在这里:https://guides.rubyonrails.org/configuring.html#rails-general-configuration