sidekiq任务调度流程分析

本文深入分析了Sidekiq任务调度流程,包括启动过程、定时任务拉取、Worker处理任务以及重试机制。Sidekiq依赖Redis实现任务队列,启动时创建Scheduled Poller和Processor实例,定时任务通过Scheduled::Poller从Redis的定时队列中拉取并加入执行队列。Worker在Processor的循环中获取任务并调用相应方法执行。当任务失败时,Sidekiq使用中间件实现重试机制,按照指数退避策略计算重试时间。
摘要由CSDN通过智能技术生成

sidekiq是 Ruby 中一个非常优秀而且可靠的后台任务处理软件,其依赖 Redis 实现队列任务的增加、重试以及调度等。而 sidekiq 从启动到开始不断处理任务、定时任务以及失败任务的重试,都是如何调度的呢?遇到问题的时候,又该如何调优呢?

注意

  1. 今天的分析所参考的 sidekiq 的源码对应版本是 4.2.3;

  2. 今天所讨论的内容,将主要围绕任务调度过程进行分析,无关细节将不赘述,如有需要,请自行翻阅 sidekiq 源码;

  3. 文章内容真的很长,请做好心理准备。

你将了解到什么?

  1. sidekiq 的任务调度机制:定时任务、重试任务的检查机制,队列任务的排队以及队列权重对处理优先级的影响;

  2. 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. 循环里的 enqueuewait。这都是什么呢?

注意: #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::Managerstart 的时候只做一件事:分别调用其管理的所有 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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值