Foundry
분산 시스템
심화

분산 락과 단일 실행 보장

락은 만료된다. 안전한 상호배제의 조건

분산 락과 단일 실행 보장

어디서 필요한가

  • 배치 서버 3대가 같은 스케줄을 돌리면 같은 작업이 세 번 실행된다
  • 정산 마감, 쿠폰 발급처럼 한 번만 실행돼야 하는 작업에 상호배제가 필요하다

최소 요구 조건

조건이유
원자적 획득SET key value NX PX 30000 처럼 한 명령으로 획득
TTL 만료보유자가 죽어도 영구 잠김이 되지 않도록
소유자 확인 후 해제토큰을 비교해 남의 락을 지우지 않도록
갱신 또는 충분한 TTL작업이 TTL보다 길어지면 두 프로세스가 동시 진입

락만으로 부족한 순간

일이 잠금 수명보다 길어지면 쥐었다고 믿는 사이 남이 같은 잠금을 얻는다 잠금 수명 10초 여기서 만료된다 내 일은 15초 걸렸다 아직 하고 있다 그 사이 남이 같은 잠금을 얻는다. 둘이 함께 고친다 그래서 잠금만으로는 부족하다. 쓸 때 내가 아직 주인인지 확인한다 잠금마다 번호를 두고 그 번호가 최신일 때만 쓰게 한다 수명을 늘리면 죽었을 때 아무도 못 들어가는 시간이 길어진다 둘 중 하나를 고르는 문제이고 없애는 문제가 아니다
프로세스A 락 획득 → GC 정지 20s → TTL 만료
                    프로세스B 락 획득 → 쓰기
프로세스A 깨어나 쓰기 → 결과적으로 두 번 쓰기
  • 저장소가 펜싱 토큰(단조 증가 번호)을 검사해 낮은 번호의 쓰기를 거부해야 정확성이 보장된다

만료 시간이 가장 어려운 값이다

너무 짧으면 일하는 중에 락이 풀립니다. 너무 길면 붙잡고 죽은 뒤 아무도 못 들어갑니다.

만료위험
작업 시간보다 짧다둘이 동시에 일한다. 락이 없는 것과 같다
훨씬 길다프로세스가 죽으면 그만큼 멈춘다

둘 다 피하려면 일하는 동안 만료를 미룹니다. 그러면 짧게 잡아도 되고, 죽으면 미루기가 멈춰 곧 풀립니다. 다만 미루기가 실패하면 일을 멈추도록 만들어야 합니다.

락이 있어도 두 번 실행된다

락은 동시에 두 개가 도는 것을 막습니다. 순서대로 두 번 도는 것은 막지 못합니다.

락을 얻고 일하다 응답 직전에 죽었다
재시도가 들어와 락을 얻고 같은 일을 또 했다

락으로는 이것을 막을 수 없습니다. 그래서 락과 멱등성은 둘 다 필요합니다. 락은 동시성을, 멱등성은 반복을 막습니다. 하나로 다른 하나를 대신할 수 없습니다.

실무 포인트

  • 성능 최적화용 락은 Redis로 충분하지만, 정확성이 돈과 직결되면 etcd, ZooKeeper 같은 합의 기반을 쓴다
  • 락 구간은 최소로 잡고 외부 API 호출을 락 안에 넣지 않는다
  • 락이 필요 없게 만드는 편이 낫다. 파티션 키로 단일 소비자를 만들거나, DB 유니크 제약과 낙관적 락을 쓴다
면접에서 이렇게 나옵니다

Q.배치 서버가 여러 대일 때 중복 실행을 어떻게 막나요?

방법은 셋이고 일의 성격에 따라 고릅니다.

방법내용맞는 곳
분산 락먼저 잡은 한 대만 돈다주기 작업, 정리 배치
작업 큐작업을 메시지로 만들고 한 번만 소비한다건별 처리
DB 유일 제약실행 기록을 유일 키로 넣어 둘째를 실패시킨다하루 한 번 같은 고정 주기

세 번째가 가장 단순하고 튼튼합니다. (작업이름, 실행일자) 를 유일 키로 넣고, 넣기에 성공한 한 대만 일을 합니다. 별도 인프라가 필요 없고 DB 가 살아 있는 한 정확합니다. 다만 실행 주기가 고정되어 있어야 키를 만들 수 있습니다.

분산 락은 주기가 불규칙하거나 작업이 길 때 씁니다. 이때 만료 시간이 가장 어려운 값입니다. 작업 시간보다 짧으면 일하는 중에 풀려 둘이 동시에 돌고, 너무 길면 붙잡고 죽었을 때 그만큼 멈춥니다. 일하는 동안 만료를 미루는 방식으로 둘 다 피합니다.

흔한 실수: 락만 걸고 끝내는 것. 락은 동시에 두 개가 도는 것을 막지만, 앞의 실행이 응답 직전에 죽고 재시도가 들어와 순서대로 두 번 도는 것은 막지 못합니다. 그래서 작업 자체가 두 번 돌아도 결과가 같아야 합니다.

Q.분산 락에 TTL을 두는 이유와 그로 인한 위험은?

