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

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

기한은 예산이다: 여러 홉에 나눠 쓰기

TT Lab 에서 이어서 보기

한 줄 요약

기한(deadline)은 호출이 끝나야 하는 시각입니다. 와이어에는 남은 시간(grpc-timeout 머리)으로 전해지므로, 홉마다 그 순간 남은 시간을 다시 재고 자기 몫을 뗀 나머지만 뒤에 넘기는 「예산」 으로 다뤄야 합니다.

왜 이게 필요했나

홉마다 「타임아웃 5초」 를 따로 걸던 시스템을 떠올려 봅시다. 앞 서비스는 뒤 서비스를 5초 기다리고, 뒤 서비스는 또 그 뒤를 5초 기다립니다. 사용자는 3초 만에 화면을 닫았는데 맨 뒤는 아무도 받지 않을 결과를 위해 15초까지 일할 수 있습니다. 장애 때는 이 쓸모없는 일이 스레드와 연결과 DB 를 붙잡아 멀쩡한 요청까지 막습니다.

기한은 반대로 생각합니다. 맨 앞의 클라이언트가 「이 일은 1초 안에 끝나야 한다」 를 정하면, 그 사슬의 모든 홉이 같은 마감을 공유합니다. 늦은 것을 아는 순간 모두가 일을 접을 수 있습니다.

어떻게 동작하나

클라이언트가 timeout=1.0 으로 부르면 요청 머리에 grpc-timeout: 999m 처럼 남은 시간이 실립니다. 시각이 아니라 남은 시간이라 두 서버의 시계가 어긋나도 문제가 없고, 그 대신 전송에 걸린 만큼 뒤가 조금 덜 받습니다. 서버는 context.time_remaining() 으로 남은 시간을 읽습니다.

Go 에서는 받은 context 를 그대로 뒤 호출에 넘기면 기한이 저절로 이어집니다. 파이썬 grpcio 는 그렇지 않아서 뒤를 부를 때 timeout 을 손으로 계산해 넘겨야 합니다. realtime-protocols 코스의 rt-grpc-control-lab 이 한 홉짜리 전달과 취소를 다뤘고, 이 모듈은 홉이 여럿일 때의 규칙을 다룹니다.

클라이언트 1.0초 ─▶ Edge (몫 0.2) ─▶ Pricing (몫 0.2) ─▶ Stock
                   0.8초 넘김          0.6초 넘김          남은 0.6초 안에 끝내야 함

몫을 남기는 이유는 실패를 처리할 시간 때문입니다. 남은 기한을 그대로 넘기면 맨 뒤가 늦는 순간 모든 홉이 같은 순간에 기한을 맞고, 클라이언트는 DEADLINE_EXCEEDED 만 받습니다. 홉마다 몫을 남겨 두면 뒤 호출이 먼저 실패하고, 앞 홉은 남은 몫으로 오류를 정리하거나 부분 응답을 보낼 수 있습니다.

나머지 규칙은 세 가지입니다. 첫째, 기한 없이 들어온 호출에는 서버가 기본 기한을 겁니다. 파이썬 grpcio 는 기한이 없는 호출의 time_remaining() 으로 약 9.2e18 을 돌려줍니다(실측). 그대로 넘기면 뒤가 멈출 때 이 홉도 영원히 기다립니다. 둘째, 넘길 기한이 너무 짧으면 부르지 않습니다. 결과를 받을 수 없다는 것을 이미 아는 호출은 뒤 서비스에 일만 더합니다. 셋째, 여러 뒤 서비스를 부를 때는 같은 예산을 병렬로 씁니다. 0.25초짜리 호출 넷을 차례로 부르면 1초가 걸리지만 future 로 한꺼번에 걸면 가장 느린 하나만큼 걸립니다.

기한과 취소는 다르게 흐릅니다. 기한은 머리에 실려 뒤로 저절로 전해지지만, 앞 클라이언트가 호출을 취소했다는 사실은 머리로 가지 않습니다. 앞 홉의 context 만 비활성이 되고, 그 홉이 붙잡고 있는 뒤 호출은 모릅니다. 그래서 파이썬에서는 뒤 호출을 future 로 걸고 context.add_callback 으로 취소를 이어 주며, Go 에서는 받은 context 를 그대로 넘기면 취소까지 함께 전해집니다. 기한 예산을 아무리 잘 나눠도 취소가 끊기면 사용자가 떠난 뒤의 일이 남습니다.

재시도도 예산 안에서 합니다. 남은 시간이 모자라는데 다시 부르면 그 호출은 결과를 돌려줄 수 없습니다. 그리고 세 홉이 각자 세 번씩 재시도하면 맨 뒤는 한 요청에 스물일곱 번까지 불릴 수 있습니다. 장애가 퍼질 때 재시도가 부하를 몇 배로 키우는 것이 이 곱셈 때문이라, 재시도는 한 층에서만 하고 남은 예산을 확인한 뒤에 합니다.

현장에서 만나는 모습

가장 흔한 신호는 맨 앞에는 DEADLINE_EXCEEDED 가 쌓이는데 맨 뒤의 지연 그래프는 멀쩡해 보이는 장면입니다. 대개 중간 홉이 차례로 부르거나, 기한 없이 부르거나, 몫 없이 넘기고 있습니다. 반대로 맨 뒤의 CPU 가 높은데 성공 응답이 적다면 아무도 받지 않을 일을 하고 있는지 의심합니다.

조사할 때 도움이 되는 습관이 하나 있습니다. 홉마다 접근 기록에 「받은 순간 남은 기한」 과 「뒤로 넘긴 기한」 을 함께 남겨 두면, 어느 홉이 예산을 다 써 버렸는지가 숫자로 보입니다. 다음 실습의 기준 재고 서버가 바로 그 숫자를 적어 둡니다.

서비스 메시나 프록시가 자기 타임아웃을 따로 가지고 있으면 둘 중 짧은 쪽이 이깁니다. DB 쿼리 타임아웃도 남은 기한에서 계산해 걸어야 같은 사슬에 묶입니다. grpc-timeout 머리의 모양과 실측 사례는 블로그 글 스트리밍, deadline, 재시도, keepalive 에 있습니다.

다음 실습에서 할 것

Edge 와 Pricing 을 만들고 기준 재고 서버까지 세 홉을 잇습니다. 재고 서버가 「받은 순간 남아 있던 기한」 을 적어 두므로, 몫을 남겼는지, 기본 기한을 걸었는지, 모자랄 때 부르지 않았는지를 그 숫자로 확인합니다. 마지막에 병렬 팬아웃, 부분 응답, 예산 안의 재시도를 차례로 더합니다.