12

我正在构建一个 Web 应用程序,它为用户提供上传大图像并对其进行处理的核心功能。完成处理大约需要 3 分钟,我认为 Heroku 将是一个理想的平台,能够按需运行这些处理作业,并且以高度可扩展的方式。处理任务本身的计算成本相当高,并且需要运行高端 PX 测功机。我想最大化并行化,并最小化(有效地消除)作业在队列中等待的时间。换句话说,我想为 N 个工作提供 N PX 测功机。

值得庆幸的是,我可以使用 Heroku 的 API(或可选的像 Hirefire 之类的服务)轻松完成此任务。每当有新的处理请求进来时,我可以简单地增加工作人员数量,新工作人员将从队列中获取作业并立即开始处理。

然而,虽然扩大规模是无痛的,但缩小规模是麻烦的开始。Heroku API 令人沮丧地受到限制。我只能设置正在运行的工人数量,不能专门杀死空闲的工人。这意味着,如果我有 20 个工作人员,每个工作人员处理一个图像,并且一个人完成了它的任务,我不能安全地将工作人员数量扩展到 19,因为 Heroku 会杀死一个任意工作人员测功机,无论它是否真的在工作中!让所有工人运行直到所有工作完成是不可能的,因为成本将是天文数字。想象一下,在高峰期创建的 100 名工人继续无限期地闲置,因为全天都有一些新工作在滴滴答答!

我已经搜索了网络,人们建议的最佳“解决方案”是让您的工作进程优雅地处理终止。好吧,如果您的工作人员只是在发送大量电子邮件,那很好,但我的工作人员正在对图像进行一些非常冗长的分析,正如我上面提到的,大约需要 3 分钟才能完成。

在一个理想的世界里,我可以在完成任务后杀死一个特定的工人测功机。这将使缩小规模与扩大规模一样容易。

事实上,通过从工人测功机切换到一次性测功机(它在进程终止时终止,即在它的“根程序”退出后您停止为测功机付费),我已经接近了那个理想的世界。但是,Heroku 设置了可以同时运行 5 个一次性 dyno 的硬性限制。我可以理解这一点,因为我肯定在某种意义上滥用了一次性测功机……但这仍然令人沮丧。

有什么办法可以更好地缩减我的员工人数?我宁愿不必从根本上重新设计我的处理算法......将它分成几块在 30-40 秒内运行而不是 3 分钟的时间(这样就不会意外杀死正在运行的工人灾难性的)。这种方法会极大地复杂化我的处理代码并引入几个新的故障点。但是,如果这是我唯一的选择,我将不得不这样做。

任何想法或想法表示赞赏!

4

4 回答 4

3

这是 Heroku 的支持人员对此的回答:

恐怕目前这是不可能的。在缩减您的工人时,我们将停止数量最多的工人,因此我们不必更改这些测功机的公共名称,并且您不会出现编号漏洞。

在这种情况下,我发现这条评论很有趣,尽管它并没有真正解决这个问题。

于 2015-06-01T10:20:27.313 回答
2

安排清理任务

摘要:排队任务以最低优先级运行。完成所有其他任务后,将运行清理任务。

细节

[注意:一旦我写了这个答案,我意识到它并没有解决降低特定工人测功机的需要。但是您应该能够利用此处显示的关键技术:将低(er)优先级 DJ 任务排队以在其他所有内容都已处理后进行清理。]

我很幸运地使用 Heroku 的[platform-api][1]gem 来按需启动 Delayed Job 工作人员,并在他们完成时将其关闭。为了简化事情,我创建了一个 heroku_control.rb 文件,如下所示。

我的应用只需要一名工人;我认识到您的要求涉及更多,但任何应用程序都可以利用这一技巧:在处理所有其他延迟的作业任务后,将低优先级任务排队以关闭工作人员测功机。

require 'platform-api'

# Simple class to interact with Heroku's platform API, allowing
# you to start and stop worker dynos under program control.
class HerokuControl

  API_TOKEN = "<redacted>"
  APP_NAME = "<redacted>"

  def self.heroku
    @heroku ||= PlatformAPI.connect_oauth(API_TOKEN)
  end

  # Spin up one worker dyno
  def self.worker_up(act = Rails.env.production?)
    self.worker_set_quantity(1) if act
  end

  # Spin down all worker dynos
  def self.worker_down(act = Rails.env.production?)
    self.worker_set_quantity(0) if act
  end

  def self.worker_set_quantity(quantity)
    heroku.formation.update(APP_NAME, 'worker', {"quantity" => quantity.to_s})
  end

end

在我的应用程序中,我做了这样的事情:

LOWEST_PRIORITY = 100

def start_long_process
  queue_lengthy_process
  queue_cleanup_task        # clean up when everything else is processed
  HerokuControl::worker_up  # assure there is a worker dyno running
end

def queue_lengthy_process
  # do long job here...
end
handle_asynchronously :queue_lengthy_process, :priority => 1

# This gets processed when Delayed::Job has nothing else
# left in its queue.
def queue_cleanup_task
  HerokuControl::worker_down # shut down all worker dynos
end
handle_asynchronously :queue_cleanup_task, :priority => LOWEST_PRIORITY

希望这可以帮助。

于 2014-12-17T02:41:07.073 回答
0

I know you mentioned graceful termination, but I assume you meant graceful termination as in when a worker is killed off by using the API to set the number of workers. Why not just add as a part of the worker logic to kill itself when its job has been completed?

于 2014-08-18T21:54:51.593 回答
0

现在可以使用该heroku ps:stop命令关闭特定的测功机。

例如,如果您的heroku ps输出包含:

web.1: up 2017/09/01 13:03:50 -0700 (~ 11m ago)
web.2: up 2017/09/01 13:03:48 -0700 (~ 11m ago)
web.3: up 2017/09/01 13:04:15 -0700 (~ 11m ago)

您可以运行heroku ps:stop web.2以杀死列表中的第二个测功机。

这不会完全符合您的要求,因为 Heroku 将立即启动一个新的测功机来替换已关闭的测功机。但也许这对您(或阅读此问题的其他人)仍然有用。

于 2017-09-01T20:17:37.243 回答