在Nginx中检测Slashdot效果


10

如果引荐来源网址的点击次数超过阈值,我是否可以使Nginx通知我?

例如,如果我的网站出现在Slashdot上,突然我在一小时内有2K点击,当我希望每小时超过1000 K时,我想得到通知。

Nginx是否可以做到这一点?可能没有lua?(因为我的产品不是lua编译的)


4
什么是“ Slashdot”?
ewwhite 2012年

我做了这样的事情来检测ngix上的ddos。我是通过解析访问日志来实现的。我做了一个cron作业来解析访问日志并计算每小时的唯一IP连接数。
Hex

8
您是说要让Nginx能够检测到您是否被Dice收购了?
MDMarra 2012年

1
@Hex那(可能还有您脚本中的一些片段)将很好地回答这个问题:)
voretaq7 2012年

3
可能无需担心再加上Slashdot了。您的网络服务器应该每小时可以处理4个额外的连接。也许想担心会被带到Redditted
HopelessN00b

Answers:


3

最有效的解决方案可能是写一个守护进程,将tail -faccess.log和跟踪的$http_referer领域。

但是,一种快速而肮脏的解决方案是添加一个额外的access_log文件,仅$http_referer使用custom 记录日志变量log_format,并每隔X分钟自动旋转日志。

  • 可以在标准logrotate脚本的帮助下完成此操作,该脚本可能需要正常重启nginx才能重新打开文件(例如,标准过程,请在SO上简单查看一下/ a / 15183322-基于脚本)…

  • 或者,通过使用内的变量access_log,可能$time_iso8601是借助mapif指令来取消详细说明(取决于您要放置的位置access_log)。

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

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

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


这看起来超级有用。我可能要求太多,但是像前面的答案一样,您愿意帮忙编写脚本吗?
昆丁

@QuintinPar听起来确实是课外活动!;-)如果您愿意,我可以出租和提供咨询;我的电子邮件是cnst++@FreeBSD.org,也位于Constantine.SU
cnst '17

完全了解。到目前为止,非常感谢您提供的所有帮助。希望我有一天能负担得起:-)
Quintin Par

1
@QuintinPar不客气!不用担心,它应该是具有上述规格的非常简单的脚本;基本上,只需要测试,配置和打包即可。:)
cnst

1
你是超级英雄!
奎丁(Quintin)

13

我认为使用logtail和grep会更好。即使可以使用lua内联,您也不希望每个请求都有这种开销,尤其是当您被Slashdot分隔时,您尤其不希望这样做。

这是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个访问者。您可能应该仅具有一个普通的旧流量阈值,该阈值将导致无论您的推荐人如何都向您发送电子邮件。


1
问题是要积极主动。我需要从任何站点知道。另一个问题是我将阈值放在哪里?您是说要进行其他日志解析吗?我也没有在fourmilab.ch/webtools/logtail中
Quintin Par

该阈值将取决于您的服务器可以处理的流量。只有您可以设置。如果要更快地通知,请每隔五分钟而不是每小时运行一次,并将阈值除以12。该-o 选项适用于偏移文件,因此它知道下一次从何处开始读取。
Ladadadada 2012年

@Ladadadada,我不同意开销会很大,请参阅我的解决方案— serverfault.com/a/870537/110020-我相信,如果正确实施,开销将非常小,尤其是(1),如果您的后端是真的很慢,那么这个开销可以忽略不计,或者,(2),如果您的后端已经相当混乱和/或正确地缓存了,那么您首先应该在流量处理上没有什么问题,并且会有一点额外的负载。不会造成伤害。总体而言,听起来这个问题有两个用例:(1)仅被告知;(2)自动缩放。
cnst

4

nginx的limit_req_zone指令可以基于任何变量的区域,包括$ HTTP_REFERRER。

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

    ...

    server {

        ...

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

不过,您将还需要做一些事情来限制Web服务器上所需的状态量,因为引荐来源标头可能会很长且变化很大,并且您可能会看到无限个变量。您可以使用nginx split_clients功能为所有请求(基于引荐来源标头的哈希值)设置变量。下面的示例仅使用10块钱,但您可以轻松地使用1000块钱。因此,如果您用斜线分隔,其引荐来源碰巧与斜线URL混入相同存储桶的人也将被阻止,但是您可以通过在split_clients中使用1000个存储桶将访问者限制为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;
    }

这是一种有趣的方法。但是,我认为问题在于发生Slashdot效果时会自动发出警报。您的解决方案似乎可以解决随机阻止约10%的用户的问题。此外,我相信您使用的理由split_clients可能是错误的- limit_req基于“漏斗”,这意味着总体状态永远不应超过指定区域的大小。
cnst

2

是的,NGINX当然可以!

您可以执行以下DFA

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

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

  3. 捕获先前请求的HTTP状态代码(返回状态代码≥300并使用proxy_intercept_errors on,或者使用not-default-by-default auth_requestadd_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,因为a return 200优先于限制,所以不需要真实的后端对于其余的处理)。

第二个请求超出了限制,因此,我们发送警报(获取200),并将其缓存,然后返回429(由于上述限制,无法捕获低于300的请求,因此这是必要的),随后被前端捕获,现在可以免费进行任何所需的操作了。

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

做完了! 不要忘记在GitHub上进行分叉!


两个限速条件可以一起使用吗?我现在正在使用此服务器:serverfault.com/a/869793/26763
Quintin Par

@QuintinPar :-)我认为这取决于您的使用方式-明显的问题是要在一个位置上区分引入该条件的限制;但是如果这个是a limit_req,而另一个是a limit_conn,则只需使用limit_req_status 429上面的代码(需要非常新的nginx),我认为您应该是黄金的;可能还有其他选择(可以肯定的是将nginx w /链接起来set_real_ip_from,但是,根据您要执行的操作,可能会有更有效的选择)。
cnst

@QuintinPar如果我的答案中缺少任何内容,请告诉我。顺便说一句,请注意,一旦达到限制并调用您的脚本,直到nginx正确缓存了该脚本,您的内容可能会延迟;例如,您可能想使用类似的东西异步实现脚本golang,或者查看上游的超时选项;同样,可能还需要使用proxy_cache_lock on,并且可能添加一些错误处理,以防止脚本失败时的操作(例如,使用error_pageproxy_intercept_errors再次使用)。我相信我的POC是一个好的开始。:)
cnst

谢谢您的尝试。对我来说,一个主要问题仍然是,我已经在http级别上使用limit_req和limit_conn,它适用于我拥有的所有网站。我不能覆盖它。因此,此解决方案使用的是其他功能。还有其他解决方案吗?
Quintin Par

@QuintinPar拥有嵌套的nginx实例,每个实例将使用一组limit_req/ limit_conn怎么样?例如,只需将上面的配置放在当前的前端服务器之前。您可以set_real_ip_from在上游nginx中使用,以确保IP正确地沿线计算。否则,如果仍然不合适,我想您必须更清楚地阐明确切的限制条件和规格-我们在说什么流量水平?统计数据需要多久运行一次(1min / 5min / 1h)?旧logtail解决方案有什么问题?
cnst
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.