Sidekiq

作者:Reuven Lerner

在我看来,作为 Web 开发人员最棒的部分之一就是即时满足感。您编写一些代码,几分钟之内,世界各地的人们就可以通过 Web 浏览器访问您的服务器来使用它。从想法到开发到部署,再到实际用户从您的工作中受益(并做出反应)的这种快速性,以我的经验来看,是非常有动力的。

用户也喜欢新开发成果部署的速度。在 Web 应用程序的世界中,用户不再需要考虑、下载或安装程序的“最新版本”;当他们将页面加载到浏览器中时,他们会自动获得最新版本。事实上,用户已经开始期望定期推出新功能。一个 Web 应用程序如果不能随着时间的推移快速变化和改进,很快就会在用户眼中失宠。

用户考虑的另一个因素是 Web 应用程序响应其点击的速度。我们越来越被亚马逊和谷歌这样的公司宠坏了,它们不仅拥有成千上万台服务器供其支配,而且还针对最大可能的响应时间调整其应用程序和服务器。我们以毫秒而不是秒来衡量 Web 应用程序的速度,并且就在过去几年中,我们已经达到了即使花费一秒钟来响应用户也越来越不可接受的地步。

为用户提供更快的速度的驱动力导致了许多减少用户遇到延迟的技术。最简单的方法之一是延迟作业。与其尝试在单个用户请求的范围内完成所有操作,不如将其中一部分搁置到以后。

例如,假设您正在开发一个实现地址簿和日历的 Web 应用程序。如果用户要求查看未来一周的所有约会,您几乎肯定可以立即显示它们。但是,如果用户要求查看未来一年的所有约会,则可能需要一些时间才能从数据库中检索出来,将其格式化为 HTML,然后发送到用户的浏览器。

一种解决方案是将问题分解为两个或多个部分。与其让 Web 应用程序一起呈现整个响应,包括未来一年的约会列表,不如返回一个不包含任何约会的 HTML 页面。但是,该页面可以包含一段 JavaScript 代码,该代码在页面加载后,会向服务器发送请求以获取列表。这样,您可以呈现页面的轮廓,并在数据传入时填充数据。

有时,您无法以这种方式划分作业。例如,假设当您向日历添加新约会时,您希望系统向每位参与者发送电子邮件,指示他们应将会议添加到他们的日历中。发送电子邮件不需要很长时间,但确实需要服务器付出一些努力。如果您必须向大量用户发送电子邮件,则等待时间可能会难以忍受——或者只是令人恼火,具体取决于您的用户和手头的任务。

因此,多年来,开发人员一直在利用各种“延迟作业”机制,从而可以这样说:“是的,我想执行此功能,但稍后在与处理 HTTP 请求分开的线程或进程中执行。” 像这样延迟作业很可能意味着完成工作需要更长的时间。但是,如果电子邮件需要额外 30 秒才能发送,则没有人会在意。相比之下,如果向用户的浏览器发送 HTTP 响应需要额外 30 秒,用户肯定会在意。在 Web 世界中,用户可能不会抱怨,而是会转向另一个站点。

本月,我将探讨延迟作业的使用,特别关注 Sidekiq,这是一个由 Mike Perham 编写的 Ruby gem(以及随附的服务器),它使用与某些前辈不同的方法来提供此功能。如果您像我一样,您会发现使用后台作业是如此自然和容易,它很快成为您创建 Web 应用程序的日常工具箱的一部分——无论您是发送大量电子邮件、将文件从一种格式转换为另一种格式,还是生成可能需要时间处理的大型报告,后台作业都是您武器库的重要补充。

后台队列

在特别关注 Sidekiq 之前,让我们考虑一下后台作业要工作所必需的条件,至少在像 Ruby 这样的面向对象语言中是这样。基本思想是创建一个具有单个方法(称为“perform”)的类,该方法执行您想要执行的操作。例如,您可以执行以下操作


class MailSender
    def perform(user)
        send_mail_to_user(user)
    end
end

假设 send_mail_to_user 方法已在您的系统中定义,您可以使用类似以下内容发送电子邮件


MailSender.new.perform(user)

但关键是:您永远不会实际执行该代码。实际上,您永远不会直接创建 MailSender 的实例。相反,您将调用一个类方法,如下所示


MailSender.perform_async(user)

请注意区别。在这里,类方法采用您最终想要传递给“perform”方法的参数。但是,“perform_async”类方法改为将请求存储在队列中。在未来的某个时刻,一个单独的进程(或线程)将查看已存储在队列中的方法调用,并逐个执行它们,彼此独立且与 HTTP 请求没有任何关联。

