LabHub
배우기 러닝패스 코스

Integration and Deployment

They Slowed Down and We Died First

LabHub 에서 이어서 보기

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

목표

상위 요청 하나에 주어진 시간을 예산으로 두고, 그 예산을 연결·읽기·재시도·하위 호출에 나눠 주는 클라이언트를 직접 만든다. 예산이 모자라면 걸지 않고 빨리 실패하고, 남은 예산을 헤더로 아래에 넘긴다.

왜 중요한가

타임아웃을 넉넉히 잡는 습관은 안전해 보이지만 반대다. 상대가 30초로 느려졌을 때 우리 상한이 60초라면 우리 워커는 30초 동안 묶이고, 우리를 부르는 쪽에서 보면 느려진 것은 우리다. 그 지연은 한 계층 위로 또 번진다. 그래서 타임아웃은 상대를 기다려 주는 배려가 아니라 우리를 지키는 차단기다. 그리고 하나의 값이 아니라 두 값이다 — 연결은 닿는 시간이라 몇 초가 걸리면 느린 것이 아니라 안 되는 것이고, 읽기는 상대가 일하는 시간이라 업무에 따라 다르다. 예산으로 생각하면 규칙은 세 줄이다. 상위 요청에 총 예산을 정하고, 각 호출의 읽기 상한은 그 순간 남은 예산으로 하고, 남은 예산이 하한보다 작으면 걸지 않는다. 재시도와 그 대기 시간도 같은 예산에서 나간다. 채점기는 여러분의 문장을 믿지 않는다. 느려지는 파트너를 채점기가 고른 포트에 직접 띄우고, 여러분의 호출기를 실제로 돌려 걸린 시간과 출력의 구간 이름을 함께 잽니다.

단계

  1. /root/budget/partner.py 를 만들어 포트 8014 에 띄우고, 실패 세 모양을 재어 /root/budget/probe.json 에 적으세요.
  2. /root/budget/call.py 를 만들어 연결과 읽기의 상한을 따로 잡고 어느 구간에서 끊겼는지 말하게 하세요.
  3. /root/budget/deadline.py 를 만들어 총 예산 하나로 여러 호출을 하고, 남은 예산이 다음 호출의 읽기 상한이 되게 하세요.
  4. deadline.py 에 하한(--min-ms)을 붙여 남은 예산이 하한보다 작으면 걸지 않고 no_budget 으로 건너뛰게 하세요.
  5. /root/budget/retry.py 를 만들어 재시도 대기를 지수로 늘리고 무작위로 흩뜨리되, 그 대기까지 예산에서 빼게 하세요.
  6. /root/budget/fanout.py 를 만들어 그때 남은 예산을 X-Budget-Ms 헤더로 하위 호출에 넘기세요.
  7. /root/budget/budget_plan.json 에 화면 하나의 예산표를 숫자로 적으세요.
  8. /root/budget/budget_report.md 에 네 절로 보고하세요.

참고

실패의 세 가지 모양 재 보기

/root/budget/partner.py 를 만들어 포트 8014 에 띄우고, 연결 거부·연결 타임아웃·읽기 타임아웃 세 가지를 직접 재어 /root/budget/probe.jsonkind·target·elapsed_ms·error 세 줄로 적으세요.

느린 응답은 파트너가 sleep 으로 흉내 냅니다. 연결 거부는 아무도 듣지 않는 127.0.0.1 포트로, 연결 타임아웃은 어디로도 라우팅되지 않는 주소로 만듭니다. 세 경우의 걸린 시간이 서로 어떻게 다른지가 이 단계의 핵심입니다.

연결과 읽기를 나눠 잡기

/root/budget/call.py 를 만들어 --connect--read 를 따로 받고, 실패했을 때 phaseconnect 인지 read 인지 적게 하세요. 응답을 받았으면 상태 코드가 4xx·5xx 여도 phase 는 done 입니다.

requests 의 timeout 은 두 값을 담은 짝을 받습니다. 예외도 종류가 나뉘어 있어서 연결 쪽과 읽기 쪽을 구별할 수 있습니다 — 다만 연결 거부는 타임아웃 예외가 아니므로 따로 잡아야 하고, 그것도 상대에게 닿기 전에 끝난 일이라 connect 구간입니다.

남은 예산이 다음 호출의 상한이다

