본문으로 건너뛰기

여러 Pod(replica) 환경 동시성 제어: 단일 SQL CAS 패턴

서비스를 pod replica 여러 개로 띄우면 처리량은 늘지만, 같은 데이터를 두 pod가 동시에 건드릴 수 있다. 내 경우는 job 하나를 완료(Done)로 쓰는 순간이 문제였다. 분산락까지 갈 것 없이 SQL 문 하나로 이 경합을 막았는데, 알고 보니 CAS(Compare-And-Swap) 패턴이었다.

문제: 마지막에 쓴 쪽이 이긴다

취소 요청을 받은 pod(leader가 아님)와 job을 실행하던 executor(leader pod)가 같은 job row를 동시에 Done으로 쓰려고 했다.

Pod ACanceljobid=3Pod Bexecutor읽기 running읽기 running쓰기 cancelled쓰기 success취소했는데 success로 덮였다
락이 없으면 둘 다 running을 읽고, 마지막에 쓴 쪽이 결과를 정한다

락이 없으니 두 pod 모두 state=running을 읽고, 둘 다 쓰기에 성공한다. 결과는 마지막에 쓴 쪽을 따르니 cancelled가 success로 뒤집히거나 그 반대가 된다. 사용자 입장에선 분명 취소했는데 성공으로 떠 있는 버그다.

해법: UPDATE에 기대 상태를 건다

해결은 단순했다. UPDATE의 WHERE에 내가 기대하는 상태를 조건으로 걸어서, 첫 번째 writer만 성공하게 만든다.

job 완료 처리
UPDATE job
SET state = ?, result = ?, finished_at = ?
WHERE id = 3
AND run_seq = 3 -- 내가 알던 세대(generation)
AND state = 'running' -- 아직 진행 중
AND action = 'start' -- 같은 액션

성공 여부는 RowsAffected로 판단한다. 1이면 내가 첫 writer이니 그대로 완료하고, 0이면 다른 쪽이 먼저 끝냈으니 건너뛴다.

Pod ACanceljobid=3Pod Bexecutor조건부 UPDATE1행 → 완료조건부 UPDATE0행 → skipA만 반영되고 B는 건너뛴다
WHERE에 기대 상태를 걸면 먼저 도착한 쪽만 1행을 바꾼다

기대 상태와 비교(Compare)해서 맞을 때만 바꾸니(Swap) CAS다. DB가 UPDATE를 row 단위 락으로 원자적으로 처리하니, 애플리케이션이 따로 락을 잡을 필요가 없다.

run_seq를 같이 비교하는 이유

state = 'running'만 보면, 재시작이나 재실행으로 같은 상태가 다시 와도 구분하지 못한다. run_seq(세대 번호)는 지금 row가 내가 알던 그 실행인지를 확인해 준다. 값은 같아 보여도 사실 다른 세대인 ABA 문제를 막는 장치다.

기존 코드에도 같은 패턴이 있었다

같은 코드베이스의 다른 작업 흐름은 이미 이 패턴을 중간 checkpoint 저장(SaveStatusIfCurrent)에 쓰고 있었다. 이번에 만든 완료 처리(FinishIfCurrent)는 WHERE 조건을 그대로 가져오고, 완료 전용이라 SET 부분만 단순하게 했다.

완료 처리 흐름
finish(Done, Cancelled)
├─ 조건: {JobID=3, ExpectRunSeq=3, ExpectAction=start}
├─ FinishIfCurrent(cond, Done, Cancelled, now, logs)
│ └─ UPDATE ... WHERE run_seq=3 AND state=running AND action=start
├─ matched=true → 정상 완료
└─ matched=false → "superseded by another actor" warn 로그, DB는 그대로

분산락과 비교

예전에는 이 경합을 분산락으로 풀었다.

분산락

  1. 락을 잡는다.
  2. 상태를 확인한다.
  3. 쓴다.
  4. 락을 푼다.

단계마다 etcd를 오가고, 그 사이 어디서든 실패할 수 있다.

✓CAS 조건부 UPDATE

  • SQL 한 문장으로 같은 보장을 준다. etcd 왕복이 없다.
  • 멱등하다. 같은 호출을 여러 번 해도 한 번만 반영된다.
  • 정상, 실패, 취소 완료를 한 메서드로 처리한다.

언제 CAS, 언제 분산락

pod 사이의 동시성 문제는 생각보다 거창한 인프라 없이 WHERE 한 줄로 풀리는 경우가 많다.

GitHub에서 고치기
esc

최근 글

↑↓ 이동↵ 열기검색 페이지에서 보기 ›