【问题标题】:Puma sleeps an important thread on boot of rails applicationPuma 在启动 Rails 应用程序时休眠了一个重要线程
【发布时间】:2017-04-20 00:15:14
【问题描述】:

我正在使用 postgresql 在 puma 上运行带有 Ruby 2.3.3 的 Rails 3。我有一个初始化程序/twitter.rb 文件,它在启动时使用 twitter 的流 api 启动一个线程。当我使用rails server 启动我的应用程序时,推特流媒体工作正常,我可以正常访问我的网站。 (如果我不将流媒体放在不同的线程上,流媒体工作但我无法在浏览器中查看我的应用程序,因为线程被 twitter 流阻止)。但是当我使用puma -C config/puma.rb 启动我的应用程序时,我收到以下消息,告诉我我的线程在启动时被发现并进入睡眠状态。如何告诉 puma 让我在启动时在后台运行这个线程?

初始化程序/twitter.rb

### START TWITTER THREAD ### if production

if Rails.env.production?
  puts 'Starting Twitter Stream...'
  Thread.start {
    twitter_stream.user do |object|
      case object
        when Twitter::Tweet
          handle_tweet(object)
        when Twitter::DirectMessage
          handle_direct_message(object)
        when Twitter::Streaming::Event
          puts "Received Event: #{object.to_yaml}"
        when Twitter::Streaming::FriendList
          puts "Received FriendList: #{object.to_yaml}"
        when Twitter::Streaming::DeletedTweet
          puts "Deleted Tweet: #{object.to_yaml}"
        when Twitter::Streaming::StallWarning
          puts "Stall Warning: #{object.to_yaml}"
        else
          puts "It's something else: #{object.to_yaml}"
      end
    end
  }
end

config/puma.rb

workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count

preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'


on_worker_boot do
  # Valid on Rails up to 4.1 the initializer method of setting `pool` size
  ActiveSupport.on_load(:active_record) do
    config = ActiveRecord::Base.configurations[Rails.env] ||
    Rails.application.config.database_configuration[Rails.env]
    config['pool'] = ENV['RAILS_MAX_THREADS'] || 5
    ActiveRecord::Base.establish_connection(config)
  end
end

启动消息

2017-04-19T23:52:47.076636+00:00 app[web.1]: Connecting to database specified by DATABASE_URL
2017-04-19T23:52:47.115595+00:00 app[web.1]: Starting Twitter Stream...
2017-04-19T23:52:47.229203+00:00 app[web.1]: Received FriendList: --- !ruby/array:Twitter::Streaming::FriendList []
2017-04-19T23:52:47.865735+00:00 app[web.1]: [4] * Listening on tcp://0.0.0.0:13734
2017-04-19T23:52:47.865830+00:00 app[web.1]: [4] ! WARNING: Detected 1 Thread(s) started in app boot:
2017-04-19T23:52:47.865870+00:00 app[web.1]: [4] ! #<Thread:0x007f4df8bf6240@/app/config/initializers/twitter.rb:135 sleep> - /app/vendor/ruby-2.3.3/lib/ruby/2.3.0/openssl/buffering.rb:125:in `sysread'
2017-04-19T23:52:47.875056+00:00 app[web.1]: [4] - Worker 0 (pid: 7) booted, phase: 0
2017-04-19T23:52:47.865919+00:00 app[web.1]: [4] Use Ctrl-C to stop
2017-04-19T23:52:47.882759+00:00 app[web.1]: [4] - Worker 1 (pid: 11) booted, phase: 0
2017-04-19T23:52:48.148831+00:00 heroku[web.1]: State changed from starting to up

提前感谢您的帮助。我看过其他几篇提到WARNING: Detected 1 Thread(s) started in app boot 的帖子,但答案说如果线程不重要,则忽略警告。就我而言,线程非常重要,我需要这个线程不休眠。

