기한 1초를 세 홉이 나눠 쓴다
목표
클라이언트가 건 기한 하나를 Edge·Pricing·Stock 세 홉이 나눠 쓰게 만듭니다. 홉마다 몫을 남기고, 기한 없는 호출에 기본값을 걸고, 모자라면 부르지 않고, 병렬로 부르고, 늦은 것만 빼고 답하고, 예산 안에서만 재시도합니다.
왜 중요한가
gRPC 의 기한은 시각이 아니라 남은 시간(grpc-timeout 머리)으로 전해집니다. 그래서 홉마다 그 순간 남은 시간을 다시 재고 그중 얼마를 뒤에 줄지 정할 수 있고, 그 결정이 장애 때의 모습을 바꿉니다. 모든 홉이 같은 기한을 그대로 넘기면 맨 뒤가 늦는 순간 모두가 동시에 실패해 사용자는 빈 화면만 봅니다. 몫을 남겨 두면 앞 홉이 부분 응답이나 대체 값을 돌려줄 수 있습니다. 반대로 기한 없이 부르는 홉이 하나라도 있으면 뒤가 멈출 때 스레드와 연결이 끝없이 묶입니다. 이 실습은 그 차이를 재고 서버가 받은 숫자로 확인합니다.
단계
- 사슬의 계약에서 코드를 만든다: cd /root/grpcops/deadline && /opt/rt-lab/bin/python -m grpc_tools.protoc -I /opt/fixtures/grpcops/proto --python_out=. --grpc_python_out=. chain/v1/chain.proto 로 /root/grpcops/deadline/chain/v1/ 에 코드를 만드세요. 그리고 /opt/fixtures/grpcops/proto/chain/v1/chain.proto 를 열어 Stock·Pricing·Edge 세 서비스와 delay_ms·slow_skus 가 무엇인지 읽어 두세요. 기준 재고 서버는 /opt/rt-lab/bin/python /opt/fixtures/grpcops/stock.py --port 50061 로 직접 띄울 수 있습니다.
- 자기 몫을 남기고 넘긴다: /root/grpcops/deadline/pricing.py 에 serve(port, stock_target) 를 만드세요. 127.0.0.1:port 에서 Pricing 을 듣고, Quote 는 stock_target 의 Stock.Check 를 같은 sku·delay_ms 로 불러 price_cents=1200, qty=재고 서버의 qty 로 답합니다. Check 의 timeout 은 자기 호출에 남은 기한(context.time_remaining())에서 RESERVE=0.2초를 뺀 값입니다. 재고 서버가 오류로 끝나면 같은 상태 코드와 설명으로 끝냅니다. 채점기는 Quote 를 기한 1초로 부릅니다.
- 기한 없는 호출에 기본 기한을 건다: Quote 가 기한 없이 들어오면(time_remaining() 이 None 이거나 1e6 보다 크면) DEFAULT=2.0초를 남은 기한으로 보고 같은 계산을 하게 하세요. 그러면 재고 서버는 약 1.8초의 기한을 받습니다.
- 모자라면 부르지 않는다: Quote 에서 뒤로 넘길 기한(남은 기한 빼기 0.2초)이 MIN_DOWNSTREAM=0.3초보다 짧으면 재고 서버를 부르지 말고 곧바로 DEADLINE_EXCEEDED 로 끝내게 하세요. 채점기는 Quote 를 기한 0.45초로 부르고 재고 서버의 호출 수가 늘지 않았는지 봅니다.
- 여러 뒤 서비스를 같은 예산으로 병렬로 부른다: /root/grpcops/deadline/edge.py 에 serve(port, pricing_target) 를 만드세요. Checkout 은 skus 의 품목마다 Pricing.Quote 를 sku 와 delay_ms 로 부르되, Quote.future(...) 로 한꺼번에 걸어 병렬로 기다립니다. 각 호출의 timeout 은 Pricing 과 같은 방식(남은 기한 빼기 RESERVE=0.2초, 기한 없으면 DEFAULT=2.0초, MIN_DOWNSTREAM 미만이면 부르지 않고 DEADLINE_EXCEEDED)으로 정합니다. 결과는 요청 순서대로 Line(sku, available=True, price_cents, qty) 로 담습니다. 이 단계에서는 하나라도 실패하면 그 상태 코드로 끝냅니다.
- 늦은 품목만 빼고 답한다: Checkout 을 고쳐 slow_skus 에 든 품목은 delay_ms 대신 3000 을 넘기고, Quote 가 실패한 품목은 오류로 끝내지 말고 Line(sku, available=False) 로 담아 정상 응답하게 하세요. 채점기는 품목 셋 중 하나를 느리게 하고 기한 1초로 부릅니다. 응답은 1초 안에 와야 합니다.
- 재시도도 예산 안에서: Checkout 에서 Quote 가 UNAVAILABLE 로 실패한 품목은, 그 시점에 다시 계산한 뒤로 넘길 기한이 RETRY_MIN=0.7초 이상일 때만 한 번 더 부르게 하세요. 모자라면 다시 부르지 않고 available=False 로 둡니다. 기준 재고 서버는 flaky- 로 시작하는 품목의 첫 호출만 UNAVAILABLE 로 실패합니다. 채점기는 기한 1.5초와 0.8초로 각각 부르고 재고 서버가 그 품목을 몇 번 받았는지 셉니다.
참고
- 작업 폴더는 /root/grpcops/deadline 입니다. mkdir -p /root/grpcops/deadline 로 먼저 만드세요.
- 세 서버를 직접 띄워 보려면 터미널 셋에서 차례로 /opt/rt-lab/bin/python /opt/fixtures/grpcops/stock.py --port 50061, cd /root/grpcops/deadline && /opt/rt-lab/bin/python -c "import pricing; pricing.serve(50062, '127.0.0.1:50061')", cd /root/grpcops/deadline && /opt/rt-lab/bin/python -c "import edge; edge.serve(50063, '127.0.0.1:50062')" 를 실행합니다. 채점기는 빈 포트를 골라 따로 띄웁니다.
- 기준 재고 서버의 기록은 rpcurl 을 쓸 수 없으니(리플렉션 없음) 파이썬으로 Stock.Stats 를 불러 봅니다. seen_remaining 에 호출마다 받은 기한이 초 단위로 쌓입니다.
- 흔한 실수 두 가지입니다. 남은 기한을 몫 없이 그대로 넘기는 것, 그리고 품목마다 차례로 불러 앞 품목이 쓴 시간만큼 뒤 품목의 기한이 줄어드는 것입니다.
- 한 홉짜리 기한 전달과 취소는 realtime-protocols 코스의 rt-grpc-control-lab 에서 다뤘습니다. 블로그 글 스트리밍, deadline, 재시도, keepalive 에 grpc-timeout 머리의 모양이 있습니다.
- 파이썬은 /opt/rt-lab/bin/python 으로 실행합니다. grpcio·grpcio-tools·protobuf 가 그 가상환경에만 있어서, 그냥 python3 로 돌리면 ModuleNotFoundError 가 납니다. alias rpy=/opt/rt-lab/bin/python 처럼 줄여 두면 편합니다.
- 코드 생성기는 /opt/rt-lab/bin/python -m grpc_tools.protoc 입니다. protoc 과 같은 깃발(-I, --python_out, --grpc_python_out)을 받고, google/protobuf 의 기본 타입(any·duration 등)을 찾는 경로를 스스로 더합니다.
- 실습 파드는 바깥으로 나가는 연결이 막혀 있습니다. 모든 통신은 같은 파드 안의 127.0.0.1 에서 일어나며, 설치나 다운로드는 필요 없습니다.
- 채점기는 여러분의 코드를 별도 프로세스로 띄워 실제로 호출해 봅니다. 서버를 미리 켜 둘 필요는 없고, 채점기는 빈 포트를 골라 따로 띄웁니다. 앞 단계에서 완성한 함수는 지우지 마세요.
- 실습 세션이 끝나면 /root 의 파일은 남지 않습니다. 필요한 코드는 끝내기 전에 따로 보관하세요.
사슬의 계약에서 코드를 만든다
cd /root/grpcops/deadline && /opt/rt-lab/bin/python -m grpc_tools.protoc -I /opt/fixtures/grpcops/proto --python_out=. --grpc_python_out=. chain/v1/chain.proto 로 /root/grpcops/deadline/chain/v1/ 에 코드를 만드세요. 그리고 /opt/fixtures/grpcops/proto/chain/v1/chain.proto 를 열어 Stock·Pricing·Edge 세 서비스와 delay_ms·slow_skus 가 무엇인지 읽어 두세요. 기준 재고 서버는 /opt/rt-lab/bin/python /opt/fixtures/grpcops/stock.py --port 50061 로 직접 띄울 수 있습니다.
세 서비스가 한 파일에 있어 생성 코드도 하나입니다. 기준 재고 서버는 받은 호출마다 그 순간 남아 있던 기한을 적어 두고 Stats 로 보여 줍니다.
자기 몫을 남기고 넘긴다
/root/grpcops/deadline/pricing.py 에 serve(port, stock_target) 를 만드세요. 127.0.0.1:port 에서 Pricing 을 듣고, Quote 는 stock_target 의 Stock.Check 를 같은 sku·delay_ms 로 불러 price_cents=1200, qty=재고 서버의 qty 로 답합니다. Check 의 timeout 은 자기 호출에 남은 기한(context.time_remaining())에서 RESERVE=0.2초를 뺀 값입니다. 재고 서버가 오류로 끝나면 같은 상태 코드와 설명으로 끝냅니다. 채점기는 Quote 를 기한 1초로 부릅니다.
남은 기한을 그대로 넘기면 뒤가 기한을 다 쓰고 실패했을 때 이 홉은 응답을 만들 시간이 없습니다. 그러면 클라이언트는 무엇이 늦었는지 모른 채 DEADLINE_EXCEEDED 만 받습니다. 몫을 남겨야 이 홉이 오류를 정리하거나 대체 응답을 보낼 수 있습니다.
기한 없는 호출에 기본 기한을 건다
Quote 가 기한 없이 들어오면(time_remaining() 이 None 이거나 1e6 보다 크면) DEFAULT=2.0초를 남은 기한으로 보고 같은 계산을 하게 하세요. 그러면 재고 서버는 약 1.8초의 기한을 받습니다.
파이썬 grpc 는 기한이 없는 호출의 time_remaining() 으로 약 9.2e18 을 줍니다. 기한 없는 호출을 그대로 두면 뒤가 멈췄을 때 이 홉의 스레드도 영원히 묶입니다. 클라이언트가 기한을 걸지 않는 실수는 흔해서 서버가 기본값으로 막습니다.
모자라면 부르지 않는다
Quote 에서 뒤로 넘길 기한(남은 기한 빼기 0.2초)이 MIN_DOWNSTREAM=0.3초보다 짧으면 재고 서버를 부르지 말고 곧바로 DEADLINE_EXCEEDED 로 끝내게 하세요. 채점기는 Quote 를 기한 0.45초로 부르고 재고 서버의 호출 수가 늘지 않았는지 봅니다.
평소 0.3초 넘게 걸리는 뒤쪽 서비스를 0.25초 기한으로 부르면, 그 서비스는 연결을 쓰고 요청을 풀고 일을 시작했다가 끝내지 못합니다. 결과를 받을 수 없다는 것을 이미 아는 호출은 보내지 않는 편이 뒤 서비스를 지킵니다. 장애 때 늘어나는 쓸모없는 일을 줄이는 장치입니다.
여러 뒤 서비스를 같은 예산으로 병렬로 부른다
/root/grpcops/deadline/edge.py 에 serve(port, pricing_target) 를 만드세요. Checkout 은 skus 의 품목마다 Pricing.Quote 를 sku 와 delay_ms 로 부르되, Quote.future(...) 로 한꺼번에 걸어 병렬로 기다립니다. 각 호출의 timeout 은 Pricing 과 같은 방식(남은 기한 빼기 RESERVE=0.2초, 기한 없으면 DEFAULT=2.0초, MIN_DOWNSTREAM 미만이면 부르지 않고 DEADLINE_EXCEEDED)으로 정합니다. 결과는 요청 순서대로 Line(sku, available=True, price_cents, qty) 로 담습니다. 이 단계에서는 하나라도 실패하면 그 상태 코드로 끝냅니다.
품목 넷을 0.25초짜리 뒤 서비스에 차례로 부르면 1초가 걸립니다. future 로 동시에 걸면 가장 느린 하나만큼 걸립니다. 기한은 호출마다 따로 계산하지 않고 같은 남은 기한에서 나눠 줍니다. 채점기는 품목 넷을 기한 1.2초로 부르고, 재고 서버가 받은 기한에 두 홉의 몫이 빠졌는지도 봅니다.
늦은 품목만 빼고 답한다
Checkout 을 고쳐 slow_skus 에 든 품목은 delay_ms 대신 3000 을 넘기고, Quote 가 실패한 품목은 오류로 끝내지 말고 Line(sku, available=False) 로 담아 정상 응답하게 하세요. 채점기는 품목 셋 중 하나를 느리게 하고 기한 1초로 부릅니다. 응답은 1초 안에 와야 합니다.
느린 품목 하나 때문에 장바구니 전체가 DEADLINE_EXCEEDED 가 되면 사용자는 아무것도 보지 못합니다. 각 홉이 0.2초씩 남겨 두었기 때문에 뒤의 호출이 먼저 기한을 맞고, Edge 는 남은 몫으로 부분 응답을 만들 수 있습니다. 몫을 남기지 않으면 모든 홉이 같은 순간에 기한을 맞아 이 응답을 보낼 틈이 없습니다.
재시도도 예산 안에서
Checkout 에서 Quote 가 UNAVAILABLE 로 실패한 품목은, 그 시점에 다시 계산한 뒤로 넘길 기한이 RETRY_MIN=0.7초 이상일 때만 한 번 더 부르게 하세요. 모자라면 다시 부르지 않고 available=False 로 둡니다. 기준 재고 서버는 flaky- 로 시작하는 품목의 첫 호출만 UNAVAILABLE 로 실패합니다. 채점기는 기한 1.5초와 0.8초로 각각 부르고 재고 서버가 그 품목을 몇 번 받았는지 셉니다.
재시도는 공짜가 아닙니다. 남은 시간이 모자란데 다시 부르면 그 호출은 결과를 돌려줄 수 없고 뒤 서비스에 일만 더합니다. 장애가 퍼질 때 재시도가 부하를 몇 배로 키우는 것도 같은 이유입니다. 재시도할 상태 코드를 고르고, 남은 예산을 다시 보고, 횟수를 묶습니다.