/root/budget/deadline.py 를 만들어 --budget-ms 하나로 여러 --url 을 차례로 부르게 하세요. 각 호출의 읽기 상한은 그 순간 남은 예산이어야 하고, 그 값을 read_timeout_ms 에 적어야 합니다.

앞 단계의 call.py 를 모듈로 불러 쓰면 같은 코드를 두 번 쓰지 않아도 됩니다. 시작 시각을 한 번 잡아 두고 매 호출 직전에 '예산 빼기 지금까지 쓴 시간' 을 계산하세요. 이렇게 하면 호출이 몇 개든 전체가 예산을 넘길 수 없습니다.

가망 없는 호출은 걸지 않는다

deadline.py 에 --min-ms 를 붙여 남은 예산이 그 값보다 작으면 호출을 걸지 않고 phaseno_budget 으로 기록하며 skipped 를 올리게 하세요. 건너뛴 호출의 elapsed_ms 는 0 입니다.

200ms 남았는데 평소 400ms 걸리는 호출을 거는 것은 실패를 200ms 뒤로 미루는 일입니다. 그 자리에서 실패하면 남은 시간으로 부분 응답이라도 만들 수 있습니다. 하한은 인자로 받아 상황마다 다르게 줄 수 있어야 합니다.

재시도도 예산에서 나간다

/root/budget/retry.py 를 만들어 실패한 호출을 다시 걸되, 대기 상한을 base-ms * 2^(시도-1) 로 늘리고 실제 대기는 0 부터 그 상한 사이에서 고르게 하세요. 기다린 뒤 한 번 더 걸 자리가 예산에 없으면 stopped_reasonbudget 으로 하고 멈춥니다.

무작위를 섞지 않으면 같은 순간에 실패한 클라이언트들이 같은 순간에 다시 몰립니다. 그리고 대기 시간도 예산에서 나가므로, 자고 일어나서 호출을 걸 자리가 남아 있는지를 자기 전에 물어야 합니다. 멈춘 이유를 세 가지로 갈라 적으면 나중에 로그만 보고도 원인을 압니다.

남은 예산을 아래로 넘기기

/root/budget/fanout.py 를 만들어 <BASE>/budget?ms=200--n 번 부르되, 매번 X-Budget-Ms 헤더에 그때 남은 예산을 실어 보내세요. 출력의 각 call 에 sent_budget_ms·received·elapsed_ms 를 적습니다.

우리가 1200ms 만 남았는데 하위 서비스가 자기 기준으로 5초를 기다리면 그 4초는 아무도 안 보는 답을 만드는 데 쓰입니다. 헤더 이름은 우리가 정하는 약속이고, 받는 쪽은 그 값을 자기 상한으로 삼습니다. 파트너의 /budget 은 받은 헤더 값을 그대로 돌려주므로 실제로 전달됐는지 확인할 수 있습니다.

화면 하나의 예산표

/root/budget/budget_plan.json 에 예산표를 적으세요. total_ms·reserve_ms·worst_case_mscalls(이름·budget_ms·max_attempts) 3개 이상이 필요합니다. reserve_ms 는 100 이상, worst_case_ms 는 호출 예산의 합에 예비를 더한 값이며 total_ms 를 넘지 않아야 합니다.

예비(reserve)는 우리 쪽 직렬화·템플릿 렌더링처럼 호출이 아닌 시간입니다. 이것을 빼놓고 호출에 예산을 다 나눠 주면 화면은 늘 상한에서 아슬아슬합니다. 재시도가 있는 호출은 그 호출의 예산 안에서 재시도까지 마쳐야 한다는 뜻입니다.

예산 점검 보고서

/root/budget/budget_report.md## 지금 무엇이 시간을 먹는가 ## 구간을 어떻게 나눴나 ## 재시도가 먹는 몫 ## 예산을 넘겼을 때 무엇을 하나 네 절로 적으세요. probe.json 과 budget_plan.json 의 숫자가 본문에 들어가야 합니다.

읽는 사람은 '타임아웃을 좀 늘려 주세요' 라고 말한 사람입니다. 늘리면 무엇이 나빠지는지를 숫자로 보여 주세요. 재시도 몫은 시도 횟수와 대기 상한을 곱해 최악의 경우를 계산해 적습니다.