3 분 소요

과거 작업을 재개할 수 있는 시스템을 운영하는 경우 도움될 수 있는 아이디어인 것 같아서 업무에 어떻게 적용했는지 참고차 기록했습니다.

호출 실패 구간이 발생했다

현재 운영중인 제휴 주문 연동 시스템에서 외부 API로 일정 시간대 데이터를 주기적으로 조회하는 플로우가 있다. 최근 N시간을 기준으로 주기적으로 호출하기 때문에 슬라이딩 윈도우로 동작하고, 조회 범위가 서로 겹치는 요청이 여러차례 있기 때문에 일부 호출이 실패하더라도 커버되는 구조였다.

문제는 호출하지 못한 시간이 최근 N시간보다 길어졌을 때였다. 사유는 다양할 수 있다.

  • 내부 or 제휴사 정기점검으로 인한 stop
  • 내부 or 제휴사 서버 장애
  • 기타 등등

예를 들면 아래처럼 표현할 수 있다.

최근 1시간을 조회하는 작업이 10시 이후 중단되고 13시에 재개되면, 10시부터 12시까지는 기본 조회 범위 밖에 남는 예시

이런 경우를 대비해서 시간대를 요청값으로 받아 복구하는 수기 작업은 가능했으나 문제는 실패 시간대를 인지하지 못했을 때다. 정기 점검은 정해진 시간동안 처리 불가한 점을 알고 있어서 수기 복구가 가능하지만 시스템 장애는 불시에 발생하기 때문에 누락이 발생할 수 있다.

Redis Bitmap으로 하루 호출 이력 기록하기

하루를 일정한 시간 슬롯으로 쪼개서 관리하면 호출 성공실패 이력을 기록할 수 있는 공간이 된다. (세세할 수록 좋겠지만 용량 제약과 update가 잦을 것으로 예상돼 10분을 슬롯 size로 지정)

이 기록은 최근 일주일만 보관할 거고 어떤 요건이 추가 반영될지 모르기 때문에 굳이 RDB 테이블에 적재하지 않고 Redis에 Bitmap 형식으로 데이터를 관리했다.

  • key는 제휴사별로 구분하고, value는 bitmap이 된다.

10분을 한 슬롯으로 잡으면 하루는 24 * 6 = 144bit = 18byte로 표현이 가능해 공간적으로 부담이 되지 않는 크기다. 제휴사 N개, 일주일간 bitmap을 관리하면 N * 7 * 18byte가 된다.

시간 구간  00:00~00:10  00:10~00:20  00:20~00:30  ...
비트            1            1            0      ...
  • 1: 성공
  • 0: 실패 or 미호출

성공 기준은 외부 API 성공(2xx) 응답이면서 정상 데이터를 응답으로 내려줄 때를 기준으로 했다. (2xx인데 응답은 null로 내려주는 엣지 케이스가 있어서 케바케로 디테일하게 잡았는데 이건 요건에 따라 잡아가면 된다)

10분 슬롯에 일부만 걸치는 요청은 어떻게 기록할까

10:05부터 10:25까지 조회에 성공했다면, 10:00~10:10과 10:20~10:30은 일부만 조회한 셈이다. 이 슬롯까지 1로 바꾸면 실제로 조회하지 않은 시간도 성공 기록에 포함된다.

그래서 요청 범위에 완전히 포함된 슬롯만 1로 기록했다. 이 예시에서는 10:10~10:20만 해당한다.

10:05부터 10:25까지 조회했을 때 완전히 포함된 10:10부터 10:20까지의 슬롯만 1로 기록하는 예시

슬롯 안의 부분 구간까지 따로 저장하면 더 정확하게 표현할 수 있지만, 그만큼 상태와 병합 로직이 복잡해진다. 여기서는 공간 효율성과 구현 단순성을 선택했다.

양쪽 경계는 이후의 sliding window가 해당 슬롯 전체를 여러차례 포함하기 때문에 성공으로 기록될 가능성이 높고, 그래도 실패한다면 추후 복구 API를 통해 미호출 시간대를 식별해서 재처리를 한다.

