标题:【Nginx26】Nginx学习:日志与镜像流量复制
Nginx学习:日志与镜像流量复制
总算到了日志模块,其实这个模块的指令之前我们就用过了,而且也是是非常常见的指令。相信这一块的学习大家应该不会有什么难度。另一个则是镜像功能,这个估计用过的同学就比较少了,不过也并不是特别的复杂,一会讲到时候咱们再详细说哦。
今天的两个模块都是包含在 Nginx 源码中的,不需要额外单独编译安装。所有的配置指令大分部都可以在 http、server、location 中使用,仅有一个指令是只能配置在 http 中的,我会单独说明。
日志
日志模块是非常常用的模块,这里指的是访问日志哦,不是 error_log 。错误日志是属于 HTTP 核心模块的内容,之前我们已经学习过了。大多数情况下,我们只是去为每个 Server 指定一个单独的日志文件,这样便于管理,或者有特殊需要就为某些 Location 指定。另外还有一些情况就是会屏蔽掉一些比较说 favicon.ico 文件或者静态图片等资源文件的访问日志记录。
它的全名是 ngx_http_log_module 模块,用于以指定的格式写入请求日志。请求记录在处理结束位置的上下文中。如果在请求处理期间发生内部重定向,它可能与原始位置不同。
还是先来学习它的配置指令,最后再进行简单地测试。
access_log
设置缓冲日志写入的路径、格式和配置。
access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
access_log off;
默认值是 logs/access.log combined 。可以在同一配置级别上指定多个日志。可以通过在第一个参数中指定“syslog:”前缀来配置日志记录到 syslog。特殊值 off 取消当前级别的所有 access_log 指令。如果未指定格式,则使用预定义的“组合”格式。
必须要有的这个 path 参数,是指定日志要记录到哪个文件的,可以相对也可以是绝对路径的文件。后面的 format 则是根据后面要讲的 log_format 配置,确定使用哪个日志格式。这个参数可以不填,不填走默认的 combined 格式,下面会看到。
如果使用了 buffer 或 gzip (1.3.10, 1.2.7) 参数,写入日志将被缓冲。缓冲区大小不得超过对磁盘文件的原子写入大小。对于 FreeBSD,这个大小是无限的。这个一会我们会测试。
启用缓冲后,这些数据将马上写入文件:
如果下一个日志行不适合缓冲区
如果缓冲的数据比刷新参数(1.3.10、1.2.7)指定的时间早
当工作进程重新打开日志文件或正在关闭时
如果使用 gzip 参数,则缓冲的数据将在写入文件之前进行压缩。压缩级别可以设置在 1(最快,较少压缩)和 9(最慢,最佳压缩)之间。默认情况下,缓冲区大小等于 64K 字节,压缩级别设置为 1。由于数据是按原子块压缩的,因此日志文件可以随时被“zcat”解压缩或读取。
文件路径可以包含变量(0.7.6+),但是这样的日志有一些限制:
工作进程使用其凭据的用户应有权在具有此类日志的目录中创建文件
缓冲写入不起作用
每次写入日志时都会打开和关闭文件。但是,由于常用文件的描述符可以存储在缓存中,因此可以在 open_log_file_cache 指令的有效参数指定的时间内继续写入旧文件
在每次日志写入期间,都会检查请求的根目录是否存在,如果不存在,则不会创建日志。因此,在同一配置级别上同时指定 root 和 access_log 是一个好主意
if 参数 (1.7.0) 启用条件日志记录。如果条件评估为“0”或空字符串,则不会记录请求。就是可以有条件的记录到日志,后面我们也会测试这个参数。
另外,如果是新安装好的 Nginx ,没有别的访问日志配置,并且在编译时有 --http-log-path=/var/log/nginx/access.log
那么就是默认找不到 access_log 和 log_format 配置的其它所有日志全被记录到这个编译时参数所指定的日志文件中。如果你找不到日志在哪了,可以 nginx -V
看一下是不是记录到这里了哦。
log_format
指定日志的格式。
log_format name [escape=default|json|none] string ...;
默认值 combined "..." ,它只能配置在 http 模块下。转义参数 (1.11.8) 允许在变量中设置 json 或默认字符转义,默认情况下使用默认转义。 none 值 (1.13.10) 禁用转义。对于默认转义,字符“"”、“\”和其他值小于 32 (0.7.0) 或大于 126 (1.1.6) 的字符将转义为“\xXX”。如果找不到变量值,将记录一个连字符(“-”)。对于json转义,JSON字符串中不允许的所有字符都将被转义:字符“”和“\”被转义为“\”和“\\”,值小于32的字符被转义为“\n”, “\r”、“\t”、“\b”、“\f”或“\u00XX”。
必须的 name 参数就是上面 access_log 所需要的那个 format 参数。这样两边配合起来就是一套完整的日志记录功能了。
日志格式可以包含公共变量,以及仅在写入日志时存在的变量:
$bytes_sent
发送到客户端的字节数$connection
连接序列号$connection_requests
当前通过连接发出的请求数 (1.1.18)$msec
以秒为单位的时间,在日志写入时以毫秒为单位$pipe
“p”如果请求是流水线的,“.”否则$request_length
请求长度(包括请求行、请求头和请求体)$request_time
以毫秒为单位的请求处理时间;从客户端读取第一个字节到最后一个字节发送到客户端后写入日志之间经过的时间$status
响应状态$time_iso8601
ISO 8601 标准格式的当地时间$time_local
通用日志格式的本地时间
在现代 nginx 版本中,变量 $status (1.3.2, 1.2.2), $bytes_sent (1.3.8, 1.2.5), $connection (1.3.8, 1.2.5), $connection_requests (1.3.8, 1.2 .5), $msec (1.3.9, 1.2.6), $request_time (1.3.9, 1.2.6), $pipe (1.3.12, 1.2.7), $request_length (1.3.12, 1.2.7 )、$time_iso8601 (1.3.12, 1.2.7) 和 $time_local (1.3.12, 1.2.7) 也可用作公共变量。
发送到客户端的标题行具有前缀“sent_http_”,例如,$sent_http_content_range。
配置始终包含预定义的“组合”格式:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
默认安装完成后提供的 nginx.conf 或 nginx.conf.default 中,有两段注释掉的日志配置,其实就是最基本的日志配置信息。你可以打开试试哦。
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
open_log_file_cache
定义一个缓存,用于存储名称包含变量的常用日志的文件描述符。
open_log_file_cache max=N [inactive=time] [min_uses=N] [valid=time];
open_log_file_cache off;
该指令具有以下参数:
max
设置缓存中描述符的最大数量;如果缓存已满,则关闭最近最少使用 (LRU) 描述符inactive
如果在此期间没有访问,则设置缓存描述符关闭的时间;默认情况下,10 秒min_uses
设置在由 inactive 参数定义的时间内文件使用的最小数量,以让描述符在缓存中保持打开状态;默认情况下,1valid
设置应检查文件是否仍以相同名称存在的时间;默认情况下,60 秒off
禁用缓存
这个确实没用过,不过看样子是缓存打开的日志文件的句柄的。也不知道怎么测出效果,所以咱们就不演示了哈。有兴趣的小伙伴可以自己看一下,有相关的经验的大佬也欢迎留言评论哈。
日志测试
好了,日志模块的配置指令就是上面那三个,不是特别复杂吧,接下来我们就简单测试一下。先配置两个 log_format ,名字就叫 log1 和 log2 吧,为了测试方便。真实业务场景下还是要根据自己的需求好好起名字哈。
log_format log1 'This is Log1 Format: $remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$arg_a"' ;
log_format log2 escape=json '{"remote_addr":"$remote_addr"}';
然后找一个 Server ,建个 lcoation 。
location /log1/ {
alias html/;
access_log logs/log1_access_con.log.gz log1 buffer=1000 gzip=1 if=$arg_a;
access_log logs/log1_access.log log1;
}
这个配置是啥意思呢?第一条 access_log ,加了一些参数,分别是缓冲区配置、gzip压缩一下,然后还有条件。也就是说,当我们的 GET 参数中有 a 这个参数时,访问日志会记录到 logs/log1_access_con.log.gz 这个文件中。
第二条 access_log ,没有别的参数,就是最简单的使用 log1 的配置。现在直接访问一下吧,先看下不带任何参数。
# /usr/local/nginx/logs/log1_access.log
This is Log1 Format: 192.168.56.1 - [05/Sep/2022:09:18:47 +0800] "GET /log1/ HTTP/1.1" 200 621 "" "PostmanRuntime/7.29.2" "" ""
普通的日志记录文件中,清楚地看到了访问请求的情况。注意最后有两个空的双引号,并且 logs/log1_access_con.log.gz 创建了,但还是空的。然后我们再带一个 ?a=111
这样的 GET 参数试一下。
# /usr/local/nginx/logs/log1_access.log
This is Log1 Format: 192.168.56.1 - [05/Sep/2022:09:19:22 +0800] "GET /log1/?a=111 HTTP/1.1" 200 621 "" "PostmanRuntime/7.29.2" "" "111"
看出来不同了吧,最后的一个空比引号有值了,因为我们在上面的 log1 的 log_format 中最后加入了 $arg_a
这个变量。现在 if 参数的条件满足了,我们再看一下 log1_access_con.log.gz 文件,我这里还是 0 ,还是没有内容,这是为啥?因为 buffer 参数的问题啦,日志内容还没有刷到文件中。这时可以多刷几下,或者重载配置 nginx -s reload
,重载日志 nginx -s reopen
都可以。然后就会看到 log1_access_con.log.gz 有内容了。直接使用 gzip 命令解压。
gzip -d log1_access_con.log.gz
查看解压出来文件内容,应该和上面记录的日志是一样的。不过这边记录的日志都是有最后那个 GET 参数的。
好了,上面还有一个 log2 的格式配置,使用了 escape=json
,就是要记录一个 JSON 格式的日志。那么咱们也单独再来一个 location 测试吧,为了看得清楚一点。
location /log2/ {
alias html/;
access_log logs/log2_access.log log2;
}
访问之后查看日志文件,记录的内容是这样的。
# /usr/local/nginx/logs/log2_access.log
{"remote_addr":"192.168.56.1"}
记录 POST 日志
在之前学习内嵌变量时,我们就说过,所有的变量都可以用在日志记录中,而且之前还学习过一些变量是只能在日志中体现的。比如说 $gzip_ratio
这种。另外,还有一种非常常见的需求就是要记录 POST 参数,为啥呢?因为现在很多大数据分析系统,会直接通过日志分析来提取数据。就像我们在讲 emptygif 时说过的数据采集。不过空图片一般都是以 GET 参数的提取分析为主,实际业务中还是可能会有 POST 数据提取的需求。
为了利用 Nginx 的强大性能,并同步收集到 POST 数据埋点信息,我们也可以直接简单地将 POST 数据变量放到日志格式配置中就可以实现这种功能了。
log_format postinfo '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$request_body"';
server {
listen 8026;
location ~ \.php$ {
root html;
fastcgi_pass unix:/var/sock/php-fpm/www.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
access_log logs/26.post.log postinfo;
}
上面的配置中,只是在默认日志最后多加了一个 $request_body
变量。它的意思就是获取 Body 中的内容,也就是 POST 的数据信息。这个变量之前我们已经学习过哦,不记得的小伙伴可以回到 Nginx学习:HTTP核心模块(十二)内嵌变量 https://mp.weixin.qq.com/s/0ks7GgfcvJpQp7mA1IgQcA 再看一下哦。
curl --request POST 'http://192.168.56.88:8026/1.php?a=123' --data-urlencode 'bbb=4444'
192.168.56.1 - - [05/Sep/2022:10:55:34 +0800] "POST /1.php?a=123 HTTP/1.1" 200 1366 "-" "PostmanRuntime/7.29.2" "bbb=4444"
看到日志记录的结果了吧。不过需要注意的是,POST 还有一种提交格式是 FORM-DATA ,就是需要上传文件时要用的那种格式,这种格式获得的结果是不一样的哦。
curl --request POST 'http://192.168.56.88:8026/1.php?a=123' --form 'aaa=555'
192.168.56.1 - - [05/Sep/2022:10:59:00 +0800] "POST /1.php?a=123 HTTP/1.1" 200 1102 "-" "curl/7.79.1" "--------------------------02b9bcad6dd94cb3\x0D\x0AContent-Disposition: form-data; name=\x22aaa\x22\x0D\x0A\x0D\x0A555\x0D\x0A--------------------------02b9bcad6dd94cb3--\x0D\x0A"
大数据提取日志时需要注意这两块的不同哦。
镜像 Mirror
这个镜像是啥?在没系统学习文档之前我都不知道还有这么个东西。它的全称是 ngx_http_mirror_module 模块(1.13.4)通过创建后台镜像子请求来实现原始请求的镜像,并且会忽略对镜像子请求的响应。
还是看不懂吧?其实呀,它就是可以将我们的请求再发给另一个镜像地址,它只管发送,不管那边会有什么响应。就相当于是将流量复制了一份。我们先来看它的配置。
mirror
设置原始请求将被镜像到的 URI。
mirror uri | off;
默认值是 off ,打开的话就是设置一个 URI 就可以了,这个 URI 就是要发送到的地址。可以在同一配置级别上指定多个镜像。
mirror_request_body
指示客户端请求正文是否被镜像。启用后,将在创建镜像子请求之前读取客户端请求正文。
mirror_request_body on | off;
在这种情况下,由 proxy_request_buffering、fastcgi_request_buffering、scgi_request_buffering 和 uwsgi_request_buffering 指令设置的无缓冲客户端请求正文代理将被禁用。
这个配置默认是 on ,如果改成 off 的话,POST 请求中的 Body 部分就不会被发送到镜像地址上了。
镜像流量复制测试
好了,直接来测试吧,这个东西要是上面没看懂,那就手动测试一下,不动手,光看概念,不懂的始终还是不懂。我们先准备一个镜像 location 。
location /mirror1/ {
alias html/;
mirror /26.php;
# mirror_request_body off;
}
设置一个 mirror 指定地址为当前目录的 26.php 这个文件,当然我们也可以设置成静态文件,不过为了记录请求是否真的发过去了,使用动态文件还是方便些。
这个 26.php 里面就是记录一下请求的内容到一个日志文件中。不需要返回什么响应,因为镜像是会忽略响应的嘛,它只是将请求发走,响应还是按正常的响应,不会走镜像中的响应。
<?php
$req = json_encode($_REQUEST);
file_put_contents('./26.REQ.log', $req, FILE_APPEND);
上面两个准备好了之后,我们就可以测试了。
// curl 请求
curl --request POST 'http://192.168.56.88/mirror1/?a=123' --data-urlencode 'bbb=4444'
// 26.REQ.log
{"a":"123","bbb":"ssss"}{"a":"123","bbb":"ssss"}
可以看到,请求内容被完整记录了下来,但是访问路径返回的响应还是我们的静态首页的内容。不过你会发现一个问题,这个日志被记录了两条,也就是说,这个 26.php 被访问了两次。如果我们直接访问这个 php 页面,是只会正常记录一次的。
curl --request POST 'http://192.168.56.88/26.php?a=123' --data-urlencode 'bbb=4444'
这个原因我也没找到,网上也没有相关的资料。
那么这个镜像功能可以有什么实际的应用吗?一是可以做流量放大,比如多次请求;二是灰度发布验证,通过镜像到新版来验证新版本是否会报错;三是忽略响应很重要,可以发送一些回调验证之类的请求。
不过需要注意的是,虽然会忽略响应,但如果镜像的地址无响应或者响应慢的时候,也会拖累主请求的响应速度。另外如果是 POST/PUT/DELETE 这些请求,一定要清楚地知道自己发送的请求会对数据产生的影响,并且镜像端要做好日志记录。
总结
日志功能很重要,虽然错误日志可能更有利于我们进行调试,发现问题。但是访问日志同样也很关键,网站的很多统计,流量、热点链接、爬虫分析、黑客防护等也都需要通过分析访问日志来实现。因此,大部分情况下,咱们还是要打开并且配置好相应的访问日志信息的。但是访问日志一般会比较大,因此会有日志需要分割保存的问题,关于这个问题,我们后面再说,但其实非常简单,大家自己先去找找相关的资料也没问题。
另一个镜像模块,真的之前从来不知道,现在虽然了解了,但是要想灵活运用还是需要更多的实践的。所以咱们也不瞎吹牛了,功能用法了解之后就是实践运用尝试一下咯。
参考文档:
http://nginx.org/en/docs/http/ngx_http_log_module.html
http://nginx.org/en/docs/http/ngx_http_mirror_module.html