워커는 죽었지만, 동기화는 죽지 않았습니다

배치 도중에 워커를 강제 종료하고 연락처 하나를 따라갑니다. 어떤 커서가 저장되었는지, 재실행된 쓰기가 왜 두 번 들어가지 않는지, 재시작하는 동안 누가 동기화를 맡는지 살펴봅니다. 이어서 복구가 일의 절반에 불과하다는 것을 보여 준 두 건의 장애를 다룹니다.

데이터 동기화 장애 복구

배치 도중에 워커가 죽으면 실행 중인 동기화는 어떻게 됩니까?

커서는 깔끔하게 끝난 배치의 끝에서만 움직이므로, 배치 도중에 강제 종료된 워커는 커서를 그 배치의 시작에 남겨 두고 다음 실행은 같은 페이지를 읽습니다. 각 레코드의 전송 키는 페이지 커서와 위치에서 도출되고, 완료된 쓰기를 7일 동안 보관하는 원장이 재생된 쓰기가 두 번 들어가지 않도록 막습니다. 동기화마다 리스(lease)가 하나뿐이라 두 실행이 동시에 작업하지 못합니다. 여기까지가 쉬운 절반이었습니다. 한 동기화는 무언가가 일시 중지하기 전까지 실행할 때마다 계속 실패했고, 한 워크스페이스는 무언가가 알아차리기 전까지 오류만 쌓인 채 성공 없이 조용히 있었습니다. 그래서 이제 5회 연속 실패 후 일시 중지하는 차단기와, 24시간 동안 성공이 없을 때 울리는 시간별 알림이 있습니다.

자세히 알아보기
이 페이지에서

AI Engineer가 2026년 10월 3일에 공개한 강연에서 Crusoe의 개발자 애드버킷이 A100 노드 두 대에서 PyTorch 학습 작업을 시작했습니다. 그런 다음 데모 자체의 모니터링 시스템에 GPU 하나가 XID 79 오류를 냈다고 알렸습니다. 노드는 다운으로 표시되었고 작업은 취소되었습니다. 프로세스는 SIGTERM을 받고 “그 SIGTERM을 처리해 체크포인트를 저장하거나 로그를 내보낼 시간을 최대 2분” 얻었습니다. 예비 노드가 투입되었고, “slurm 작업이 재시작됩니다. 체크포인트를 불러와 멈춘 지점부터 학습을 이어 갑니다”. 처음부터 끝까지 15분이 조금 안 걸렸습니다. 아무도 호출되지 않았습니다.

동기화는 학습 작업보다 훨씬 작은 동물입니다. 그래도 질문은 같습니다. 워커를 죽여도 몇 분만 잃고 중복은 생기지 않으려면 실행에 무엇이 필요합니까?

이 글은 Tajo 자체 동기화 실행기에 대한 답이며, 브로슈어가 아니라 코드에서 읽어 낸 것입니다. 리버스 ETL은 이 작업의 모양을 설명합니다. 이 글은 그 작업이 죽었을 때 일어나는 일입니다.

연락처 하나 따라가기

실행은 저장된 커서에서 시작해 소스를 한 페이지씩 읽습니다. 페이지 커서를 c3, 우리가 따라갈 연락처를 그 페이지의 212번째 레코드라고 하겠습니다. 무엇이든 기록되기 전에 이 연락처에 대해 세 가지가 성립합니다.

첫째, 전송 키가 있습니다. 키는 동기화 id, 소스, 페이지 커서, 페이지 내 위치, 레코드 자체의 해시입니다. 커서와 레코드는 해시에 입력될 뿐 키에 포함되지 않으므로 키에는 이메일 주소가 담기지 않습니다.

둘째, 쓰기는 대상 시스템으로 바로 가지 않고 원장(ledger)을 거칩니다. 대상 시스템을 호출하기 전에 키는 대기 중으로 선점됩니다. 쓰기가 성공하면 그 행은 완료로 표시되고 7일 동안 보관됩니다.

셋째, 커서는 아직 움직이지 않았습니다. 커서는 배치가 끝날 때만, 그 배치의 모든 레코드가 기록된 경우에만, 그리고 실행이 여전히 동기화를 맡고 있음을 증명한 뒤에만 움직입니다.

이제 쓰기와 체크포인트 사이에서 워커를 죽여 봅니다.

