35
Sidekiq 源码分析 zhangyuan 13917日星期二

Sidekiq 源码分析

Embed Size (px)

Citation preview

Page 1: Sidekiq 源码分析

Sidekiq 源码分析zhangyuan

13年9月17日星期二

Page 2: Sidekiq 源码分析

不谈什么

• 不谈 Celluloid

• 不谈 Sidekiq 的 web 界面

• 不谈 Sidekiq 的测试代码

13年9月17日星期二

Page 3: Sidekiq 源码分析

阅读源码的好处

13年9月17日星期二

Page 4: Sidekiq 源码分析

如何阅读源码

• 从功能(尤其是常用的用法)寻找线索• 工具• grep

• VIM、ctags、vim-scripts/taglist.vim

13年9月17日星期二

Page 5: Sidekiq 源码分析

简单介绍• 多线程的后台任务工具。• 虽然 Sidekiq 是多线程的,但没有过多地显式使用线程库,而是通过 Celluloid 实现并发。Celluloid 是⼀一个实现了 Actor Model 并发模型的 Ruby 库,它不仅仅隐藏了线程的细节。

• 本幻灯片适用于 sidekiq 2.11.2 版本。

13年9月17日星期二

Page 6: Sidekiq 源码分析

整体架构

• 客户端• 任务入队• 服务器端• 读取任务并处理

13年9月17日星期二

Page 7: Sidekiq 源码分析

任务分类

• 普通任务• 放入后台后执行• 定时任务• 在某个时刻执行

13年9月17日星期二

Page 8: Sidekiq 源码分析

客户端(1)• 消息入队• 搜索 “def perform_async” / “def perform_in”

• lib/sidekiq/worker.rb

• 序列化任务信息后,使用集合(set)保存队列名称

• SADD key member [member ...]

• key 就是 queues

13年9月17日星期二

Page 9: Sidekiq 源码分析

客户端(2)

• 普通队列使用不同的列表(list)保存任务

• LPUSH key value [value ...]

• key 是队列名称,以 “queue:” 为前缀

• 定时任务使用有序集合(SortedSet),保存在同⼀一个名称为 schedule 的有序集合中

• ZADD key score member [[score member] [score member] ...]

• key 是队列名, score 是执行时刻,member 是任务信息

13年9月17日星期二

Page 10: Sidekiq 源码分析

服务器

• 服务器端在命令行执行• 接下来都是服务器端

13年9月17日星期二

Page 11: Sidekiq 源码分析

解析命令行参数

• CLI - Command-Line Interface

• 可执行文件⼀一般在 bin/ 目录里

• 代码⼀一般会在⼀一个叫 cli.rb 的文件里

• 通常使用 optparse 来解析命令参数

13年9月17日星期二

Page 12: Sidekiq 源码分析

解析队列和权重参数

:queues: - [default, 2] - [high, 5]

13年9月17日星期二

Page 13: Sidekiq 源码分析

根据权重选取任务queues_and_weights = [["default", 2], ["high", 5]]queues = ["default", "default", "high", "high", "high", "high", "high"]

queues = ["queue:default", "queue:default", "queue:high", "queue:high", "queue:high", "queue:high", "queue:high"]

queues_cmd = queues.shuffle.uniqqueues_cmd << Sidekiq::Fetcher::TIMEOUT

Sidekiq.redis { |conn| conn.brpop(*queues_cmd) }

13年9月17日星期二

Page 14: Sidekiq 源码分析

选取任务的代码

• lib/sidekiq/cli.rb

• Sidekiq::CLI#parse_config

• Sidekiq::BasicFetch#initialize

• Sidekiq::BasicFetch#retrieve_work

13年9月17日星期二

Page 15: Sidekiq 源码分析

队列权重小结

• 将队列权重问题转换为概率问题,来选取任务

13年9月17日星期二

Page 16: Sidekiq 源码分析

处理系统信号 self_read, self_write = IO.pipe

%w(INT TERM USR1 USR2 TTIN).each do |sig| trap sig do self_write.puts(sig) end end while readable_io = IO.select([self_read]) signal = readable_io.first[0].gets.strip handle_signal(signal) end

13年9月17日星期二

Page 17: Sidekiq 源码分析

处理系统信号小结• 使用 trap {} 注册系统信号

• 使用 IO.select 等待IO就绪,使用while循环,进程不退出

• 只有⼀一个 self_read ,能否改成阻塞IO?

• 为什么要使用 IO.pipe 和 select ,而不是直接使用 loop {}

• 参考书籍 Working With Unix Processes

13年9月17日星期二

