Nos últimos anos Sidekiq se tornou uma das principais ferramentas relacionadas à processamento assíncrono na comunidade Ruby.
Trabalhei com suas versões open source, pro e enterprise, e após mais de 3 bilhões de jobs processados aprendi algumas lições, algumas à duras penas.
Nesta palestra irei falar sobre Sidekiq, suas versões e sobre pequenos detalhes que fazem toda a diferença quando o utilizamos em larga escala.
9. class ImagesController < ApplicationController
def create
@image = Image.create(params[:image])
@image.process
end
end
operação síncrona bloqueante
10. class ImagesController < ApplicationController
def create
@image = Image.create(params[:image])
ImageProcessor.perform_async(@image.id)
end
end
operação assíncrona não bloqueante
18. diversas formas de executar
sidekiq # executa com a configuração padrão
sidekiq -c 25 # 25 de concorrência
sidekiq -q default -q high # executa duas filas
24. class NamedParamsWorker
include Sidekiq::Worker
def perform(a: , b: )
...
end
end
RSpec.describe NamedParamsWorker do
describe '#perform' do
it 'prints information' do
subject.perform(a: 1, b: 1)
end
end
end
seus testes unitários podem te enganar
class HashParamsWorker
include Sidekiq::Worker
def perform(params)
logger.info params[:a]
end
end
RSpec.describe HashParamsWorker do
describe '#perform' do
it 'prints information' do
subject.perform(a: 1, b: 1)
end
end
end
25. RSpec.describe NamedParamsWorker do
describe '#perform' do
it 'prints information' do
subject.perform(a: 1, b: 1)
end
end
end
RSpec.describe HashParamsWorker do
describe '#perform' do
it 'prints information' do
subject.perform(a: 1, b: 1)
end
end
end
seus testes unitários podem te enganar
26. require 'sidekiq/testing'
RSpec.describe NamedParamsWorker do
describe '#perform' do
it 'prints information' do
Sidekiq::Testing.inline! do
described_class.perform_async(a: 1, b: 1)
end
end
end
end
teste utilizando chamada inline, funciona?
27. RSpec.describe HashParamsWorker do
describe '#perform' do
it 'prints information' do
described_class.perform_async(a: 1, b: 1)
described_class.drain
end
end
end
utilize o .drain para simular as chamadas
reais à API do Sidekiq
29. class ReportWorker
include Sidekiq::Worker
def perform(until_date)
objects = Model.where('created_at <= ?', until_date)
end
end
# Fri, 20 Oct 2017 19:00:00 BRST -02:00
ReportWorker.perform_async(1.day.ago)
o que acontece se:
32. class ReportWorker
include Sidekiq::Worker
def perform(until_date)
parsed_time = Time.zone.parse(until_date)
objects = Model.where('created_at <= ?', parsed_time)
end
end
sempre que receber uma data,
faça parse dela no timezone atual
35. salvando histórico de intenções de compra
class PurchasesController < ApplicationController
def new
@product = Product.find(params[:product_id])
StorePurchaseIntentWorker.perform_async(current_user.id,
@product.id)
end
end
36. salvando histórico de intenções de compra
class StorePurchaseIntentWorker
include Sidekiq::Worker
def perform(user_id, product_id)
PurchaseIntent.create(user_id: user_id,
product_id: product_id)
end
end
37. salvando histórico de intenções de compra
class StorePurchaseIntentWorker
include Sidekiq::Worker
def perform(user_id, product_id)
PurchaseIntent.create(user_id: user_id,
product_id: product_id)
end
end
38. salvando histórico de intenções de compra
class StorePurchaseIntentWorker
include Sidekiq::Worker
def perform(user_id, product_id, created_at)
PurchaseIntent.create(
user_id: user_id,
product_id: product_id,
created_at: Time.zone.parse(created_at)
)
end
end
40. processando compras com cartão de crédito
class CreditCardPurchaseWorker
include Sidekiq::Worker
def perform(purchase_id, number, name, expiration_date, cvc)
...
end
end
42. credit_card_params = {
number: '4444333322221111', name: 'Joao Silva',
expiration_date: ’10/20', cvc: '123'
}
json_credit_card_params = credit_card_params.to_json
SecureCreditCardPurchaseWorker.perform_async(
14151,
json_credit_card_params.encrypt(
:symmetric,
password: ENV['SECRET_KEY_BASE']
)
)
criptografando os dados do cartão
43. decriptografando os dados do cartão
class SecureCreditCardPurchaseWorker
include Sidekiq::Worker
def perform(purchase_id, cryptographed_credit_card)
@cryptographed_credit_card = cryptographed_credit_card
credit_card_information = JSON.parse(decrypted_credit_card)
puts credit_card_information['number']
end
private
def decrypted_credit_card
@cryptographed_credit_card.decrypt(
:symmetric,
password: ENV['SECRET_KEY_BASE']
)
end
end
55. crie uma forma fácil de ativar/desativar
logs através de ENV vars
# config/initializers/sidekiq.rb
if (ENV["SIDEKIQ_LOGS_DISABLED"] == "true")
Sidekiq::Logging.logger = nil
end
60. qual o erro aqui?
batch = Sidekiq::Batch.new
products.find_each do |product|
batch.jobs { ProductWorker.perform_async(product.id) }
end
61. agende todos os jobs dentro
de uma única chamada de batch.jobs
batch = Sidekiq::Batch.new
batch.jobs do
products.find_each do |product|
ProductWorker.perform_async(product.id)
end
end
63. o que acontece?
batch = Sidekiq::Batch.new
batch.jobs do
UniqueWorker.perform_async(1)
raise
end
batch = Sidekiq::Batch.new
batch.jobs do
UniqueWorker.perform_async(1)
UniqueWorker.perform_async(2)
end
64.
65. não, o sidekiq só processou 1 job do batch
https://github.com/mperham/sidekiq/issues/3662
67. qual o problema aqui?
class EnqueueInvitationEmailWorker
include Sidekiq::Worker
def perform
batch = Sidekiq::Batch.new
batch.jobs do
while attributes = EmailSerializer.pop
SendInvitationEmailWorker.perform_async(attributes)
end
end
end
end
68. nesse caso é melhor não utilizar batch
class EnqueueInvitationEmailWorker
include Sidekiq::Worker
def perform
while attributes = EmailSerializer.pop
SendInvitationEmailWorker.perform_async(attributes)
end
end
end
71. criptografando parâmetros manualmente
require 'encrypted_strings'
require 'json'
credit_card_params = {}.to_json
encrypted_params = credit_card_params.encrypt(:symmetric, password: ENV['SECRET_KEY_BASE'])
SecureCreditCardPurchaseWorker.perform_async(12, encrypted_params)
class SecureCreditCardPurchaseWorker
include Sidekiq::Worker
def perform(purchase_id, cryptographed_credit_card)
credit_card_information = decrypted_credit_card(cryptographed_credit_card)
end
def decrypted_credit_card(cryptographed_credit_card)
JSON.parse(cryptographed_credit_card.decrypt(:symmetric, password: ENV['SECRET_KEY_BASE']))
end
end
72. configurando a extensão ent encryption
# config/initializers/sidekiq.rb
version = ENV.fetch('SIDEKIQ_CRYPTO_VERSION')
Sidekiq::Enterprise::Crypto.enable(active_version: version) do |version|
Base64.decode64(ENV.fetch("SIDEKIQ_CRYPTO_KEY_V#{version}"))
end unless Rails.env.test?
# .env
SIDEKIQ_CRYPTO_VERSION=2
SIDEKIQ_CRYPTO_KEY_V1=XXXXXXXX
SIDEKIQ_CRYPTO_KEY_V2=YYYYYYYY
73. utilizando ent encryption em um worker
class SecureCreditCardPurchaseWorker
include Sidekiq::Worker
sidekiq_options encrypt: true
def perform(purchase_id, cryptographed_credit_card)
puts cryptographed_credit_card['number']
end
end
credit_card_params = {
number: '4444333322221111', name: 'Joao Silva',
expiration_date: '10/20', cvc: '123'
}
SecureCreditCardPurchaseWorker.perform_async(12341, credit_card_params)
79. PaymentProcessorWorker
queue: critical
ImageProcessorWorker
queue: default
SalesReportWorker
queue: low
Rápido e crítico Lento e não crítico Lento, pesado e não crítico
sidekiq -c 10 -q critical -q default -q low
10 50 1
múltiplas filas priorizadas
Pros Contras
Divisão clara de processos por
níveis de prioridade.
Existe risco de acúmulo de jobs
nas filas de menor prioridade se
houver alto volume de jobs nas
filas mais priorizadas.
80. PaymentProcessorWorker
queue: critical
ImageProcessorWorker
queue: default
SalesReportWorker
queue: low
Rápido e crítico Lento e não crítico Lento, pesado e não crítico
sidekiq -c 10 -q critical,4 -q default,2 -q low,1
10 50 1
múltiplas filas com pesos diferentes
Pros Contras
Com um único processo do
sidekiq você tem priorização
sem que as filas fiquem
esperando umas as outras.
Processos pesados ainda
podem interferir diretamente na
performance de processos
críticos.
81. PaymentProcessorWorker
queue: critical
ImageProcessorWorker
queue: default
SalesReportWorker
queue: low
Rápido e crítico Lento e não crítico Lento, pesado e não crítico
sidekiq -c 10 -q critical
10 50 1
processos isolados para cada queue
Pros Contras
Isolamento completo de cada fila
Controle de escala de cada
queue individual
Sonho?
Maior consumo de memória/
máquina
sidekiq -c 10 -q default sidekiq -c 2 -q low
86. monitorando o tamanho das filas
Sidekiq::Queue.all.each do |queue|
Enjoei::Metrics.event(
'SidekiqQueueReport', name: queue.name, size: queue.size
)
end
87. monitorando processos online
queues_processing = Sidekiq::ProcessSet.new.map do |p|
p['queues'].flatten
end.uniq
queues_processing.each do |queue|
Enjoei::Metrics.event(
'SidekiqOnlineQueueReport', name: queue
)
end
89. PaymentProcessorWorker
queue: critical
ImageProcessorWorker
queue: default
SalesReportWorker
queue: low
Rápido e crítico Lento e não crítico Lento, pesado e não crítico
sidekiq -c 10 -q critical
10 50 1
processos separados por criticidade
Pros Contras
Isolamento completo de filas
mais críticas
Permite escalar filas de forma
diferente
Maior consumo de memória/
máquina
sidekiq -c 10 -q default,2 -q low,1
98. processo com concorrência por causa
de rate limit da API de terceiro
# Antes:
sidekiq -q invoice -c 3
sidekiq -q coupon_reminder_emails -c 10
sidekiq -q counter_cache_update -c 10
# Depois:
sidekiq -q low -c 10
101. período de ociosidade vs concorrência
ociosidade concorrência leve concorrência pesada
102. proposta 1:
unificar processos em fila única
# Antes:
sidekiq -q read_only_default -c 10
sidekiq -q read_only_low -c 10
sidekiq -q read_only_high -c 10
# Depois:
sidekiq -q read_only -c 10
Pros Contras
Única fila para gerenciar
Para escalar basta subir um
novo processo
Não existe prioridade de
execução
Em momento de concorrência
irá somar o tempo de execução
103. proposta 2:
unificar filas por prioridade
# Antes:
sidekiq -q read_only_default -c 10
sidekiq -q read_only_low -c 10
sidekiq -q read_only_high -c 10
# Depois:
sidekiq -q read_only_high
-q read_only_default
-q read_only_low -c 10
Pros Contras
Prioridade na execução
Só executará processos com menos
prioridade quando acabarem os
prioritários
Em momento de concorrência irá somar
o tempo de execução
104. proposta 3:
unificar filas por peso
# Antes:
sidekiq -q read_only_default -c 10
sidekiq -q read_only_low -c 10
sidekiq -q read_only_high -c 10
# Depois:
sidekiq -q read_only_high,4
-q read_only_default,2
-q read_only_low,1 -c 10
Pros Contras
Para escalar basta subir um novo
processo
Irá processar simultaneamente todas
as filas com prioridades diferentes
Em momento de concorrência
irá somar o tempo de execução
105. proposta 4:
fila única e utilizar autoscale
# Antes:
sidekiq -q read_only_default -c 10
sidekiq -q read_only_low -c 10
sidekiq -q read_only_high -c 10
# Depois:
sidekiq -q read_only -c 10
Pros Contras
Única fila para gerenciar
Auto scaling vai aumentar número
de processos para dar vazão a
todas as filas quando houver picos
Sem um limite para auto scaling você
pode acabar consumindo muitos
recursos e derrubar sua base de dados
Pode demorar mais para processar os
jobs em momentos de picos
106. recursos sobre auto scaling
https://engenharia.elo7.com.br/sidekiq-workers/
http://www.pablocantero.com/blog/2013/05/04/auto-scale-sidekiq-workers-on-amazon-ec2/
http://manuelvanrijn.nl/blog/2012/11/13/scalable-heroku-worker-for-sidekiq/
http://blog.honeybadger.io/cleanly-scaling-sidekiq/
118. sidekiq -q default
workers por tipo de acesso aos dados
PaymentProcessorWorker
queue: default
ImageProcessorWorker
queue: default
SalesReportWorker
queue: readonly
SalesMailer.completed
queue: readonly
master
DATABASE_URL=$FOLLOWER_DATABASE_URL
sidekiq -q readonly
follower
replicação de dados
123. o que ocorre quando buscamos o produto?
class ProductMailer < ApplicationMailer
def published(product_id)
@product = Product.find(product_id)
...
end
end
class Product < ActiveRecord::Base
after_create do |product|
ProductMailer.delay.published(product.id)
end
end
124. cuidado com os callbacks do
ActiveRecord::Base
class ProductMailer < ApplicationMailer
def published(product_id)
@product = Product.find(product_id)
...
end
end
class Product < ActiveRecord::Base
after_create do |product|
ProductMailer.delay.published(product.id)
end
end
125. adicione um pequeno delay em
jobs que usam base follower
class ProductMailer < ApplicationMailer
def published(product_id)
@product = Product.find(product_id)
...
end
end
class Product < ActiveRecord::Base
after_create_commit do |product|
ProductMailer.delay_for(10.seconds).published(product.id)
end
end
126. sempre utilize callbacks de commit
class ProductMailer < ApplicationMailer
def published(product_id)
@product = Product.find(product_id)
...
end
end
class Product < ActiveRecord::Base
after_create_commit do |product|
ProductMailer.delay_for(10.seconds).published(product.id)
end
end
129. RubyApp::Workers::ProducerWorker
queue: ruby
ruby_app: sidekiq -q ruby
CrystalApp::Workers::ReceiverWorker
queue: crystal
crystal_app: sidekiq -q crystal
Toda a comunicação entre as
aplicações é feita através de
escritas em filas.
comunicando através de filas
138. nossa solução usando stack da AWS
Substitui a queue
do Sidekiq com
baixa latência.
Substitui os workers e
agrupa “jobs”
automaticamente.
Substitui a base.