Mas antes de explicar a solução é preciso demonstrar o problema. O caso é que embora tenhamos threads no ruby, alguns drivers como o do mysql são bloqueantes, ou seja, quando estão em uma operação de IO eles bloqueiam o processo inteiro, inclusive todas suas threads. Veja por exemplo o seguinte código:
class TestesController < ApplicationController
def index
Thread.new { Teste.connection.execute("insert into testes (id) select sleep(2)") }
render :text => 'ok'
end
end
Teoricamente ao fazermos a requisição a este controller de teste, a requisição não deveria durar os dois segundos de espera pelo retorno do insert ao mysql. Mas não é isso que acontece. As requisições acabam sendo enfileiradas pois o processo inteiro fica bloqueado a cada execução de query no banco. Isso pode ser comprovado utilizando o Apache Benchmark.
> ab -c 10 -n 10 http://localhost:3000/testes
...
Concurrency Level: 10
Time taken for tests: 20.514 seconds
Complete requests: 10
...
É bom deixar claro que o bloqueio ocorre somente ao usar o método execute do driver, que é responsável por fazer atualizações no banco. Fazendo somente consultas, o bloqueio não ocorre.
Na palestra em questão, foi demonstrado que utilizando Event Machine e Fibers é possivel desbloquear o processo utilizando callbacks. No final o tempo total foi reduzido para 2 segundos e alguns milésimos. Esse mesmo resultado eu obtive configurando um nginx com passenger configurado com 10 forks. Uma solução bem mais limpa. São apenas dois parametros, um do nginx, e outro do passenger:
worker_processes 10;
#...
http {
passenger_max_pool_size 10;
}
E o resultado do teste:
> ab -c 10 -n 10 http://localhost/testes
Concurrency Level: 10
Time taken for tests: 2.424 seconds
Complete requests: 10
É claro que ainda sim a solução proposta na palestra é mais performática, até porque nela o consumo de memória é bem menor. Resta saber se essa economia vale a pena quando se pesa na balança o custo de manter um código mais complicado e os problemas que a concorrência podem trazer ao seu projeto. E para demonstrar o quão escalável é dividir as requisições em processos, fiz ainda um ultimo teste, com 1000 requisições, sendo 200 simultâneas, configurando o nginx e o passenger para trabalhar com 200 forks:
> ab -c 200 -n 1000 http://localhost/testes
Concurrency Level: 200
Time taken for tests: 13.043 seconds
Complete requests: 1000
Ou seja, o tempo total de teste manteve-se estável. Levando- em conta que dificilmente alguém fará um insert no banco com sleep, acredito que esssa seja uma boa solução para o problema de IO bloqueante. Ao invés de threads, utilizar processos.