여러 서버를 거치는 요청의 시간 제한은 어떻게 나눌까?

반응형

화면에서 주문 내역을 불러올 때 첫 서버가 회원 서버를 부르고, 다시 결제 서버를 부른다고 해 보자. 화면은 2초까지만 기다리는데 서버마다 시간 제한을 2초씩 새로 주면, 앞에서 쓴 시간이 사라지는 셈이다. 화면이 기다리기를 끝낸 뒤에도 뒤쪽 서버가 일을 계속할 수 있다.

시간 초과(timeout)는 정해진 시간 안에 응답을 받지 못해 기다림을 끝내는 규칙이다. 처리 시한(deadline)은 그 요청을 언제까지 끝내야 하는지 정한 마감 시각이다. 두 개를 구분하면 여러 서버가 같은 시간 예산을 쓰는 방법이 보인다.

2초를 세 서버에 각각 주면

아래 숫자는 동작을 살펴보려고 만든 예다. 실제 서비스의 권장 설정값은 아니다.

구간 걸린 시간 첫 요청 뒤 경과 시간
첫 서버의 대기와 처리 500밀리초 500밀리초
회원 서버의 대기와 처리 700밀리초 1,200밀리초
결제 서버 호출 시작 — 1,200밀리초

이때 결제 서버에 다시 2초를 주면 완료는 첫 요청 뒤 최대 3.2초까지 밀릴 수 있다. 화면의 2초 제한과 맞지 않는다. 결제 서버를 부르는 시점에 남은 시간은 800밀리초보다 적다. 마지막 응답을 화면으로 보내는 시간까지 필요하기 때문이다.

앞 단계가 얼마나 썼는지 모른 채 서버별 제한만 더하면 전체 응답 시간을 통제하기 어렵다. 더 긴 서버별 제한은 장애 때 쓸데없는 작업도 오래 붙잡는다.

남은 시간을 다음 서버에 넘긴다

처음에 전체 처리 시한을 정하고, 다음 서버를 부를 때 현재까지 쓴 시간을 뺀다. 예를 들어 총 2초 중 1.2초가 지났다면 남은 예산은 800밀리초다. 네트워크 전송과 최종 응답을 위한 여유를 남기면 결제 서버에 줄 수 있는 시간은 그보다 짧다.

남은 시간 = 전체 처리 시한 - 지금까지 지난 시간
하위 호출 제한 = 남은 시간 - 응답을 마무리할 여유

시계를 각 서버에서 독립적으로 새로 시작해 같은 2초를 주는 설정과 다르다. 원격 프로시저 호출(Remote Procedure Call, RPC)은 다른 서버의 함수를 부르듯 요청과 응답을 주고받는 통신 방식이다. gRPC는 이런 호출을 위한 틀로, 처리 시한을 하위 호출에 전파할 수 있다. 언어별로 자동 전파 여부가 다르므로 실제 사용 중인 구현을 확인해야 한다. gRPC는 서버 사이 시계 차이 때문에 마감 시각을 그대로 복사하는 대신, 이미 지난 시간을 뺀 남은 시간으로 전달한다.

한 구간에만 시간을 몰아주지 않기

전체 예산에는 소켓 연결, 작업 큐 대기, 데이터베이스 연결 획득, 질의 실행, 응답 전송이 모두 들어간다. 결제 서버 자체는 빠른데 앞단 작업 큐에서 700밀리초를 썼다면, 결제 호출의 제한만 늘려도 화면의 2초 제한은 지킬 수 없다.

서버가 하위 호출을 시작하기 전에 남은 시간을 확인할 수 있다. 이미 처리 시한이 지났거나 정상적으로 끝내기 어려울 만큼 짧다면 새 일을 시작하지 않고 실패를 돌려준다. 반대로 오래 걸리는 구간의 제한을 무조건 잘게 쪼개면 정상 요청도 불필요하게 실패한다. 구간별 실제 대기 시간과 실패율을 보고 정해야 한다.

처리 시한은 재시도에도 적용된다. 첫 시도에 700밀리초를 썼고 재시도 전 200밀리초를 기다렸다면, 두 번째 시도에 또 처음과 같은 시간을 줄 수 없다. 남은 예산 안에 대기와 재시도를 넣어야 한다.

시간이 다 되면 뒤쪽 작업도 멈출까?

앞 서버가 기다리기를 끝내면 하위 서버에 취소를 알릴 수 있다. 그러나 이미 시작된 데이터베이스 변경이나 외부 결제가 반드시 되돌아가는 것은 아니다. gRPC 문서도 서버 애플리케이션이 자신이 시작한 작업의 취소 여부를 확인하고 정리해야 한다고 설명한다. 시간 초과는 결과를 관찰하지 못했다는 뜻이지, 작업이 실행되지 않았다는 증거가 아니다.

읽기 요청은 취소 후 자원 정리에 집중하면 된다. 주문 생성이나 결제처럼 결과가 남는 작업은 요청 식별자로 처리 결과를 조회하고, 재시도할 때 같은 논리 요청이 중복 반영되지 않도록 해야 한다. 시간 초과와 작업 취소를 다룬 글에서는 이 경계를 더 자세히 설명했다.

여러 서버의 시간 제한을 정할 때는 화면이나 호출자가 기다릴 전체 시간을 먼저 잡는다. 각 구간이 실제로 얼마나 기다리는지 재고, 하위 호출에는 남은 시간을 넘긴다. 실패 시에는 기다림이 끝난 것과 작업 결과가 확정된 것을 따로 확인한다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글