queue, lease, lock은 왜 같이 등장했을까

여러 worker가 데이터 작업을 처리할 때 왜 queue, lease, 대상 단위 lock이 함께 필요해지는지 쉬운 예시로 정리한다.

앞 글에서는 왜 단순한 script 실행만으로는 부족해졌는지 정리했다. 핵심은 작업이 오래 걸리고, 실패하고, 중복 실행되면 위험하기 때문에 시스템이 지금 어떤 상태인지 알아야 한다는 점이었다.

이번 글의 질문은 조금 더 좁다. 여러 worker가 있을 때, 어떤 보장만은 반드시 있어야 작업이 꼬이지 않을까.

그 다음에 생긴 질문은 worker였다. 작업을 상태로 관리한다고 해도, 여러 worker가 동시에 떠 있다면 누가 어떤 작업을 가져갈지 정해야 한다.

처음 떠올릴 수 있는 구조는 단순하다.

작업 테이블에서 아직 실행 안 된 것 하나 조회
-> worker가 실행

하지만 이 구조는 바로 깨진다. worker 두 개가 거의 동시에 같은 row를 읽으면 둘 다 같은 작업을 실행할 수 있기 때문이다.

그래서 queue가 필요해진다. 여기서 queue는 별도의 메시지 브로커일 수도 있지만, 작은 시스템에서는 DB table 하나로도 충분할 수 있다.

jobs
- id
- target_id
- status
- due_at

작업은 먼저 대기 상태로 들어간다.

QUEUED

worker는 이 작업을 바로 실행하지 않고, 먼저 자신이 가져갔다고 표시한다.

QUEUED -> LEASED

여기서 lease는 어렵게 말하면 임시 소유권이고, 쉽게 말하면 “내가 이 작업을 잠깐 맡겠다”는 표시다.

worker-1이 이 작업을 30분 동안 맡음

왜 바로 RUNNING으로 가지 않고 LEASED가 필요할까. worker가 작업을 가져간 직후 죽을 수 있기 때문이다.

worker가 작업을 가져감
-> 아직 실행 시작 전
-> process가 죽음

이때 lease 만료 시간이 있으면 시스템이 의심할 근거가 생긴다.

LEASED 상태인데 시간이 지남
-> 실행 시작 전이라고 보장되는 구간이면 다시 QUEUED

여기서 조심할 점이 있다. lease 만료는 worker가 반드시 죽었다는 증명이 아니다. 장시간 실행 작업이라면 정상 worker도 최초 lease 시간을 넘길 수 있다. 그래서 실제 운영에서는 lease를 주기적으로 연장하는 heartbeat나 renewal 정책이 필요할 수 있다.

LEASED -> QUEUED 복구는 “아직 외부 side effect가 시작되지 않았다”는 경계가 분명할 때 안전하다. 외부 process를 띄운 뒤 상태 기록 전에 죽을 수 있다면, 단순 재큐잉은 중복 실행을 만들 수 있다. 그래서 claim과 start 기록은 가능한 한 원자적으로 다뤄야 한다.

이제 worker가 실제 실행을 시작할 준비가 되면 상태를 바꾼다.

LEASED -> RUNNING

다만 RUNNING은 job 실행 상태를 뜻할 뿐, 같은 대상을 건드릴 권한까지 자동으로 보장하지는 않는다. 동일 대상의 동시 변경을 막으려면 별도의 resource lock이 필요하다.

여기까지는 작업 하나를 안전하게 가져가는 문제다. 그런데 데이터 작업에는 또 다른 축이 있다. 같은 dataset, 같은 저장소, 같은 partition처럼 동일한 대상을 동시에 처리하면 안 되는 경우다.

예를 들어 target A를 동기화하고 있는데, 같은 target A에 대한 다음 동기화가 또 시작되면 파일이나 데이터 상태가 꼬일 수 있다.

worker-1 -> target A sync
worker-2 -> target A sync

worker가 여러 개 있어도 같은 대상은 하나만 실행되어야 한다. 그래서 resource lock이 필요해진다.

resource lock 획득
-> 작업 실행
-> lock 해제

다른 대상은 동시에 실행할 수 있다.

worker-1 -> target A
worker-2 -> target B
worker-3 -> target C

하지만 같은 대상은 막는다.

worker-1 -> target A 실행 중
worker-2 -> target A 시도
-> lock 경합
-> 실행하지 않음

이때 경합난 작업을 바로 실패로 닫을지, lease를 반납하고 다시 대기시킬지, 잠시 기다리며 lease를 연장할지는 별도의 운영 정책이다. 여기서 중요한 건 그 정책이 무엇이든 같은 대상을 동시에 변경하지 않는다는 원칙이다.

결국 queue, lease, resource lock은 서로 다른 문제를 푼다.

  • queue는 실행할 작업 목록을 관리한다
  • lease는 worker가 작업을 가져간 뒤 죽었을 때 복구할 근거를 만든다
  • resource lock은 같은 대상을 동시에 건드리지 못하게 한다

표로 보면 구분이 더 선명하다.

장치막으려는 문제없으면 생기는 일
queue실행할 작업의 보관과 순서작업이 흩어지고 추적이 어려워진다
leaseworker가 가져간 작업의 임시 소유권가져간 뒤 죽은 작업이 애매하게 남는다
resource lock같은 대상의 동시 변경파일이나 데이터 상태가 꼬일 수 있다

이 셋을 합치면 구조는 이렇게 된다.

QUEUED
-> worker가 lease 획득
-> LEASED
-> resource lock 획득
-> 실행 시작
-> RUNNING
-> pipeline 실행
-> terminal 상태 기록

여기서 RUNNING을 언제 기록할지는 시스템마다 다를 수 있다. 중요한 기준은 실제 side effect가 시작되기 전에 같은 대상에 대한 resource lock이 잡혀 있어야 한다는 점이다. lease는 job 소유권이고, resource lock은 대상 변경 권한이다. 둘은 비슷해 보이지만 서로 다른 안전장치다.

이때 중요한 점은 worker 수가 곧 전체 병렬 처리 상한이라는 것이다. worker가 4개면 동시에 최대 4개 작업을 처리할 수 있다. 다만 같은 대상 작업이 몰려 있으면 lock 때문에 실제 실행은 1개만 가능할 수 있다.

worker 수 = 전체 동시 처리 상한
resource lock = 같은 대상 중복 실행 차단

처음에는 이게 과한 구조처럼 보일 수 있다. 하지만 worker가 죽고, 같은 대상이 중복 실행되고, 취소 요청이 들어오는 상황을 하나씩 생각하면 결국 필요한 이름들이 된다.

정리하고 나니 다음 질문은 실행 순서로 넘어갔다.

  • 작업을 안전하게 가져왔다면, 그 안의 여러 단계는 어떻게 이어야 하는가

이어서 읽기

작성 수정