LabHub
배우기 러닝패스 코스

FDE Capstone: The Warehouse Got the Same Order Three Times

3 a.m., and the commands in the handoff doc were wrong

LabHub 에서 이어서 보기

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

목표

인계받을 운영팀을 위해 지표 → 경보 조건 → 런북 항목 → 실행 가능한 확인 명령의 사슬을 만들고, 채점기가 가짜 서비스를 여러 장애 모드로 띄워 그 사슬이 정확한 항목을 가리키는지 훈련으로 확인한다.

왜 중요한가

인계 문서는 쓴 날에만 맞다. 경보가 어떤 조건에서 울리는지, 울렸을 때 무엇으로 확인하는지가 코드와 따로 적혀 있으면 서비스가 바뀌는 순간 문서가 낡고, 그 사실은 새벽의 당번이 명령을 칠 때 드러난다. 그래서 경보 규칙은 스크립트가 읽는 데이터로 두고, 런북의 확인 줄은 종료 코드로 답하는 명령으로 쓰고, 그 명령이 실제로 도는지 점검기와 훈련으로 되풀이해 확인한다. 조용한 대시보드(지표를 못 긁음)를 정상으로 읽지 않는 것도 같은 사슬의 일부다.

예상 75분이다. 기본 60분 세션이 끝나기 전에 +시간으로 연장하자(최대 180분). 세션이 끝나면 /root/drill 의 파일은 사라지니, 남기고 싶은 코드는 종료 전에 따로 보관한다.

재료

단계

  1. 가짜 서비스를 정상 모드로 띄우고 /metrics 응답을 가공 없이 /root/drill/normal.prom 에 저장한다. 원문의 # TYPE 줄에서 이름 형식 을 한 줄에 하나씩 /root/drill/families.txt 에 적는다.
  2. /root/drill/promtext.py 를 쓴다. python3 promtext.py FILE 이 샘플마다 {"name","labels","value","timestamp"} 객체의 JSON 배열을 출력한다. 값은 숫자, 무한대·NaN 은 문자열 "+Inf"·"-Inf"·"NaN", 타임스탬프가 없으면 null. 주석·빈 줄은 건너뛰고, 레이블 값 안의 쉼표·중괄호와 \\·\"·\n 이스케이프를 올바르게 읽는다. 잘못된 샘플 줄이 하나라도 있으면 종료 코드 2.
  3. 인계 메모의 여섯 경보를 /root/drill/alerts.json 에 옮긴다. {"alerts": [{"alert","severity","runbook","rule"}]} 형식이고 rule.kind 는 threshold(metric·op·value), ratio(metric·denominator·op·value), expires_within(metric·seconds), scrape, absent(metrics) 중 하나다. 채점기는 경계값을 포함한 여러 상황에서 여러분의 규칙을 판정해 본다.
  4. /root/drill/diagnose.py 를 쓴다. python3 diagnose.py --url URL [--rules 경로](기본 /root/drill/alerts.json)가 URL/metrics 를 한 번 긁어 규칙으로 판정하고 {"target": URL, "firing": [{"alert","severity","runbook","labels"}]} 를 출력한다. labels 는 그 샘플의 레이블이다. 종료 코드는 경보가 없으면 0, 있으면 1. 문턱은 규칙 파일에서 읽는다.
  5. /root/drill/diagnose.py 가 못 긁은 상황을 경보로 올리게 고친다. 연결 실패·HTTP 200 아님·형식 오류·orders_up 없음이면 OrdersTargetDown(labels {}) 하나만 내고 종료 코드 2. 필수 지표가 빠지면 빠진 지표마다 OrdersMetricAbsent(labels {"metric": 이름})를 낸다.
  6. 여섯 경보의 런북 항목을 /root/drill/runbook.md 에 쓴다. 항목은 ## 런북이름 제목과 - 경보:·- 확인:·- 판단:·- 조치:·- 에스컬레이션: 다섯 줄이다. 확인 줄은 백틱으로 감싼 한 줄 명령이고 $ORDERS_URL 로 서비스를 가리키며, bash -o pipefail -c 로 돌렸을 때 정상이면 0, 그 장애면 0 이 아닌 값으로 곧바로 끝나야 한다.
  7. /root/drill/check_runbook.py 를 쓴다. python3 check_runbook.py 런북.md --url URL 이 항목마다 확인 명령을 ORDERS_URL 환경 변수와 함께 bash -o pipefail -c 로 5초 제한 안에 돌리고, 런북 순서대로 [{"id","check","exit","status","reason"}] 를 출력한다. status 는 종료 코드 0 이면 ok, 아니면 broken. reason 은 ok·timeout·not-found(127)·exit N·no-check(확인 줄 없음). 하나라도 broken 이면 종료 코드 1. 벤더 런북에 돌려 고장 난 줄을 찾는다.
  8. 당번 훈련 스크립트 /root/drill/oncall.sh 를 쓴다. bash oncall.sh URL 이 diagnose.py 로 경보를 얻고, 경보마다 runbook.md 의 항목을 찾아 확인 명령을 돌려 {"target": URL, "incidents": [{"alert","severity","runbook","labels","check_exit","confirmed","escalation"}]} 를 출력한다. confirmed 는 확인 명령이 0 이 아닌 값으로 끝났을 때(장애 확증) true, escalation 은 런북의 에스컬레이션 줄 그대로. 사건이 있으면 종료 코드 1, 없으면 0.

