博客 | NGINX

使用 NGINX 条件日志记录对请求进行采样

NGINX-F5-horiz-black-type-RGB 的一部分
欧文·加勒特缩略图
欧文·加勒特
2019 年 4 月 24 日发布

NGINX 可以记录其处理的每个事务的非常详细的日志。 此类日志称为访问日志,您可以使用可自定义的日志文件格式微调针对不同服务或位置记录的详细信息。

默认情况下,NGINX 会记录其处理的每个事务。 出于合规性或安全性目的,这可能是必要的,但对于繁忙的网站来说,生成的数据量可能是巨大的。 在本文中,我们展示了如何根据各种标准有选择地记录事务,以及如何利用这些知识以快速、轻量的方式对请求的数据点进行采样。

除非另有说明,本文适用于 NGINX Open Source 和 NGINX Plus。 为了方便阅读,我们将始终引用NGINX

背景——NGINX 访问日志配置快速概览

NGINX 访问日志是使用log_format指令定义的。 您可以定义几种不同的命名日志格式;例如,名为main的完整日志格式和名为notes的缩写日志格式以记录有关请求的三个数据点:

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

log_format notes '$remote_addr "$request" $status';

日志格式可以引用NGINX变量和记录时计算的其他值。

然后,使用access_log指令指示 NGINX 在事务完成后记录该事务。 该指令指定日志文件的位置和要使用的日志格式:

访问日志 /var/log/nginx/access.log 主要;

默认情况下,NGINX 使用以下配置记录所有事务:

access_log 日志/access.log 合并;

如果您定义自己的access_log,它将覆盖(替换)默认访问日志。

条件记录

有时您可能希望只记录某些请求。 这是使用条件日志记录完成的,如下所示:

server {
listen 80;

set $logme 0;
if ( $uri ~ ^/secure ) {
set $logme 1;
}

# 审计人员需要对 /secure 请求添加额外日志
access_log /var/log/nginx/secure.log notes if=$logme;

# 如果我们有全局访问日志,则需要在此处重新声明它
access_log /var/log/nginx/access.log main;

location / {
# ...
}
}

访问日志不会被继承

访问日志设置不会堆叠或继承;一个上下文中的access_log指令会覆盖(替换)在父上下文中声明的访问日志。

例如,如果您想记录有关到 URI /secure 的流量的其他信息,您可以在location /secure {...}块中定义访问日志。 此访问日志取代了配置中其他地方定义的一般访问日志。

上一节中的示例解决了这个问题。 它在同一上下文中使用两个访问日志,并带有条件日志记录,将对/secure 的请求记录到专用日志文件中。

访问日志的挑战

假设您希望确定有关网站流量的一些统计信息:

  • 用户的典型地理分布是怎样的?
  • 我的用户使用哪些 SSL/TLS 密码和协议?
  • 网络浏览器的分布情况如何?

一般访问日志通常不是记录此信息的适当地方。 您可能不希望用研究所需的附加字段来污染访问日志,而在繁忙的站点上,记录所有交易的开销会太高。

在这种情况下,您可以将有限的一组字段记录到专门的日志中。 为了减轻系统负载,您可能还希望对请求子集进行抽样。

采样技术

从 1% 的请求中抽样

以下配置使用$request_id变量作为每个请求的唯一标识符。 它使用split_clients块通过仅记录 1% 的请求来采样数据:

split_clients $request_id $logme {
1% 1;
* 0;
}

服务器 {
listen 80;

access_log /var/log/nginx/secure.log notes if=$logme;

# ...
}

从 1% 的唯一用户中抽样

假设我们希望从每个用户(或 1% 的用户)中采样一个数据点,例如User-Agent标头。 我们不能仅从所有请求中进行抽样,因为生成大量请求的用户在我们的数据中占比过高。

我们使用map块来检测会话 cookie 的存在,它告诉我们请求是来自新用户还是来自我们以前见过的用户。 然后,我们仅对来自新用户的请求进行抽样:

map $cookie_SESSION $logme {
"" $perhaps; # 如果 cookie 缺失,我们会记录 if $perhaps
default 0;
}

split_clients $request_id $perhaps {
1% 1; # $perhaps 在 1% 的时间内为真
* 0;
}

server {
listen 80;

access_log /var/log/nginx/secure.log notes if=$logme;

# 可选: 如果应用没有生成会话 cookie,我们
# 生成我们自己的
add_header 设置 Cookie SESSION=1;

# ...
}

品尝独特的美食

然而并非所有客户端都尊重会话 cookie。 例如,网络蜘蛛可能会忽略 cookie,因此它发出的每个请求都会被识别为来自新用户,从而扭曲我们的结果。

如果我们第一次看到一个新事物时就能从请求中取样,那不是很好吗? 该事物可以是一个新的 IP 地址、一个新的会话 cookie 值、一个新的User-Agent标头、一个之前未见过的主机标头,甚至是这些的组合。 这样,我们对每件事仅采样一次数据。

显然,我们需要存储状态(我们所见过的事物的列表),为此我们转向 NGINX Plus 的键值存储。 键值存储维护一个内存键值数据库,可以通过 NGINX Plus 配置使用变量进行访问;该数据库可选地支持条目的自动过期(超时参数)、持久存储(状态)和集群同步(同步)。 对于商店中尚未存在的每件物品,我们都会记录请求并将添加到商店中,以免再次记录。

NGINX Plus R18 及更高版本中,处理交易时设置键值对非常容易:

