Uma solução para isso é usar o recurso de followers que já está bem documentado. Quando você cria uma instância de Heroku Postgresql ele vai criar duas variáveis de ambiente baseado numa cor, por exemplo, HEROKU_POSTGRESQL_RED_URL e DATABASE_URL apontando pro endereço da máquina do Postgresql na Amazon AWS EC2.
Um follower é um banco slave do Postgresql, cujo endereço vai ser apontado numa outra variável de ambiente como, por exemplo, HEROKU_POSTGRESQL_AMBER_URL (digite o comando heroku config para ver essas variáveis). Um follower sempre vai estar pelo menos alguns milissegundos atrasado em relação ao banco master. Você sempre precisa se lembrar que nunca deve depender de um dado lido de um follower logo depois de ter feito o comando de Insert/Update.
No conceito básico, toda escrita deve ser feita somente no banco master (e ela sempre será gargalo de escrita, caso você tenha volumes muito estúpidos de escrita - e nesse caso vai ter que cair em outras opções que não é escopo deste post). E as leituras podem ser divididas em múltiplos slaves/followers ("slave" se você for old-school, "follower" se você for politicamente correto).
Porém uma aplicação Rails normal só sabe do banco que está apontado na variável DATABASE_URL, então como fazer para dividir as leituras no follower? Para isso existe a gem Octopus, criada pelo Thiago Pradi e bem documentada pelo próprio Heroku. Resumindo o que já está documentado, comece adicionando a gem no Gemfile:
1 |
gem 'ar-octopus', require: 'octopus' |
Rode o bundle install e agora baixe os seguintes arquivos:
1 2 |
curl -L -o config/shards.yml https://gist.github.com/catsby/6923840/raw/0aaf94ccc383951118c43b9b794fc62e427c2e51/shards.yml curl -L -o config/initializers/octopus.rb https://gist.github.com/catsby/6923632/raw/87b5abba2e22c3acf8ed35d06e0ab9ca1bd9f0d0/octopus.rb |
Com isso feito, em desenvolvimento o master e o follower vão ser o seu banco normal de desenvolvimento. Em produção no Heroku ele vai usar os bancos HEROKU_POSTGRESQL*URL que não são o DATABASE_URL como followers.
Abra o arquivo config/initializers/octopus.rb e adicione o seguinte método:
1 2 3 4 5 6 7 |
module Octopus ... def self.random_follower followers.sample.to_sym end ... end |
O que acontece é o seguinte, você pode simplesmente adicionar o seguinte método a um model que queira dividir a carga entre escrita no master e leitura no follower:
1 2 3 4 |
class User replicated_model ... end |
Nunca coloque o método acima em todos os models indiscriminadamente. Você quer controlar em quais followers quer ler o que. No caso comum basta dividir entre todos os followers, mas caso tenha queries hiper-pesadas e queira dedicar um follower só pra isso, você também pode.
Um caso de uso comum é pegar workers de Sidekiq que façam muitos pré-cálculos, gerem relatórios ou denormalize tabelas grandes e faça o seguinte:
1 2 3 4 5 |
# query pesada normal: ModelPesado.includes(:abc, :xyz).scope_pesado.group(:foo).find_each { |m| m.algo_pesado } # query modificada pra rodar num follower: ModelPesado.using(Octopus.random_follower).includes(:abc, :xyz).scope_pesado.group(:foo).find_each { |m| m.algo_pesado } |
Sem o método Octopus.random_follower você teria que manualmente digitar o nome do follower que quer usar baseado na cor de sua variável de ambiente, por exemplo o HEROKU_POSTGRESQL_AMBER_URL você faria: .using(:amber_follower). Com o método ele vai devolver o único follower que você tem ou aleatoriamente entre a lista de followers que você tem.
Outra forma é o seguinte:
1 2 3 |
Octopus.using(Octopus.random_follower) do ModelPesado.includes(:abc, :xyz).scope_pesado.group(:foo).find_each { |m| m.algo_pesado } end |
Uma coisa que parece necessária, como foi no caso de uma issue do DelayedJob se por algum motivo você precisar rodar um comando como Update dentro de um find_by_sql, lembre-se de explicitamente colocá-la no escopo do banco master:
1 2 3 |
Octopus.using(:master) do Model.find_by_sql("UPDATE ...") end |
Como disse antes, isso não é uma solução para que você jogue tudo pro follower. Avalie no próprio dashboard do seu banco de dados quais são as queries pesadas e no New Relic e comece movendo somente essas queries primeiro. Performance não é algo que você codifica sem medir: senão como saber se o que você fez realmente fez alguma diferença? Meça antes e meça depois. E continue nesse ciclo.
A gem Octopus e o recurso de Followers do Heroku Postgresql são excelentes opções para mover queries pesadas e descarregar seu banco master principal, o que vai garantir que sua aplicação possa escalar muito mais com poucas mudanças de código.