TT Lab
블로그 시작하기
배우기 러닝패스 코스

gRPC 서비스를 직접 세우고 운영한다

기한 1초를 세 홉이 나눠 쓴다

TT Lab 에서 이어서 보기

목표

클라이언트가 건 기한 하나를 Edge·Pricing·Stock 세 홉이 나눠 쓰게 만듭니다. 홉마다 몫을 남기고, 기한 없는 호출에 기본값을 걸고, 모자라면 부르지 않고, 병렬로 부르고, 늦은 것만 빼고 답하고, 예산 안에서만 재시도합니다.

왜 중요한가

gRPC 의 기한은 시각이 아니라 남은 시간(grpc-timeout 머리)으로 전해집니다. 그래서 홉마다 그 순간 남은 시간을 다시 재고 그중 얼마를 뒤에 줄지 정할 수 있고, 그 결정이 장애 때의 모습을 바꿉니다. 모든 홉이 같은 기한을 그대로 넘기면 맨 뒤가 늦는 순간 모두가 동시에 실패해 사용자는 빈 화면만 봅니다. 몫을 남겨 두면 앞 홉이 부분 응답이나 대체 값을 돌려줄 수 있습니다. 반대로 기한 없이 부르는 홉이 하나라도 있으면 뒤가 멈출 때 스레드와 연결이 끝없이 묶입니다. 이 실습은 그 차이를 재고 서버가 받은 숫자로 확인합니다.

단계

  1. 사슬의 계약에서 코드를 만든다: 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 로 직접 띄울 수 있습니다.
  2. 자기 몫을 남기고 넘긴다: /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초로 부릅니다.
  3. 기한 없는 호출에 기본 기한을 건다: Quote 가 기한 없이 들어오면(time_remaining() 이 None 이거나 1e6 보다 크면) DEFAULT=2.0초를 남은 기한으로 보고 같은 계산을 하게 하세요. 그러면 재고 서버는 약 1.8초의 기한을 받습니다.
  4. 모자라면 부르지 않는다: Quote 에서 뒤로 넘길 기한(남은 기한 빼기 0.2초)이 MIN_DOWNSTREAM=0.3초보다 짧으면 재고 서버를 부르지 말고 곧바로 DEADLINE_EXCEEDED 로 끝내게 하세요. 채점기는 Quote 를 기한 0.45초로 부르고 재고 서버의 호출 수가 늘지 않았는지 봅니다.
  5. 여러 뒤 서비스를 같은 예산으로 병렬로 부른다: /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) 로 담습니다. 이 단계에서는 하나라도 실패하면 그 상태 코드로 끝냅니다.
  6. 늦은 품목만 빼고 답한다: Checkout 을 고쳐 slow_skus 에 든 품목은 delay_ms 대신 3000 을 넘기고, Quote 가 실패한 품목은 오류로 끝내지 말고 Line(sku, available=False) 로 담아 정상 응답하게 하세요. 채점기는 품목 셋 중 하나를 느리게 하고 기한 1초로 부릅니다. 응답은 1초 안에 와야 합니다.
  7. 재시도도 예산 안에서: Checkout 에서 Quote 가 UNAVAILABLE 로 실패한 품목은, 그 시점에 다시 계산한 뒤로 넘길 기한이 RETRY_MIN=0.7초 이상일 때만 한 번 더 부르게 하세요. 모자라면 다시 부르지 않고 available=False 로 둡니다. 기준 재고 서버는 flaky- 로 시작하는 품목의 첫 호출만 UNAVAILABLE 로 실패합니다. 채점기는 기한 1.5초와 0.8초로 각각 부르고 재고 서버가 그 품목을 몇 번 받았는지 셉니다.

참고

사슬의 계약에서 코드를 만든다

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초로 각각 부르고 재고 서버가 그 품목을 몇 번 받았는지 셉니다.

재시도는 공짜가 아닙니다. 남은 시간이 모자란데 다시 부르면 그 호출은 결과를 돌려줄 수 없고 뒤 서비스에 일만 더합니다. 장애가 퍼질 때 재시도가 부하를 몇 배로 키우는 것도 같은 이유입니다. 재시도할 상태 코드를 고르고, 남은 예산을 다시 보고, 횟수를 묶습니다.