Quando e como configurar uma réplica de leitura no Django

Uma das tarefas mais importantes da engenharia de software é escolher a arquitetura que melhor resolve um problema específico. Uma escolha errada pode resultar em um site que não funciona e afetar seu negócio. Um dos problemas mais comuns das arquiteturas é o desempenho.
Problemas de desempenho costumam aparecer em sistemas grandes e são difíceis de diagnosticar porque é difícil reproduzi-los; às vezes, só ocorrem em um ambiente específico. Podem ser causados por fatores como hardware, rede ou um programa pouco eficiente. Nesta publicação, vamos focar nos problemas de desempenho do banco de dados e em como resolvê-los com réplicas de leitura.
Problemas de desempenho em bancos de dados
O banco de dados é uma das principais fontes de lentidão de um sistema, causada por vários fatores. Um dos mais comuns é um projeto de esquema incorreto. Um banco que não foi projetado considerando as operações previstas provavelmente terá desempenho ruim. Porém, um esquema bem projetado não garante bom desempenho; outros fatores, como o número de conexões, podem afetá-lo. Se milhares de usuários usam um único banco de dados, ele receberá muitas conexões. Quando são numerosas demais, não há recursos suficientes para atender a todas em tempo real, causando problemas de desempenho. Felizmente, há uma solução: as réplicas.
Réplicas em bancos de dados
A replicação em software e hardware consiste em usar várias cópias de um recurso para garantir desempenho, disponibilidade e tolerância a falhas. Em bancos de dados, falamos de vários servidores trabalhando com os mesmos “dados”. Existem diversas configurações de replicação, por exemplo:
-
Todas as instâncias permitem leitura e escrita
-
Uma única instância principal que permite leitura e escrita, e várias instâncias somente de leitura . Nesse esquema, a replicação e a sincronização dos dados podem ser síncronas ou assíncronas.
Nesta publicação, vamos falar da segunda configuração, supondo que a réplica de leitura é atualizada de forma síncrona.
Configurar réplicas de leitura em uma aplicação Django
Para configurar e usar réplicas de leitura em uma aplicação Django, precisamos cuidar destes pontos:
-
Declarar as instâncias de réplica de leitura como bancos de dados na aplicação Django
-
Configurar um roteador para escolher as réplicas de leitura quando necessário
Declarar as instâncias de réplica de leitura como bancos de dados
Para permitir seu uso, precisamos declará-las como bancos adicionais no arquivo de configurações do Django. Esse passo é fácil e pode ser feito seguindo a documentação oficial sobre múltiplos bancos de dados.
A ideia é definir a instância principal, o servidor de leitura e escrita, como banco default, e declarar as réplicas como bancos adicionais. Por exemplo, se configuramos duas réplicas, o arquivo deve ficar assim:
DATABASES = {
'default': {
'NAME': 'master',
'ENGINE': 'django.db.backends.postgresql',
'HOST': 'host_to_the_master_instance'
'USER': 'master_postgres_user',
'PASSWORD': 'master_password'
},
'replica_1': {
'NAME': 'replica_1',
'ENGINE': 'django.db.backends.postgresql',
'HOST': 'host_to_the_replica_1_instance'
'USER': 'replica_1_postgres_user',
'PASSWORD': 'replica_1_password'
},
'replica_2': {
'NAME': 'replica_2',
'ENGINE': 'django.db.backends.postgresql',
'HOST': 'host_to_the_replica_2_instance'
'USER': 'replica_2_postgres_user',
'PASSWORD': 'replica_2_password'
},
}
Depois dessa configuração, as duas réplicas devem estar disponíveis na aplicação Django. Para verificar, podemos usar o método using em um queryset. Por exemplo, podemos testar o acesso ao modelo User a partir de todos os bancos, tanto o principal quanto as réplicas.
# The users are retrieved from the master instance
User.objects.all()
# The users are retrieved from the replica 1 instance
User.objects.using('replica_1').all()
# The users are retrieved from the replica 2 instance
User.objects.using('replica_2').all()
Também podemos especificar qual banco usar ao salvar um objeto. Se tentarmos salvá-lo em uma réplica, receberemos um erro porque a instância só permite operações de leitura.
# To save the instance in the default database.
# The operation is OK
u = User(email='user@user.com')
u.save()
# Try to save the instance in the replica instance.
# The operation will throw an error
u = User(email='user@user.com')
u.save(using='replica_1')
Configurar um roteador para escolher as réplicas de leitura quando necessário
Com o passo anterior, a aplicação Django tem as réplicas disponíveis e podemos acessá-las manualmente quando necessário. Porém, essa solução não escala, pois é responsabilidade do desenvolvedor identificar operações somente de leitura e escolher a réplica. Além disso, se criarmos mais réplicas no futuro, precisaremos revisar todo o código e escolher manualmente qual usar em cada operação.
A alternativa é um roteador de bancos de dados do Django. Podemos criar um roteador personalizado que use a instância padrão para escrita e escolha uma réplica aleatória para leitura. Um exemplo é CustomRouter:
import random
class CustomRouter:
def db_for_read(self, model, **hints):
"""
Return one of the replicas
"""
return random.choice(['replica_1', 'replica_2'])
def db_for_write(self, model, **hints):
# Always return the default database
return 'default'
def allow_relation(self, obj1, obj2, **hints):
return True
def allow_migrate(self, db, app_label, model_name=None, **hints):
return True
Depois, precisamos indicar nas configurações do Django que queremos usar esse roteador em vez do padrão.
DATABASE_ROUTERS = ['path.to.CustomRouter’]
Após essas mudanças, todas as operações de leitura usarão as réplicas, enquanto as de escrita usarão a instância principal.
Em resumo, habilitar réplicas de leitura no Django é bastante simples e pode melhorar o desempenho. Como mostramos, as únicas mudanças necessárias são de configuração; o restante do código continuará igual. Esperamos que esta publicação ajude você a entender como usar réplicas em uma aplicação Django e conseguir um desempenho melhor.