开发 Rails 项目中难免遇到一些需要做定时任务的情况,比如每天晚上去跑一些简单的统计,定时更新一下缓存等等情况,虽然是一个简单的事情,可是随着时间的推动,一个项目中的定时任务可能会很多,比如目前我们有一个项目,定时任务有上百条,已经非常的难以管理和容易出问题了。因此,本文简单总结了一些关注点帮助大家理解和规范定时任务的写法。
先简单介绍一下定时任务,定时任务是指那些周期性执行或者某些时刻固定执行的任务,一般来讲大家都熟悉 Linux 系统的 crontab 配置文件,这里面就是常见的定时任务了。Rails 环境下,最简单的定时任务直接利用 rake 或者 rails runner 执行一个命令,然后把命令放到 crontab 配置文件中即可。
对于那些一次性执行的任务,大家都知道应该使用 Job 系统来完成,但是有时候有那种延期执行的任务,这个我个人觉得也不是定时任务应该负责的范畴,一般的 Job 系统,比如 delayed job、sidekiq 等也都提供延时执行的功能,直接使用这些就可以了。所以本文主要指那种周期性执行的任务。
就跟一个 Class、一个 method 一样,先确定职责是比较重要的一件事。那么定时任务系统的职责是什么呢?
最初级的做法是直接在 rake task 里面写一通逻辑,直接把任务的主逻辑放在这里。这样的话有一个最明显的问题是难以测试,大家可以 google 一下相关的方案,可以解决,但是较繁琐。
稍好一点的做法是把业务逻辑封装在一个 Service 里面,然后在 task 里面去调用这个 Service。至少这样的话可以解决掉测试的问题。但是这样做还是有一个问题,就是错误处理的问题。一般 task 中的业务逻辑还相对比较复杂,当这个 task 出错了怎么办呢?比如使用 crontab 来执行某个任务,任务出错时一般可能就是记录日志,再根据错误日志做个报警而已。可是很多时候任务的错误处理需要更及时、同时也属于业务逻辑的一部分,由报警系统处理再反馈给业务系统显然很不合理。
所以,目前社区内比较推荐的做法是把定时任务作为一个调度器(scheduler)而存在,定时任务只是一个调度器,真正的业务逻辑都是封装在 Job 中。比如下面这种方式:
# lib/tasks/some_task.rake
task some_task: :environment do
SomeJob.perform_async # 调用 Sidekiq 的 Job 来异步执行
end
这样的话整个定时任务的代码非常简单,只是充当一个调度器(scheduler)的功能,同时通过使用异步任务的形式还获得了下面的好处:
比如 Job 大概长这样(拿 Sidekiq 举例):
# app/jobs/some_job.rb
class SomeJob
include Sidekiq::Workder
sidekiq_retries_exhausted do |job, e|
process_failure_job(job, e)
end
def perform
# 正常业务逻辑
end
def process_failure(job, error)
# 多次重试失败后的处理逻辑
end
end
总之,定时任务书写的时候应该是轻量级的,最好是与 Job 系统联合使用,定时任务只是作为一个调度器,Job 系统来真正执行业务逻辑。
rails 社区中大家使用最多的部署方案就是 whenever + linux crontab 方案了,这种方案简单来说也没太问题,简单易用。但是随着项目的复杂度增加,我不太推荐这种部署方案,主要基于以下理由:
一个 web 应用也期望都以一个或多个无状态的进程来运行的形式来运行,从这个角度上来讲,也应该以一个独立进程来运行 cron 任务,而不是依赖于系统的 cron 组件。
那么有没有好的方案呢,其实方案有挺多的,都可以解决问题,比如:
还有一类是 Job 系统的插件形式,但是也可以以独立进程来运行,我没有实践过,应该也差不多:
下面以 crono 来例简单示例一下定时任务的写法,也非常简单。
# Gemfile
gem 'crono', '~> 1.1', '>= 1.1.2'
# config/cronotab.rb
class HourlyPerformSomething
def perform
SomeModel.pending.each {|s| SomeJob.perform_async(s.id) }
end
end
Crono.perform(HourlyPerformSomething).every 1.hour
执行时:
bundle exec crono RAILS_ENV=production
简单总结下就是下面的 2 条吧:
欢迎讨论~