日志格式只能在事故发生前修改
一句话总结
访问日志不是开关,而是设计格式的工作。要决定四件事:记录什么(命令操作符)、以什么形态记录(文本、JSON)、记录到哪里(sink)、只记录什么(过滤器)。如果事先没有定好,事故发生之后留下的就只有 200 GET /。
为什么需要它
出故障时,最先问的问题总是一样的:谁请求了什么,发到了哪里,花了多长时间,为什么失败。如果这四点能从一行日志中回答,几分钟内就能缩小原因范围。如果没有,就要去翻应用日志;再没有,就开始猜测。
问题在于,这个格式必须在事故发生之前定好。事故发生之后才意识到“上游地址也该记下来”,那次事故的日志里也已经没有了。所以在可观测性配置中,访问日志是唯一无法回溯的部分。
工作原理
命令操作符。格式字符串中的 %...% 表示一个值。常用的大致如下。
| 操作符 | 含义 |
|---|---|
%RESPONSE_CODE% |
状态码 |
%RESPONSE_FLAGS% |
Envoy 附加的简短原因标记(UT、UF、NR 等) |
%UPSTREAM_HOST% |
实际接收请求的上游地址和端口 |
%DURATION% |
整个请求耗费的毫秒数 |
%BYTES_SENT% |
发出的字节数 |
%REQ(이름)% · %RESP(이름)% |
请求头、响应头(占位符为头名称)。像 :METHOD 这样以冒号开头的是伪头 |
没有值的时候。没有发往上游的响应(direct_response)中,%UPSTREAM_HOST% 没有可填的值,没有发送的头也是如此。Envoy 不会把这个位置留空,而是用固定的标记填充。用机器读取日志时,如果把这个标记误当成真实的值,统计就会悄悄出错。
文本还是 JSON。用分隔符切分的文本便于人阅读,但只要值里出现分隔符,字段就会错位。如果打算发送到采集器并进行查询,一开始就用 json_format 更好。键的名称可以随意定,值的位置使用同样的操作符。
sink 与过滤器。access_log 是一个列表,可以放置多个,每个都有各自的格式和过滤器。过滤器可以按状态码、耗时、响应标记、头条件来设置。实际工作中最常见的配置是:全量日志保持简短,错误日志则另存并保持详细。因为混在一个文件里,就无法分别设定保留期限。这里有一个经常碰到的语法陷阱——filter 要写在 typed_config 之外,与 name 处于同一层,而不是写在里面。
在现场相遇的样子
“日志没有留下来。”文件 sink 不会在每个请求时都写入磁盘。它先在缓冲区中累积,再定期清空,这个周期的默认值是 10 秒(--file-flush-interval-msec)。请求之后立刻 cat,文件为空是正常的。如果容器马上就退出,缓冲区里的内容真的会连同缓冲区一起消失,所以在寿命很短的 sidecar 中,要缩短这个值,或者改为输出到标准输出。
日志占满磁盘的情况。访问日志是唯一会随流量成比例增长的可观测性信号。每秒数千个请求,一天就会达到几十 GB。所以要保持格式简短,详细格式只用于设置了过滤器的 sink。
把日志输出到标准输出的情况。在容器环境中,常见的做法是不用文件,而把 /dev/stdout 作为路径。因为采集器已经在抓取容器日志,所以把日志搭载到这条路径上。但此时应用日志和访问日志会混成一条流,必须在格式中加入能够区分二者的标记,之后才能拆开查看。而且标准输出比文件更早堵塞——接收方变慢时,这份压力会一直传到代理。
相信请求 ID 而出的问题。请求 ID 是把经过多个服务的同一个请求串起来的线,最前面的代理在没有值时会生成,有值时则原样传递。这也意味着外部可以随意填入任何值发送进来。在从信任边界之外进入的位置,最好把这个头删除后重新生成。
官方文档:Access logging · Command line options
下一项实验要做什么
启用文件 sink,确认日志没有立即出现的原因,然后设计一个八个字段的格式,记录三种请求。亲眼看看没有值的位置会打印什么,改为 JSON 格式并用 jq 读取,另设一个只保留 500 及以上的 sink,最后确认客户端提供请求 ID 与不提供时有什么不同。