락을 잡은 프로세스가 죽으면 아무도 풀어 줄 수 없기 때문입니다. 만료가 없으면 그 작업은 영원히 막힙니다.

그런데 만료를 두는 순간 새 위험이 생깁니다.

만료위험
작업 시간보다 짧다일하는 중에 풀린다. 둘이 동시에 돌아 락이 없는 것과 같다
훨씬 길다프로세스가 죽으면 그 시간만큼 아무도 못 들어온다

첫 줄이 더 위험합니다. 락이 있다고 믿고 짠 코드가 동시에 도는 것이라, 증상이 "가끔 중복 처리" 로 나타나고 재현이 어렵습니다.

해결은 일하는 동안 만료를 미루는 것입니다. 그러면 만료를 짧게 잡아도 되고, 죽으면 미루기가 멈춰 곧 풀립니다. 다만 미루기가 실패했을 때는 작업을 멈춰야 합니다. 미루기에 실패했다는 것은 이미 락을 잃었을 수 있다는 뜻입니다.

풀 때도 조심해야 합니다. 내가 잡은 락인지 확인하지 않고 지우면, 이미 만료돼 남이 잡은 락을 내가 푸는 일이 생깁니다. 락에 고유 값을 넣고 그 값이 같을 때만 지웁니다.

흔한 실수: 만료 시간을 작업 시간의 평균으로 잡는 것. 평균으로 잡으면 절반은 넘깁니다. 미루기를 쓰지 않을 거라면 최악의 시간을 기준으로 잡아야 합니다.

Q.락을 잡았는데도 동시 실행이 발생하는 시나리오를 설명해주세요

대표적인 시나리오 셋입니다.

1. 만료가 작업보다 짧았다. A 가 락을 잡고 일하는 중에 만료돼 B 가 같은 락을 잡습니다. A 는 자기가 락을 잃은 줄 모르고 계속 일합니다. 둘이 같은 자원을 동시에 건드립니다.

2. 정지 구간이 있었다. A 가 락을 잡은 직후 긴 GC 정지나 가상 머신 정지에 들어갑니다. 그 사이 만료되고 B 가 들어옵니다. A 는 깨어나서 아무 일 없었다는 듯 이어서 씁니다. 락을 확인한 시점과 실제로 쓰는 시점 사이가 벌어지는 것이 본질입니다.

3. 락 저장소가 장애를 겪었다. 복제 구성에서 리더가 바뀌면, 아직 복제되지 않은 락 정보가 사라져 두 프로세스가 같은 락을 잡을 수 있습니다.

대응내용
만료 미루기일하는 동안 갱신하고, 갱신 실패 시 즉시 중단
펜싱 토큰락을 잡을 때 증가하는 번호를 받고, 쓰기 때 그 번호를 함께 넘긴다
최종 방어쓰는 쪽에서 조건부 갱신이나 유일 제약으로 한 번 더 막는다

펜싱 토큰이 2번을 진짜로 막는 유일한 방법입니다. 뒤늦게 깨어난 A 의 번호는 낮으므로 저장소가 거절합니다.

흔한 실수: 락을 걸었으니 안전하다고 보고 뒤쪽에 방어를 안 두는 것. 락은 확률을 크게 낮출 뿐이고, 마지막 방어는 데이터를 쓰는 자리에 있어야 합니다.

Q.락을 쓰지 않고 단일 실행을 보장하는 방법이 있나요?

있습니다. 그리고 많은 경우 락보다 낫습니다.

방법내용조건
DB 유일 제약(작업, 실행키) 를 넣고 성공한 쪽만 실행실행 키를 만들 수 있어야 한다
조건부 갱신상태가 대기 일 때만 진행 으로 바꾸고, 바뀐 쪽만 실행상태 컬럼이 있어야 한다
큐 소비작업을 메시지로 만들고 한 소비자만 가져간다메시지 인프라가 있어야 한다
리더에게만 배정이미 리더가 있는 구성에서 리더만 실행합의 기반 시스템이 있을 때
파티셔닝키의 범위를 서버마다 나눠 겹치지 않게 한다작업을 나눌 수 있어야 한다

두 번째가 실무에서 가장 자주 맞습니다. 상태 전이를 원자적으로 하고 바뀐 행 수가 1인 쪽만 진행하면, 별도 인프라 없이 단일 실행이 보장됩니다. 게다가 어디까지 했는지가 데이터에 남아 재시작도 쉽습니다.

락 방식과 결정적으로 다른 점은 진실의 위치입니다. 락은 별도 저장소에 있고 데이터는 다른 곳에 있어서 둘이 어긋날 수 있습니다. 조건부 갱신은 데이터 자신이 자기 상태를 들고 있어서 어긋날 자리가 없습니다.

흔한 실수: 단일 실행이 필요하다고 하면 반사적으로 분산 락을 떠올리는 것. 락은 만료와 갱신, 소유자 확인, 펜싱까지 제대로 해야 맞는데, 데이터 쪽 제약으로 끝낼 수 있으면 그게 더 적은 부품으로 같은 보장을 줍니다.

먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.

읽었으면 문제로 확인해보세요

분산 시스템 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.