기존 로직에 영향주지 않기

기존 조회 로직에 쉽게 반영하고 쉽게 떼어낼 수 있도록 어노테이션 기반 AOP로 반영했다.

대상 요청에서 외부 API의 HTTP 응답 상태를 확인하고, 성공 응답을 받으면 조회 구간을 Redis에 기록하는 방식이다.

서버 내 조회 로직 중 어노테이션이 있는 A와 B에만 AOP가 적용되고, HTTP 응답 상태를 확인해 성공한 조회 구간을 Redis에 기록하는 흐름

  • 혹시 Redis 기록이 실패해서 본 작업으로 에러 전파가 안되도록 try-catch로 잡아줬다.

(페이징이 있다면) 어디까지 성공으로 봐야할까?

같은 시간대여도 대상이 많아서 조회 API를 여러 page로 호출해야 하는 경우가 있다.

일반적이라면 확실하게 체크하려고 모두 호출 성공했을 때만 1로 기록하려고 했겠지만, 현재 운영하는 연동 시스템은 실제로 1페이지에서 끝나는 요청이 99.999%고 외부 API를 덜 호출(일부 페이지만 처리해서)하는 것보다 아예 호출을 하지 못해서 문제가 된 경우가 전부였기 때문에 한 페이지라도 성공 응답을 받으면 해당 조회 구간을 성공으로 기록했다.

즉, 모든 페이지의 완료 여부를 추적하기보다는, 아예 조회되지 못한 시간 구간을 복구하는 데 초점을 맞췄다. 반복되는 조회 범위가 겹치면서 일부 실패를 재처리할 확률이 높다는 기대도 있었다.

다만 늘 같은 상황이 발생하지 않을 수 있기때문에 2페이지 이상에서 조회 실패 시 알림을 받는 여지는 남겨 두어서 대응이 필요할지는 추후 판단으로 남겨두었다.

성공 기록이 없는 구간 복구하기

복구 기능은 별도의 API로 구현하고 스케줄러에서 주기적으로 호출하도록 했다. 동작은 다음과 같다.

  1. 복구 대상 시간 범위에서 Bitmap에 성공 기록이 없는 슬롯을 확인한다.
  2. 누락 구간을 한 번에 조회할 수 있는 크기로 나눈다. (최대 1시간)
  3. 각각 나눈 시간대로 외부 API(조회 API)를 순차 호출한다.
  4. 성공 응답을 받으면 Redis Bitmap에 해당 시간대 슬롯을 1로 기록한다. (위에서 기록한 프로세스가 그대로 적용됨)

성공 기록이 있는 구간은 복구 대상에서 제외했고, 조회 API 부하를 고려해서 상한은 1시간으로 설정했다.

이 복구 API는 스케쥴러를 통해서 넉넉한 주기로 트리거했다.

호출 현황 대시보드

이해를 돕기 위해 구현했던 대시보드 화면을 재구성한 예시입니다.

Redis에 관리하던 Bitmap은 쉽게 확인할 수 있도록 대시보드를 만들었다.

기획팀에서도 개발팀에 더 이상 호출 이력 확인 문의를 할 필요가 없어졌다.

가상 데이터로 재구성한 호출 현황 대시보드: 대상별 성공 기록과 선택 구간 재호출

  • 처음엔 단순히 잘 기록되고 있는지 확인하려고 만들었지만 의외로 제휴사 정기점검이 잦아서 확인이 쉬워 유용했다!
  • 긴급 대응용으로 수기 호출 기능도 만들었다.

더 추가하면 좋을 것들

언급하진 않았지만 조회하는 대상이 여러가지가 될 수 있으므로 이걸 type으로 관리한다면 확장성있게 적용할 수 있다.

예를들면 주문도 주문, 선물주문, 취소, 교환, 반품으로 나눌 수 있고 물류 데이터를 연동한다면 배송권역, 배송 시간대 등이 있다.

카테고리:

업데이트: