마이크로서비스 아키텍처 · 동기 통신(REST·gRPC)과 그 대가 · 실습
서비스 간 REST 호출에 타임아웃과 재시도 넣기
목표
불안정한 다운스트림을 상대로 타임아웃, 지수 백오프, 지터, 재시도 금지 조건, 폴백을 직접 구현해 동기 호출의 기본기를 완성한다.
왜 중요한가
동기 호출에서 사고는 대개 "타임아웃이 없어서" 또는 "재시도를 잘못해서" 납니다. 타임아웃이 없으면 다운스트림이 느려질 때 업스트림의 커넥션 풀이 먼저 마르고, 다운스트림과 무관한 요청까지 실패합니다. 반대로 재시도를 고정 간격으로 무제한 걸면 다운스트림이 회복하려는 순간마다 같은 파도가 덮칩니다. 실제 사례에서 주문 인스턴스 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.log 에 attempt=<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.txt 에 instances=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} 를 준다.
참고
- 타임아웃 값은 상대의 p99 에서 출발합니다. p99 의 10배로 잡으면 없는 것과 같습니다.
- full jitter:
wait = random.uniform(0, min(cap, base * 2 ** attempt)) - 흔한 실수 1: 재시도 로그를 성공한 시도만 남기는 것 — 실패한 시도도 남겨야 예산을 계산할 수 있습니다.
- 흔한 실수 2: 타임아웃을 예외로 잡지 않아 스택트레이스만 남고 경과 시간을 못 재는 것.
단계 8개
- 불안정한 다운스트림 띄우기
- 정상 호출로 기준선 잡기
- 느린 응답을 타임아웃으로 끊기
- 지수 백오프 재시도 구현하기
- 지터로 재시도 시점 흩뜨리기
- 확정적 오류는 재시도하지 않기
- 재시도 예산 계산하기
- 폴백이 있는 게이트웨이로 종합하기