Pular para conteúdo

Desempenho

Apple M4 Max. PostgreSQL e MySQL rodam em Docker na mesma máquina; o SQLite escreve no SSD local com synchronous=NORMAL. Medianas; veja o código do benchmark para as faixas de valores.

PostgreSQL MySQL 8.4 SQLite (modernc)
Insert em massa, 10k jobs por chamada ~250k jobs/s ~77k jobs/s ~300k jobs/s
Captura + finalização, 50 por busca ~45k jobs/s ~15k jobs/s ~80k jobs/s
Um servidor, 100 workers, handler no-op ~21k jobs/s ~6k jobs/s ~70k jobs/s
Do enfileiramento até o handler começar p50 3.3ms, p99 6.6ms ~5–20ms com redisbus, sem ele depende do PollInterval p50 0.16ms no mesmo processo

Os números do MySQL são dominados pela latência de commit nesse setup (binlog com sync_binlog=1, 4-6ms por commit); um servidor com um disco mais rápido tem um resultado bem melhor.

Comparado com o River v0.47 no mesmo PostgreSQL, alternando rodadas entre as duas bibliotecas (bench/):

Cenário kiln River (padrão) River (pausa de busca de 1ms)
Insert em massa 251k jobs/s 134k (InsertMany), 222k (InsertManyFast)
8 goroutines inserindo um job por vez 5.1k jobs/s 4.4k jobs/s
Esvaziar 50k jobs no-op, 100 workers 21.2k jobs/s 1.0k jobs/s 20.3k jobs/s
Esvaziar 20k jobs com um handler de 1ms 19.9k jobs/s 1.0k jobs/s 13.7k jobs/s
Do enfileiramento até o handler começar, p50 3.3ms 55ms 4.7ms

Por padrão, o River busca no máximo uma vez a cada 100ms, e é isso que limita ele a cerca de 1k jobs/s; com essa pausa reduzida, o esvaziamento no-op empata, e a latência p99 dos dois ficou ruidosa demais nessa máquina para decidir a favor de um ou de outro.

Sob uma carga parecida com produção

Os números acima são benchmarks de propósito único. Para ver o kiln em algo mais parecido com produção, um pequeno backend de loja roda o mesmo código nos quatro stores: PostgreSQL e MySQL com um processo de API e dois workers, SQLite e memstore em um único processo. Fazer um pedido grava o pedido e, na mesma transação, enfileira um fluxo: um job de pagamento sob Limit{Max: 4, Rate: 20, Per: time.Second, Burst: 5}, depois um e-mail de confirmação e uma nota fiscal que esperam por ele. O gateway de pagamento é um fake que rejeita mais de 20 requisições por segundo ou 4 de uma vez, falha 15% das cobranças e deixa pagamentos PIX pendentes. Os servidores rodam com heartbeat de 2s, DeadAfter de 15s e LeaderTTL de 6s; o MySQL faz poll a cada 200ms e não tem bus.

v0.3.1 v0.4.0
Segundo mais movimentado no gateway, 150 pedidos de 15 clientes até 40 requisições (429s) 24–25 em todos os stores, sem 429
O mesmo, com a linha de limits travada por 1s no meio da execução (PostgreSQL) 40 25
Job de pagamento, do enfileiramento até o início, MySQL p50 684ms 72ms
Continuação, do pai concluído até o início do filho, MySQL p50 61ms 46ms
Os mesmos dois no PostgreSQL 6ms / 5ms 7ms / 6ms
Export retomado depois que o worker levou um kill -9, MySQL 41s 14s
O mesmo com SQLite, onde o único processo reinicia 38s 24s

Jobs resgatados agora reiniciam dentro de 100ms depois do resgate; o resto desse tempo é perceber que o worker sumiu (DeadAfter, mais LeaderTTL quando o servidor morto era o líder). Ao longo das execuções, 25 compradores disputando 5 unidades em estoque sempre terminaram com 5 pedidos, 20 rejeições e nenhum job esquecido para trás de um pedido com rollback, e toda cobrança chegou ao gateway exatamente uma vez.

Sob caos

soak/ roda processos worker contra PostgreSQL ou MySQL pelo tempo que você pedir, enquanto mata eles com SIGKILL, para eles com SIGTERM e reinicia o banco de dados, e depois verifica que todo job chegou a um estado final, que nenhum rodou mais vezes do que suas tentativas mais as execuções interrompidas, que limits e rates se mantiveram, e que o store não precisa de reparo. Os jobs misturam jobs simples, retentativas, jobs que sempre falham, fluxos fan-in que leem as saídas dos pais, jobs lentos, e jobs sob um mutex, um Max, um Rate e os dois.

Uma execução de 15 minutos no PostgreSQL, 4 workers, 50 enfileiramentos por segundo:

Jobs 57.211
Caos 31 SIGKILLs, 22 SIGTERMs, 3 restarts do banco de dados de cerca de 1.5s
Jobs perdidos 0
Jobs executados mais vezes do que o permitido 0
Pico de concorrência por chave limitada igual ou abaixo do seu Max
Store depois da execução contadores de limit em 0, nada para o Sweep reparar

Uma execução de 24 horas no PostgreSQL 17, 4 workers, 20 enfileiramentos por segundo, em um droplet da DigitalOcean com 2 vCPU / 2 GB, com o kiln v0.8.0:

Jobs 2.233.828
Caos 2.886 SIGKILLs, 2.139 SIGTERMs, 360 restarts do banco de dados
Jobs perdidos 0
Jobs executados mais vezes do que o permitido 0
Jobs órfãos resgatados depois de um SIGKILL 2.307
Pico de concorrência por chave limitada igual ou abaixo do seu Max, ao longo de cerca de 85.000 execuções cada
Inícios por janela nas chaves com Rate no máximo 6 de 8 permitidos em 1s, 32 de 34 em 10s
Store depois da execução contadores de limit em 0, nada para o Sweep reparar, esvaziado em 7s
Memória do harness e dos seus 4 workers 44 a 58 MB ao longo das 24 horas

Os workers registraram erros do store em volta de cada restart do banco de dados (capturas, heartbeats, leases), como esperado; nenhum deles custou um job.

cd soak && go run . -duration 24h -rate 20

soak/vps roda a mesma coisa em um servidor que só tem Docker: um compose file com seu próprio PostgreSQL, que guarda o resumo, os logs dos workers, CPU e memória a cada 5 minutos, e um dump do banco de dados em soak/vps/evidence.

Veja soak/README.md para as flags. -container none pula os restarts do banco de dados quando outros testes compartilham o mesmo banco.