Page 18: Sidekiq 源码分析

修改程序名称

• $0 显示当前的worker状态

• $PROGRAM

• 定时刷新• Sidekiq::Manager#procline

13年9月17日星期二

Page 19: Sidekiq 源码分析

任务的分类处理• 普通任务• Sidekiq::Manager#async.start

• 使用列表(list)存取

• 定时任务• Sidekiq::Scheduled::Poller#async.poll(true)

• 使用有序集合(SortedSet) 存取

13年9月17日星期二

Page 20: Sidekiq 源码分析

普通任务• N 个 worker 并发地读取 Redis,弹出任务

• BRPOP key [key ...] timeout

• redis-rb 是线程安全的,每个命令都是同步过的(使用MonitorMixin)

• 弹出后处理任务,可以使用 middleware

• lib/sidekiq/manager.rb

• lib/sidekiq/fetch.rb

13年9月17日星期二

Page 21: Sidekiq 源码分析

定时任务• 使用有序集合保存队列

• ⼀一个Actor轮询 schedule 有序集合。先用 ZRANGEBYSCORE 从 schedule 查出⼀一个任务,接着用 ZREM 删除 schedule 中的这个任务,再把该任务使用 LPUSH 放回对应队列的列表(List)。

• ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

• 有序集合的 score 为执行时刻

• 返回执行时刻在 min 和 max 之间的定时任务(但没有从有序集合删除)

• ZREM key member [member ...]

• 移除有序集合里的成员

• 从有序集合删除定时任务

• lib/sidekiq/scheduled.rb

13年9月17日星期二

Page 22: Sidekiq 源码分析

任务小结

• 读取并从队列(List)中删除元素,是原子性的操作。使用 List 存取普通任务。

• 使用有序集合(SortedSet)存取定时任务,先查询,再从有序集合删除,然后放回对应的普通队列。定时任务并不是定时执行,而是定时放入普通任务队列中。

13年9月17日星期二

Page 23: Sidekiq 源码分析

Middleware

• lib/sidekiq/middleware/chain.rb

• 举例• Sidekiq::Middleware::Server::RetryJobs

• sidekiq-failures

• kiqstand

• 在每个任务完成后断开 mongoid 的连接

13年9月17日星期二

Page 24: Sidekiq 源码分析

用法 Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Kiqstand::Middleware end end Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Sidekiq::Failures::Middleware end end

13年9月17日星期二

Page 25: Sidekiq 源码分析

链式调用实现(1)

Sidekiq.server_middleware.invoke(worker, msg, queue) do worker.perform(*cloned(msg['args'])) end

13年9月17日星期二

Page 26: Sidekiq 源码分析

链式调用实现(2) def invoke(*args, &final_action) chain = retrieve.dup traverse_chain = lambda do if chain.empty? final_action.call else chain.shift.call(*args, &traverse_chain) end end traverse_chain.call end

13年9月17日星期二

Page 27: Sidekiq 源码分析

任务重试

• Sidekiq::Middleware::Server::RetryJobs

• lib/sidekiq/middleware/server/retry_jobs.rb

• 使用middleware实现

13年9月17日星期二

Page 28: Sidekiq 源码分析

任务重试• 有异常后,给任务添加更多的重新信息,然后保存在有序集合 “retry” 中作为定时任务执行(处理方式和定时任务相同)

• 最大重试次数

• retry_attempts_from(msg['retry'], DEFAULT_MAX_RETRY_ATTEMPTS)

• 在任务中保存重试信息

• retry_count、retried_at 等

• 重试的时间

• seconds_to_delay(count)

13年9月17日星期二

Page 29: Sidekiq 源码分析

任务失败

• 默认重试25次,该任务将不再重试

• 给 worker 定义 retries_exhausted 方法,在终止重试后执行

13年9月17日星期二

Page 30: Sidekiq 源码分析

任务重试小结

• 重试任务使用名称为 retry 的有序集合,将失败的任务转换成定时任务

13年9月17日星期二

Page 32: Sidekiq 源码分析

多线程下的日志

• Ruby 标准库的 Logger 是线程安全的

• 使用 MonitorMixin

• lib/ruby/1.9.1/logger.rb

13年9月17日星期二

Page 33: Sidekiq 源码分析

其他

• 如何并发的?• Celluloid

13年9月17日星期二

Page 34: Sidekiq 源码分析

参考资料

• https://github.com/mperham/sidekiq

• https://github.com/celluloid/celluloid

• Working with Ruby Threads

• Working With Unix Processes

13年9月17日星期二

Page 35: Sidekiq 源码分析

谢谢!

13年9月17日星期二