我有一个运行Ruby 2.4.4的rails应用程序,使用Unicorn作为Web服务器,它利用单例在后台线程中读取Kafka。这个想法是每个独角兽进程有一个单例实例。所以 4 个进程,4 个单例。
我在独角兽配置中的after_fork
钩内启动了卡夫卡消费。我可以成功等待历史消息的消耗完成(通过紧接着撬动来验证(。
但是,当我到达提供流量的点时,单例实例是 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
我确认我在同一个进程中,甚至是同一个线程,因为我可以通过用自定义实现和 Thread 局部变量替换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
。我仍然得到空/新单例,但如果我直接查询它,线程局部变量仍然存在。
我目前的假设是这与写入时复制有关,通过设置线程局部变量,我以某种方式强制 ruby 仅为该过程创建一个单例并将其保存到该变量中。
所以我的问题是单例实例如何在单个线程中像这样消失?我怎样才能阻止它发生?如果我能提供帮助,我宁愿不使用这些线程局部变量。
这个问题的答案最终是由于一个相当小众的轨道配置:cache_classes
。我在本地运行我的独角兽服务器,因此没有缓存类。
Rails(在生产模式以外的任何模式下运行时,通常在暂存和生产中都使用,但不在本地使用(如果类级对象在生产中发生其他静态更改,则会重新加载类级对象。
实际上,rails看到了一些变化并重新加载了类,因为这会阻止程序员重新启动服务器。
这是由一个名为cache_classes
的配置控制的 - 我以前听说过它,这就是为什么您需要在生产中运行迁移后重新启动服务器的原因,以便可以从 ActiveRecord 对象访问任何更改。我没有把两个和两个放在一起,因为我不知道这些类会被重新加载。我仍然不确定为什么它们会被视为已更改并需要重新加载。
最终,如果我不尝试在本地运行独角兽服务器,我就不会看到这个问题,并且可以通过在development.rb
中设置config.cache_classes = true
来防止它
文档在这里: https://guides.rubyonrails.org/configuring.html#rails-general-configuration