turicas / brasil.io

Backend do Brasil.IO (para código dos scripts de coleta de dados, veja o link na página de cada dataset)
https://brasil.io/
GNU General Public License v3.0
923 stars 145 forks source link

Limitar requisições abusivas na API e na interface #316

Open turicas opened 4 years ago

turicas commented 4 years ago

Analisando os logs de acesso, vi que diversas requisições provavelmente abusivas são feitas na API e na interface do site, como por exemplo:

Um sinal que pode ajudar a detectar essas requisições são números de páginas muito grandes, como /api/dataset/covid19/caso/data/?&format=json&page=52 - nas últimas 24 horas esse endpoint foi o que mais teve hits no backend (!).

O objetivo aqui é limitar o uso abusivo para preservar os recursos do servidor (atualmente a concorrência por processamento está grande e algumas páginas estão lentas). A API foi feita para facilitar o trabalho de quem precisa eventualmente fazer algum filtro nos dados e não quer/precisa baixar o dataset completo, inclusive para utilização diretamente em alguma interface frontend sem a necessidade de um backend fornecer os dados (pegando diretamente da API do Brasil.IO), mas se algumas pessoas utilizam a API de forma abusiva (ex: percorrendo todas as páginas), essas pessoas prejudicam a abertura do site para todas as outras (o percentual de cache na CDN diminui, aumenta o processamento do Django e do PostgreSQL etc.)

Possíveis Soluções

Pensei, inicialmente, em três possibilidades para resolver o problema. Acredito que a melhor solução seria B & C. cc @berinhard

Solução A: Limitar a quantidade de requisições na API por cliente

Esse é o padrão do mercado, mas acho que pode ser ruim por vários motivos, como: A.1- Para que conseguíssemos rastrear as requisições, todas as pessoas que acessam a API teriam que se cadastrar e gerar um token (ou passar a enviar um cookie); A.2- Teríamos mais tarefas para executar no backend a cada requisição na API (gravar quantas requisições cada token fez, checar limites etc.); A.3- Limitaríamos clientes que realmente precisam fazer muitas requisições e não estão abusando da API.

Solução B: Limitar a quantidade de páginas que podem ser consultadas

Poderíamos estabelecer um limite, por exemplo: no máximo será permitido pegar via API os 1.000.000 primeiros registros (só para referência: a funcionalidade "baixar como CSV" da interface permite hoje download de, no máximo, 350.000 registros). Na API cada página tem 1.000 registros por padrão (esse número pode ser alterado com o page_size, para até 10.000), então isso quer dizer que aceitaríamos requisições em que page * page_size <= 1_000_000, caso contrário devolveríamos um erro 4xx para o cliente, com um JSON explicando o motivo do erro, um link para baixar os dados completos e outro link para a documentação da API. Dessa forma, não afetaríamos os clientes que já acessam a API (não precisariam se cadastrar, gerar token etc.) e os que acessam de maneira abusiva receberiam o erro, educando-os sobre as melhores práticas de acesso à API. Isso poderia valer para a interface também: em geral não faz muito sentido uma pessoa acessar a página 200 de uma consulta: ou ela vai filtrar por algo bem específico e já ter o resultado na tela (e copiar/colar do próprio navegador), ou vai filtrar por filtros que devolvem muitos registros e vai baixar o CSV com esse recorte (só lembrando que na interface a quantidade de itens por página é de 50). O bônus pra essa abordagem é que evitaríamos consultas com o OFFSET muito grande no PostgreSQL (essas consultas podem ser bastante custosas).

Solução C: limitar os User-Agents aceitos na interface

Essa solução poderia ser utilizada em conjunto com uma das duas acima. Se alguém está fazendo scraping na interface do Brasil.IO, claramente está utilizando o serviço de forma inadequada, dado que a API existe para isso. Bloqueando as requisições feitas para a interface com uma mensagem de erro educativa, conseguimos ensinar às essas pessoas que devem utilizar a API (seria legal termos uma página de referência no site para linkar na resposta dessas requisições - essa página poderia ter explicações sobre a API). Lista de User-Agents que poderíamos bloquear nesse caso (o ideal seria pegar por tudo que vem antes da "/", para pegar qualquer versão):

vitorbaptista commented 4 years ago

Sobre a solução A, poderia também usar o endereço IP pra evitar ter que criar chaves. Se quiser evitar limitações pra pessoas com mesmo IP (numa Universidade, por exemplo), pode pegar uma combinação do IP e o user-agent.

Isso só limita a quantidade de acessos por minuto. Se alguém realmente precisar usar a API pra pegar os dados, não tem problema. Só vai ser na velocidade que a gente estabelecer.

Achei este projeto que implementa isso https://django-ratelimit.readthedocs.io/en/stable/. Ele usa o Django Cache, que pode ser configurado pra colocar na memória ou usar Memcached/Redis.

No futuro, quando for realmente implementar api keys e outras funcionalidades, sugeriria dar uma olhada no Kong (https://konghq.com/kong/).

berinhard commented 4 years ago

Acho a solução A a de menor custo de implementação incialmente. Isso porque o Django Rest Framework já possui uma interface de throttling que dá bastante conta do recado. Já usei em outros projetos e funciona desde uma simples entrada no settings.py até customização endpoint a endpoint.

O benefício do DRF é que não precisamos implementar nada de token, api key ou coisas do tipo. Aqui tem mais informações sobre como os clientes são identificados.

berinhard commented 4 years ago

Atualizações sobre essa issue:

  1. Vamos usar o controle de throttling do DRF
  2. Idealmente queremos configurar a mensagem de erro
  3. Precisamos estudar os logs para definirmos bons limites
  4. Mudarmos o template do DRF que abre no browser pra incluir uma msg falando pra baixar a o arquivo completo ao invés de ficar paginando na API
vcborsolan commented 4 years ago

@berinhard , como anda essa issue? Pela minha pouca experiencia com crawlers , ja tive dificuldades de transpassar sites com gerenciamento de acesso pela cloudflare , pode ser uma saida tambem.

berinhard commented 4 years ago

@turicas podemos fechar essa issue dado a introdução do django-ratelimit e também do uso do controle de throttling via Django Rest Framework, né?

turicas commented 4 years ago

@turicas podemos fechar essa issue dado a introdução do django-ratelimit e também do uso do controle de throttling via Django Rest Framework, né?

Acho melhor ainda não. Parece que vamos ter que mudar a estratégia de bloqueio do ratelimit, pois estão usando proxies e user-agents diferentes. :-/ Estou pensando que talvez valha a pena fazer o acesso à API apenas para quem está autenticado e limitar muito o acesso não autenticado ao site (e limitar menos para os autenticados).