평균은 같은데 한쪽만 대기열이 터졌다
목표
같은 평균 요청률로 일정 간격 · 지수분포 · 묶음 세 가지 도착을 직접 만들어 같은 서버에 보내고, 대기열과 지연이 어떻게 갈리는지 잽니다. 이어서 닫힌 루프의 생각 시간이 실효 요청률을 정하는 법칙을 실측과 대조하고, 고른 도착을 가정한 용량 산정이 몇 배 빗나가는지 숫자로 보인 뒤 이 서비스의 도착 모델과 워커 수를 근거와 함께 결정합니다.
왜 중요한가
용량 산정은 거의 언제나 평균으로 시작한다 — 초당 몇 건, 한 건에 몇 밀리초, 그러니 워커 몇 개. 그 계산에는 '요청이 고르게 온다' 는 가정이 숨어 있는데, 실제 트래픽은 크론과 알림과 재시도 때문에 묶음으로 온다. 대기열은 평균이 아니라 순간에 반응하므로, 평균 이용률이 25% 여도 묶음이 오는 순간에는 워커가 모자라고 그 대기가 다시 타임아웃과 재시도를 만든다. 반대편에는 닫힌 루프의 함정이 있다. 가상 사용자 수로 시험하면 서버가 느려질 때 부하도 함께 줄어, 보고서의 '초당 몇 건까지 버텼다' 가 서버의 한계가 아니라 시험 설정의 한계가 된다. 이 두 가지를 한 번 손으로 재 보면, 다음부터는 부하를 적을 때 평균 옆에 도착 모양을 함께 적게 된다.
단계
/opt/lab/lt/lt-arrival-process/server.py를python3 /opt/lab/lt/lt-arrival-process/server.py 8080 40 4로 띄우세요(포트 8080 · 서비스 40밀리초 · 워커 4). 한가할 때의 응답 시간을curl -s -o /dev/null -w '%{time_total}\n'로 여섯 번 재서 나온 초 단위 숫자를/root/lt-arrival-process/01-probe.txt에 한 줄에 하나씩 그대로 남기세요. 그리고/root/lt-arrival-process/01-target.txt에 다섯 줄을 적습니다 —service_ms=(여섯 번의 중앙값을 밀리초로 반올림) ·workers=(워커 수) ·max_rate=(워커 수 나누기 서비스 시간, 초당 건수) ·chosen_rate=25·utilization=(chosen_rate 를 max_rate 로 나눈 값, 소수 둘째 자리까지)./root/lt-arrival-process/gen.py를 만드세요.python3 gen.py <even|poisson|burst> <요청수> <초당요청> <시드> <출력CSV> [URL]로 부릅니다. URL 을 주지 않으면 보내지 않고 계획만 CSV 에 적습니다 — 머리글i,intended에 이어 요청마다 번호와 예정 시각(시작 기준 초)을 적습니다.even은 간격이 늘 1/요청률,poisson은 간격이 평균 1/요청률 인 지수분포,burst는 12건을 같은 시각에 몰아 보내고 12/요청률 만큼 쉽니다. 시드를 받아 난수를 고정하세요. 확인용으로python3 gen.py even 300 25 7 /root/lt-arrival-process/sched-even.csv를 한 번 돌려 두세요.- 발생기를 마른 실행으로 세 번 돌려
/root/lt-arrival-process/sched-even.csv·/root/lt-arrival-process/sched-poisson.csv·/root/lt-arrival-process/sched-burst.csv를 만드세요(모두300 25 7). 그리고/root/lt-arrival-process/schedules.tsv에 세 줄을 적습니다. 각 줄은 탭으로 나눈 네 칸<모양> <요청 수> <평균 간격> <변동계수>이고 모양은even·poisson·burst순서, 평균 간격은 소수 넷째 자리까지 초 단위, 변동계수는 간격의 표준편차를 평균으로 나눈 값으로 소수 셋째 자리까지 적습니다. - 세 도착 방식으로 같은 서버에 같은 수의 요청을 실제로 보내세요. 방식마다
curl -s http://127.0.0.1:8080/reset으로 기록을 지우고,python3 gen.py <모양> 150 25 7 /root/lt-arrival-process/run-<모양>.csv http://127.0.0.1:8080/work로 보낸 뒤,curl -s http://127.0.0.1:8080/dump > /root/lt-arrival-process/srv-<모양>.csv로 서버 기록을 받습니다(모양은even·poisson·burst). 그다음/root/lt-arrival-process/latency.tsv에 세 줄을 적으세요 — 탭으로 나눈 네 칸<모양> <성공 건수> <p50> <p95>이고 지연은run-*.csv의received빼기sent를 밀리초로 바꿔 가장 가까운 순위로 구해 소수 첫째 자리까지,code가 200 인 줄만 셉니다. - 서버가 남긴
/root/lt-arrival-process/srv-*.csv에는 요청마다qlen(그 요청이 닿은 순간 시스템 안에 있던 요청 수)이 들어 있습니다./root/lt-arrival-process/queue.tsv에 세 줄을 적으세요 — 탭으로 나눈 네 칸<모양> <기록 수> <최대 대기열> <평균 대기열>이고 모양은even·poisson·burst순서, 평균은 소수 둘째 자리까지입니다. 그리고/root/lt-arrival-process/05-note.txt에 두 줄worst=<최대 대기열이 가장 큰 모양>과ratio=<그 최대값을 even 의 최대값으로 나눈 값, 소수 첫째 자리까지>를 적으세요. /root/lt-arrival-process/closed.py를 만드세요.python3 closed.py <URL> <사용자수> <생각시간초> <지속초> <출력CSV>로 부르면 사용자마다 요청 하나를 보내고 생각 시간만큼 쉬는 일을 지속 시간 동안 되풀이합니다. CSV 는 머리글user,sent,received,code에 이어 시작 기준 초를 적습니다.python3 closed.py http://127.0.0.1:8080/work 8 0.2 10 /root/lt-arrival-process/closed.csv로 한 번 돌리고,/root/lt-arrival-process/think.txt에 여섯 줄을 적으세요 —users=8·think_s=0.200·mean_r_s=(평균 응답 시간) ·predicted_rate=(사용자수 나누기 (mean_r_s 더하기 think_s)) ·measured_rate=(성공 건수 나누기 처음 sent 부터 마지막 received 까지의 시간) ·naive_rate=(응답 시간을 0 으로 보고 사용자수 나누기 think_s). 요청률은 소수 셋째 자리까지, 시간은 소수 넷째 자리까지입니다./root/lt-arrival-process/capacity.txt에 다섯 줄을 적으세요.formula_workers=(고른 도착을 가정한 계산:chosen_rate x service_s를 올림한 정수) ·observed_even=·observed_burst=(각각srv-even.csv와srv-burst.csv에서 동시에 시스템 안에 있던 요청 수의 최댓값) ·underestimate_factor=(observed_burst 를 formula_workers 로 나눈 값, 소수 첫째 자리까지) ·verdict=<under|ok>(formula_workers 가 observed_burst 보다 작으면 under). 동시 요청 수는arrive에 1 을 더하고end에 1 을 빼며 시각 순으로 훑어 최댓값을 잡습니다./root/lt-arrival-process/arrival-model.txt에 네 줄을 적으세요.model=<even|poisson|burst>는 앞으로 이 서비스의 용량을 잡을 때 쓸 도착 모델,workers=는 그 모델로 정한 워커 수(정수),evidence=는 왜 그 모델인지를 앞 단계에서 만든 파일 이름과 숫자를 들어 60자 이상으로,risk=는 그 모델이 틀렸을 때 무엇이 먼저 무너지는지 40자 이상으로 적습니다. 채점기는workers가 7단계에서 관측한 최대 동시 요청 수 이상인지, 그리고 고른 도착을 가정한formula_workers보다 큰지를 봅니다.
참고
- 작업 디렉터리는
/root/lt-arrival-process입니다. 없으면 먼저 만드세요. - 재료는
/opt/lab/lt/lt-arrival-process/server.py하나입니다./work가 일하는 길이고,/reset은 기록을 지우며,/dump는 요청마다seq,arrive,start,end,qlen를 CSV 로 돌려줍니다. 시간은 CPU 가 아니라time.sleep으로 씁니다. - 부하 대상은 컨테이너가 아니라 파이썬 표준 라이브러리 서버입니다. 이 실습 환경에서는 컨테이너를 띄울 수 없습니다.
(nohup python3 /opt/lab/lt/lt-arrival-process/server.py 8080 40 4 >/dev/null 2>&1 &)로 띄우고curl로 한 번 확인한 뒤 부하를 거세요. - 흔한 실수: 방식을 바꾸기 전에
/reset을 부르지 않는 것. 앞 실행의 기록이 남아 대기열 비교가 뒤섞입니다. - 흔한 실수: 묶음 도착의 지연을 '서버가 느려졌다' 로 읽는 것. 서비스 시간은 그대로이고 늘어난 것은 대기 시간입니다 —
srv-*.csv의start빼기arrive가 그 대기입니다. - k6 open vs closed models · k6 executors · k6 constant-arrival-rate · k6 ramping-arrival-rate · Service Level Objectives (SRE Book)
워커가 몇이고 한 건에 얼마나 걸리는가
/opt/lab/lt/lt-arrival-process/server.py 를 python3 /opt/lab/lt/lt-arrival-process/server.py 8080 40 4 로 띄우세요(포트 8080 · 서비스 40밀리초 · 워커 4). 한가할 때의 응답 시간을 curl -s -o /dev/null -w '%{time_total}\n' 로 여섯 번 재서 나온 초 단위 숫자를 /root/lt-arrival-process/01-probe.txt 에 한 줄에 하나씩 그대로 남기세요. 그리고 /root/lt-arrival-process/01-target.txt 에 다섯 줄을 적습니다 — service_ms=(여섯 번의 중앙값을 밀리초로 반올림) · workers=(워커 수) · max_rate=(워커 수 나누기 서비스 시간, 초당 건수) · chosen_rate=25 · utilization=(chosen_rate 를 max_rate 로 나눈 값, 소수 둘째 자리까지).
/work 가 일하는 길이고 /reset 과 /dump 는 기록을 다루는 길입니다. 한가할 때란 부하를 걸지 않은 상태를 말합니다 — 한 번에 하나씩 보내세요. 워커 4개가 각각 40밀리초씩 쓰면 초당 몇 건을 처리할 수 있는지 계산해 보면, 앞으로 걸 초당 25건이 얼마나 여유 있는 부하인지 보입니다.
도착 모양을 고를 수 있는 발생기
/root/lt-arrival-process/gen.py 를 만드세요. python3 gen.py <even|poisson|burst> <요청수> <초당요청> <시드> <출력CSV> [URL] 로 부릅니다. URL 을 주지 않으면 보내지 않고 계획만 CSV 에 적습니다 — 머리글 i,intended 에 이어 요청마다 번호와 예정 시각(시작 기준 초)을 적습니다. even 은 간격이 늘 1/요청률, poisson 은 간격이 평균 1/요청률 인 지수분포, burst 는 12건을 같은 시각에 몰아 보내고 12/요청률 만큼 쉽니다. 시드를 받아 난수를 고정하세요. 확인용으로 python3 gen.py even 300 25 7 /root/lt-arrival-process/sched-even.csv 를 한 번 돌려 두세요.
지수분포 간격은 random.Random(seed).expovariate(rate) 로 만듭니다. burst 의 예정 시각은 (i // 12) * (12 / rate) 한 줄이면 됩니다. 세 방식 모두 마지막 요청의 예정 시각이 거의 같아야 합니다 — 같은 평균 요청률이라는 뜻입니다. 채점기는 자기가 정한 인자로 이 도구를 직접 돌려 봅니다.
같은 평균, 다른 모양
발생기를 마른 실행으로 세 번 돌려 /root/lt-arrival-process/sched-even.csv · /root/lt-arrival-process/sched-poisson.csv · /root/lt-arrival-process/sched-burst.csv 를 만드세요(모두 300 25 7). 그리고 /root/lt-arrival-process/schedules.tsv 에 세 줄을 적습니다. 각 줄은 탭으로 나눈 네 칸 <모양> <요청 수> <평균 간격> <변동계수> 이고 모양은 even · poisson · burst 순서, 평균 간격은 소수 넷째 자리까지 초 단위, 변동계수는 간격의 표준편차를 평균으로 나눈 값으로 소수 셋째 자리까지 적습니다.
간격은 이웃한 예정 시각의 차이입니다(299개). 표준편차는 모집단 기준으로 계산하세요. 세 평균 간격이 거의 같은데 변동계수만 0 · 1 근처 · 1보다 훨씬 큼 으로 갈리는 것이 이 단계가 보여 주려는 것입니다. burst 의 간격에는 0 이 잔뜩 들어 있습니다.
세 방식으로 같은 서버를 때린다
세 도착 방식으로 같은 서버에 같은 수의 요청을 실제로 보내세요. 방식마다 curl -s http://127.0.0.1:8080/reset 으로 기록을 지우고, python3 gen.py <모양> 150 25 7 /root/lt-arrival-process/run-<모양>.csv http://127.0.0.1:8080/work 로 보낸 뒤, curl -s http://127.0.0.1:8080/dump > /root/lt-arrival-process/srv-<모양>.csv 로 서버 기록을 받습니다(모양은 even · poisson · burst). 그다음 /root/lt-arrival-process/latency.tsv 에 세 줄을 적으세요 — 탭으로 나눈 네 칸 <모양> <성공 건수> <p50> <p95> 이고 지연은 run-*.csv 의 received 빼기 sent 를 밀리초로 바꿔 가장 가까운 순위로 구해 소수 첫째 자리까지, code 가 200 인 줄만 셉니다.
세 방식의 평균 요청률이 같으니 총 시간도 비슷하게 끝납니다. 그런데 지연 분포는 같지 않습니다 — 특히 p50 과 p95 의 벌어짐을 보세요. 일정 간격에서는 두 값이 거의 붙어 있고 묶음에서는 멀리 떨어집니다. 세 번 보내는 데 20초쯤 걸립니다.
서버가 센 대기열
서버가 남긴 /root/lt-arrival-process/srv-*.csv 에는 요청마다 qlen(그 요청이 닿은 순간 시스템 안에 있던 요청 수)이 들어 있습니다. /root/lt-arrival-process/queue.tsv 에 세 줄을 적으세요 — 탭으로 나눈 네 칸 <모양> <기록 수> <최대 대기열> <평균 대기열> 이고 모양은 even · poisson · burst 순서, 평균은 소수 둘째 자리까지입니다. 그리고 /root/lt-arrival-process/05-note.txt 에 두 줄 worst=<최대 대기열이 가장 큰 모양> 과 ratio=<그 최대값을 even 의 최대값으로 나눈 값, 소수 첫째 자리까지> 를 적으세요.
세 기록의 줄 수가 같아야 합니다 — 같은 수의 요청을 보냈으니까요. 그런데 최대 대기열은 같지 않습니다. 일정 간격에서는 워커 수를 넘지 않는데, 묶음에서는 한 번에 몰려온 만큼이 그대로 쌓입니다. 평균 요청률이 같아도 순간의 동시 요청 수는 도착 모양이 정한다는 뜻입니다.
생각 시간이 요청률을 정한다
/root/lt-arrival-process/closed.py 를 만드세요. python3 closed.py <URL> <사용자수> <생각시간초> <지속초> <출력CSV> 로 부르면 사용자마다 요청 하나를 보내고 생각 시간만큼 쉬는 일을 지속 시간 동안 되풀이합니다. CSV 는 머리글 user,sent,received,code 에 이어 시작 기준 초를 적습니다. python3 closed.py http://127.0.0.1:8080/work 8 0.2 10 /root/lt-arrival-process/closed.csv 로 한 번 돌리고, /root/lt-arrival-process/think.txt 에 여섯 줄을 적으세요 — users=8 · think_s=0.200 · mean_r_s=(평균 응답 시간) · predicted_rate=(사용자수 나누기 (mean_r_s 더하기 think_s)) · measured_rate=(성공 건수 나누기 처음 sent 부터 마지막 received 까지의 시간) · naive_rate=(응답 시간을 0 으로 보고 사용자수 나누기 think_s). 요청률은 소수 셋째 자리까지, 시간은 소수 넷째 자리까지입니다.
리틀의 법칙을 사용자 한 명의 왕복에 적용하면 실효 요청률은 N / (R + Z) 입니다. naive_rate 는 R 을 빼먹었을 때의 예측이고, 실측과 비교해 보면 왜 빼먹으면 안 되는지 보입니다. 이 단계는 열몇 초 걸립니다.
고른 도착을 가정한 용량은 몇 배 빗나가나
/root/lt-arrival-process/capacity.txt 에 다섯 줄을 적으세요. formula_workers=(고른 도착을 가정한 계산: chosen_rate x service_s 를 올림한 정수) · observed_even= · observed_burst=(각각 srv-even.csv 와 srv-burst.csv 에서 동시에 시스템 안에 있던 요청 수의 최댓값) · underestimate_factor=(observed_burst 를 formula_workers 로 나눈 값, 소수 첫째 자리까지) · verdict=<under|ok>(formula_workers 가 observed_burst 보다 작으면 under). 동시 요청 수는 arrive 에 1 을 더하고 end 에 1 을 빼며 시각 순으로 훑어 최댓값을 잡습니다.
qlen 칸을 그대로 최댓값으로 써도 같은 수가 나오는지 확인해 보세요 — 서버가 도착 순간에 센 값이라 대개 같습니다. 다만 직접 훑어 보면 '평균 이용률이 낮아도 순간 동시성은 훨씬 클 수 있다' 는 것이 왜 그런지 눈으로 보입니다. 나눗셈의 분모가 1 이 될 수 있으니 소수로 계산하세요.
이 서비스의 도착 과정을 무엇으로 잡을 것인가
/root/lt-arrival-process/arrival-model.txt 에 네 줄을 적으세요. model=<even|poisson|burst> 는 앞으로 이 서비스의 용량을 잡을 때 쓸 도착 모델, workers= 는 그 모델로 정한 워커 수(정수), evidence= 는 왜 그 모델인지를 앞 단계에서 만든 파일 이름과 숫자를 들어 60자 이상으로, risk= 는 그 모델이 틀렸을 때 무엇이 먼저 무너지는지 40자 이상으로 적습니다. 채점기는 workers 가 7단계에서 관측한 최대 동시 요청 수 이상인지, 그리고 고른 도착을 가정한 formula_workers 보다 큰지를 봅니다.
3단계의 변동계수와 5단계의 최대 대기열이 이 서비스의 도착이 고르지 않다는 증거입니다. 그 증거를 받아들이면 워커 수는 평균이 아니라 묶음의 크기가 정합니다. 여유를 얼마나 둘지는 여러분이 정하되, 관측한 최대 동시 요청 수 아래로 내려가면 그 묶음이 올 때마다 대기열이 생깁니다.