SIGTERM을 받으면 모든 큐 워커는 공유된 15초 기한 안에서 작업을 비우고, 프로세스는 무엇이 아직 실행 중이든 25초 뒤에 종료됩니다. systemd는 강제 종료 전에 45초를 줍니다. 그 시간 안에 끝나지 않은 실행은 진행 중으로 표시된 채 남고, 다음 워커가 큐의 정체 작업 복구(stalled-job recovery)를 통해 이어받습니다. 강제 종료(hard kill)에서는 비우는 과정이 전혀 없습니다. 어느 쪽이든 같은 두 가지 사실이 남습니다. 대상 시스템에는 연락처가 들어가 있습니다. 커서는 여전히 c3입니다.

재생

다음 실행은 c3에서 시작해 같은 페이지를 읽고, 212번째 레코드에 대해 같은 키를 도출한 뒤 원장에 묻습니다. 원장은 완료라고 답합니다. 대상 시스템은 호출되지 않고, 저장된 결과가 재생되며 실행은 다음으로 넘어갑니다. Brevo의 가져오기 엔드포인트가 한 번의 호출로 페이지 전체를 받는 대량 처리 경로에서도, 요청을 만들기 전에 키마다 같은 확인이 실행됩니다. 모든 키가 이미 완료된 페이지는 아무것도 보내지 않습니다.

이것이 비결의 전부이며, 왜 통하는지 짚어 둘 가치가 있습니다. 키는 마침 실행 중이던 실행이 아니라 배치 시작 시점의 페이지 커서에서 도출됩니다. 장애, 60초 뒤의 재시도, 내일의 예약 실행이 모두 같은 레코드에 대해 같은 키를 도출합니다. 원장은 그중 누가 묻고 있는지 신경 쓰지 않습니다.

이제 비용입니다. 원장은 7일이 지나면 잊습니다. 코드 자체의 주석은 7일이 지연된 재시도와 짧은 장애를 감당한다고 말하며, 주장하는 것은 그것뿐입니다. 그보다 오래된 커서에서 재생하면 다시 씁니다. 또 강제로 중단된 실행은 기다려야 할 수 있습니다. 죽은 실행의 리스(lease)는 최대 15분까지 유지되고, 예약 동기화에서 리스가 풀린 실행은 시작한 지 한 시간이 넘어야 정리되어 다시 예약됩니다. Crusoe의 데모는 15분 안에 돌아옵니다. 우리는 항상 그렇지는 않습니다.

재시작하는 동안 누가 동기화를 맡는가

한 번에 하나의 실행만 동기화를 맡습니다. 실행은 자신의 토큰으로 Redis에 리스를 잡으며, 수명은 15분이고 5분마다 갱신합니다. 체크포인트 전마다 아직 그 리스를 쥐고 있는지 확인합니다. 그렇지 않으면 멈춥니다. 그 실행은 사용자가 읽게 될 평이한 문장으로 기록됩니다. “The run lost its slot part-way through and stopped to avoid writing twice.” (실행이 도중에 자리를 잃어 두 번 쓰지 않도록 중지했습니다) 살아 있는 소유자와 충돌한 두 번째 실행은 재시도되지 않습니다. “Another run of this sync was already in progress” (이 동기화의 다른 실행이 이미 진행 중이었습니다)로 끝나고, 다음 예약 실행은 정상적으로 시작됩니다.

수정의 책임자도 기록되어 있습니다. 모든 실패 코드는 무슨 일이 있었는지 설명하는 한 문장, 다음에 무엇을 해야 하는지 설명하는 한 문장, 그리고 담당자(고객 또는 Tajo)에 대응합니다. 매핑이 잘못되어 대상 시스템이 모든 레코드를 거부했다면 고객 담당입니다. 리스를 잃었다면 우리 담당입니다. 실행 페이지에 다음 단계가 표시되고, 어느 쪽인지는 동기화 복구 가이드에 정리되어 있습니다.

여기까지가 쉬운 절반입니다. 장애 한 번의 비용은 재생 한 번이고, 아무것도 두 번 들어가지 않습니다. 나머지 절반은 두 건의 장애가 가르쳐 주었습니다.

실패에 실패가 이어지다

한 동기화가 계속해서 실패했습니다. 촘촘한 루프가 아니라 스케줄러 주기마다 한 번씩, 실행할 때마다, 매번 같은 메시지로 실패했습니다. 사용자 지정 id에는 콜론을 넣을 수 없다는 메시지였습니다. 실행은 한 번도 대상 시스템에 닿지 못했습니다. 어떤 실행기도 보기 전에 큐에 넣어지는 도중 죽었기 때문에, 실행기에 연결된 알림에는 연결할 대상이 아예 없었습니다.

