TT Lab
开始
学习 学习路径 课程

Envoy 内部结构

设计日志格式并单独收集错误

在 TT Lab 中继续学习

目标

依次设计访问日志的格式、形态、sink 和过滤器四个方面,并确认它们各自在实际日志文件中如何呈现。

为什么重要

在可观测性配置中,访问日志是唯一无法回溯的部分。事故发生之后才意识到“上游地址也该记下来”,那次事故的日志里也已经没有了。所以选择格式的标准不是喜好,而是一个问题——凌晨三点,仅凭这一行能不能缩小原因范围。此外,文件 sink 会先累积到缓冲区再清空,没有值的位置会打印什么,过滤器应该写在哪一层,这些光读文档很难记住,亲身经历一次才能留下印象。

步骤

  1. 启动两个上游——8091 为 ok,8092 为 fail。在 /root/envd-log/log-text.yaml 中放置 /ping(direct_response pong)、/bad(集群 bad)、/(集群 good)三条路由,以及文件访问日志 sink——路径为 /root/envd-log/access.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%。加上 --file-flush-interval-msec 200 启动,请求一次 /hello 后,等到日志出现再确认。
  2. 创建 /root/envd-log/log-wide.yaml——日志路径为 /root/envd-log/wide.log,格式为八个字段:%RESPONSE_CODE%|%REQ(:METHOD)%|%REQ(:PATH)%|%PROTOCOL%|%DURATION%|%UPSTREAM_HOST%|%RESPONSE_FLAGS%|%BYTES_SENT%。用该配置启动后,分别请求一次 /hello、/ping、/bad/x,确认留下三行。
  3. 创建 /root/envd-log/log-miss.yaml——路径为 /root/envd-log/miss.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%|%REQ(X-TENANT)%(最后一个字段是请求头 x-tenant)。启动后,不带头各请求一次 /ping 和 /hello,在 /root/envd-log/03-missing.txt 中写入 ping_upstream=(/ping 那一行的第三个字段)、ping_tenant=(第四个字段)、hello_tenant= 三行。
  4. 创建 /root/envd-log/log-json.yaml——路径为 /root/envd-log/access.json,格式为 json_format,键为 code、path、upstream、flags、duration_ms 五个。启动后,各请求一次 /hello 和 /bad/x,并确认能用 jq 读取。
  5. 创建 /root/envd-log/log-err.yaml——只放一个 sink,用 filter.status_code_filter 只保留 500 及以上(比较运算符 GE,值 500),路径为 /root/envd-log/errors.json,键为 code、path、upstream、flags。启动后,请求两次 /hello 和一次 /bad/x,在 /root/envd-log/05-filter.txt 中写入 lines=(errors.json 的行数)和 codes=(这些行的 code 值,用空格分隔)两行。
  6. 创建 /root/envd-log/log-two.yaml——两个 sink。一个用文本把所有请求记录到 /root/envd-log/all.log(格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%),另一个用 JSON 只把 500 及以上记录到 /root/envd-log/only5xx.json。启动后,请求三次 /hello 和两次 /bad/x,在 /root/envd-log/06-two.txt 中写入 all=、err=、ratio_ok=(all 与 err 的差)三行。
  7. 创建 /root/envd-log/log-reqid.yaml——路径为 /root/envd-log/reqid.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%REQ(X-REQUEST-ID)%。启动后,请求 /given 时附加头 x-request-id: envd-777,请求 /none 时不附加任何头。在 /root/envd-log/07-reqid.txt 中写入 given=(第一个请求那一行的第三个字段)、none_len=(第二个请求那一行第三个字段的字符数)、none_is_given=(该值与 envd-777 相同则为 yes,否则为 no)三行。
  8. 在 /root/envd-log/08-report.md 中写入 fields=(第 2 步格式的字段数)、missing_marker=(第 3 步中没有值时打印的字符)、all_lines=、error_lines=(第 6 步的两个值)四行,并在下面至少写四行学到的内容。

参考

把日志保存为文件,并了解它为什么出现得晚

启动两个上游——8091 为 ok,8092 为 fail。在 /root/envd-log/log-text.yaml 中放置 /ping(direct_response pong)、/bad(集群 bad)、/(集群 good)三条路由,以及文件访问日志 sink——路径为 /root/envd-log/access.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%。加上 --file-flush-interval-msec 200 启动,请求一次 /hello 后,等到日志出现再确认。

文件 sink 不会在每个请求时都写入磁盘——它先在缓冲区中累积,再定期清空。这个周期的默认值是 10 秒,所以请求之后立刻 cat,看起来什么都没有。“日志没有留下来”的报告,相当一部分就是这个原因。在实验中,用 --file-flush-interval-msec 200 缩短周期,消除等待时间。即便如此,也请使用循环一直等到出现行,而不是固定的 sleep。

事先确定事故之后需要的值

