浩文 联系 关于本站 登录 注册

问题和分析解决

commit信息:Updated Problems and Troubleshooting (markdown) | 提交者:mperham | 提交时间:2020-05-13 | 版本:12f5015

帮助!

阅读以下提示。如果仍需要帮助,你可以:

不应该私自发email给任何Sidekiq贡献者。请尊重我们的时间和精力,使用以上两个建议之一。记着,Sidekiq是自由的、开源的软件:支持是得不到保障的,它依赖有效的Sidekiq贡献者来提供最大努力的支持。Sidekiq Pro和企业版客户能得到支持保障。

线程

Sidekiq是多线程的,因此你的作业必须是线程安全的。

只使用线程安全的库

在我的印象中最流行的Rubygems都是线程安全的,有一些例外...

一些线程不安全的gems,不要使用:

  • right_aws
  • aws-s3
  • basecamp
  • therubyracer #270

一些gems可能是令人讨厌的:

编写线程安全的代码

构造良好的代码通常不用任何修改就是线程安全的。请使用实例变量和实例方法,绝不要使用类变量。在启动时请求所有需要的类,这样在执行作业时,你就不用请求代码了。

执行子进程

做一些像是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
  • 在autoloadpaths或者eagerload_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将不会被调用。testaftercommit被用来帮助解决该问题。这在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在SERVERNAME和HTTPHOST之间提供一个检测

你必须确保你的服务器在SERVERNAME和HTTPHOST中发送相同的值。
使用Nginx,例如,可以使用catch-all配置而不是给$httphost设置SERVERNAME。
Heroku已经在发送良好的header。

线程不处理作业

如果你正在从Resque迁移到sidekiq,请确保Redis数据库不包含任何老旧的任务。你可以使用redis-cli flushall来彻底地清理所有数据库中的所有keys和values。

另一个普遍的问题:你或许在 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_batchesin_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进程里的任何内存泄露。

上一篇: 分片