연속된 실패를 지켜보는 것이 아무것도 없었습니다. 발견한 것은 그것이 전부입니다.

해결책은 차단기입니다. 실패가 기록될 때마다 차단기는 동기화가 마지막으로 변경된 이후 끝난 최근 다섯 번의 실행을 읽습니다. 다섯 번 모두 오류라면 동기화를 일시 중지하고, 사유 circuit_breaker_tripped와 함께 감사 행 하나를 기록하고, 심각 알림을 한 번 보냅니다. 연속 실패는 워커 안의 카운터가 아니라 저장된 실행에서 도출됩니다. 어떤 경로로 기록된 실패든 집계되며, 장애의 원인이 된 큐 등록 실패도 포함됩니다. 코드 주석에는 그 장애를 다섯 번째 실행에서 멈췄을 것이라고 적혀 있습니다. 한 번 성공하면 연속 실패가 초기화됩니다. 일시 중지를 해제하면 오래된 행 때문에 다시 차단되는 대신 차단기가 새로 준비됩니다. Shopify 커넥터 가이드도 같은 내용을 한 줄로 말합니다. 5회 연속으로 실행이 실패하면 동기화가 스스로 일시 중지하고, 해당 페이지에 마지막 오류가 표시됩니다.

차단기가 하지 않는 일은 무언가를 고치는 것입니다. Crusoe는 묻지 않고 노드를 교체하고 작업을 재시작합니다. Tajo는 멈추고 묻습니다. 일시 중지된 동기화는 사람이 원인을 고치고 다시 게시할 때까지 멈춰 있습니다. 이 신중한 기본값은 의도된 것입니다. 고객 레코드에 쓰는 동기화를 자동으로 재시도하는 것 자체가 하나의 쓰기이기 때문입니다.

아무도 보지 못한 침묵

타당한 반론이 있고, 이렇습니다. “다섯 번 실패하면 일시 중지하는 차단기라면 두 번째 장애도 잡았을 것입니다. 왜 두 번째 감시자를 만듭니까?” 두 번째 장애는 실패처럼 보이지 않았기 때문입니다. 오랫동안 한 워크스페이스에는 활성 동기화가 있었고, 오류 행이 계속 쌓였고, 성공은 단 한 번도 없었지만, 아무것도 알림을 보내지 않았습니다. 존재하던 모든 신호는 활동에 연결되어 있었습니다. 실행이 실패했다, 웹훅이 도착했다, 예산을 넘었다. 성공이 0인 것은 이벤트가 아닙니다. 이벤트가 없는 것이며, 차단기는 실패가 기록될 때만 동작합니다. 죽은 스케줄러, 막힌 큐, 끝내 부팅되지 않는 워커는 아무것도 기록하지 않고, 차단기는 결코 깨어나지 않습니다.

기관차의 데드맨 장치는 반대로 작동합니다. 기관사는 계속 눌러야 하고, 침묵이 열차를 세웁니다. 동기화 버전은 한 시간에 한 번, 매시 13분에 실행됩니다. 활성 동기화가 있는 모든 워크스페이스에 대해 질문 하나를 던집니다. 지난 24시간 동안 성공한 실행이 있는가? 가장 오래된 활성 동기화가 활성화된 지 하루가 안 된 워크스페이스는 그대로 둡니다. 아직 성공할 하루가 주어지지 않았기 때문입니다. 그보다 오래되었고 조용한 워크스페이스는 심각 알림을 한 번 받습니다. 알림은 어떤 침묵인지 알려 줍니다. 실행은 기록되었지만 하나도 성공하지 못했는지, 아니면 실행이 아예 기록되지 않았는지입니다. 알림은 장애마다 한 번 발생합니다. Redis의 마커가 첫 번째 실행이 성공할 때까지 알림을 닫아 두었다가 지워지므로, 다음 장애에서 다시 알림이 울립니다.

두 감시자는 사후에 하나의 커밋으로 들어갔습니다. 차단기의 주석은 첫 번째 장애를 다섯 번째 실행에서 멈췄을 것이라고 말합니다. 데드맨의 주석은 오래전에 활성화되었는데 실행이 0인 동기화는 알림을 보내야 한다고 말합니다. 둘 다 필요할 때는 존재하지 않았습니다. 우리는 나중에 그것들을 작성했고, 아무도 이유를 잊지 않도록 두 장애를 파일 머리말에 기록해 두었습니다.

만든 것과 만들지 않은 것

