Detectando o efeito Slashdot no nginx

10

Existe alguma maneira de eu fazer o Nginx me avisar se os hits de um referenciador ultrapassarem um limite?

Por exemplo, se meu site for exibido no Slashdot e de repente eu tiver 2K acessos em uma hora, eu quero ser notificado quando ultrapassar 1K acessos por hora.

Será possível fazer isso no Nginx? Possivelmente sem lua? (já que meu prod não é lua compilado)

    
por Quintin Par 27.09.2012 / 18:52

4 respostas

3

A solução mais eficiente pode ser escrever um daemon que tail -f the access.log e rastrear o campo $http_referer .

No entanto, uma solução rápida e suja seria adicionar um arquivo extra access_log , para registrar apenas o $http_referer variável com um log_format personalizado e para girar o log automaticamente a cada X minutos.

  • Isso pode ser feito com a ajuda de scripts padrão de logrotate, que podem precisar fazer reinicializações graciosas do nginx para que os arquivos sejam reabertos (por exemplo, o procedimento padrão, dê uma olhada em /a/15183322 no SO para um script simples baseado em hora)…

  • Ou, usando variáveis em access_log , possivelmente retirando a especificação de minuto de $time_iso8601 com a ajuda do map ou de um Diretiva if (dependendo de onde você gostaria de colocar seu access_log ).

Assim, com o acima, você pode ter 6 arquivos de log, cada um cobrindo um período de 10 minutos, http_referer.Txx{0,1,2,3,4,5}x.log , por exemplo, obtendo o primeiro dígito do minuto para diferenciar cada arquivo.

Agora, tudo o que você precisa fazer é ter um script de shell simples que possa ser executado a cada 10 minutos, cat todos os arquivos acima juntos, canalize-o para sort , canalize-o para uniq -c , para sort -rn , para head -16 , e você tem uma lista das 16 variações mais comuns de Referer - livre para decidir se quaisquer combinações de números e campos excedem seus critérios e executar uma notificação.

Posteriormente, após uma única notificação bem-sucedida, você poderá remover todos esses 6 arquivos e, em execuções subseqüentes, não emitir nenhuma notificação, A MENOS QUE todos os seis arquivos estejam presentes (e / ou um determinado número ).

    
por 31.08.2017 / 03:37
13

Acho que isso seria muito melhor feito com o logtail e o grep. Mesmo que seja possível fazer com lua inline, você não quer essa sobrecarga para cada solicitação e você especialmente não a quer quando você é Slashdotted.

Aqui está uma versão de 5 segundos. Coloque-o em um script e coloque um texto mais legível em torno dele e você é de ouro.

5 * * * * logtail -f /var/log/nginx/access_log -o /tmp/nginx-logtail.offset | grep -c "http://[^ ]slashdot.org"

Claro, isso ignora completamente o reddit.com e o facebook.com e todos os milhões de outros sites que podem enviar muito tráfego para você. Sem mencionar 100 sites diferentes, enviando-lhe 20 visitantes cada. Provavelmente, você deve ter apenas um limite de tráfego antigo simples que faz com que um email seja enviado para você, independentemente do referenciador.

    
por 27.09.2012 / 18:58
4

A diretiva nginx limit_req_zone pode basear suas zonas em qualquer variável, incluindo $ http_referrer.

