Rails Rails 最佳实践 - 定时任务

zamia · 发布于 2017年01月18日 · 最后由 geophyli 回复于 2017年10月23日 · 4737 次阅读
3214
本帖已被设为精华帖!

开发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。这样的好处是更具有移植性、更易运维。

欢迎讨论~

共收到 21 条回复
De6df3 huacnlee 将本帖设为了精华贴 01月18日 16:09
2564

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

1

我还是相信 crontab

3214

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

20

我感觉用 whenever 就够了。

9695

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

370

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

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

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

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

3214

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

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

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

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

410

我们的方案,用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
370

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

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

3214

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

8744

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

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

1342

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

377

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

7907

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

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

后来选用了sidetiq

14293

sidekiq-cron 另外一种选择。

15420

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

2369

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

1968

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

15317

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

1107

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

28365
7907besfan 回复

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

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