먼저 읽는 30초 요약
이 글에서 가져갈 것
- 작업 큐에서 가장 먼저 해결해야 할 문제는 속도가 아니라 한 작업의 현재 소유자를 한 명으로 설명하는 일입니다.
- 후보 선택·임대 생성·running 전환을 하나의 BEGIN IMMEDIATE 트랜잭션으로 묶어 중복 소유 위험을 줄였습니다.
- 현재 구현은 단일 조정자용 스타터이며, 대형 산출물 저장·복수 조정자·실제 모델 런타임은 다음 범위입니다.
처음 질문은 ‘지금 누가 이 작업을 갖고 있는가’였습니다
처음에는 각 워커가 공용 대기 목록에서 작업을 하나 읽고 처리하면 충분해 보였습니다. 하지만 작업을 읽는 시점과 상태를 바꾸는 시점이 떨어져 있으면 두 워커가 같은 waiting 행을 거의 동시에 선택할 수 있습니다. 처리 중이던 장비가 꺼졌을 때 running 상태가 그대로 남는 문제도 같은 뿌리에서 시작합니다. 상태가 단순한 표시라면 다음 워커는 기다려야 하는지, 다시 실행해도 되는지 판단할 수 없습니다.
이 문제를 실제 전체 장비망에서 이미 여러 번 발생한 장애처럼 쓰지는 않았습니다. 대신 스타터 조정기가 반드시 재현하고 막아야 할 실패 조건으로 정의했습니다. 작업마다 job_id, 우선순위, 요구 역할, 현재 상태를 두고, 별도의 lease에 소유 워커와 만료 시각을 저장했습니다. 그러면 재시작 뒤에도 ‘누가 언제까지 맡았고 지금 다시 배정해도 되는가’를 DB 행으로 설명할 수 있습니다.
여기서 중요한 변화는 성공 로그보다 실패 뒤의 다음 행동이 명확해졌다는 점입니다. waiting이면 배정 후보, running이면서 유효한 lease가 있으면 현재 워커의 소유, lease가 만료됐으면 회수 후보가 됩니다. 아직 실제 모델 프로세스와 연결하지 않았기 때문에 계산 자체의 중복 가능성까지 제거했다고 말할 수는 없습니다. 이 단계에서 검증한 것은 작업 소유권을 저장하고 전이시키는 규칙입니다.
구현한 세 테이블과 확장 대상 하나
스타터 스키마는 workers, jobs, leases 세 테이블로 시작합니다. workers에는 worker_id, 마지막 heartbeat 원문과 수신 시각을 저장합니다. jobs에는 kind, required_role, priority, payload_json, status, created_at, updated_at과 완료 후 result_json이 들어갑니다. leases는 job_id를 기본키로 사용해 한 작업에 활성 임대가 하나만 존재하게 하고 worker_id와 expires_at을 보존합니다.
이 구분은 장애를 찾는 순서를 단순하게 만듭니다. 작업이 선택되지 않으면 workers의 heartbeat와 역할·자원 조건을 먼저 보고, running에서 멈췄다면 leases의 소유자와 만료 시각을 봅니다. 완료됐는데 결과가 없다면 jobs의 status와 result_json을 확인합니다. 모든 정보를 하나의 JSON 파일에 덮어쓰는 방식보다 상태 전이와 참조 관계를 쿼리로 확인할 수 있습니다.
artifacts는 일부 설계 문서에는 등장하지만 기본 스타터 스키마에는 없습니다. 현재는 승인된 결과 JSON 본문을 jobs.result_json에 직접 저장하며 해시 필드도 없습니다. 큰 결과를 파일이나 객체 저장소로 분리하고 DB에는 위치·해시·검증 단계만 남기는 방식은 확장 권고입니다. 이 경계를 표시하지 않으면 독자는 설계안과 현재 구현을 같은 완성 기능으로 오해할 수 있습니다.
| 대상 | 현재 저장 내용 | 현재 상태 |
|---|---|---|
| workers | heartbeat JSON·last_seen | 구현됨 |
| jobs | 입력·상태·result_json | 구현됨 |
| leases | 작업 소유자·expires_at | 구현됨 |
| artifacts | 해시·외부 위치·검증 단계 | 확장 설계 |
작업 선택과 임대 생성을 함께 처리합니다
가장 위험한 구간은 waiting 작업을 읽은 뒤 running으로 바꾸기 전입니다. 두 연결이 동시에 같은 후보를 읽을 수 있기 때문입니다. SELECT 결과를 애플리케이션 메모리에 가져온 다음 별도의 쓰기를 실행하는 구조에서는 각 명령이 정상 종료돼도 전체 작업 소유권은 중복될 수 있습니다. 개별 SQL 성공과 운영 규칙의 성공은 같은 뜻이 아닙니다.
스타터는 claim 시작과 함께 BEGIN IMMEDIATE를 실행합니다. 이 트랜잭션 안에서 최신 heartbeat를 확인하고, 우선순위와 생성 시각 순으로 waiting 후보를 읽고, 역할과 RAM·온도·전원·사용자 유휴 조건을 통과한 첫 작업을 고릅니다. 이어서 jobs를 running으로 바꾸고 leases 행을 만든 뒤 한 번에 commit합니다. 중간에 예외가 생기면 rollback해 반쪽짜리 소유권이 남지 않게 합니다.
BEGIN IMMEDIATE가 모든 규모의 동시성 문제를 해결하는 것은 아닙니다. SQLite는 쓰기 경쟁이 커지면 대기 시간이 늘고, 긴 트랜잭션은 다른 쓰기를 막습니다. 그래서 claim 안에서는 모델 호출이나 파일 처리를 하지 않고 상태 선택과 변경만 끝냅니다. 단일 조정자와 낮은 쓰기 빈도라는 현재 전제가 깨지면 트랜잭션 범위를 다시 측정하고 서버형 DB를 검토해야 합니다.
BEGIN IMMEDIATE;
SELECT job_id, required_role, priority
FROM jobs
WHERE status = 'waiting'
ORDER BY priority ASC, created_at ASC;
-- 적격 후보 하나를 선택한 뒤
-- leases 생성 + jobs.status='running'
COMMIT;빈 DB보다 백업본까지 열어 보는 것이 중요했습니다
프로그램이 새 DB 파일을 만들었다는 사실만으로 복구 준비가 끝난 것은 아닙니다. 스키마가 실제로 적용됐는지, 외래키 설정이 켜졌는지, 상태 명령이 빈 테이블을 정상적으로 읽는지부터 확인해야 합니다. 초기 상태의 status 응답은 workers 0, active_leases 0과 빈 작업 집계를 설명할 수 있어야 합니다.
다음 확인은 PRAGMA quick_check입니다. 운영 파일에서 quick_check가 통과해도 단순 파일 복사본이 일관된 시점의 백업이라는 보장은 없습니다. 그래서 SQLite의 .backup 명령으로 복사본을 만들고 그 파일을 별도로 열어 같은 검사를 반복합니다. 복구 자료는 ‘파일이 존재한다’가 아니라 ‘별도 연결에서 열리고 검사된다’까지 확인해야 의미가 있습니다.
이 절차는 자동시험 5건에 포함된 항목이 아니라 배포 전 수동 실습 순서입니다. 또한 quick_check가 애플리케이션 수준의 모든 관계나 결과 의미를 검증하지는 않습니다. 실제 운영에서는 백업 주기, 보관 위치, 권한, 복구 소요시간과 복구 뒤 첫 claim까지 별도의 훈련이 필요합니다. 샘플에서는 자동시험과 수동 운영 확인을 의도적으로 구분했습니다.
python3 coordinator.py --db demo.db status
sqlite3 demo.db 'PRAGMA quick_check;'
sqlite3 demo.db '.backup demo-backup.db'
sqlite3 demo-backup.db 'PRAGMA quick_check;'실패 증상마다 먼저 볼 곳을 정했습니다
SQLite를 선택했다는 사실보다 중요한 것은 실패 메시지를 다음 행동으로 연결하는 일입니다. database locked가 보이면 먼저 트랜잭션 안에서 오래 실행되는 작업이 있는지 확인합니다. 모델 호출이나 네트워크 요청이 트랜잭션 안에 들어가 있다면 잠금 시간을 키울 수 있습니다. 무조건 재시작하거나 timeout만 늘리면 원인을 가릴 수 있습니다.
중복 실행이 의심되면 jobs의 status만 보지 않고 leases의 job_id 유일성과 현재 소유자를 함께 확인합니다. 백업본이 열리지 않으면 원본 DB 손상으로 단정하기 전에 복사 방식과 복사 시점을 봅니다. DB 크기가 예상보다 빠르게 늘면 payload_json과 result_json에 대형 본문이 반복 저장되는지 확인합니다. 같은 ‘느림’이나 ‘오류’라도 첫 쿼리와 조치가 달라야 합니다.
현재 스타터에는 장기 운영을 위한 자동 vacuum 정책, DB 크기 경고, 잠금 대기 시간 통계나 관리자 화면이 없습니다. 아래 표는 완성된 자동 복구 기능이 아니라 사람이 진단을 시작하는 순서입니다. 운영 데이터가 쌓이면 각 증상의 발생 시각, 작업 ID, 트랜잭션 소요시간과 해결 결과를 남겨 정책을 조정해야 합니다.
| 관측 증상 | 첫 확인 | 현재 첫 조치 |
|---|---|---|
| database locked | 쓰기 트랜잭션의 길이 | 상태 변경 구간만 남기기 |
| 중복 소유 의심 | job_id별 활성 lease | 원자적 claim 경로 확인 |
| 백업 열기 실패 | .backup 사용 여부 | 새 백업 생성 후 재검사 |
| DB 급증 | payload_json·result_json 크기 | 외부 산출물 저장 검토 |
다섯 자동시험으로 확인한 범위
첫 시험은 같은 역할의 waiting 작업 중 P0 사용자 요청이 P3 idle_learning보다 먼저 선택되는지 확인합니다. 두 번째 시험은 memory_used_pct가 90인 워커가 85% 정책 한도를 넘어 신규 claim을 받지 못하는지 봅니다. 이 두 시험은 대기열 순서와 자원 게이트가 코드에 실제로 연결됐는지 확인합니다.
복구 시험은 시각을 고정해 모호함을 줄였습니다. t=102에 받은 45초 lease는 t=147에 만료됩니다. t=148에 이전 워커가 complete를 보내면 아직 reaper가 실행되지 않았더라도 만료 시각 검사에서 거부돼야 합니다. 이어서 reap_expired가 작업을 waiting으로 되돌리고, t=150에 다른 claim이 가능해야 합니다.
나머지 시험은 한 번 완료된 작업에 두 번째 결과가 들어왔을 때 거부되는지 확인합니다. 다섯 시험이 통과했다고 해서 실제 여러 PC의 네트워크 단절, 프로세스 강제 종료, 대형 결과 저장과 처리량이 검증된 것은 아닙니다. 이 결과는 큐·우선순위·RAM 게이트·임대 만료·늦은 완료 거부라는 스타터 규칙의 범위를 증명합니다.
- P0 작업 우선 선택
- RAM 90%에서 신규 claim 거부
- 45초 임대 만료 뒤 waiting 복귀
- 완료 뒤 중복 결과 거부
- reap 전에도 만료된 완료 거부
현재 구현이 멈추는 경계
현재 구현은 Python 표준 라이브러리와 SQLite를 사용하는 단일 조정자 스타터입니다. 워커 heartbeat를 받고 작업을 enqueue·claim·renew·complete·reap할 수 있지만 Ollama나 다른 모델 런타임을 직접 호출하지 않습니다. 실제 프로세스 시작, 취소, 체크포인트와 결과 파일 수집은 워커 어댑터가 담당해야 합니다.
결과는 jobs.result_json에 직접 들어갑니다. 따라서 대형 문서, 이미지, 임베딩 묶음처럼 크고 수명이 긴 산출물에는 적합하지 않습니다. artifacts 테이블, 내용 해시, 외부 저장 위치, 검증 단계와 보존 정책을 추가해야 합니다. DB 자체의 백업과 산출물 저장소의 백업도 같은 복구 시점으로 맞춰야 합니다.
마지막으로 실제 fleet 처리량과 장애 복구 시간은 공개할 만큼 반복 측정하지 않았습니다. 조정자를 여러 대 운영할 때의 합의, 쓰기 경쟁이 큰 환경의 성능, 네트워크 파티션과 보안 경계도 이 글이 증명하지 않습니다. 샘플의 상세 설명은 기능을 더 크게 보이게 하기 위한 것이 아니라, 독자가 검증된 범위와 다음 구현을 분리해 판단하도록 돕기 위한 것입니다.
자주 묻는 질문
Redis나 PostgreSQL이 더 낫지 않나요?
규모가 커지면 그럴 수 있지만 첫 운영 규칙을 재현하고 복구하기에는 SQLite의 단순성이 유리했습니다. 작업 큐에서 가장 먼저 해결해야 할 문제는 속도가 아니라 한 작업의 현재 소유자를 한 명으로 설명하는 일입니다. 실제로 적용할 때는 본문의 ‘처음 질문은 ‘지금 누가 이 작업을 갖고 있는가’였습니다’ 절차를 따라 한 조건씩 확인하고 결과를 기록하세요.
현재 모델 출력도 SQLite에 저장하나요?
네. 스타터는 result_json에 저장합니다. 큰 결과를 파일로 분리하고 DB에 위치·해시·상태만 남기는 방식은 다음 확장 권고입니다.
코드는 완제품인가요?
큐와 임대 규칙을 검증하는 스타터 구현이며 실제 런타임 호출은 어댑터로 연결해야 합니다. 현재 구현은 단일 조정자용 스타터이며, 대형 산출물 저장·복수 조정자·실제 모델 런타임은 다음 범위입니다. 실제로 적용할 때는 본문의 ‘작업 선택과 임대 생성을 함께 처리합니다’ 절차를 따라 한 조건씩 확인하고 결과를 기록하세요.
공식 출처
세부 동작과 최신 버전은 아래 원문을 함께 확인하세요.
SQLite Transactions SQLite BEGIN IMMEDIATE