创建 /root/envd-log/log-wide.yaml——日志路径为 /root/envd-log/wide.log,格式为八个字段:%RESPONSE_CODE%|%REQ(:METHOD)%|%REQ(:PATH)%|%PROTOCOL%|%DURATION%|%UPSTREAM_HOST%|%RESPONSE_FLAGS%|%BYTES_SENT%。用该配置启动后,分别请求一次 /hello、/ping、/bad/x,确认留下三行。

格式字符串中的 %...% 是命令操作符。请求头用 %REQ(이름)%(占位符为头名称),响应头用 %RESP(이름)%(占位符为头名称),像 :METHOD 和 :PATH 这样以冒号开头的是 HTTP/2 的伪头。选择放入哪些值的标准只有一个——凌晨三点仅凭这一行,能不能缩小原因范围。只有状态码,无法缩小范围;必须有哪个上游接收的、花了多少毫秒、附加了什么标记,才能缩小。

了解没有值时会打印什么

创建 /root/envd-log/log-miss.yaml——路径为 /root/envd-log/miss.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%|%REQ(X-TENANT)%(最后一个字段是请求头 x-tenant)。启动后,不带头各请求一次 /ping 和 /hello,在 /root/envd-log/03-missing.txt 中写入 ping_upstream=(/ping 那一行的第三个字段)、ping_tenant=(第四个字段)、hello_tenant= 三行。

/ping 是 direct_response,不会去往上游——那么 %UPSTREAM_HOST% 就没有可填的值。没有发送的头也一样。Envoy 不会把这样的位置留空,而是用只有一个字符的标记填充。如果不先了解这个标记是什么,用机器读取日志时,就会把它当成真实的值直接存储下来。数字段数时,awk -F'|' 很方便。

改成机器可读的格式

创建 /root/envd-log/log-json.yaml——路径为 /root/envd-log/access.json,格式为 json_format,键为 code、path、upstream、flags、duration_ms 五个。启动后,各请求一次 /hello 和 /bad/x,并确认能用 jq 读取。

用分隔符切分的文本格式便于人阅读,但只要值里出现分隔符(路径中带有问号和值的情况),字段就会错位。如果打算把日志发送到采集器并进行查询,一开始就用 json_format 更好。键的名称可以随意定,值的位置使用同样的命令操作符。确认用 jq -c . 파일(占位符为文件名)——哪怕只有一行损坏,jq 也会当场失败。

把错误单独收集起来

创建 /root/envd-log/log-err.yaml——只放一个 sink,用 filter.status_code_filter 只保留 500 及以上(比较运算符 GE,值 500),路径为 /root/envd-log/errors.json,键为 code、path、upstream、flags。启动后,请求两次 /hello 和一次 /bad/x,在 /root/envd-log/05-filter.txt 中写入 lines=(errors.json 的行数)和 codes=(这些行的 code 值,用空格分隔)两行。

每个 sink 都可以设置 filter。filter 写在与 name 相同的层,而不是 typed_config 里面——写在里面会提示“没有这样的字段”,配置会被拒绝。过滤器除了状态码,还有耗时(duration_filter)、响应标记、头条件。把错误单独收集起来,即使保留期设得很长,容量也能承受。

同时使用两个 sink

创建 /root/envd-log/log-two.yaml——两个 sink。一个用文本把所有请求记录到 /root/envd-log/all.log(格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%),另一个用 JSON 只把 500 及以上记录到 /root/envd-log/only5xx.json。启动后,请求三次 /hello 和两次 /bad/x,在 /root/envd-log/06-two.txt 中写入 all=、err=、ratio_ok=(all 与 err 的差)三行。

sink 是一个列表,可以放置多个,并且每个都有各自的格式和过滤器。实际工作中常见的配置就是这样——全量日志用简短的格式短期保留,错误日志用详细的格式长期保留。把两者混在一个文件里,就无法分别设定保留期限。

请求 ID 由谁生成

创建 /root/envd-log/log-reqid.yaml——路径为 /root/envd-log/reqid.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%REQ(X-REQUEST-ID)%。启动后,请求 /given 时附加头 x-request-id: envd-777,请求 /none 时不附加任何头。在 /root/envd-log/07-reqid.txt 中写入 given=(第一个请求那一行的第三个字段)、none_len=(第二个请求那一行第三个字段的字符数)、none_is_given=(该值与 envd-777 相同则为 yes,否则为 no)三行。

请求 ID 是把经过多个服务的同一个请求串起来的线,所以最前面的代理在没有值时会生成,有值时则原样传递。默认行为是直接使用客户端给出的值,因此也要记住,外部可以随意填入任何值发送进来(对于来自信任边界之外的值,有专门的配置可以将其删除)。字符数用 awk '{print length($0)}' 或 wc -c 统计。

留下日志设计备忘

在 /root/envd-log/08-report.md 中写入 fields=(第 2 步格式的字段数)、missing_marker=(第 3 步中没有值时打印的字符)、all_lines=、error_lines=(第 6 步的两个值)四行,并在下面至少写四行学到的内容。

这份备忘是下次确定某个服务的日志格式时你自己要读的文字。与其写“放了什么”,不如写“为什么决定放它”。值取自前面步骤创建的文件。