Este é um simples exemplo de projeto trabalhando com replicação read/write. Com o objetivo de tornar simples o entendimento, a aplicação conta apenas com um pequeno cadastro de contatos.
Para simular dois bancos de dados distintos, subi duas imagens do container wnameless/oracle-xe-11g no Docker. Por convenção subi as portas com um intervalo de 100.
docker run -d -p 49160:22 -p 49161:1521 wnameless/oracle-xe-11g
docker run -d -p 49260:22 -p 49261:1521 wnameless/oracle-xe-11g
Ambas as conexões do oracle estão configuradas no arquivo "tnsnames.ora". Dessa forma você não precisa passar a configuração completa da conexão na aplicação, apenas o tnsname que deseja.
Nota:
- A conexão por TNS depende da variável de ambiente que aponta para o diretório onde o arquivo se encontra: export TNS_ADMIN=/opt/oracle/instantclient_11_2/network/admin
- Antes de conectar com o usuário abaixo você precisa criar o usuário e senha desejado.
SHARED_LIST_DEV =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.99.100)(PORT = 49161))
(LOAD_BALANCE = yes)
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = XE)
(FAILOVER_MODE =
(TYPE = SELECT)
(METHOD = BASIC)
(RETRIES = 180)
(DELAY = 5)
)
)
)
SHARED_LIST_DEV_2 =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.99.100)(PORT = 49261))
(LOAD_BALANCE = yes)
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = XE)
(FAILOVER_MODE =
(TYPE = SELECT)
(METHOD = BASIC)
(RETRIES = 180)
(DELAY = 5)
)
)
)
A conexão com o banco pode ser testada pelo client do oracle sqlplus com o comando:
sqlplus $USERNAME/$PASSWD@$TNSNAME
sqlplus SHARED_LIST_DEV/SHARED_LIST_DEV@SHARED_LIST_DEV
O pooling de conexões esta implementado em apenas um ambiente "development", e é gerenciado pela gem "ar-octopus". O octopus permite você configurar replication e sharding de maneira bem simples.
A base de configuração do octopus é o arquivo shards.yml, que deve ficar na pasta RAILS_APP/config do projeto. Por padrão o master/writer será a conexão no 'database.yml'.
shards.yml
octopus:
replicated: true
environments:
- development
development:
slave1:
adapter: oracle_enhanced
database: SHARED_LIST_DEV_2
username: SHARED_LIST_DEV
password: SHARED_LIST_DEV
Ok, agora que você viu o arquivo deve estar pensando "O que mais eu posso fazer?!". A resposta é, muita coisa. Como o ActiveRecord e o Octopus são bem flexíveis, então vamos as possíveis perguntas:
####Posso configurar bancos diferentes nos slaves?
Sim, basta ter o adapter instalado e passar as configurações nos shards(slaves) desejados.
Por que não? Vamos fazer:
octopus:
replicated: true
environments:
- development
- production
development:
so_leitura:
adapter: oracle_enhanced
database: SHARED_LIST_DEV_2
username: SHARED_LIST_DEV
password: ( ͡° ͜ʖ ͡°)
production:
brazil:
adapter: mysql
database: replication_prod
username: root
password: ( ͡° ͜ʖ ͡°)
usa:
adapter: mysql
database: replication_prod
username: root
password: ( ͡° ͜ʖ ͡°)
Tudo bem, o octopus tem um propriedade chamada fully_replicated que por default é true, mas podemos passar false e apartir de então só os models com o método replicated_model() serão replicados. Exemplos:
#contact.rb
#Esta classe é replicada, escritas na master and leitura na slave.
class Contact < ActiveRecord::Base
replicated_model()
end
#shards.yml
octopus:
replicated: true
fully_replicated: false
environments:
- development
development:
slave1:
adapter: oracle_enhanced
database: SHARED_LIST_DEV_2
username: SHARED_LIST_DEV
password: SHARED_LIST_DEV
Sem problemas, você pode agrupar os servidores ;)
octopus:
replicated: true
environments:
- development
development:
slave_group_a:
slave1:
# ...
slave2:
# ...
slave_group_b:
slave3:
# ...
slave4:
# ...
Por padrão as migrations são rodadas apenas na master, caso você deseje que elas rodem nos slaves/shards, basta especificar quem você quer utilizar. Exemplo:
class CreateContactsUsingBlockAndUsing < ActiveRecord::Migration
using(:brazil, :argentina)
def self.up
using(:usa) do
Contact.create!(:name => "Tio San")
end
Contact.create!(:name => "O Pelé é melhor")
end
def self.down
Contact.delete_all()
end
end
##Using o Octopus Com a replicação configurada você não precisa fazer mais nada, todas as queries de leitura trabalharam com os slaves e as escritas com o master. Porém, você também pode especificar manualmente de onde quer que os dados venham, exemplo:
@brazil_contact = Contact.using(:brazil).find_by_name("Zé")
E se eu quiser trabalhar com grupos:
Contact.using(slave_group: :slave_group_a).all
Pronto, todos os selects estarão rodando nos slaves do grupo especificado.
#That's all =)