# 使用适当的参数定义一个 keyval 区域keyval_zone zone=clients:80m timeout=3600s;

# 为每个唯一的 $remote_addr 创建一个变量 $seen
keyval $remote_addr $seen zone=clients;

log_format notes '$remote_addr "$request" $status';

server {
listen 80;

# 如果 $seen 为空,则更新 keyval (set $seen 1;) 并记录此信息
# request (set $logme 1;)
# 否则,$logme 未设置,我们不会记录请求
# 请注意,在配置的超时后,$seen 会重置为 ""
if ($seen = "") {
set $seen 1;
set $logme 1;
}
access_log /var/log/nginx/secure.log notes if=$logme;

位置 / {
返回 200 "全部成功:-$seen-$logme-\n";
}

位置 /api {
api;
}
}

真实示例——采样 TLS 参数

本文的灵感来自于一个现实问题——如何根据良好做法配置 TLS,而不排除使用旧设备的用户?

TLS 最佳实践是一个不断变化的目标。 TLS 1.3 于一年前获得批准,但许多客户端只使用以前的 TLS 版本;密码被宣布为“不安全”并已退役,但较旧的实现依赖于它们;ECC 证书提供比 RSA 更高的性能,但并非所有客户端都能接受 ECC。许多 TLS 攻击依赖于“中间人”,他们会拦截密码协商握手并迫使客户端和服务器选择安全性较低的密码。 因此,将 NGINX Plus 配置为不支持弱密码或旧密码非常重要,但这样做可能会排除旧客户端。

在以下配置示例中,我们对每个 TLS 客户端进行采样,记录 SSL 协议、密码和User-Agent标头。 假设每个客户端都选择它支持的最新协议和最安全的密码,那么我们可以评估采样数据并确定如果我们取消对旧协议和密码的支持,有多少比例的客户端会被排除。

我们通过 IP 地址和User-Agent 的唯一组合来识别每个客户端,但通过会话 cookie 或其他方法识别客户端也同样有效。

log_format sslparams '$ssl_protocol $ssl_cipher '
'$remote_addr "$http_user_agent"';

# 定义一个具有适当参数的 keyval 区域
keyval_zone zone=clients:80m timeout=3600s;

# 为 $remote_addr 和 
# 'User-Agent' 标头的每个唯一组合创建一个变量 $seen
keyval $remote_addr:$http_user_agent $seen zone=clients;

server {
listen 443 ssl;

# 默认 NGINX SSL 配置
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

if ($seen = "") {
set $seen 1;
set $logme 1;
}
access_log /tmp/sslparams.log sslparams if=$logme;

# ...
}

这将生成一个包含如下条目的日志文件:

TLSv1.2 AES128-SHA 1.1.1.1“Mozilla/5.0(X11;Linux x86_64;rv:45.0)Gecko/20100101 Firefox/45.0”
TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 2.2.2.2“Mozilla/5.0(iPhone;CPU iPhone OS 9_1,如 Mac OS X)AppleWebKit/601.1.46(KHTML,如 Gecko)版本/9.0 Mobile/13B143 Safari/601.1”
TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 3.3.3.3“Mozilla/5.0(Windows NT 6.1;WOW64;rv:58.0) Gecko/20100101 Firefox/58.0"
TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 4.4.4.4 "Mozilla/5.0 (Android 4.4.2; 平板电脑; rv:65.0) Gecko/65.0 Firefox/65.0"
TLSv1 AES128-SHA 5.5.5.5 "Mozilla/5.0 (Android 4.4.2; 平板电脑; rv:65.0) Gecko/65.0 Firefox/65.0"
TLSv1.2 ECDHE-RSA-CHACHA20-POLY1305 6.6.6.6 "Mozilla/5.0 (Linux; U; Android 5.0.2; en-US; XT1068 Build/LXB22.46-28) AppleWebKit/537.36(KHTML,如 Gecko)版本/4.0 Chrome/57.0.2987.108 UCBrowser/12.10.2.1164 Mobile Safari/537.36"

然后我们可以使用各种方法处理文件来确定数据的传播:

$ cat /tmp/sslparams.log | cut -d ' ' -f 2,2 | sort | uniq -c | sort -rn | perl -ane 'printf "%30s %s\n", $F[1], "="x$F[0];' ECDHE-RSA-AES128-GCM-SHA256 =========================== ECDHE-RSA-AES256-GCM-SHA384 ======== AES128-SHA ==== ECDHE-RSA-CHACHA20-POLY1305 == ECDHE-RSA-AES256-SHA384 ==

我们识别低容量、安全性较低的密码,检查日志以确定哪些客户端正在使用它们,然后做出明智的决定,从 NGINX Plus 配置中删除密码。

结论

NGINX 的条件日志记录可用于对 NGINX 管理的请求子集进行抽样,并编写标准或专用日志。 如果您需要快速获取流量样本以进行统计分析(例如确定 SSL 参数的传播),则此技术很有用。

您需要仔细考虑如何对数据进行采样,以便繁忙的用户或蜘蛛不会过多地代表数据。 您可以使用 NGINX 配置中的变量以及mapsplit_clients指令来选择和过滤请求。

对于决策更复杂或需要高度准确信心的情况,您可以在 NGINX 配置中构建复杂的选择器。 NGINX Plus 键值存储使您能够累积状态并在需要时在集群中的 NGINX Plus 实例之间共享它。

亲自尝试使用 NGINX Plus 进行请求采样 - 立即开始30 天免费试用联系我们讨论您的用例。


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”