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

Loki — 不索引日志的日志库

开着重试就下班了,第二天早上队列满了

在 TT Lab 中继续学习

目标

亲手制造 Loki 拒绝传入请求的四种原因,做出把 400 和 429 区别对待的客户端,一行不丢地全部存下。

为什么重要

写入一侧的拒绝有两种。违反标签语法、标签个数、行长度是 400,这样的行不管重发多少次,永远都会被拒绝。超过每个流的速率是 429,这种情况等一等再发就会成功。把两者用同一个代码处理,一边会把队列填满,另一边会丢数据。速率限制以流为单位,这一点也很重要——为了降低基数而合并标签,流量就会集中到一个流上,出现 429,为了避开 429 而拆分,流的数量就会增加。在这两股力量反向拉扯的位置上取得平衡,正是本课程想教的判断。

步骤

  1. 在 /root/lk-ingest-limits 中启动 Loki,把 date +%s 写入 /root/lk-ingest-limits/anchor.txt。然后从 http://localhost:3100/config 中读出五个限额,写入 /root/lk-ingest-limits/limits.tsv,不要表头,共五行,每行是用制表符分隔的两列 <설정이름><탭><값>(占位符依次为设置名称、制表符、值)。顺序是 per_stream_rate_limit、per_stream_rate_limit_burst、max_line_size、max_label_names_per_series、unordered_writes。
  2. 发出四个 push,把状态码写入 /root/lk-ingest-limits/labels.tsv,不要表头,共四行,每行是用制表符分隔的两列 <이름><탭><코드>(占位符依次为名称、制表符、状态码)。名称和内容依次为——ok(标签 app="probe")、digit(标签名是 1bad)、dash(标签名是 bad-name)、toomany(16 个标签,名称从 l0 到 l15)。时间戳全部使用基准时刻,正文随便一个短字符串即可。
  3. 做一行超过第 1 步表中 max_line_size 的行并发送。命令行传不了,所以把请求体做成文件,用 --data-binary @파일(占位符为文件)发送。把结果写成三行存入 /root/lk-ingest-limits/03-size.txt——code=<정수>、limit_bytes=<본문에 적힌 한도 바이트>、sent_bytes=<보낸 줄의 길이>(占位符依次为整数、正文里写的限额字节数、所发送行的长度)。
  4. 写出 /root/lk-ingest-limits/loki-low.yaml,在 3300 端口(gRPC 9097)再启动一个 Loki。存储路径放在 /tmp/lokilow 之下,limits_config.per_stream_rate_limit 是 4KB,per_stream_rate_limit_burst 是 8KB。从那个服务器的 /config 里读出两个值,以 rate=<값> 和 burst=<값>(占位符均为值)两行写入 /root/lk-ingest-limits/04-low.txt。
  5. 向 3300 端口的 Loki,把 900 字节的行以一个流({"app":"flood"})连续发送,收到 429。在 /root/lk-ingest-limits/05-429.txt 中写两行——first_429=<처음 429 가 난 순번, 1부터> 和 limit_in_body=<본문에 적힌 한도 문자열>(占位符依次为第一次出现 429 的序号(从 1 开始)、正文里写的限额字符串)。并把 429 响应正文整个保存到 /root/lk-ingest-limits/429.body。
  6. 用第 1、4 步读出的两个值和第 5 步的序号来说明突发。在 /root/lk-ingest-limits/06-burst.txt 中写四行——rate_bytes=<초당 허용 바이트 정수>、burst_bytes=<버스트 바이트 정수>、sent_bytes=<429 가 나기까지 보낸 본문 바이트 합>、why=<공백 뺀 40자 이상 한 문장>(占位符依次为每秒允许字节数整数、突发字节数整数、出现 429 之前所发送的正文字节数合计、去掉空格后至少 40 个字的一句话)。
  7. 创建 /root/lk-ingest-limits/send.sh,向 {"app":"keep"} 流往 3300 端口发送 20 条 900 字节的行,收到 429 就用指数退避重试,使它们全部存下。时间戳从基准时刻往前的 20 秒内,每行间隔 1 秒。再在 /root/lk-ingest-limits/07-send.txt 中写三行 sent=20、stored=<저장된 줄 수>、retries=<재시도 횟수>(占位符依次为已存储的行数、重试次数)。已存储的行数要通过查询来确认。
  8. 假设有一个服务每秒输出 12,000 字节,要低于第 4 步的限额,计算至少要把流拆成几个,写成四行存入 /root/lk-ingest-limits/08-design.txt——throughput_bytes=12000、limit_bytes=<4단계 한도의 바이트>、streams_needed=<올림한 정수>、tradeoff=<공백 뺀 60자 이상>(占位符依次为第 4 步限额的字节数、向上取整后的整数、去掉空格后至少 60 个字的内容)。最后一行里,同时写出增加流的代价。

参考

读取服务器持有的限额

在 /root/lk-ingest-limits 中启动 Loki,把 date +%s 写入 /root/lk-ingest-limits/anchor.txt。然后从 http://localhost:3100/config 中读出五个限额,写入 /root/lk-ingest-limits/limits.tsv,不要表头,共五行,每行是用制表符分隔的两列 <설정이름><탭><값>(占位符依次为设置名称、制表符、值)。顺序是 per_stream_rate_limit、per_stream_rate_limit_burst、max_line_size、max_label_names_per_series、unordered_writes。

