Redis를 신뢰하지 않는 Job Scheduler를 만들면서 배운 점
Source: Dev.to

Intro
이것은 무엇인가?
Tickr는 Go, Redis, MySQL을 사용하여 만든 백그라운드 작업 스케줄러입니다.
작업을 할당받기 위해 대기하는 워커 풀로 구성됩니다. 작업은 Redis로 구현된 대기 및 준비 큐를 통해 이동하며, 스케줄러에 의해 워커에게 할당됩니다. 모든 것이 동시에 실행되며 이벤트 기반입니다.
왜 만들었나요?
Node.js와 Go로 CRUD 백엔드 API를 만드는 것이 지겨워졌습니다.
엔드포인트와 스키마만이 아니라 동시성, 오류 처리, 시스템 동작에 대해 생각하도록 강요하는, 다르고 낯선 무언가를 작업하고 싶었습니다.
Source: …
v1에서 무엇이 깨졌나요?
Polling
v1에서는 스케줄러가 매초 폴링을 통해 작업이 실행될 준비가 되었는지 확인했습니다. 작동은 했지만 비효율적이었습니다. 저는 폴링을 버퍼가 없는 Go 채널과 블로킹 연산으로 교체했습니다—채널은 프로젝트에서 가장 중요한 개념 중 하나가 되었습니다.
Assumptions
많은 것이 자동으로 동작할 것이라고 가정했습니다. 이러한 가정이 깨지면 디버깅과 복구가 어려워졌습니다.
예시: 서버가 종료될 때 아직 대기 큐에 작업이 남아 있다면, 재시작 시 해당 작업들을 복구하는 것이 복잡하고 신뢰할 수 없었습니다.
Redis as single source of truth
초기에는 Redis만을 유일한 진실의 원천으로 사용했습니다. 엣지 케이스 테스트를 진행하면서 불편한 질문들이 떠올랐습니다:
- 작업이 아직 큐에 남아 있는 상태에서 Redis가 다운되면 어떻게 되나요?
- Redis가 크래시 후 상태를 잃어버리면 어떻게 되나요?
- 작업이 중간에 실패하면 어떻게 처리하나요?
- 완료된 작업에 대한 로그나 히스토리가 필요하면 어떻게 하나요?
이 질문들에 답하기 위해 설계를 재구성했으며, 그 결과 Tickr v2가 탄생했습니다.
Source:
v2 아키텍처
MySQL을 진실의 원천으로
v2에서는 MySQL이 전체 작업 데이터를 저장합니다. JobID와 스케줄링 정보만 Redis에 푸시되며, Redis는 순전히 스케줄링 및 조정을 위해 사용되고 내구성을 담당하지 않습니다.

이 접근 방식은:
- 충돌 시 작업 손실 방지
- 불필요한 MySQL 폴링 회피
- 가벼운 ID만으로 큐 간 작업 이동 가능
- 실행이 필요할 때만 워커가 MySQL에서 전체 작업 데이터를 가져오게 함
스케줄러와 워커
스케줄러는 단일 고루틴(PopReadyQueue)을 실행하며, 작업이 준비될 때까지 Redis에서 블록됩니다.
Redis 연결이 끊긴 경우:
- 스케줄러는 Redis가 다시 접근 가능해질 때까지 대기합니다.
- Redis가 상태를 잃었는지 확인합니다.
- 상태가 손실된 경우, MySQL에서 큐를 재구성합니다.
준비 큐에서 꺼낸 작업은 모든 워커가 수신 대기 중인 버퍼가 없는 작업 채널로 전송됩니다. 채널에 작업이 나타나면 정확히 한 명의 워커가 이를 받아 실행합니다.
작업이 실패한 경우:
- 지연을 두고 재시도합니다.
- 재시도 횟수는 제한됩니다(최대 3회).
- 각 시도마다 지연 시간이 선형적으로 증가합니다.
처리된 엣지 케이스
Redis 충돌
Redis가 다운되면 스케줄러가 일시 중지되고 Redis가 다시 사용 가능해질 때까지 기다립니다. Redis가 복구되면 상태가 손실되었는지 확인하고 필요하면 MySQL에서 복구를 트리거합니다; 그렇지 않으면 정상적으로 계속 진행합니다.
기한이 지난 작업
작업이 예약된 상태에서 Redis가 다운되면 실행 시간이 지나도 트리거되지 않을 수 있습니다. 이전에는 새로운 작업이 큐에 들어올 때까지 이러한 작업이 실행되지 않았습니다. 대기 큐가 다시 채워질 때 채널을 통해 스케줄러에 신호를 보내 기한이 지난 작업을 즉시 재평가하도록 수정했습니다.
실행 중 종료
서버가 작업을 실행 중에 종료될 경우:
- 워커가 새로운 작업을 받는 것을 중단합니다
- 진행 중인 작업은 완료될 수 있도록 허용합니다
- 프로그램은 워커가 종료될 때까지 기다린 후 종료합니다
완료된 작업이 전역 컨텍스트가 이미 취소된 상태라 MySQL에 상태를 업데이트하지 못하는 문제가 발생했습니다. 이를 해결하기 위해 최종 상태 업데이트 전용으로 백그라운드 컨텍스트를 사용하여 종료 중에도 정확성을 보장했습니다.
배운 점
이 프로젝트는 이전에 만든 어떤 것과도 매우 달랐으며, 정말 즐거웠습니다. 저는 다음을 배웠습니다:
- 상태의 소유권이 왜 중요한지
- 가정이 얼마나 깨지기 쉬운지
- 실패를 회피하기보다 그것에 대해 어떻게 논리적으로 접근할 수 있는지
- 단순히 재시작하는 것이 아니라 복구하는 시스템을 어떻게 설계할 수 있는지
- 엣지 케이스에서도 쉽게 깨지지 않는 백엔드 코드를 어떻게 작성할 수 있는지
Tickr v2는 어떤 튜토리얼보다 백엔드 시스템에 대해 더 많이 가르쳐 주었습니다.
GitHub 저장소는 여기에서 확인할 수 있습니다: Tickr