이 글을 쓰는 시점에 만든 것:

  • 커서는 깔끔하게 끝난 모든 배치의 끝에서 저장되고, 장애 시에도 다시 저장됩니다.
  • 모든 레코드의 전송 키는 페이지 커서와 위치에서 도출되며, 대상 시스템을 호출하기 전마다 7일 원장과 대조됩니다.
  • 동기화마다 리스 하나, 수명은 15분입니다.
  • 실행마다 5회까지 시도하며, 60초부터 간격을 늘려 갑니다.
  • 5회 연속 실패 후 동기화를 일시 중지하고 그 이유를 기록하는 차단기.
  • 24시간 동안 성공이 없을 때 울리는 시간별 알림.

아직 만들지 않았으며, 의존하기 전에 알아 둘 것:

  • 데드맨은 동기화가 아니라 워크스페이스를 감시합니다. 건강한 동기화 하나가 같은 워크스페이스에서 조용한 다른 동기화를 가립니다.
  • 그 알림은 Tajo 운영자에게 갑니다. 로그 한 줄은 항상 남고, 설정된 경우 Sentry, 웹훅, 이메일로도 갑니다. 동기화 페이지에는 나타나지 않습니다. 차단기의 일시 중지는 나타납니다.
  • 일시 중지된 동기화를 재시작하는 것은 아무것도 없습니다. 사람이 합니다.
  • 강제 종료 후의 복구는 빠르지 않습니다. 리스는 15분까지 유지될 수 있고, 정리 작업은 한 시간을 기다립니다.
  • 두 임계값, 5회 실패와 24시간은 배포 단위 설정입니다. 동기화별로 설정할 수 없습니다.

거의 완벽한 단계들로 이루어진 긴 체인도 실패합니다. 빠져나오는 길은 최종 상태를 정의하고 그것을 확인하는 것입니다. 이 글은 그 이야기의 운영 쪽 절반입니다. 죽은 실행이 무엇을 남기는지 정의하고, 침묵이 남기는 아무것도 없음을 확인하십시오.

Crusoe의 마지막 말은 “실패는 피할 수 없다”였습니다. 우리의 말은 한 걸음 더 나아갑니다. 장애는 행을 남기기 때문에 쉬운 절반이었습니다. 침묵은 우리가 직접 찾아 나서야 했던 절반이었습니다.

자주 묻는 질문

배치 도중에 워커가 죽으면 실행 중인 동기화는 어떻게 됩니까?

커서는 여전히 중단된 배치의 시작을 가리킵니다. Tajo는 배치가 깔끔하게 기록된 뒤에만 커서를 옮기기 때문입니다. 다음 실행은 같은 페이지를 다시 읽습니다. 각 레코드에는 페이지 커서와 페이지 내 위치에서 도출한 전송 키가 있고, 원장(ledger)이 완료된 모든 쓰기를 7일 동안 보관합니다. 따라서 이미 기록된 레코드는 대상 시스템에 두 번 보내지지 않고 원장에서 재생됩니다.

재실행된 동기화가 같은 연락처를 두 번 쓰지 않는 이유는 무엇입니까?

대상 시스템을 호출하기 전마다 실행은 레코드의 전송 키를 원장과 대조합니다. 키는 동기화, 소스, 페이지 커서, 페이지 내 위치, 레코드 자체의 해시이므로, 장애, 1분 뒤의 재시도, 다음 날의 예약 실행 모두 같은 레코드에 대해 같은 키를 도출합니다. 7일 범위 안에서 이미 완료로 표시된 키는 다시 보내지 않습니다.

동기화의 데드맨 알림이란 무엇입니까?

실패가 아니라 부재에 반응하는 점검입니다. Tajo는 한 시간에 한 번 활성 동기화가 있는 모든 워크스페이스를 확인하고, 지난 24시간 동안 성공한 실행이 하나도 없으면 알림을 보냅니다. 실행이 아예 기록되지 않은 경우도 포함됩니다. 알림은 장애마다 한 번 발생하고, 첫 번째 실행이 성공하면 다시 활성화됩니다. 실패 알림은 기록된 실패만 볼 수 있기 때문에 이 점검이 존재합니다.

Tajo 사전 이용 신청

이름과 이메일 주소를 입력하세요. Tajo 이용 신청을 확인할 수 있는 링크를 이메일로 보내드립니다.

Brevo 받기

이 페이지의 Markdown: worker-died-sync-didnt.md

Markdown 불러오는 중…
Claude에서 열기 ChatGPT에서 열기