LabHub
배우기 러닝패스 코스

Microservice Architecture

Adding Timeouts and Retries to Service-to-Service REST Calls

LabHub 에서 이어서 보기

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

목표

불안정한 다운스트림을 상대로 타임아웃, 지수 백오프, 지터, 재시도 금지 조건, 폴백을 직접 구현해 동기 호출의 기본기를 완성한다.

왜 중요한가

동기 호출에서 사고는 대개 "타임아웃이 없어서" 또는 "재시도를 잘못해서" 납니다. 타임아웃이 없으면 다운스트림이 느려질 때 업스트림의 커넥션 풀이 먼저 마르고, 다운스트림과 무관한 요청까지 실패합니다. 반대로 재시도를 고정 간격으로 무제한 걸면 다운스트림이 회복하려는 순간마다 같은 파도가 덮칩니다. 실제 사례에서 주문 인스턴스 20대가 초당 100건을 처리하며 maxAttempts 5 로 재시도하자 결제 서비스가 초당 10,000건을 받았습니다. 재시도는 부하를 더하는 것이 아니라 곱합니다. 그래서 이 실습은 값을 넣는 순서를 중요하게 다룹니다 — 먼저 타임아웃, 그다음 재시도 조건, 그다음 백오프와 지터, 마지막에 폴백입니다.

단계

  1. /opt/app/flaky.py 를 127.0.0.1:8110 에 띄운다. GET /fast 가 200 이어야 한다.
  2. /root/rest/call.py 를 만들어 /fast 를 호출하고 응답 본문을 /root/rest/fast.out 에 저장한다.
  3. /root/rest/timeout.py 를 만들어 /slow 를 1.0초 타임아웃으로 호출한다. /root/rest/timeout.out 첫 줄에 TIMEOUT, 둘째 줄에 elapsed=<초> 를 적는다. 경과 시간은 2.0초 미만이어야 한다.
  4. /root/rest/retry.py 를 만들어 /flaky?key=lab&fail=2 를 지수 백오프로 재시도해 최종 성공시킨다. /root/rest/retry.logattempt=<n> wait=<초> 형식으로 시도마다 한 줄씩 남기고 총 3줄이 되게 한다.
  5. retry.py 의 대기 계산을 full jitter 로 바꾼다. 시도 번호 2의 대기 시간을 20번 뽑아 /root/rest/jitter.txt 에 한 줄씩 적는다. 서로 다른 값이 15개 이상이어야 한다.
  6. /root/rest/retry400.py/bad 를 호출한다. 400 은 재시도하지 않으므로 /root/rest/retry400.log 는 정확히 1줄이어야 한다.
  7. /root/rest/budget.txtinstances=20, rps=100, max_attempts=5, worst_rps=10000 네 줄을 적는다.
  8. /root/rest/gateway.py 를 127.0.0.1:8111 에 띄운다. GET /order 는 다운스트림 /always500 을 호출하되, 타임아웃과 재시도를 적용하고 최종 실패 시 200 과 {"degraded":true} 를 준다.

참고

불안정한 다운스트림 띄우기

/opt/app/flaky.py 를 127.0.0.1:8110 에 띄운다. GET /fast 가 200 이어야 한다.

/opt/app/flaky.py 를 실행하면 8110 에 뜹니다. /fast, /slow, /flaky, /bad 네 경로의 동작을 먼저 눈으로 확인하세요.

정상 호출로 기준선 잡기

/root/rest/call.py 를 만들어 /fast 를 호출하고 응답 본문을 /root/rest/fast.out 에 저장한다.

httpx 나 urllib 로 /fast 를 부르고 본문을 파일로 남깁니다. 아직 타임아웃은 신경 쓰지 않아도 됩니다.

느린 응답을 타임아웃으로 끊기

/root/rest/timeout.py 를 만들어 /slow 를 1.0초 타임아웃으로 호출한다. /root/rest/timeout.out 첫 줄에 TIMEOUT, 둘째 줄에 elapsed=<초> 를 적는다. 경과 시간은 2.0초 미만이어야 한다.

/slow 는 3초 뒤에 답합니다. 클라이언트 타임아웃을 1초로 두면 예외가 납니다. 예외를 잡아 결과와 경과 시간을 함께 기록하세요.

지수 백오프 재시도 구현하기

/root/rest/retry.py 를 만들어 /flaky?key=lab&fail=2 를 지수 백오프로 재시도해 최종 성공시킨다. /root/rest/retry.logattempt=<n> wait=<초> 형식으로 시도마다 한 줄씩 남기고 총 3줄이 되게 한다.

간격을 매 시도마다 두 배로 늘립니다. 각 시도의 번호와 대기 시간을 로그에 한 줄씩 남겨야 채점됩니다.

지터로 재시도 시점 흩뜨리기

retry.py 의 대기 계산을 full jitter 로 바꾼다. 시도 번호 2의 대기 시간을 20번 뽑아 /root/rest/jitter.txt 에 한 줄씩 적는다. 서로 다른 값이 15개 이상이어야 한다.

full jitter 는 0부터 계산된 백오프 사이의 난수를 기다립니다. 같은 시도 번호에서도 값이 매번 달라야 합니다.

확정적 오류는 재시도하지 않기

/root/rest/retry400.py/bad 를 호출한다. 400 은 재시도하지 않으므로 /root/rest/retry400.log 는 정확히 1줄이어야 한다.

400 은 몇 번을 보내도 400 입니다. 상태 코드로 재시도 여부를 가르는 분기를 넣으세요.

재시도 예산 계산하기

/root/rest/budget.txtinstances=20, rps=100, max_attempts=5, worst_rps=10000 네 줄을 적는다.

인스턴스 수 x 초당 요청 수 x 최대 시도 수 가 다운스트림이 받는 최악의 초당 요청 수입니다. 세 값과 결과를 각각 적으세요.

폴백이 있는 게이트웨이로 종합하기

/root/rest/gateway.py 를 127.0.0.1:8111 에 띄운다. GET /order 는 다운스트림 /always500 을 호출하되, 타임아웃과 재시도를 적용하고 최종 실패 시 200 과 {"degraded":true} 를 준다.

다운스트림이 죽어도 사용자에게는 200 을 주되, 응답에 품질 저하 표시를 남깁니다. 앞 스텝의 타임아웃과 재시도를 함께 씁니다.