이 내용은 minia2a가 게시된 시점의 정보를 반영하며 기록을 위해 보관됩니다. 현재 모델: Base에서 USDC로만 호출당 비용 지불 방식이며, 서명된 지갑당 5회 무료 체험 호출이 제공되고 크레딧이나 충전 기능은 없습니다.
2026-10-27 · 엔지니어링 노트
배포 문제처럼 보이지만 실제로는 읽기 경로(read-path) 문제인 버그의 한 종류가 있습니다. 형태는 항상 같습니다. 서비스가 실행되는 동안 디스크의 파일이 변경되고, 서비스는 부팅 시 메모리에 그 복사본을 유지합니다. 그러면 두 표면이 불일치하게 됩니다. 즉, 가져오는 정적 아티팩트(static artefact)는 새롭지만, 쿼리하는 인메모리 뷰는 오래된 상태가 되며, 둘 다 잘 구성된 답변이기 때문에 아무도 오류를 감지하지 못합니다.
유혹적인 해결책은 크론 작업으로 다섯 줄을 작성하는 것입니다. 파일의 체크섬(checksum)을 감시하고, 변경될 때 프로세스를 재시작하는 방식입니다. 이것은 작동합니다. 하지만 이는 잘못된 해결책이며, 그 이유를 설명할 가치가 있습니다. 이유는 단순히 "재시작이 무섭다"는 문제가 아니기 때문입니다.
재시작이 실제로 하는 일
재시작은 읽기가 아닙니다. 그것은 폭발 반경(blast radius)을 가진 일련의 사건들입니다:
- 이미 청구된 유료 요청을 포함하여 진행 중인 요청(in-flight requests)들이 드롭됩니다.
- 처리 중이던 상태가 버려집니다 — 속도 제한 창(rate windows), 캐시, 따뜻한 연결(warm connections) 등이 사라집니다;
- 누구도 선택하지 않은 벽시계 시간(wall-clock time)에 발생하며, 대신 파일의 mtime(수정 시간)에 의해 결정됩니다;
- 그리고 정확성 버그(stale data, 오래된 데이터)를 가용성 버그(service가 단순히 존재하지 않는 시간 창)로 변환합니다.
만약 재시작이 중간에 실패한다면 — 잘못된 파일, 이미 바인딩된 포트, N번의 시도 후 포기하는 슈퍼바이저 등 — 프로세스는 사람이 알아차릴 때까지 다운된 상태로 머무를 수 있습니다. 트리거는 데이터 새로고침(data refresh)이었지만, 결과는 서비스 중단(outage)이었습니다. 이 두 가지 사실은 크론 항목에 의해 결코 연결되어서는 안 됩니다.
파일 변경으로 인해 트리거된 재시작은 아무도 승인하지 않은 행동이며, 아무도 선택하지 않은 시간에 이루어지며, 애초에 가용성 문제가 아니었던 문제를 해결하기 위해 수행됩니다.
프로세스는 자신의 입력값을 검사할 수 있다
프로세스가 알아야 하는 것이 있다면, 프로세스에게 확인하도록 하십시오. 확인하는 비용은 사람들이 생각하는 것보다 적습니다. 단, 그에 상한선을 설정한다면 말입니다:
// cache는 바이트와 해당 스탬프를 모두 보유합니다
if 마지막 stat 이후 충분한 시간이 지났다면: // 예: 5초, 모든 호출자가 공유
if mtime 또는 size가 변경되었다면:
...
이 과정을 안전하게 만드는 세 가지 설계 포인트가 있으며, 이를 건너뛰면 각각 버그가 됩니다:
- 락(lock) 외부에서 스탯(stat)을 사용하고, 제한 장치(throttle)를 적용합니다. 공유 카운터가 누가
stat 비용을 지불할지 결정하며, 나머지 모든 호출자는 현재 스냅샷을 읽고 넘어갑니다. 이 제한 장치가 없다면, 가장 빈번하게 실행되는 경로(hottest path)에서 재시작당 파일 읽기 횟수를 요청당 시스템 콜(syscall)로 바꾼 것입니다.
- 불변(immutable) 스냅샷을 게시합니다. 리로드는 새 맵을 만들고 포인터를 교체할 뿐입니다. 읽기자가 가지고 있는 맵을 절대 수정하지 않습니다. 그러면 "서비스 중 리로드"는 읽기 락이 전혀 필요 없게 되며, 어떤 읽기자도 부분적으로 구축된 맵을 관찰할 수 없습니다.
- 실패한 읽기가 좋은 스냅샷을 덮어쓰면 안 됩니다. 파일을 잘라내고(truncate) 다시 쓰는 과정은 파일이 절반만 작성되어 쓰레기 데이터처럼 파싱되는 순간이 있다는 것을 의미합니다. 이 순간이 당신의 좋은 메모리 내 복사본을 빈 것으로 대체하도록 허용한다면, 데이터 새로 고침 작업이 서비스되던 표면을 단순히 공백으로 만들게 됩니다. 이전 스냅샷을 유지하고, 실패했던 타임스탬프(stamp)를 기억하며, 파일이 다시 변경될 때만 재시도해야 합니다.
이 세 번째 포인트가 단순히 "리로드(reload)"와 "대부분 작동하는 리로드(reload that mostly works)"를 구분 짓는 지점입니다. 잘라내기(truncate)와 플러시(flush) 사이의 간격은 순진한 감지기(naive watcher)가 작동하는 정확한 순간입니다.
솔직히 비용이 얼마나 드는가
| 케이스 | 비용 |
|---|
| 변경 없음 | 윈도우당 한 번의 stat, 공유; 파일 읽기는 제로 |
| ... | |
윈도우 크기는 측정값이 아니라 선택입니다. 호출자가 변경 사항을 즉시 봐야 한다면, 이를 줄이십시오. 비용은 트래픽에 비례하는 것이 아니라 스탯(stat) 개수에 비례하는데, 이는 제한 장치(throttle)가 공유되기 때문입니다. 해서는 안 될 것은 윈도우를 아예 빼고 "안전하다"는 이유로 요청당 stat을 실행하는 것입니다.
이 방법이 무엇을 고치지 못하는지에 대해서는 솔직해야 합니다. 손상된 파일을 대상으로 하는 콜드 스타트(cold start) 역시 되돌아갈 만한 좋은 것이 없습니다: 첫 번째 로드는 이전 스냅샷이 없기 때문에, 파일에 적힌 내용("empty"를 포함하여)을 그대로 가져옵니다. 이 경우는 리로드 문제가 아니라 배포 시점의 유효성 검사(deploy-time validation) 문제입니다. 프로세스가 시작하도록 허용되기 전에 포착되어야 합니다.
규칙 (The rule)
만약 여러분의 수정 사항이 효과를 보기 위해 프로덕션을 중단해야 한다면, 그것은 배포 단계(deployment step)를 찾지 못한 것입니다. 누락된 읽기 경로(missing read path)를 발견한 것입니다. 장기간 실행되는 프로세스 아래에서 변경되는 데이터는 관찰될 방법이 필요합니다—경계가 정해지고(bounded), 불변하며(immutable), 부분적으로 작성된 파일에 대해 관용적일 수 있는 방식이어야 합니다. 그리고 그 방식은 외부에서 접근하여 종료시키는 스케줄러(scheduler) 안에 있어서는 안 되며, 프로세스 내부에 존재해야 합니다.
cron은 서비스를 재시작할 수는 있습니다. 하지만 올바르게 만들 수는 없습니다. 서비스 스스로 확인해야 합니다.
minia2a는 에이전트를 위한 호출당 비용(pay-per-call) API 마켓플레이스입니다: x402 챌린지, Base의 USDC, 계정 없음. 문서에는 첫 번째 402가 발생하기 전에 각 지갑이 받는 다섯 번의 서명된 체험 호출을 포함하여 결제 경로가 설명되어 있습니다.
원래 발행일: minia2a.uk.