Meus 2 centavos para performance no Django Rest

28/05/2021 - 4 min de leitura

DJANGO

A temática de performance no Django é algo muito bem pesquisado e bem desenvolvido pela comunidade. É muito fácil você encontrar posts falando sobre o problema das n+1 queries, uso de prefetch_related e select_related, aggregate, annotate, etc. Irei abordar aqui um caso real que aconteceu no meu trabalho.

Eu estou como desenvolvedor backend Django em uma empresa de automação e chatbots. Recentemente, como em qualquer projeto, nos deparamos com o questionamento: "Como é que eu consigo melhorar a performance da API? Alguns endpoints estão demorando muito 🤔".

Esses endpoints que estavam demorando muito, em específico, tinham um problema: estavam totalmente otimizados com relação ao .values(), carregamento de dados e problema das n+1 queries.

Também não queríamos colocar cache naquele momento, por se tratar de várias rotas com o mesmo problema. Não poderíamos sair botando cache em tudo! Ainda mais porque algumas delas precisariam trazer dados no mais "tempo real" possível. Isso invalidaria a estratégia do cache.

Uma solução diferenciada

Eu e o meu xará Gabriel (meu chefe e amigo) estávamos debatendo sobre o FastAPI, quando eu comentei que vi em algum lugar que o renderizador de JSON do FastAPI era mais rápido que o do Django Rest. A velocidade é um dos motivos adoção dessa ferramenta! Ela é veloz.

Visto isso, o Gabriel encontrou um artigo de performance de bibliotecas que renderizam JSON no Python.

Analisei o artigo e vi que a lib que estava se saindo melhor, naqueles testes, era o orjson (a lib nativa é muito pouco performática em comparação a outras ☹️. O DRF usa ela como padrão). Pra minha surpresa, encontrei algumas implementações do orjson como renderizador alternativo para o Django Rest. A Lib que eu usei foi essa aqui: drf-orjson-renderer.

Ela é simples e fácil de instalar. Só tem que alterar uma linha de configuração do Django Rest.

Resultados

Eu não tenho dados do impacto real na performance do servidor, mas posso garantir que foi grande!

Mas, para não trazer algo "solto" e sem dados nesse post, eu decidi implementar um simples endpoint que retorna alguns dados. Coloquei o arquivo principal do projeto django aqui no gist. Vamos navegando pelas partes principais desse arquivo:

Retorno de um "objeto do model"

def fake_some_model_data() -> dict:
    return {
        "uuid": str(uuid.uuid4()),
        "name": "Algum nome aleatório bem grande",
        "email": "algumemailaleatorio@email.com",
        "birthday": datetime.date(2021, 5, 28),
        "is_premium": True,
        "cash": Decimal(957.78),
        "invoice_date": 28,
    }

A função acima simula o que seria os dados de algum objeto de um modelo no Django.

Já que eu consegui simular os dados, tinha que multiplicar eles. Para ver alguma diferença de performance no processamento, simulei 1 milhão de resultados desse FakeModel para ir para a response do Django Rest. O Código dessa simulação ficou assim:

Simulando 1 milhão de dados retornados numa query

def get(self, request, *args, **kwargs):
    some_big_query_data = []
    for _ in range(1000000):
        some_big_query_data.append(fake_some_model_data())
    return Response(some_big_query_data)

Vamos aos resultados. Testei esse código no meu notebook (i5 com SSD) e obtive os seguintes tempos na resposta, executando pelo Insomnia:

# Render: Padrão do Django Rest
1º Execução: 26.4s
2º Execução: 26.1s
3º Execução: 26.9s
## Média: 26.46s


# Render: Orjson
1º Execução: 18.7s
2º Execução: 17.6s
3º Execução: 20.4s
## Média: 18.9s 

Mudei 3 linhas de código, no settings, e obtive quase 10s de ganho na performance 🚀

É evidente que meu computador não é nenhum servidor, nem estou dizendo que eu retorno 1 milhão de dados numa API. O que posso dizer é que o ganho de performance com o orjson é notório, renderizando os "mesmos dados".

Conclusão

O orjson de fato é muito rápido e potente. Porém, nem tudo são flores. O orjson tem um jeito diferente de representar alguns dados comparado ao padrão do Django Rest. Por isso é necessário tomar uns cuidados e usar essa lib com cautela.

Exemplo de uma response do Django Rest com render padrão:

[
  {
    "uuid": "f33f8cf8-5ccd-46d3-9b52-7747cd34b2b8",
    "name": "Algum nome aleatório bem grande",
    "email": "algumemailaleatorio@email.com",
    "birthday": "2021-05-28",
    "is_premium": true,
    "cash": 957.78,
    "invoice_date": 28
  }
]

Agora, um exemplo retornado pelo Django Rest, usando o orjson como render:

[
  {
    "uuid": "9612e897-197f-4183-bc91-36198a868f46",
    "name": "Algum nome aleatório bem grande",
    "email": "algumemailaleatorio@email.com",
    "birthday": "2021-05-28",
    "is_premium": true,
    "cash": "957.779999999999972715158946812152862548828125",
    "invoice_date": 28
  }
]

Percebam que o campo cash, que é um Decimal() do Python, teve seu valor alterado para algo bem diferente, em comparação ao render padrão.

Mas o Django Rest é muito maleável. Você pode retornar responses com orjson em apenas endpoints específicos, se quiser. Também pode variar o render e o encoder ao seu gosto. Você quem escolhe.

Se você também passou por alguma situação parecida, fique a vontade para comentar aqui em baixo e vamos trocar figurinhas.

Foto da capa by Kolleen Gladden on Unsplash.

Compartilhe

Twitter