sidekiq是 Ruby 中一个非常优秀而且可靠的后台任务处理软件,其依赖 Redis 实现队列任务的增加、重试以及调度等。而 sidekiq 从启动到开始不断处理任务、定时任务以及失败任务的重试,都是如何调度的呢?遇到问题的时候,又该如何调优呢?
注意
今天的分析所参考的 sidekiq 的源码对应版本是 4.2.3;
今天所讨论的内容,将主要围绕任务调度过程进行分析,无关细节将不赘述,如有需要,请自行翻阅 sidekiq 源码;
文章内容真的很长,请做好心理准备。
你将了解到什么?
sidekiq 的任务调度机制:定时任务、重试任务的检查机制,队列任务的排队以及队列权重对处理优先级的影响;
sidekiq 的中间件机制以及在此基础上实现的任务重试机制。
先抛结论
时序图
对于复杂的调用关系,我习惯用时序图帮助我理解其中各部分代码之间相互协作的关系(注意:为了避免太多细节造成阅读负担,我将参数传递以及返回值等冗杂过程去除了,只保留与任务调度相关的关键调用):
人话
Sidekiq 整个任务调度过程中依赖几个不同角色的代码共同协作,其分工如下:
角色 | 对应类型 | 职责 |
定时任务拉取器 | Sidekiq::Scheduled::Poller | 负责在一定时间范围内不定时检查定时任务(scheduled)以及重试任务(retry),将计划时间已经超过当前时间的任务追加到各自对应任务队列中 |
worker 管理器 | Sidekiq::Manager | 负责按照配置的 concurrency 参数创建匹配数量的worker,以及worker的管理(停止等) |
worker | Sidekiq::Processor | 负责执行指定的任务 |
源码之旅 —— 启动
当我们在执行 sidekiq
时,源码中的 bin/sidekiq.rb
文件便是第一个开始执行的文件,让我们看看里边的主要代码:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/bin/sidekiq#L9-L12
begin
cli = Sidekiq::CLI.instance
cli.parse
cli.run # <===== 这边走
# ...
紧靠 begin
后边的两行代码首先创建 Sidekiq::CLI
类的一个实例,接着调用实例方法 #parse
解析 sidekiq 的配置参数,其中包括队列的配置、worker 数量的配置等,在此不展开了。接着实例方法 #run
将带着我们继续往下走,让我们继续看 lib/sidekiq/cli.rb
里边的代码:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/cli.rb#L46-L106
def run
# 这里打印控制台欢迎信息、打印日志以及运行环境(不同 Rails 版本)加载等
require 'sidekiq/launcher'
@launcher = Sidekiq::Launcher.new(options)
begin
launcher.run # <===== 这边走
# 进程接收到的信号处理以及退出处理
end
上面的代码主要是实例化了一个 Sidekiq::Launcher
的对象,紧随其后又调用了实例方法 #run
,所以让我们继续顺藤摸瓜,看看 Sidekiq::Launcher#run
方法到底做了哪些事情?
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/launcher.rb#L24-L28
def run
@thread = safe_thread("heartbeat", &method(:start_heartbeat))
@poller.start
@manager.start
end
#run
方法首先通过 safe_thread
创建了一个新的线程,线程主要负责执行 start_heartbeat
方法的代码,从方法名称上,我们猜测其主要是心跳代码,负责定时检查 sidekiq 健康状态,跟之前一样,这里不往下挖,我们继续看后边的两行代码:
@poller.start
@manager.start
这里的 @poller
跟 @manager
都是什么呢?让我们回头看一下,前面讲到 lib/cli.rb
的 #run
方法会负责创建 Sidekiq::Launcher
的实例,那让我们看下后者的 initialize
方法定义:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/launcher.rb#L17-L22
def initialize(options)
@manager = Sidekiq::Manager.new(options)
@poller = Sidekiq::Scheduled::Poller.new
@done = false
@options = options
end
可以看到,实际上,@manager
是在创建 Sidekiq::Launcher
实例的过程中同步创建的 Sidekiq::Manager
的实例,同理,@poller
是同步创建的 Sidekiq::Scheduled::Poller
的实例。那我们按照代码执行顺序,先看下 @poller.start
也就是 Sidekiq::Scheduled::Poller#start
方法的定义:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L63-L73
def start
@thread ||= safe_thread("scheduler") do
initial_wait
while !@done
enqueue
wait
end
Sidekiq.logger.info("Scheduler exiting...")
end
end
这里看到,#start
方法也创建了一个线程,在线程里执行了两个部分代码:1. 初始化等待;2. 循环里的 enqueue
与 wait
。这都是什么呢?
注意: #start
方法在线程创建完成后就立刻返回了,至于 #start
方法里的逻辑,请移步后面章节“继续深挖 Sidekiq::Scheduled::Poller#start”作更深一步分析。这里,我们先继续接着看看 #start
方法返回后接下来执行的 @manager.start
方法又做了什么:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L45-L49
def start
@workers.each do |x|
x.start
end
end
这里的 @workers
又是什么?一个数组?怎样的数组?我们回顾下,前面说在创建 Sidekiq::Launcher
实例的过程中同步创建了 Sidekiq::Manager
的实例,让我们就看看 Sidekiq::Manager
的 #initialize
方法:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L31-L43
def initialize(options={})
logger.debug { options.inspect }
@options = options
@count = options[:concurrency] || 25
raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
@done = false
@workers = Set.new
@count.times do
@workers << Processor.new(self)
end
@plock = Mutex.new
end
可以看到,在创建了 Sidekiq::Manager
的实例之后,又同步创建了多个 Sidekiq::Processor
的实例,实例的个数取决于 options[:concurrency] || 25
,也就是配置的 :concurrency
的值,缺省值为 25
。至此,我们知道,sidekiq 中的 worker 的数量就是在此其作用的,Sidekiq::Manager
按照配置的数量创建指定数量的 worker。
往回看刚才的 #start
方法中:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L46-L48
@workers.each do |x|
x.start
end
简言之,就是 Sidekiq::Manager
在 start
的时候只做一件事:分别调用其管理的所有 worker 的 #start
方法,也就是 Sidekiq::Processor#start
。继续往下走:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L60-L62
def start
@thread ||= safe_thread("processor", &method(:run))
end
又是我们熟悉的 safe_thread
方法,同样是创建了一个新的线程,意味着每一个 worker 都是基于自己的一个新线程的,而这个线程里执行的代码是私有方法 #run
里的代码:
# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L66-L77
def run
begin
while !@done
process_one
end
@mgr.processor_stopped(self)
rescue Sidekiq::Shutdown
@mgr.p