참고

지표 원문을 그대로 긁어 두기

정상 모드 가짜 서비스의 /metrics 응답을 /root/drill/normal.prom 에, TYPE 줄의 이름 형식 목록을 /root/drill/families.txt 에 저장한다.

# TYPE <이름> <형식> 줄은 지표 계열마다 하나뿐입니다. histogram 계열은 TYPE 줄의 이름이 기준이고, 샘플에는 _bucket·_sum·_count 가 붙어 나옵니다. awk 로 첫 두 칸이 # 와 TYPE 인 줄만 고르면 됩니다.

쉼표와 따옴표에 속지 않는 파서

Prometheus 텍스트 노출 형식을 샘플 객체 JSON 배열로 바꾸는 /root/drill/promtext.py 를 쓴다. 잘못된 샘플 줄이 있으면 종료 코드 2.

레이블 부분은 split(',') 로 자를 수 없습니다. 따옴표 안에서는 쉼표와 } 가 값의 일부이고, 역슬래시 다음 글자는 이스케이프입니다. 문자를 하나씩 읽는 작은 상태 기계(따옴표 안인가·역슬래시 직후인가)를 만드세요. 값은 float() 로 읽되 JSON 에 없는 무한대·NaN 은 문자열로 바꿉니다.

인계 메모의 경보 여섯 개를 데이터로

/opt/lab/drill/brief.md 의 경보 표를 /root/drill/alerts.json 규칙으로 옮긴다.

'미만'과 '이하', '초과'와 '이상'은 서로 다른 op 입니다. 인증서 지표는 만료 시각이라 expires_within 을, 큐는 깊이가 아니라 가장 오래된 메시지의 나이를 씁니다. 못 긁음은 scrape, 필수 지표 목록은 absent 의 metrics 에 적습니다.

한 번 긁어 규칙으로 판정하기

지표를 긁어 alerts.json 으로 판정하고 울린 경보를 JSON 으로 내는 /root/drill/diagnose.py 를 쓴다(경보 없음 0 · 있음 1).

샘플을 이름별로 묶어 두면 규칙 종류마다 필요한 것만 꺼낼 수 있습니다. ratio 는 분자와 분모를 같은 레이블끼리 짝지어야 합니다. expires_within 은 '지표값 − 지금 시각' 을 비교합니다. 앞 단계의 promtext.py 를 import 하면 파서를 다시 쓸 필요가 없습니다.

조용한 대시보드를 정상으로 읽지 않기

/root/drill/diagnose.py 가 못 긁음은 OrdersTargetDown·종료 코드 2 로, 빠진 필수 지표는 OrdersMetricAbsent 로 올리게 고친다.

urlopen 은 연결 실패에 URLError, 200 이 아닌 응답에 HTTPError 를 던집니다. 파서의 ParseError 도 못 긁음입니다. 이 셋을 빈 샘플 목록으로 바꾸면 모든 비교가 거짓이 되어 경보가 0개로 나옵니다 — 그 자체가 경보여야 합니다. absent 는 '이 이름의 샘플이 하나도 없는가' 로 판정합니다.

종료 코드로 답하는 런북

여섯 경보의 항목을 /root/drill/runbook.md 에 쓴다. 확인 명령은 정상 0 · 해당 장애 비0 으로 실제로 동작해야 한다.

진단 엔드포인트(/debug/deps·/debug/disk·/debug/tls·/debug/queue)는 JSON 이라 jq -e 가 잘 맞습니다. -e 는 결과가 false·null 이면 종료 코드 1 을 냅니다. curl 에는 -f(4xx·5xx 를 실패로)와 -m(시간 제한)을 붙이세요. target-down 은 200 인 점검 페이지도 잡아야 하므로 응답 안에 orders_up 샘플이 있는지까지 봅니다.

벤더 런북을 돌려 보고 고장 난 줄 찾기

런북의 확인 명령을 실제로 돌려 판정하는 /root/drill/check_runbook.py 를 쓰고, /opt/lab/drill/vendor-runbook.md 에 돌려 본다.

subprocess.run(timeout=…) 은 시간이 지나면 bash 만 죽이고, 파이프 뒤에 남은 curl 이 출력 파이프를 붙잡아 기다림이 끝나지 않을 수 있습니다. Popen(start_new_session=True) 로 띄우고 시간이 지나면 os.killpg 로 그룹째 끊으세요. 종료 코드 127 은 '명령을 찾을 수 없음' 입니다.

경보부터 에스컬레이션까지 당번 훈련

diagnose.py → runbook.md → 확인 명령 → 에스컬레이션을 한 번에 도는 /root/drill/oncall.sh 를 쓴다.

diagnose.py 의 종료 코드 1·2 는 오류가 아니라 판정 결과이니 set -e 로 스크립트를 끊지 마세요. 런북 파싱과 시간 제한 실행은 7단계 check_runbook.py 의 함수를 import 해 다시 쓰면 됩니다. confirmed 는 '확인 명령이 장애를 확증했는가' 라서 종료 코드가 0 이 아닐 때 true 입니다.