有没有什么方法可以让 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_iso8601
在map
或if
指令(取决于您想要放置的位置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 中当然可以!
您可以做的是执行以下操作直接融资融资:
实现速率限制,基于
$http_referer
,可能通过一些正则表达式map
使值标准化。超出限制时,会引发内部错误页面,您可以通过error_page
处理程序捕获该错误,如下所示根据相关问题,作为内部重定向转到新的内部位置(客户端不可见)。在上述超出限制的位置,您可以执行警报请求,让外部逻辑执行通知;随后缓存此请求,确保您在给定的时间窗口内只会收到 1 个唯一请求。
捕获前一个请求的 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 的请求),随后由前端捕获,现在前端可以自由地做任何它想做的事情了。
第三个请求仍然超出限制,但我们已经发送了警报,因此不会发送新的警报。