LabHub
배우기 러닝패스 코스

Loki — A Log Store That Does Not Index Logs

I left retries on and went home; by morning the queue was full

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

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_limit4KB, per_stream_rate_limit_burst8KB 입니다. 그 서버의 /config 에서 두 값을 읽어 /root/lk-ingest-limits/04-low.txtrate=<값>burst=<값> 두 줄로 적습니다.
  5. 3300 포트의 Loki 에 900바이트짜리 줄을 한 스트림({"app":"flood"})으로 연달아 보내 429 를 받으세요. /root/lk-ingest-limits/05-429.txt 에 두 줄을 적습니다 — first_429=<처음 429 가 난 순번, 1부터>limit_in_body=<본문에 적힌 한도 문자열>. 그리고 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자 이상 한 문장>.
  7. /root/lk-ingest-limits/send.sh 를 만들어 {"app":"keep"} 스트림에 900바이트 줄 20개를 3300 포트로 보내되, 429 를 받으면 지수 백오프로 다시 시도해 전부 저장되게 하세요. 타임스탬프는 기준 시각에서 과거 20초 동안 한 줄에 1초씩 씁니다. 그리고 /root/lk-ingest-limits/07-send.txtsent=20, stored=<저장된 줄 수>, retries=<재시도 횟수> 세 줄을 적으세요. 저장된 줄 수는 질의해서 확인합니다.
  8. 초당 12,000바이트를 내보내는 서비스가 있다고 할 때, 4단계의 한도 아래로 들어가려면 스트림을 최소 몇 개로 나눠야 하는지 계산해 /root/lk-ingest-limits/08-design.txt 에 네 줄로 적으세요 — throughput_bytes=12000, limit_bytes=<4단계 한도의 바이트>, streams_needed=<올림한 정수>, tradeoff=<공백 뺀 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 와는 별개로, 몸통 JSON 을 만드는 작은 파이썬을 직접 써도 됩니다. 400 본문에 한도와 보낸 길이가 바이트로 나란히 적혀 나옵니다. 이 거절이 위험한 이유는 같은 요청에 실린 멀쩡한 줄까지 함께 떨어지기 때문입니다.

낮은 한도를 건 두 번째 Loki 를 띄운다

/root/lk-ingest-limits/loki-low.yaml 을 써서 3300 포트(gRPC 9097)에 Loki 를 하나 더 띄우세요. 저장 경로는 /tmp/lokilow 아래이고 limits_config.per_stream_rate_limit4KB, per_stream_rate_limit_burst8KB 입니다. 그 서버의 /config 에서 두 값을 읽어 /root/lk-ingest-limits/04-low.txtrate=<값>burst=<값> 두 줄로 적습니다.

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 응답 본문을 통째로 /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자 이상 한 문장>.

4KB 는 4096 바이트입니다. 보낸 바이트 합은 줄 길이 곱하기 순번으로 어림합니다 (헤더와 라벨은 빼고 본문만 셉니다). 버스트가 있으면 잠잠하다 몰아치는 트래픽이 처음 얼마간은 통과한다는 것을 숫자로 설명하세요.

응용 ① — 백오프 재시도로 한 줄도 잃지 않는다

/root/lk-ingest-limits/send.sh 를 만들어 {"app":"keep"} 스트림에 900바이트 줄 20개를 3300 포트로 보내되, 429 를 받으면 지수 백오프로 다시 시도해 전부 저장되게 하세요. 타임스탬프는 기준 시각에서 과거 20초 동안 한 줄에 1초씩 씁니다. 그리고 /root/lk-ingest-limits/07-send.txtsent=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자 이상>. 마지막 줄에는 스트림을 늘리는 것의 대가를 함께 적습니다.

속도 제한은 스트림 단위라 나누면 한도가 그만큼 늘어납니다. 하지만 스트림을 늘리는 것은 곧 라벨 조합을 늘리는 것이고, 그쪽에는 카디널리티 비용이 있습니다. 두 힘이 서로 반대로 당긴다는 것을 마지막 줄에 적으세요.