본문으로 건너뛰기
개발 머꼬
개발 노트Python
hohyeon.dev2

외부 API 하나가 응답을 안 주자 배치 작업이 밤새 멈춰 있던 이유

  • #Common Pitfall
  • #Engineering Note
  • #Python

문제 발생

매일 새벽에 도는 동기화 스크립트가 아침이 되도록 끝나 있지 않았습니다. 에러 로그는 한 줄도 없었고, 프로세스도 멀쩡히 살아 있었습니다. 따라가 보니 외부 API를 부르는 이 한 줄에서 멈춰 있었습니다.

resp = requests.get(f"{BASE}/items", params={"page": page})

상대 서버가 연결은 받아 놓고 응답을 보내지 않는 상태였는데, 우리 쪽은 그걸 몇 시간째 얌전히 기다리고 있었습니다. 실패라도 했으면 알림이 왔을 텐데, 아무 일도 일어나지 않으니 아무도 몰랐습니다.

원인 분석

requests에는 기본 타임아웃이 없습니다. 문서에 그대로 적혀 있습니다 — 타임아웃 값을 명시적으로 주지 않으면 요청은 시간 초과되지 않습니다. 빠른 시작 문서는 더 세게 말합니다. 거의 모든 프로덕션 코드는 거의 모든 요청에 이 파라미터를 써야 하고, 그러지 않으면 프로그램이 무기한 멈출 수 있다고요.

timeout을 줘도 "전체 시간 제한"은 아닙니다. 여기서 한 번 더 헷갈렸습니다. 문서에 따르면 timeout은 응답 전체를 받는 데 걸리는 시간의 상한이 아니라, 서버가 timeout초 동안 아무 응답도 보내지 않을 때(정확히는 소켓에서 그 시간 동안 1바이트도 받지 못했을 때) 예외를 냅니다. read 타임아웃은 서버가 보내는 바이트 사이를 기다리는 시간이라, 서버가 조금씩이라도 계속 보내면 요청 전체는 지정한 시간보다 훨씬 길어질 수 있습니다. 문서도 connect와 read 타임아웃 둘 다 벽시계 시간이 아니라고 따로 짚어 둡니다.

해결 방안

  1. 모든 요청에 timeout을 줍니다. 값 하나를 주면 connect와 read 타임아웃에 같이 적용되고, 튜플로 주면 따로 정할 수 있습니다.
resp = requests.get(url, params=params, timeout=(3.05, 27))

connect 값이 3이 아니라 3.05인 데는 이유가 있습니다. 문서는 connect 타임아웃을 3의 배수보다 조금 크게 잡으라고 권하는데, 3초가 TCP 패킷 재전송의 기본 간격이기 때문입니다.

  1. 빠뜨리지 않게 한 군데로 모읍니다. 호출하는 곳마다 적다 보면 반드시 하나는 빠집니다. 외부 API 호출을 감싸는 함수를 하나 두고, 코드에서는 그 함수만 부르게 합니다.
DEFAULT_TIMEOUT = (3.05, 27)

def call_api(method, path, **kwargs):
    kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
    return requests.request(method, f"{BASE}{path}", **kwargs)
  1. 타임아웃이 나면 다음으로 넘어가게 합니다. 시간이 초과되면 requests.exceptions.Timeout이 발생합니다. 배치라면 그 페이지를 실패로 남기고 다음으로 넘어가거나, 정해 둔 횟수만큼만 다시 시도합니다. 예외를 잡지 않으면 이번엔 멈추는 대신 배치 전체가 죽습니다.
try:
    resp = call_api("GET", "/items", params={"page": page})
except requests.exceptions.Timeout:
    failed_pages.append(page)
    continue
  1. 배치 전체의 시간 상한은 따로 겁니다. read 타임아웃은 바이트 사이의 간격이라 "이 작업은 30분 안에 끝난다"를 보장해 주지 않습니다. 작업 전체에 대한 제한은 스케줄러나 실행 쪽에서 따로 둡니다.

  2. timeout=None은 기본값과 같다는 걸 기억합니다. 문서는 정말 느린 서버라면 None을 줘서 영원히 기다리게 할 수 있다고 소개하는데, 그게 바로 아무것도 안 적었을 때의 동작입니다. 이번 사고가 정확히 그 상태였습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.