在 nginx 中检测 Slashdot 效应

在 nginx 中检测 Slashdot 效应

有没有什么方法可以让 Nginx 在来自引荐来源的点击量超出阈值时通知我?

例如,如果我的网站在 Slashdot 上被推荐,并且突然在一小时内有 2K 次点击量,那么当每小时点击量超过 1K 次时,我希望收到通知。

在 Nginx 中可以做到这一点吗?可能不用 lua 吗?(因为我的产品不是 lua 编译的)

答案1

最有效的解决方案可能是编写一个守护进程来tail -f执行access.log并跟踪该$http_referer字段。

然而,一个快速而肮脏的解决方案是添加一个额外的access_log$http_referer文件,仅使用自定义的变量记录log_format,并每 X 分钟自动轮换一次日志。

  • 这可以借助标准 logrotate 脚本来实现,该脚本可能需要正常重启 nginx 才能重新打开文件(例如,标准程序,请查看/a/15183322 上对于简单的基于时间的脚本)...

  • 或者,通过使用 内的变量access_log,可能通过获取$time_iso8601mapif指令(取决于您想要放置的位置access_log)。

因此,通过上述内容,您可能有 6 个日志文件,每个文件涵盖 10 分钟的时间段,http_referer.Txx{0,1,2,3,4,5}x.log例如,通过获取分钟的第一位数字来区分每个文件。

现在,你所要做的就是编写一个每 10 分钟运行一次的简单 shell 脚本,cat将以上所有文件放在一起,通过管道传输到sort,通过管道传输到uniq -c, 到sort -rn, 到head -16,您将获得 16 种最常见变体的列表Referer— 您可以自由决定任何数字和字段的组合是否超出您的标准,并执行通知。

随后,在一次成功通知后,您可以删除所有这 6 个文件,并且在后续运行中不会发出任何通知,除非所有六个文件都存在(和/或您认为合适的其他某个数字)。

答案2

我认为使用 logtail 和 grep 会更好。即使可以使用 lua inline 来完成,你也不希望每个请求都产生这样的开销,而且你尤其当你被 Slashdoted 后就不想要它了。

这是 5 秒的版本。将其放入脚本中,并在其周围添加一些更易读的文本,这样就大功告成了。

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

当然,这完全忽略了 reddit.com 和 facebook.com 以及其他数百万个可以为您带来大量流量的网站。更不用说 100 个不同的网站,每个网站会为您带来 20 名访问者。您可能应该只使用一个普通的交通无论引荐来源是谁,都会向您发送电子邮件的阈值。

答案3

nginx限制请求区域指令可以根据任何变量来建立其区域,包括 $http_referrer。

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

    ...

    server {

        ...

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

不过,您还需要做一些事情来限制 Web 服务器上所需的状态量,因为 referrer 标头可能很长且多种多样,您可能会看到无限的变化。您可以使用 nginx拆分客户端功能可为所有请求设置一个基于 referrer 标头哈希值的变量。下面的示例仅使用 10 个 bucket,但您也可以同样轻松地使用 1000 个 bucket。因此,如果您被 slashdot 拦截,其 referrer 恰好与 slashdot URL 哈希值位于同一 bucket 中的用户也会被拦截,但您可以通过在 split_clients 中使用 1000 个 bucket 将其限制为 0.1% 的访问者。

它看起来像这样(完全未经测试,但方向正确):

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;
    }

答案4

是的,在 NGINX 中当然可以!

您可以做的是执行以下操作直接融资融资

  1. 实现速率限制,基于$http_referer,可能通过一些正则表达式map使值标准化。超出限制时,会引发内部错误页面,您可以通过error_page处理程序捕获该错误,如下所示根据相关问题,作为内部重定向转到新的内部位置(客户端不可见)。

  2. 在上述超出限制的位置,您可以执行警报请求,让外部逻辑执行通知;随后缓存此请求,确保您在给定的时间窗口内只会收到 1 个唯一请求。

  3. 捕获前一个请求的 HTTP 状态代码(通过返回状态代码≥300 并使用proxy_intercept_errors on或者,使用非默认构建的auth_request或者add_after_body进行“免费”子请求),并完成原始请求,就好像没有涉及前一步一样。请注意,我们需要启用递归error_page处理才能使其正常工作。

这是我的 PoC 和 MVP,同样位于https://github.com/cnst/StackOverflow.cnst.nginx.conf/blob/master/sf.432636.detecting-slashdot-effect-in-nginx.conf

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;
}

请注意,这可以按预期工作:

% 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"
%

您可以看到,第一个请求产生了一个前端和一个后端命中,正如预期的那样(我必须向具有的位置添加一个虚拟后端limit_req,因为return 200优先于限制,其余处理不需要真正的后端)。

第二个请求超过了限制,因此,我们发送警报(获取200),并将其缓存,返回429(这是必要的,因为前面提到的限制是无法捕获低于 300 的请求),随后由前端捕获,现在前端可以自由地做任何它想做的事情了。

第三个请求仍然超出限制,但我们已经发送了警报,因此不会发送新的警报。

完毕! 不要忘记在 GitHub 上 fork 它!

相关内容