问题和分析解决
commit信息:Updated Problems and Troubleshooting (markdown) | 提交者:mperham | 提交时间:2020-10-14 | 版本:b85405c
帮助!
阅读以下提示。如果仍需要帮助,你可以:
- 在Sidekiq Google网上论坛里询问你的问题
- 开启一个GitHub issue。 (不要害怕开启一个issue,即便它不是一个Sidekiq bug。issue只是交流谈话,不是批判交谈。)
你不应该私自发email给任何Sidekiq贡献者。请尊重我们的时间和精力,使用以上两个建议之一。记着,Sidekiq是自由的、开源的软件:支持是得不到保障的,它依赖有效的Sidekiq贡献者来提供最大努力的支持。Sidekiq Pro和企业版客户能得到支持保障。
线程
Sidekiq是多线程的,因此你的作业必须是线程安全的。
只使用线程安全的库
在我的印象中最流行的Rubygems都是线程安全的,有一些例外...
一些线程不安全的gems,不要使用:
- right_aws
- aws-s3
- basecamp
- therubyracer #270
一些gems可能是令人讨厌的:
- typhoeus有许多崩溃历史
- RMagick(查看#338,试着用mini_magick代替)
- sidekiq-statistic - 有一个严重的内存泄露
- mail (请使用
Mail.eager_autoload!
,依据 https://github.com/mikel/mail/issues/912 )
编写线程安全的代码
构造良好的代码通常不用任何修改就是线程安全的。请使用实例变量和实例方法,绝不要使用类变量。在启动时请求所有需要的类,这样在执行作业时,你就不用请求代码了。
执行子进程
做一些像是PDF或者图片处理的事情,把这些事情交给一个额外的程序,这是广泛的需求,但是让任务正确完成可能是非常棘手的。我建议使用一个gem,像是childprocess 或者 posix_spawn来帮助解决此问题。请确保你往命令里添加了超时处理 -- 没人想调试挂起的作业。
Add Timeouts to Everything
如果Sidekiq或者一个作业一直在挂起,它也很可能是因为缺少超时措施。 第一步:使用TTIN信号来从Sidekiq中得到回溯追踪,解密这些输出,那可以给你一个关于发生了什么坏事的好建议。 第二步:遵从Ruby超时终极指南来在必要的地方添加超时处理。
不要使用Ruby的Timeout
模块。你可能会得到随机的难以理解的卡顿或是挂起的进程。
自动加载
Rails自动加载和热加载是引起问题的常见原因,但是记着,Sidekiq做的一切只是引导Rails。如果你有一个问题,它是在代码和Rails之间,Rails是约定优于配置的。如果你在布置类时遵从了Rails代码加载约定,代码应该能正确加载。 请阅读并且学习Rails约定以便加载代码和遵从约定。
一些技巧:
- 在app/models/foo/bar.rb中的
Foo::Bar
应该在扩展模式中定义,像这样:
module Foo
class Bar
- 在autoload_paths或者eager_load_paths中不要配置额外的地址。那是一个骇客方式。遵从该约定!
任何在
app/
下的文件夹可以包含Ruby 代码,你不需要显示地配置任何东西。 lib/
文件夹只会造成痛苦。把代码移动到app/lib/
,并且确保代码内部遵从了class/filename约定。- 查看常见的陷阱。
- 请查看更详细的关于Rails内部自动加载和线程安全issues的内容。
不要开启一个Sidekiq加载问题issue,除非你在Sidekiq内发现了一个bug。
作业状态轮询在开发环境里工作,在生产环境里不工作
如果你周期地轮询模型(例如,来自ajax请求)以便确定后台作业是否完成,然后如果后台作业在一秒内完成,你可能会遇到一个issue,作业轮询逻辑在开发环境中工作,但是在产品环境中会时灵时不灵。
使用rails的Rails.cache
可能会引发该问题。默认,Model.cache_key
只精确到秒。在同一秒内完成的更新,可能造成状态轮询返回一个过时的记录。一些数据库中支持sub-second时间值(像postgres),在config/application.rb
中设置config.active_record.cache_timestamp_format = :nsec
,以便提高缓存精确度,避免使用过时的记录。
“不能使用ID=12345来找到模型名”
Sidekiq是非常快的以至于它能轻松的触发事务竞争条件(一个作业试着获取一个还没有完成提交的数据库记录)。简洁的解决方法是使用after_commit
:
class User < ActiveRecord::Base
after_commit :greet, on: :create
def greet
UserMailer.delay.send_welcome_email(self.id)
end
end
注意:如果开启了use_transactional_fixtures
,则在Rails v5.0以前的版本中的测试after_commit
将不会被调用。test_after_commit被用来帮助解决该问题。这在Rails v5.0+中是不需要。
如果你没有使用ActiveRecord模型,请在确保在事务提交完成后,使用计划执行:
MyWorker.perform_in(5.seconds, 1, 2, 3)
无论哪种方式,Sidekiq重试机制会挽回你的作业。第一次执行或许会由于‘RecordNotFound’而失败,但是重试将会执行成功。
Heroku "客户端达到的最大错误数"
你已经触及到了被允许的最大Redis链接数。
在config/sidekiq.yml中限制每个进程的redis链接数。例如,如果你正在使用Redis To Go的自由Nano方案,并且想要使用Sidekiq web 客户端,你将必须调低并发数到3。
:concurrency: 3
关于该主题,请参阅#117。请参阅这个计算器,它能帮你确定一个正确的并发数量。
Sidekiq Web 不可登录,并且Rails 返回ActionController::RoutingError错误
Sidekiq::Web构建在Rack::Builder之上。它使用Rack::URLMap来映射endpoints。
URLMap在SERVER_NAME和HTTP_HOST之间提供一个检测。
你必须确保你的服务器在SERVER_NAME和HTTP_HOST中发送相同的值。
使用Nginx,例如,可以使用catch-all配置而不是给$http_host设置SERVER_NAME。
Heroku已经在发送良好的header。
线程不处理作业
如果你在生产环境开启了自动加载,Sidekiq将会上锁。代码重载只在开发环境工作。在config/environments/production.rb
文件中:
config.cache_classes = true
config.eager_load = true
另一个普遍的问题:你或许在 Sidekiq.configure_server
中定义了一个命名空间,但是没有在Sidekiq.configure_client
中定义,或者把它在Sidekiq.configure_client
中命名为其它名字。请确保两边的配置都一样!
一些人遇到过由rspec-sidekiq
造成的另一个issue,你需要确保 rspec-sidekiq
只在test
组下面:
group :test do
gem 'rspec-sidekiq'
end
在这里http://stackoverflow.com/a/17065723/1965817有相关的stackoverflow问题
不能在5秒内获取到数据库链接
如果你的ActiveRecord连接池比Sidekiq的并发数小,会经常得到一些链接超时错误。你需要确保config/database.yml
文件的pool
属性与Sidekiq的并发数相同。记着,database.yml文件可以包含ERB,所以你可以这样做:
pool: <%= ENV['RAILS_MAX_THREADS'] || Sidekiq.options[:concurrency] %>
下面是一个很好的来自@adamlogic的Youtube视频,讲述如何解决这些错误:
Postgres链接损坏
如果你看到奇怪的postgres链接错误,请试着使用ActiveRecord清理器来清理链接。添加下面代码到你的database.yml文件:
reaping_frequency: 10
我的Sidekiq进程消失了!?
如果你的机器是运行在较低的内存上并且不能swap,Linux的OOM killer或许会杀死Sidekiq。可以使用dmesg | egrep -i 'killed process'
命令来检索OOM的活动:
[102335.319388] Killed process 6567 (ruby) total-vm:1333004kB, anon-rss:355088kB, file-rss:688kB
解决该问题的方法是获取更多的内存或者优化你的workers。请参阅下面的内存膨胀来获取解决技巧。
我的Sidekiq进程崩溃了,我该怎么做?
只有两种情况能造成Ruby VM崩溃:一个是VM bug,另一个是本地的gem bug。Sidekiq使用纯粹的Ruby,所以不会造成Ruby VM的崩溃。下面是一系列注意事项:
- Ruby可能存在一个bug - 确保你正在运行最新版的Ruby
- 本地gem bugs会造成崩溃 - 为了你有最新的补丁,请确保你正在运行的所有本地gems是最新版本
- 每次Sidekiq崩溃,任何正在处理的mesages将会丢失。你可以使用Sidekiq Pro的Reliability功能来阻止发生丢失。
你可以使用这条命令来获取所有本地gems的列表:
bundle exec ruby -e 'puts Gem.loaded_specs.values.select{ |i| !i.extensions.empty? }.map{ |i| i.name }'
一些作业神秘地消失了或者没有任何日志地失败了
通常这是因为一个老旧的、残留的Sidekiq进程依旧在运行。请确保杀死了老的进程。
也存在这种情况,如果你在多应用server中没有正确设置每一个sidekiq实例的redis namespaces,就可能出现这个issue。
为什么Sidekiq没有使用所有有效的核心
无关有多少的线程,每一个运行在MRI上的Sidekiq进程只使用一个核心。为了利用多核性能,你应该运行多个Sidekiq进程。Sidekiq企业版可以使用多进程功能来自动开启多进程。
内存膨胀
如果你有一个内存膨胀问题,也就是说,你的Sidekiq进程随着时间地推移占用的内存从很少MB到很多地MB,造成这种情况可能存在多种原因。下面是我了解到的一些原因。
ActiveRecord queries
在active record里写一个低效的查询是非常容易的, 就像加载1000多个不必要的节点。例如:
# See if product search returns no results
# Terrible, do not do this!
return "No results" unless Product.search(...).blank?
如果一个product查询了1万条记录,这个查询将创建1万个对象,然后立马把它们抛出。这会扩张堆内存,并且造成VM膨胀。想要了解Ruby堆内存是如何工作的详细信息,请查阅这些幻灯片
正确的方式:
# See if product search returns no results
# Much faster!
return "No results" if Product.search(...).count == 0
不幸的是,你需要自己探究哪个worker或者查询造成了内存膨胀。另一个例子:
糟糕,这会在内存里加载数百万个user对象:
User.all.each { |u| u.something }
正确的方法是每次枚举1000个users:
User.find_each { |u| u.something }
简短来说,很容易出现active record的低效使用。阅览你的查询,确保准确地理解它们做了什么。
ActiveRecord查询缓存
如上诉述,即使正确地执行了批量读取,Active record查询缓存通过储存不必要的查询结果,也可能会造成内存膨胀。从Rails 5.0开始,后台作业(包括Sidekiq workers)的查询缓存被默认打开。如果你的作业承载了大量的批量读取,然后它会使用很多内存,试着关闭查询缓存,或者手动清理查询缓存:
ActiveRecord::Base.uncached do
User.find_each { |u| u.something }
end
# Clear query cache
User.find_in_batches.each do |users|
users.each { |u| u.something }
ActiveRecord::Base.connection.clear_query_cache
end
从Rails 6.0.2.1开始,查询缓存会跳过find_each
, find_in_batches
和 in_batches
查询(请查看 https://github.com/rails/rails/pull/28867) 。尽管这样,在这些方法块中的代码仍然会使用查询缓存,所以依赖你的实现,你仍可能找到一些合适的地方来遵从以上的指导方针。
内存碎片
在Linux上,Ruby使用默认的glibc实现来分配内存。该实现很容易造成内存碎片化并且引发大的内存膨胀。最简单的处理方法是给你的Ruby进程添加MALLOC_ARENA_MAX=2
环境变量。更完整的解决方案是选择jemalloc。关于该主题的更多信息,请查看我的博客推文和Nate Berkopec 的博客推文。
冻结的进程
如果你的Sidekiq进程没有负载任何work,给它发送信号以便把回溯(backtraces)输出到日志文件里。那样能显示这些线程被卡在哪里。常见的是远程网络调用挂起:
- DNS查询 - 可能停止解析主机名了。
- Net::HTTP - 没有响应的远程服务器会造成Net::HTTP调用挂起和worker线程被长时间暂停。请设置
open_timeout
来确保你的代码能触发一个异常而不是一直挂起。
经验法则:使用TTIN信号来发现线程在哪里被阻塞,然后确保这些调用设置了合适的超时限制。
如果Sidekiq进程根本不响应信号(当你发送TTIN信号时在日志中不显示任何东西),你可以用GDB来转移所有线程的回溯(backtraces):
sudo gdb `rbenv which ruby` [PID]
<snip>
(gdb) info threads
Id Target Id Frame
37 Thread 0x7f8b289d8700 (LWP 7994) "ruby-timer-thr" 0x00007f8b27a20d13 in *__GI___poll (fds=<optimized out>, fds@entry=0x7f8b289d7ec0, nfds=<optimized out>,
nfds@entry=1, timeout=timeout@entry=100) at ../sysdeps/unix/sysv/linux/poll.c:87
36 Thread 0x7f8b23eb0700 (LWP 7995) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
35 Thread 0x7f8b23c2e700 (LWP 7996) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
34 Thread 0x7f8b239ac700 (LWP 7997) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
33 Thread 0x7f8b237aa700 (LWP 7998) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
32 Thread 0x7f8b28844700 (LWP 8002) "SignalSender" sem_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/sem_wait.S:86
31 Thread 0x7f8b1e1bf700 (LWP 8003) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
30 Thread 0x7f8b1e0be700 (LWP 8006) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
29 Thread 0x7f8b1dd81700 (LWP 8009) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
28 Thread 0x7f8b1dc80700 (LWP 8010) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
27 Thread 0x7f8b1db7f700 (LWP 8011) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:162
...
(gdb) set logging file gdb_output.txt
(gdb) set logging on
(gdb) set height 10000
(gdb) t a a bt
(gdb) quit
总结gdb_output.txt文件的输出信息然后开启一个Sidekiq issue。
通过在GDB中运行这个命令你可以得到当前(挂起的)线程的Ruby回溯。注意:它将会输出到进程的标准输出,那可能是一个日志文件,它将颠倒正常的ruby回溯。
(gdb) call (void)rb_backtrace()
下面有一个极好的有关通过Ruby来使用gdb的博客推文。
修复活跃的竞争条件,它会小概率地引起程序徘徊在Busy选项卡。[#2982]
要清理延迟的程序,请根据需要进行修改以便连接到Redis。延迟的程序应该在60秒后从Busy页面消失。
require 'redis'
r = Redis.new(url: "redis://localhost:6379/0")
# uncomment if you need a namespace
#require 'redis-namespace'
#r = Redis::Namespace.new("foo", redis: r)
r.smembers("processes").each do |pro|
r.expire(pro, 60)
r.expire("#{pro}:workers", 60)
end
Bundler无法获取specs
如果你正在使用商业版Sidekiq,你可能会得到Bundler::HTTPError Could not fetch specs from https://<hostname>
错误。那通常是由于你的商业订阅过期了。最简单的解决方案是在这个付费网站采购一个新的订阅。
如果你确定你的订阅仍在服务期内,确保你没有一个防火墙规则来阻碍获取,并且使用 DEBUG=1 bundle install
来开启Bundler调试以便得到详细的信息。您还可以通过Sidekiq状态页来验证gem服务器是否可用。
拜托,没有内存泄漏issues
我不接受通常的sidekiq内存泄漏issues.内存泄漏可能是由你的应用里的Ruby VM的任何部分或者gem造成。除非你能展示sidekiq是问题根源的证据,否则请不要开启一个issue。就像已经被展示过的ActiveRecord的查询缓存会造成内存膨胀(请查阅上方)。
参阅Sam Saffron的关于内存泄漏的博文以便了解如何检测和跟踪sidekiq进程里的任何内存泄露。
上一篇: 分片