【问题讨论】:

  • 更新:我了解了睡眠线程的真正含义,这不是问题所在。但是,我不确定从哪里开始我的 twitter 流线程。它应该留在初始化程序中吗?我应该使用单独的进程(后台应用程序)吗?我应该在每个 puma 工人上启动 twitter 流吗?

标签: ruby-on-rails ruby puma


【解决方案1】:

从您的代码中,我认为您手头上的问题比睡眠线程更大...我猜这可能是由于某些事情被错误命名而其他事情在依赖 Web 框架时不经常考虑.

在服务器领域,“工作者”指的是forked 进程,它们执行与服务器相关的任务,通常接受新连接并处理 Web 请求。

但是 - fork 不重复线程! - 新进程(worker)只从一个线程开始,它是调用fork的线程的副本。

这是因为进程不共享内存(通常)。无论您在进程中拥有的任何全局数据都是该进程私有的(即,如果您将连接的 websocket 客户端保存在一个数组中,那么该数组对于每个“worker”都是不同的)。

这没办法,这是操作系统和fork 设计方式的一部分。

因此,警告不是您可以规避的 - 它表明应用程序存在设计缺陷(!)。

例如,在您当前的设计中(假设线程没有休眠),handle_tweet 方法只会被原始服务器进程调用,而不会被任何工作进程调用。

如果您使用的是 pub/sub,则整个应用程序只需要一个 twitter_stream 连接(无论您的应用程序有多少服务器或工作人员) - 可能需要一个 twitter_stream 进程(或后台应用程序)比线程好。

但是,如果您以特定于进程的方式实现 handle_tweet - 即,通过向保存在数组中的每个连接的客户端发送消息 - 您需要确保每个“工作人员”都启动 twitter_stream 线程(! )。

当我编写 Iodine(与 Puma 不同的服务器)时,我使用 Iodine.run method 处理这些用例,它将任务推迟到以后。 “已保存”任务应仅在 worker 初始化且事件循环开始运行后执行,因此在每个进程中执行(允许您在每个进程中开始新的线程)。

即

Iodine.run do
   Thread.start do
    twitter_stream.user do |object|
    # ...
    end
   end
end

我认为 Puma 也有类似的解决方案。根据我对Puma Clustered-Mode Documentation 的了解,将以下块添加到您的config/puma.rb 可能会有所帮助:

# config/puma.rb
on_worker_boot do
  Thread.start do
   twitter_stream.user do |object|
   # ...
   end
  end
end

祝你好运!


编辑:关于twitter_stream使用ActiveRecord的评论

从 cmets 中我了解到,twitter_stream 回调将数据存储在数据库中并处理“推送”事件或通知。

虽然这两个问题是相互关联的,但它们之间却有很大的不同。

例如,twitter_stream 回调应该只在数据库中存储数据一次。即使您的应用程序增长到十亿用户,您也只需将数据保存在数据库中一次。

这意味着twitter_stream 回调应该有自己的专用进程,只运行一次,可能与主应用程序分开。

首先,只要您将应用程序限制为单个(仅运行一个服务器/应用程序),您就可以将fork 与initializer/twitter.rb 脚本一起使用...即:

### START TWITTER PROCESS ### if production
if Rails.env.production?
  puts 'Starting Twitter Stream...'
  Process.fork do
    twitter_stream.user do |object|
      # ...
    end
  end
end

另一方面,通知应发送给特定进程拥有的特定连接上的特定用户。

因此,通知应该与twitter_stream 数据库更新分开关注,并且它们应该在每个进程的后台运行,使用上述on_worker_boot(或Iodine.run)。

要实现这一点,您可以让on_worker_boot 启动一个后台线程,该线程将侦听诸如 Redis 之类的发布/订阅服务,而 twitter_stream 回调“发布”对发布/订阅服务的更新。

这将允许每个进程查看更新并检查它“拥有”的任何连接是否属于应收到更新通知的客户端。