现在,您可能会考虑将要执行的方法类排队的第一个地方是数据库。大多数现代 Web 应用程序都使用某种类型的数据库,这将是自然而然的第一个想法。实际上,在 Ruby 世界中,已经存在诸如“delayed job”和“background job”之类的 gem,它们确实使用数据库作为队列。

但是,此技术的最大问题是队列不需要数据库可以提供的所有功能。您可以使用更小更轻便的东西,而无需所有事务和数据安全功能。不使用数据库的第二个原因是分担负载。如果您的 Web 应用程序正在努力工作,您可能希望让数据库由 Web 应用程序拥有和使用,而不会因您的队列而分散其注意力。

因此,使用非关系数据库(又名 NoSQL 解决方案)作为后台作业的队列已变得流行。一个特别受欢迎的选择是 Redis,这是一种超快速、功能丰富的 NoSQL 存储,其工作方式类似于增强型的 memcached。在 Ruby 世界中,第一个使用 Redis 的作业队列是 Resque,它仍然很受欢迎且有效。

但是,随着应用程序规模和范围的扩大,对性能的要求也随之提高。Resque 对于大多数用途来说当然足够好,但 Sidekiq 试图更胜一筹。它也使用 Redis 作为后端存储,甚至使用与 Resque 相同的存储格式,因此您可以在 Resque 和 Sidekiq 之间共享 Redis 实例,或者轻松地从一个过渡到另一个。但是,最大的区别是 Sidekiq 使用线程,而 Resque 使用进程。

线程?在 Ruby 中?

Ruby 中的线程处理有点棘手。一方面,Ruby 中的线程非常容易使用。如果您想在线程中执行某些操作,您只需创建一个新的 Thread 对象,并向其传递一个包含您想要执行的代码的块


Thread.new do
    STDERR.puts "Hello!"  # runs in a new thread
end

问题在于,来自 Java 等语言的人们通常会感到惊讶,因为尽管 Ruby 线程是功能齐全的系统线程,但它们也具有全局解释器锁 (GIL),这会阻止一次执行多个线程。这意味着,如果您生成 20 个线程,您确实将拥有 20 个线程,但 GIL 充当一个大型互斥锁,确保一次最多只有一个线程在执行。线程执行通常会为 I/O 进行切换,并且鉴于几乎每个程序都定期使用 I/O,这几乎可以确保每个线程都有机会执行。

我应该注意,Ruby 并不是唯一存在这些问题的语言。Python 也具有 GIL,Python 的创建者 Guido van Rossum 表示,尽管他当然希望 Python 支持线程处理,但他个人更喜欢进程的易用性和安全性。由于进程不共享状态,因此它们不太容易出现难以调试的问题,而又不会牺牲太多的执行速度。

Sidekiq 是线程化的,但它使用的线程模型与大多数 Rubyist 习惯使用的模型不同。它使用 Celluloid,这是一种“基于 Actor”的线程系统,它将线程打包在对象内部,从而避免了与线程关联的大多数或所有问题。此外,Celluloid 希望在 JRuby 或 Rubinius 这两个替代的 Ruby 实现中运行,它们具有真正的线程处理并且没有 GIL。基于 Celluloid 的应用程序(例如 Sidekiq)在标准 Ruby 解释器(称为 MRI)下可以正常工作,但您将无法享受所有的速度或线程处理优势。

使用 Sidekiq

现在,让我们看看如何在 Sidekiq 中实现延迟作业的概述。首先,您需要安装 Redis NoSQL 存储。Redis 可从各种来源获得;我能够在我的基于 Ubuntu 的机器上使用以下命令安装它


apt-get install redis   # check this

安装 Redis 后,您将需要安装“sidekiq”gem。同样,如果您在 JRuby 或 Rubinius 下运行它,它将为您提供最佳功能,但您也可以在标准 Ruby 解释器下运行它。只是要意识到线程将为您提供非最佳性能。您可以使用以下命令安装 gem


sudo gem install sidekiq -V

如果您像我一样运行 Ruby Version Manager (RVM),则您不希望以 root 身份安装 gem。相反,您应该只键入


gem install sidekiq -V

(我总是喜欢使用 -V 标志,这样我就可以看到 gem 安装的详细信息。)

您可以在任何 Ruby 应用程序中使用 Sidekiq。但是,我的大部分工作都在 Rails 中,我猜您也想在 Rails、Sinatra 或类似的 Web 应用程序中使用它。因此,让我们创建一个简单的 Rails 应用程序,以便您可以尝试一下


rails new appointments

在新的“appointments”目录中,您将创建一个带有脚手架的“appointment”资源——模型、控制器和视图的组合,可以帮助您快速入门


rails g scaffold appointment name:text 
 ↪meeting_at:timestamp notes:text

完成此操作后,您必须运行迁移,在数据库中创建适当的“appointments”表。在这种情况下,因为您没有指定数据库,所以您将使用 SQLite,这对于此玩具示例来说足够好。

