Rails Rails 最佳实践 - 定时任务

zamia · 2017年01月18日 · 最后由 geophyli 回复于 2017年10月23日 · 13360 次阅读
本帖已被管理员设置为精华贴

开发 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)的功能,同时通过使用异步任务的形式还获得了下面的好处:

  1. 任务有重试机制。一般的 Job 都可以很简单的配置重试机制,保证了最终 Job 一定会被成功执行。即使有 bug,修复上线之后无需维护,下次重试的时候就可以正常完成任务,非常方便。
  2. 任务有更实时的错误处理机制。比如大鱼内部有一种 Job,在多次重试失败之后需要更改某个 ActiveRecord 的状态为 fail。这种情况通过 Job 的形式就非常方便了。

比如 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 方案了,这种方案简单来说也没太问题,简单易用。但是随着项目的复杂度增加,我不太推荐这种部署方案,主要基于以下理由:

  1. crontab 方案每一条任务都是独立的,都需要完整加载整个 Rails 运行环境,而 Rails 运行环境相对是比较耗资源的。想象一下每次几十个、上百个定时任务不停的启动停止,系统的负载可想而知。
  2. crontab 本身是系统的组件,这种方案需要依赖于系统的组件,在很多 production 环境下可能没有 crontab 组件,比如运行在 heroku 上,比如运行在 docker 上。

一个 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 条吧:

  1. 优先把定时任务当做调度器使用,主要业务逻辑通过 Job 系统来完成。特别是不要在 rake 任务中写执行大量的业务逻辑,会造成难以测试、无法重试和错误处理的问题。
  2. 优先使用独立的进程来管理定时任务,而不是依赖于 Linux 系统的 crontab。这样的好处是更具有移植性、更易运维。

欢迎讨论~

huacnlee 将本帖设为了精华贴。 01月18日 16:09

及时,最近正需要这样的东西

我还是相信 crontab

#3 楼 @Rei crontab 当然是最健壮的~ 主要还是挑选适合自己的方案吧,这种最佳实践也都有局限性,『没有银弹』

我感觉用 whenever 就够了。

有考虑过 Sidetiq 么? 我也觉得 whenever 与 Linux contrib 耦合度太强了。

在高强度下,定时任务没有比 crontab 更可靠的了。

楼主说“用独立的进程来实现定时任务,提高可移植性和容易运维。”,这一点,很难认同:

  1. 可移植性:所有 *nix 都有完善的 crontab 支持,而且语法一致。但是独立进程却未必在所有系统上都兼容。
  2. 容易运维:独立进程需要监控进程错误,crash 重启,系统资源占用情况收集和控制,这大大增加了运维工作量和不可靠性。crontab 本身不会挂,每次启动很干净,由操作系统负责管理。

所以,独立进程实现定时任务,实际上降低了可移植性,并增加了运维负担。

#7 楼 @kgen crontab 当然很可靠,但是不用的理由请看原文:

  1. crontab 方案每一条任务都是独立的,都需要完整加载整个 Rails 运行环境,而 Rails 运行环境相对是比较耗资源的。想象一下每次几十个、上百个定时任务不停的启动停止,系统的负载可想而知。
  2. crontab 本身是系统的组件,这种方案需要依赖于系统的组件,在很多 production 环境下可能没有 crontab 组件,比如运行在 heroku 上,比如运行在 docker 上。

另外,通过把定时任务转换成 scheduler 来使用,尽量避免把 cron 任务变成一个『高强度』的东东。

当然,最终用什么还是要根据自己的需求和实际情况来就行了,每个方案都有自己的优劣吧

我们的方案,用 cronjob 去添加任务到 sidekiq 里,利用 sidekiq 的 queue 保证服务器内存在跑任务的时候不受太大的影响。

job_type :sidekiq,  'cd :path && RAILS_ENV=:environment bundle exec sidekiq-client push :task :output'

  # backup and clean log
  every 1.day, at: Time.zone.parse("2:31 am") do
    command "#{app_path}/bin/ruby_logrotate.sh"
  end

  # ------- begin of activity report ---------
  every 1.day, at: Time.zone.parse("2:40 am") do
    sidekiq "EventReport.daily_job"
  end

  every 1.day, at: Time.zone.parse("2:43 am") do
    sidekiq "SmartActivityReport.daily_job"
  end

#8 楼 @zamia 我完全同意具体问题选择具体方案。

我在 #7 楼 说的,主要是反对你在总结中说“用独立的进程来实现定时任务,提高可移植性和容易运维”。独立进程有不少优势场景,但你总结中说的这句,其实恰恰是它的劣势场景,所以,我才回复了。

#10 楼 @kgen 大家对于移植和运维的理解不同吧

#10 楼 @kgen 感觉用了 docker-compose 以后,独立的进程运维起来反而方便了

然后把我之前的帖子链进来 Sidekiq 定时任务的尝试

crontab 的最小单位只能到分钟,需要更高度时间精度的话可能不适用

赞同楼主,crontab 在多实例部署就有容易有问题,另外无重试、统计,精度到分钟级别,顶多算个触发器,是比较粗糙的解决方案,不过也能解决 80% 的早期项目需求。跟 sidekiq 结合是不错的实践,可充分利用 slidekiq 的重试、统计。

用 whenever + linux crontab 还会有以下问题:

  1. 多机器负载均衡的时候,需要指定某一台机器来执行。按照默认的会在所有机器都生效,有些任务只希望执行一次。
  2. crontab 每次都需要启动 rails 进程。之前遇到一个启动失败的情况,应用启动时要去配置服务拉取配置,而那时配置服务挂了,导致启动失败。

后来选用了sidetiq

sidekiq-cron 另外一种选择。

#15 楼 @besfan 共用一台 redis 服务器,是不是就不会了?

某些特定工作,數量多,但工作時間可能只有幾秒鐘,使用 crontab 得不停啟動 rails,其啟動時間比執行工作時間還要長很多,不划算。這些我就用 sidetiq 解決。

#16 楼 @hww 支持一个,用 sidekiq 时用 sidekiq-cron 最简单好用

crontab 只是一个触发器而已,还要求什么呢,schedule,retry 等都自己实现好,“每一条任务都是独立的,都需要完整加载整个 Rails 运行环境” ,写到一个调度器里面,也就多个独立的变成了一个,然后调度器再往 delayed job 或着 sidekiq resque 等中 push 任务好了。我感觉需要精确到秒级别的东西本身就很少了。

在容器环境下,或者守护进程式设计替代 Crontab 可以看一下 clockwork

besfan 回复

sidetiq 已经停止维护了,wiki 也不在了, https://github.com/endofunky/sidetiq/wiki ,我在 sidetiq 0.3.5 版本找到了Sidetiq:Clock#stop ,但是我用的 0.7.2 也就是最新的版本,没有这个方法。所以我添加了任务以后一直执行没有办法取消,请问您能跟我说一下,周期性执行的这个定时任务应该怎么取消么?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号