它们全都在 limits_config 块里。值要原样写下服务器印出来的字符串——有时会带着像 3MB 这样的单位,有时只有数字。这五个在后面的步骤里全都会用到。

违反标签名规则就是 400

发出四个 push,把状态码写入 /root/lk-ingest-limits/labels.tsv,不要表头,共四行,每行是用制表符分隔的两列 <이름><탭><코드>(占位符依次为名称、制表符、状态码)。名称和内容依次为——ok(标签 app="probe")、digit(标签名是 1bad)、dash(标签名是 bad-name)、toomany(16 个标签,名称从 l0 到 l15)。时间戳全部使用基准时刻,正文随便一个短字符串即可。

用 curl -o /dev/null -w '%{http_code}' 可以只取出状态码。400 的正文请亲自读一遍——它会连违反了什么规则、在什么位置都告诉你。标签个数的上限在第 1 步的表里。

一行太长,整个请求都会被拒绝

做一行超过第 1 步表中 max_line_size 的行并发送。命令行传不了,所以把请求体做成文件,用 --data-binary @파일(占位符为文件)发送。把结果写成三行存入 /root/lk-ingest-limits/03-size.txt——code=<정수>、limit_bytes=<본문에 적힌 한도 바이트>、sent_bytes=<보낸 줄의 길이>(占位符依次为整数、正文里写的限额字节数、所发送行的长度)。

除了 /opt/lab/d5/gen.py,也可以自己写一小段 Python 来生成请求体 JSON。400 的正文里会把限额和所发送的长度以字节为单位并排写出来。这种拒绝之所以危险,是因为同一个请求里好好的行也会一起掉。

启动设置了较低限额的第二个 Loki

写出 /root/lk-ingest-limits/loki-low.yaml,在 3300 端口(gRPC 9097)再启动一个 Loki。存储路径放在 /tmp/lokilow 之下,limits_config.per_stream_rate_limit 是 4KB,per_stream_rate_limit_burst 是 8KB。从那个服务器的 /config 里读出两个值,以 rate=<값> 和 burst=<값>(占位符均为值)两行写入 /root/lk-ingest-limits/04-low.txt。

只改 HTTP 端口的话,gRPC 会冲突而挂掉——日志文件的最后一行会原样写出来。存储路径也不能与第一个重叠。默认限额(3MB/秒)在本实验里很难造出 429,所以特意把它调低。

亲自收到一次真正的 429

向 3300 端口的 Loki,把 900 字节的行以一个流({"app":"flood"})连续发送,收到 429。在 /root/lk-ingest-limits/05-429.txt 中写两行——first_429=<처음 429 가 난 순번, 1부터> 和 limit_in_body=<본문에 적힌 한도 문자열>(占位符依次为第一次出现 429 的序号(从 1 开始)、正文里写的限额字符串)。并把 429 响应正文整个保存到 /root/lk-ingest-limits/429.body。

时间戳每次都必须不同(时间戳相同、内容相同会被折叠成重复)。正文里会用句子写出哪个流超过了哪个限额。第几次出现,由于突发而不容易一下子看出来——这就是下一步。

开头几次为什么通过了

用第 1、4 步读出的两个值和第 5 步的序号来说明突发。在 /root/lk-ingest-limits/06-burst.txt 中写四行——rate_bytes=<초당 허용 바이트 정수>、burst_bytes=<버스트 바이트 정수>、sent_bytes=<429 가 나기까지 보낸 본문 바이트 합>、why=<공백 뺀 40자 이상 한 문장>(占位符依次为每秒允许字节数整数、突发字节数整数、出现 429 之前所发送的正文字节数合计、去掉空格后至少 40 个字的一句话)。

4KB 是 4096 字节。所发送的字节合计,用行长乘以序号来估算(不计头部和标签,只算正文)。请用数字说明,有突发的话,平时平静、突然爆发的流量,开头一段时间能通过。

应用 ① ——用退避重试一行不丢

创建 /root/lk-ingest-limits/send.sh,向 {"app":"keep"} 流往 3300 端口发送 20 条 900 字节的行,收到 429 就用指数退避重试,使它们全部存下。时间戳从基准时刻往前的 20 秒内,每行间隔 1 秒。再在 /root/lk-ingest-limits/07-send.txt 中写三行 sent=20、stored=<저장된 줄 수>、retries=<재시도 횟수>(占位符依次为已存储的行数、重试次数)。已存储的行数要通过查询来确认。

退避就是每次失败都把等待时间加长(例如 1 秒、2 秒、4 秒)。要设上限,别让它永远循环下去——这是把 400 误当成 429 时的安全装置。确认存储,只要以基准时刻前后为区间查询那个流,数一下行数即可。

应用 ② ——设计成低于限额

假设有一个服务每秒输出 12,000 字节,要低于第 4 步的限额,计算至少要把流拆成几个,写成四行存入 /root/lk-ingest-limits/08-design.txt——throughput_bytes=12000、limit_bytes=<4단계 한도의 바이트>、streams_needed=<올림한 정수>、tradeoff=<공백 뺀 60자 이상>(占位符依次为第 4 步限额的字节数、向上取整后的整数、去掉空格后至少 60 个字的内容)。最后一行里,同时写出增加流的代价。

速率限制以流为单位,所以拆开后限额就相应增加。但增加流,就是增加标签组合,而那一边有基数的成本。请在最后一行写出,这两股力量是反向拉扯的。