现在您可以启动您的应用程序 (rails s) 并转到 /appointments。从该 URL,您可以创建、查看、编辑和删除约会。但是,重点不是创建约会,而是延迟执行与约会相关的某些操作。让我们做一些非常简单的事情,例如发送电子邮件


rails g mailer notifications

在 app/mailers/notifications.rb 中,添加以下方法


def appointment_info(person, appointment)
    @person = person
    @appointment = appointment
    mail(to:person.email, subject:"Appointment update")
    end
end

并且,在 app/views/notifications/appointment_info.html.erb 中,编写以下内容


<p>Hello! You have an appointment with <%= @person %>
at <%= @appointment.meeting_at %>.</p>

最后,让我们将所有内容联系起来,从您的 AppointmentWorker 类中发送通知。定义此类文件的位置没有规则,但将其放在 app/workers 中似乎越来越标准,部分原因是 app 下的文件在 Rails 启动时都会加载


class AppointmentWorker
  include Sidekiq::Worker

  def perform(appointment)
    Notifications.deliver_appointment_info(appointment)
  end
end

请注意此处的几件事。首先,该类不继承任何特殊的东西。Sidekiq 不使用继承,而是让您包含一个模块——Ruby 中没有实例的类——其方法随后可用于您类的实例。这就是 perform_async 方法在您类上定义的方式。通过一点魔法,导入模块可以同时定义类方法和实例方法。

现在您要做的就是更改您的控制器,以便在创建报告后,您还可以发送通知


AppointmentWorker.perform_async(@appointment)

请注意,您没有传递约会的 ID,而是传递了约会对象本身!Sidekiq 使用 Ruby 内置的序列化工具来存储几乎任何类型的对象,而不仅仅是数字 ID。对象和方法调用存储在 Redis 中,直到它们被 Sidekiq 进程检索。

实际上,这是您需要就位的 Sidekiq 的最后一部分:后端进程,它将查找延迟的作业,并按顺序依次运行每个作业。幸运的是,运行它非常容易,只需


bundle exec sidekiq

是的,这就是您需要做的全部。诚然,您可以设置一些选项,但一般来说,这将启动一个 Sidekiq 服务器,该服务器查看当前队列(存储在 Redis 中),从队列中获取作业并处理它。您可以配置 Sidekiq 以使用超时或指定的重试次数运行,甚至可以指定您希望同时工作的并发 worker(即线程)的数量。

请记住,尽管这些确实是线程,但 Sidekiq(通过 Celluloid)确保它们没有任何共同状态。当然,您需要确保您的 worker 方法是线程安全的,这样即使 worker 完成了 90% 的工作然后被停止,它也能够重新启动而不会受到任何惩罚或错误。因此,您的进程必须是事务性的,就像您对数据库查询的期望一样。

除了在模块中定义方法(如上例所示)之外,还有其他方法可以调度 Sidekiq 作业。如果存在您想要作为后台进程运行的现有方法,只需在实际方法调用之前插入“delay”方法即可。即


my_object.delay.do_something_big

如果您正在使用 Rails 和内置的 ActiveSupport 模块进行简单的时间描述,您甚至可以这样做


my_object.delay_for(5.days).do_something_big
结论

Sidekiq 自发布以来在 Ruby 社区中变得非常流行,这在很大程度上归功于其高性能、易于安装和易于使用。它也适用于商业托管服务,例如 Heroku,前提是您首先安装 Redis 实例。

使用延迟作业在某种程度上改变了您对 Web 的看法——您意识到并非所有事情都需要立即发生。相反,您可以延迟某些作业,将它们放在后台,从而使您的 Web 服务器比其他情况更快地响应用户。而且,当速度是 Web 应用程序成功的关键要素时,明智地使用 Sidekiq 可能会产生很大的影响。

资源

Sidekiq 的主页位于 http://sidekiq.org。虽然 Sidekiq.org 指向商业版本,但基本版本仍然是免费和开源的,源代码可在 GitHub 上找到:http://mperham.github.com/sidekiq,包括包含大量有用信息的 Wiki。

Sidekiq 的作者 Mike Perham 在一篇博文中描述了基于 Actor 的模型:http://blog.carbonfive.com/2011/04/19/concurrency-with-actors

最后,鉴于 Sidekiq 使用 Redis,您可能需要阅读更多关于这个高性能 NoSQL 数据库的信息,网址为:https://redis.ac.cn

Reuven M. Lerner 是一位资深的 Web 开发人员,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并为程序员发布免费的每周新闻通讯,网址为 http://lerner.co.il/newsletter。Reuven 在 Twitter 上发推文 @reuvenmlerner,与他的妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论