http {
    limit_req_zone  $http_referrer  zone=one:10m   rate=1r/s;

    ...

    server {

        ...

        location /search/ {
            limit_req   zone=one  burst=5;
        }

Você também desejará fazer algo para limitar a quantidade de estado exigida no servidor da Web, já que os cabeçalhos de referência podem ser bastante longos e variados e você poderá ver uma variedade infinita. Você pode usar o recurso nginx split_clients para definir uma variável para todas as solicitações baseadas no hash do cabeçalho do referenciador. O exemplo abaixo usa apenas 10 buckes, mas você pode fazer isso com 1000 com a mesma facilidade. Portanto, se você tiver slashdotted, as pessoas cujo referrer passou a hash no mesmo intervalo que o URL slashdot também seriam bloqueadas, mas você pode limitar isso a 0,1% dos visitantes usando 1000 buckets em split_clients.

Seria algo como isso (totalmente não testado, mas direcionalmente correto):

http {

split_clients $http_referrer $refhash {
               10%               x01;
               10%               x02;
               10%               x03;
               10%               x04;
               10%               x05;
               10%               x06;
               10%               x07;
               10%               x08;
               10%               x09;
               *                 x10;
               }

limit_req_zone  $refhash  zone=one:10m   rate=1r/s;

...

server {

    ...

    location /search/ {
        limit_req   zone=one  burst=5;
    }
    
por 27.09.2012 / 19:41
2

Sim, claro que é possível no NGINX!

O que você pode fazer é implementar os seguintes DFA :

  1. Limite de taxa de implementação, com base em $http_referer , possivelmente usando algum regex por meio de um map para normalizar o valores. Quando o limite é excedido, é gerada uma página de erro interna, que você pode capturar por meio de um manipulador error_page como por uma questão relacionada , indo para um novo local interno como um redirecionado interno (não visível para o cliente).

  2. No local acima para limites excedidos, você realiza uma solicitação de alerta, permitindo que a lógica externa execute a notificação; essa solicitação é armazenada em cache subsequentemente, garantindo que você receba apenas uma solicitação exclusiva por uma determinada janela de tempo.

  3. Pegue o código de status HTTP da solicitação anterior (retornando um código de status ≥ 300 e usando proxy_intercept_errors on ou, em alternativa, utilize o auth_request ou add_after_body para fazer uma sub-requisição" livre ") e completar a solicitação original como se a etapa anterior não estivesse envolvida. Observe que precisamos ativar o tratamento error_page recursivo para que isso funcione.

Aqui está meu PoC e um MVP, também em link :

limit_req_zone $http_referer zone=slash:10m rate=1r/m;  # XXX: how many req/minute?
server {
    listen 2636;
    location / {
        limit_req zone=slash nodelay;
        #limit_req_status 429;  #nginx 1.3.15
        #error_page 429 = @dot;
        error_page 503 = @dot;
        proxy_pass http://localhost:2635;
        # an outright 'return 200' has a higher precedence over the limit
    }
    recursive_error_pages on;
    location @dot {
        proxy_pass http://127.0.0.1:2637/?ref=$http_referer;
        # if you don't have 'resolver', no URI modification is allowed:
        #proxy_pass http://localhost:2637;
        proxy_intercept_errors on;
        error_page 429 = @slash;
    }
    location @slash {
        # XXX: placeholder for your content:
        return 200 "$uri: we're too fast!\n";
    }
}
server {
    listen 2635;
    # XXX: placeholder for your content:
    return 200 "$uri: going steady\n";
}
proxy_cache_path /tmp/nginx/slashdotted inactive=1h
        max_size=64m keys_zone=slashdotted:10m;
server {
    # we need to flip the 200 status into the one >=300, so that
    # we can then catch it through proxy_intercept_errors above
    listen 2637;
    error_page 429 @/.;
    return 429;
    location @/. {
        proxy_cache slashdotted;
        proxy_cache_valid 200 60s;  # XXX: how often to get notifications?
        proxy_pass http://localhost:2638;
    }
}
server {
    # IRL this would be an actual script, or
    # a proxy_pass redirect to an HTTP to SMS or SMTP gateway
    listen 2638;
    return 200 authorities_alerted\n;
}

Observe que isso funciona como esperado:

% sh -c 'rm /tmp/slashdotted.nginx/*; mkdir /tmp/slashdotted.nginx; nginx -s reload; for i in 1 2 3; do curl -H "Referer: test" localhost:2636; sleep 2; done; tail /var/log/nginx/access.log'
/: going steady
/: we're too fast!
/: we're too fast!

127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.1" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.0" 200 16 "test" "curl/7.26.0"

127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 200 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"

127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
%

Você pode ver que a primeira solicitação resulta em um front-end e um backend, como esperado (eu tive que adicionar um back-end fictício ao local que tem limit_req , porque um return 200 teria precedência sobre o limites, um back-end real não é necessário para o resto do manuseio).

O segundo pedido está acima do limite, então, enviamos o alerta (obtendo 200 ), e o armazenamos em cache, retornando 429 (isso é necessário devido à limitação mencionada que pedidos abaixo de 300 não podem ser capturados), que é posteriormente capturado pelo front-end, que agora é livre para fazer o que quiser.

O terceiro pedido ainda excede o limite, mas já enviamos o alerta, por isso, nenhum novo alerta é enviado.

Feito! Não se esqueça de usar o GitHub!

    
por 26.08.2017 / 02:38

Tags