서버가 2초 멈췄는데 p99 는 60밀리초였다
목표
같은 서버를 닫힌 루프와 열린 모델로 한 번씩 재서 p99 가 얼마나 벌어지는지 직접 만들어 보고, 닫힌 루프 표본에 HdrHistogram 식 보정을 손으로 구현한 뒤, 보정 전후로 SLA 판정이 뒤집히는 것을 확인하고 시험 설계 규칙을 파일로 남깁니다.
왜 중요한가
닫힌 루프 부하 발생기는 앞 요청의 응답을 받아야 다음 요청을 보낸다. 그래서 서버가 멈춰 있는 동안에는 요청을 아예 보내지 않고, 그 구간은 지연 분포에 거의 남지 않는다. 실제 사용자는 서버 사정을 봐 가며 요청을 미루지 않으므로, 이 측정은 항상 실제보다 좋아 보이는 쪽으로만 틀린다. 무작위로 흔들리는 오차가 아니라 한 방향으로만 기우는 오차라서 아무도 의심하지 않고 출시 심사를 통과한다. 고치는 방법은 두 가지다 — 처음부터 발사 시각을 못 박는 열린 모델로 재거나, 이미 잰 표본을 기대 간격으로 메우거나. 둘 중 하나는 반드시 해야 하고, 보고서에는 어느 쪽이었는지를 남겨야 한다.
단계
/opt/lab/lt/lt-coordinated-omission/target.py를 포트 8080 에 띄우세요(정상 응답 0.05초, 멈춤 2.0초, 150번째 요청에서 멈춤). 그다음curl -s -o /dev/null -w '%{time_total}\n'로/를 여섯 번 불러 그 여섯 줄을/root/lt-coordinated-omission/01-probe.txt에 그대로 남기고,/root/lt-coordinated-omission/01-target.txt에 네 줄port=·base_ms=·stall_ms=·stall_at=을 적으세요.base_ms는 여섯 줄의 중앙값을 밀리초 정수로,stall_ms와stall_at은 서버 소스에서 읽은 값입니다.- 요청 번호를 되돌린 뒤(
/reset)hey -n 200 -c 1 -q 12.5 -t 60 -o csv로 재서 원본 CSV 를/root/lt-coordinated-omission/closed.csv에 그대로 남기세요. 그 CSV 의response-time칸만 써서/root/lt-coordinated-omission/02-closed.txt에 네 줄samples=·p50_ms=·p99_ms=·max_ms=를 적습니다. 밀리초 정수로 반올림해 적으세요. 백분위는 오름차순 정렬 뒤인덱스 = int(백분위 ÷ 100 × 표본수)(0부터 세고 표본수−1 을 넘으면 마지막) 자리의 값으로 계산합니다. hey 가 쓰는 방식과 같습니다. /root/lt-coordinated-omission/schedule.txt에 200줄을 만드세요. 한 줄에 하나씩, 요청 i 를 보내기로 의도한 시각을 초 단위 소수 셋째 자리까지 적습니다. 첫 줄은0.000이고 간격은 0.080초로 일정합니다. 이 파일은 다음 단계에서 보정 지연의 기준이 됩니다./root/lt-coordinated-omission/openloop.py를 직접 짜세요.schedule.txt의 시각마다 요청을 하나씩 보내되 앞 요청을 기다리지 않습니다. 결과는/root/lt-coordinated-omission/open.csv에 머리글i,intended,sent,received,code와 200줄로 남기고(시각은 측정 시작부터의 초),/root/lt-coordinated-omission/04-open.txt에 보정 지연(received − intended)의samples=·p50_ms=·p99_ms=·max_ms=네 줄을 적으세요./root/lt-coordinated-omission/hdrfix.py를 짜서closed.csv의 지연 표본을 기대 간격 0.080초로 메우세요. 표본 하나의 값이 기대 간격보다 크면 그 값에서 기대 간격을 계속 빼 가며 0보다 큰 동안 표본을 더 만들어 넣습니다(HdrHistogram 의recordValueWithExpectedInterval이 하는 일). 메운 결과를 한 줄에 하나씩/root/lt-coordinated-omission/closed-corrected.txt에 초 단위로 남기고,/root/lt-coordinated-omission/05-hdr.txt에 다섯 줄expected_interval_ms=·samples=·p50_ms=·p99_ms=·max_ms=를 적으세요./root/lt-coordinated-omission/compare.tsv를 만드세요. 머리글 없이 세 줄이고 각 줄은 탭으로 나눈 네 칸<이름> <p50_ms> <p99_ms> <max_ms>입니다. 이름은 순서대로closed(2단계) ·hdr(5단계) ·open(4단계) 이고 숫자는 앞 단계에서 적은 밀리초 정수를 그대로 옮깁니다./root/lt-coordinated-omission/sla.txt에 여섯 줄을 적으세요.sla_p99_ms=에는 100 과 1000 사이의 정수 임계값을,closed_p99_ms=와corrected_p99_ms=에는 각각 닫힌 루프(2단계)와 열린 모델 보정(4단계)의 p99 를,closed_verdict=와corrected_verdict=에는 그 p99 가 임계값 이하면pass, 크면fail을,flipped=에는 두 판정이 다르면yes, 같으면no를 적습니다./root/lt-coordinated-omission/08-summary.txt에 두 줄worst_case_ms=(열린 모델 보정 지연의 최댓값)과omitted_requests=(닫힌 루프가 멈춰 있는 동안 보내지 못한 요청 수 = 가장 큰 지연 표본을 기대 간격 0.080초로 나눈 몫)을 적으세요. 그리고/root/lt-coordinated-omission/rules.md에- <열쇠>: <설명>꼴의 규칙 네 줄을 적습니다. 열쇠는 순서대로rate·correct·report·gate이고 설명은 각각 40자 이상이어야 합니다.
참고
- 작업 디렉터리는
/root/lt-coordinated-omission입니다. 없으면 먼저 만드세요. - 부하 대상과 교통 자료는
/opt/lab/lt/lt-coordinated-omission/에 있습니다 —target.py한 개입니다. 파드에서는 컨테이너를 띄울 수 없으므로 부하 대상은 파이썬 표준 라이브러리 HTTP 서버를 127.0.0.1 에 직접 띄워 씁니다. - 측정을 시작하기 전에
curl http://127.0.0.1:8080/reset으로 요청 번호를 되돌리세요. 되돌리지 않으면 멈추는 요청의 위치가 달라집니다. - 흔한 실수: 최댓값이 크니까 p99 도 클 것이라고 짐작하는 것. 표본 200개에서 나쁜 표본이 하나면 p99 에는 잡히지 않습니다. 두 숫자를 늘 함께 보세요.
- 흔한 실수: 열린 모델 스크립트가 앞 응답을 기다리게 짜는 것.
open.csv의sent가intended에서 멀어지면 그것은 열린 모델이 아니라 느린 닫힌 루프입니다. - hey 사용법과 옵션 · wrk2 — coordinated omission 보정 · HdrHistogram · k6 — open vs closed model · vegeta — 고정 비율 공격
주기적으로 멈추는 부하 대상을 띄운다
/opt/lab/lt/lt-coordinated-omission/target.py 를 포트 8080 에 띄우세요(정상 응답 0.05초, 멈춤 2.0초, 150번째 요청에서 멈춤). 그다음 curl -s -o /dev/null -w '%{time_total}\n' 로 / 를 여섯 번 불러 그 여섯 줄을 /root/lt-coordinated-omission/01-probe.txt 에 그대로 남기고, /root/lt-coordinated-omission/01-target.txt 에 네 줄 port= · base_ms= · stall_ms= · stall_at= 을 적으세요. base_ms 는 여섯 줄의 중앙값을 밀리초 정수로, stall_ms 와 stall_at 은 서버 소스에서 읽은 값입니다.
백그라운드로 띄우려면 nohup python3 ... > server.log 2>&1 & 입니다. 뜰 때까지 1~2초 기다리세요. 8080 가 이미 열려 있으면 두 번 띄울 수 없습니다 — curl http://127.0.0.1:8080/reset 이 reset 을 돌려주면 이미 떠 있는 것입니다. 중앙값은 sort -n 뒤 가운데 줄을 보면 됩니다.
닫힌 루프로 재고 원본 백분위를 적는다
요청 번호를 되돌린 뒤(/reset) hey -n 200 -c 1 -q 12.5 -t 60 -o csv 로 재서 원본 CSV 를 /root/lt-coordinated-omission/closed.csv 에 그대로 남기세요. 그 CSV 의 response-time 칸만 써서 /root/lt-coordinated-omission/02-closed.txt 에 네 줄 samples= · p50_ms= · p99_ms= · max_ms= 를 적습니다. 밀리초 정수로 반올림해 적으세요. 백분위는 오름차순 정렬 뒤 인덱스 = int(백분위 ÷ 100 × 표본수) (0부터 세고 표본수−1 을 넘으면 마지막) 자리의 값으로 계산합니다. hey 가 쓰는 방식과 같습니다.
-c 1 은 워커 하나, -q 12.5 는 워커마다 초당 12.5건이 상한이라는 뜻입니다(간격 80밀리초). -o csv 를 주면 요약 대신 요청 하나당 한 줄이 나오고 첫 줄은 머리글입니다. p99 와 최댓값이 몇 배 차이 나는지 보세요 — 그 간격이 이 실습의 주제입니다.
의도한 발사 시각을 미리 못 박는다
/root/lt-coordinated-omission/schedule.txt 에 200줄을 만드세요. 한 줄에 하나씩, 요청 i 를 보내기로 의도한 시각을 초 단위 소수 셋째 자리까지 적습니다. 첫 줄은 0.000 이고 간격은 0.080초로 일정합니다. 이 파일은 다음 단계에서 보정 지연의 기준이 됩니다.
닫힌 루프에는 '의도한 시각' 이라는 것이 아예 없습니다 — 앞 응답이 오면 그때 보내니까요. 열린 모델은 그 시각을 먼저 정해 두고 서버 사정과 무관하게 지킵니다. seq 나 awk 한 줄이면 만들 수 있습니다. 마지막 줄은 (200 − 1) × 0.08 입니다.
열린 모델로 다시 재고 보정 지연을 구한다
/root/lt-coordinated-omission/openloop.py 를 직접 짜세요. schedule.txt 의 시각마다 요청을 하나씩 보내되 앞 요청을 기다리지 않습니다. 결과는 /root/lt-coordinated-omission/open.csv 에 머리글 i,intended,sent,received,code 와 200줄로 남기고(시각은 측정 시작부터의 초), /root/lt-coordinated-omission/04-open.txt 에 보정 지연(received − intended)의 samples= · p50_ms= · p99_ms= · max_ms= 네 줄을 적으세요.
요청마다 스레드를 하나 만들고 그 스레드가 자기 시각까지 time.sleep 한 뒤 보내면 됩니다. urllib.request.urlopen 이면 충분합니다. 표준 라이브러리만 씁니다(pip 설치 불가). sent 와 intended 가 거의 같아야 열린 모델입니다 — 벌어지면 발생기가 못 따라간 것입니다.
HdrHistogram 의 보정을 손으로 구현한다
/root/lt-coordinated-omission/hdrfix.py 를 짜서 closed.csv 의 지연 표본을 기대 간격 0.080초로 메우세요. 표본 하나의 값이 기대 간격보다 크면 그 값에서 기대 간격을 계속 빼 가며 0보다 큰 동안 표본을 더 만들어 넣습니다(HdrHistogram 의 recordValueWithExpectedInterval 이 하는 일). 메운 결과를 한 줄에 하나씩 /root/lt-coordinated-omission/closed-corrected.txt 에 초 단위로 남기고, /root/lt-coordinated-omission/05-hdr.txt 에 다섯 줄 expected_interval_ms= · samples= · p50_ms= · p99_ms= · max_ms= 를 적으세요.
지연 2.0초짜리 표본 하나는 기대 간격 0.08초에서 1.92 · 1.84 · ... 로 표본 여러 개를 만들어 냅니다. 원래 표본은 그대로 두고 만들어 낸 것을 더합니다. 기대 간격은 2단계에서 -q 로 정한 간격 (1 ÷ 12.5 = 0.08초)입니다. 백분위 계산 방식은 앞 단계와 같습니다.
세 분포를 한 표에 놓는다
/root/lt-coordinated-omission/compare.tsv 를 만드세요. 머리글 없이 세 줄이고 각 줄은 탭으로 나눈 네 칸 <이름> <p50_ms> <p99_ms> <max_ms> 입니다. 이름은 순서대로 closed(2단계) · hdr(5단계) · open(4단계) 이고 숫자는 앞 단계에서 적은 밀리초 정수를 그대로 옮깁니다.
세 줄의 p50 은 거의 같은데 p99 만 갈라지는 것이 핵심입니다. p50 이 같다는 것은 서버가 평소에 잘 돌고 있었다는 뜻이고, p99 가 갈라진다는 것은 나쁜 구간의 표본이 어느 쪽에는 있고 어느 쪽에는 없다는 뜻입니다. 채점기는 세 원본 파일에서 값을 다시 계산해 맞춰 봅니다.
같은 시험, 뒤집히는 SLA 판정
/root/lt-coordinated-omission/sla.txt 에 여섯 줄을 적으세요. sla_p99_ms= 에는 100 과 1000 사이의 정수 임계값을, closed_p99_ms= 와 corrected_p99_ms= 에는 각각 닫힌 루프(2단계)와 열린 모델 보정(4단계)의 p99 를, closed_verdict= 와 corrected_verdict= 에는 그 p99 가 임계값 이하면 pass, 크면 fail 을, flipped= 에는 두 판정이 다르면 yes, 같으면 no 를 적습니다.
임계값을 어디에 걸든 상관없지만, 두 판정이 갈리는 자리를 고르면 이 단계의 요점이 드러납니다. 판정이 뒤집혔다면 고쳐야 하는 것은 임계값이 아니라 '어느 숫자로 약속했는가' 입니다. 보고서에 보정 여부를 적지 않으면 반년 뒤에 아무도 구분하지 못합니다.
다시 속지 않기 위한 시험 설계 규칙
/root/lt-coordinated-omission/08-summary.txt 에 두 줄 worst_case_ms= (열린 모델 보정 지연의 최댓값)과 omitted_requests= (닫힌 루프가 멈춰 있는 동안 보내지 못한 요청 수 = 가장 큰 지연 표본을 기대 간격 0.080초로 나눈 몫)을 적으세요. 그리고 /root/lt-coordinated-omission/rules.md 에 - <열쇠>: <설명> 꼴의 규칙 네 줄을 적습니다. 열쇠는 순서대로 rate · correct · report · gate 이고 설명은 각각 40자 이상이어야 합니다.
네 규칙이 답해야 하는 질문은 이렇습니다 — 부하를 어떤 모델로 걸 것인가(rate), 이미 잰 표본을 어떻게 보정할 것인가(correct), 보고서에 무엇을 함께 적을 것인가(report), 어떤 숫자로 통과를 판정할 것인가(gate). 각 줄에 자기 말로 쓰되 앞 단계에서 본 숫자를 근거로 삼으세요.