【讨论】:

  • 感谢您的帮助。因为我有两个工人,当我把我的线程放在 on_worker_boot 块中时,twitter 流启动了两次。可以吗?在这种情况下,一切都会被复制吗? handle_tweet 方法应该只被调用一次,因为它将每条推文的信息存储在 Active Record 中,并可能将直接消息发送回用户。如果我需要为 twitter 流使用整个其他进程(后台应用程序),您对如何开始使用有任何指示吗?感谢所有的帮助,并为我对这个主题的无知感到抱歉。
  • 嗨@ToddSutter,感谢您的评论。我更新了我的答案以反映 twitter_stream 回调将数据存储在数据库中的脂肪。
  • 额外的信息非常有帮助。我的 twitter 流基本上是一个数据库接口,所以我将尝试运行一个单独的进程,比如上面描述的 Process.fork。我不知道如何使用,甚至不知道什么是单独的过程,所以我很高兴我伸出了手!我会尽快测试并回复您。
【解决方案2】:

按照我阅读您的问题的方式,这看起来不是问题。 sleeping 线程与 dead 线程不同。睡眠只是意味着线程正在等待空闲,不消耗任何 cpu。如果其他一切都正确连接,那么一旦 twitter api 检测到事件,它应该唤醒线程,运行您定义的任何处理程序,然后重新进入睡眠状态。睡眠不是“在后台运行”,而是“等待某事发生(例如,有人在推特上@我),所以我可以在后台运行。”

一个简单的例子来证明这一点:

2.4.0 :001 > t = Thread.new { TCPServer.new(1234).accept ; puts "Got a connection! Dying..." }
 => #<Thread:0x007fa3941fed90@(irb):1 sleep> 
2.4.0 :002 > t
 => #<Thread:0x007fa3941fed90@(irb):1 sleep> 
2.4.0 :003 > t
 => #<Thread:0x007fa3941fed90@(irb):1 sleep> 
2.4.0 :004 > TCPSocket.new 'localhost', 1234
 => #<TCPSocket:fd 35> 
2.4.0 :005 > Got a connection! Dying...
t
 => #<Thread:0x007fa3941fed90@(irb):1 dead> 

睡觉只是意味着“等待行动”。


Puma 是一个基于线程的服务器,并且非常注重在其启动过程中旋转线程,因此会在应用程序启动时发出有关线程启动的警告。

尽管如此,但在网络服务器中让一个线程监听来自 api 的更新有点奇怪。也许您应该考虑让工作人员使用Resque 之类的东西来处理推特事件?或者ActionCable 可能与您的用例相关?

【讨论】:

  • 那么您是否建议我运行另一台仅处理此 twitter api 的服务器,与我的 Web 服务器分开?我对这一切都很陌生(如果这还不明显的话)。或者这些模块,Resque 和 ActionCable,是我网络服务器的插件吗?
  • 这真的取决于你的回调在做什么。他们是否向用户传达了一些信息?他们是否将记录保存到数据库?他们完全在做其他事情吗?同样重要的是你愿意在你的应用上投入多少时间。如果没有第二台服务器,那么至少有一个单独的进程处理这些请求,这是更标准和更严格的做事方式,因为这些回调不用于响应即时请求。从架构的角度来看,它需要做更多的工作,但它通过分离服务器/工作人员关注点来抢占此类问题
  • 旁注:Puma 不是基于线程的服务器,而是基于反应器的服务器。 Puma 使用事件循环和有限的线程池,而不是每个连接一个线程的基于线程的服务器设计。 Puma 还支持集群模式(使用进程进行并发)。
  • 感谢您的回复!非常感激!我根本不需要响应用户,它更像是一个数据库交互。看来我需要运行一个单独的进程来处理 twitter 流
猜你喜欢
  • 2016-11-13
  • 1970-01-01
  • 1970-01-01
  • 2023-04-04
  • 2017-07-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-15
相关资源
最近更新 更多