재시작 폭풍을 유발하지 않고 멈춘 워커 자동 재시작하기
저희 모니터는 2분 만에 멈춘 GPU 워커를 감지했고, 그 후 6시간 동안 작동 불능 상태로 있는 것을 지켜봤습니다. 탐지와 복구 사이의 간극을 안전하게 메웁니다.
GPU 플릿의 워커 두 대가 같은 날 저녁 6분 간격으로 멈췄습니다. 모니터링 스택은 2분 안에 이를 감지하고 서버 이름, 판정, 그리고 해당 서버를 중단시킨 작업 등 상세한 경고를 게시했습니다. 그 후 6시간 동안 아무 일도 없다가, 사람이 일어나서 docker restart를 입력했습니다.
경고는 완벽했습니다. 그럼에도 불구하고 중단 시간은 길었습니다. 이 글은 이 두 가지 사실 사이의 간극, 즉 탐지 시스템이 종종 눈은 있지만 손은 없는 이유와, 더 심각한 문제, 즉 정상적인 머신을 중단시키는 재시작 폭풍(restart storm)을 일으키지 않으면서 탐지 경로에 재시작을 연결하는 데 필요한 것이 무엇인지에 대해 다룹니다.
탐지와 교정은 따로 성장한다
아무도 이 간극을 의도적으로 설계하지 않습니다. 간극은 점차 쌓입니다.
우리 시스템에는 서로 다른 인시던트를 위해 몇 달의 간격을 두고 만들어진 두 개의 독립적인 안전 메커니즘이 있었습니다. 첫 번째는 행(hang) 탐지기였습니다. 워커가 processing 상태를 보고하지만 하트비트가 120초 이상 오래되면, 해당 워커를 응답 없음으로 선언하고 채널에 호출을 보냅니다. 두 번째는 자동 재시작기였습니다. 워커가 명시적인 error 상태에 30분 동안 머무르면, 로그를 수집하여 게시하고 SSH를 통해 컨테이너를 재시작합니다. 이때 서버당 재시작 횟수를 하루 세 번으로 제한하는 일일 서킷 브레이커가 적용됩니다.
각각은 설계된 대로 작동했습니다. 함정은 둘의 결합 지점에 있었습니다. 재시작기는 탐지기와 다른 조건자를 기준으로 작동했습니다. 재시작에는 state == "error" 조건이 필요했습니다. 우리가 실제로 겪은 행은 작업 중간에 프로세스를 멈추게 한 힙 손상이었습니다. 그래서 상태 필드는 영원히 processing으로 남아 있었고 하트비트는 끊겼습니다. 탐지기는 작동했습니다. 재시작기의 조건은 결코 참이 되지 않았습니다. 설상가상으로, 재시작기에는 양보 규칙이 있었습니다. 행 탐지기가 행을 확인하면, 물러나서 탐지기가 처리하도록 하는 것입니다. 그것이 처리를 미룬 컴포넌트는 아무런 처리 능력이 없었습니다.
교정이 탐지와 다른 조건자를 기준으로 작동할 때, 둘 중 하나만 인식하는 모든 실패 모드는 후속 조치 없는 알림이 됩니다. 이 둘을 하나의 단위로 감사하십시오. 탐지기가 내릴 수 있는 각 판정에 대해, 그에 따라 행동하는 컴포넌트가 무엇인지 명시하십시오. '결국엔 사람이'라는 답이 나오는 모든 판정은 당신이 선택한 간극이며, 적어도 문서화된 선택이어야 합니다.
재시작은 기본적으로 안전한 조치가 아닙니다
해결책은 사소하게 들립니다. hang 경로에서 재시작 함수를 호출하는 것이죠. 한 줄짜리 패치 대신 설계 검토가 필요했던 이유는 자동화된 재시작이 장전된 총과 같기 때문입니다. 잘못된 재시작 정책의 실패 모드는 한 대의 워커가 다운되는 것이 아니라, 자동화가 머신이 부팅되는 속도보다 더 빨리 머신을 중단시켜 전체 시스템에 대란이 발생하는 것입니다.
이를 출시 가능하게 만든 안전 장치는 실행되는 순서대로 다음과 같습니다.
조치 전 지속성 확인. 확인된 hang 상태는 재시작이 실행되기 전에 고정된 지연 시간(저희는 3분으로 선택) 동안 지속되어야 합니다. 탐지에는 이미 두 번의 연속적인 비정상 관측이 필요했으며, 이 지연 시간은 GC 일시 중지, 일시적인 부하 급증, 모니터링 오류에 대한 허용 범위를 추가합니다. 타이머는 아래에서 다룰 이유로 프로세스 메모리가 아닌 SETNX를 통해 Redis에 저장됩니다.
서버별 실행 잠금. 두 개의 백엔드 인스턴스가 동일한 모니터링 루프를 실행합니다. 둘 다 거의 동시에 같은 결론에 도달할 것입니다. 짧은 TTL을 가진 SETNX 잠금은 둘 중 정확히 하나만 재시작을 실행하도록 보장하며, 실패한 쪽은 그냥 건너뜁니다. 알림 중복 제거와 조치 직렬화는 서로 다른 문제이므로, 키를 분리하여 사용합니다. 중복 제거 키는 인시던트당 중복 알림을 억제하고, 잠금은 서버당 중복 명령을 억제합니다.
마지막 순간의 재검증. 결정과 SSH 명령어 사이에는 로그 수집 및 알림 게시가 있으며, 최대 1분의 지연 시간이 발생할 수 있습니다. 그 시간 동안 워커가 스스로 복구되었을 수 있습니다. 실행 직전에 상태를 다시 읽고, 하트비트가 최신이면 취소하고 정리합니다. 1분 전에 아팠다는 이유로 건강한 머신을 재시작하는 것은 자동화가 절대로 해서는 안 되는 종류의 해악입니다.
공유 서킷 브레이커. 모든 재시작 경로는 서버당 하루에 하나의 카운터를 증가시키며, 세 번을 초과하면 자동화는 중지되고 알림만 보냅니다. 중요한 점은 카운터가 파이프라인이 시작될 때가 아니라, 명령이 실제로 실행되기 직전에 증가한다는 것입니다. 초기 버전에서는 모든 시도를 계산했기 때문에, 로그 수집에서 실패한 실행도 예산을 소모했으며, 노이즈가 많은 탐지기는 단 한 번의 재시작도 없이 허용량을 모두 소진할 수 있었습니다.
처방이 실패했을 때의 에스컬레이션. hang을 해결하지 못하는 재시작으로 이야기가 끝나서는 안 됩니다. 우리는 재시작 시간을 기록합니다. 10분 후에도 동일한 서버가 여전히 hang 상태이면, 멘션과 함께 "재시작으로 복구되지 않음"이라는 일회성 페이지(알림)를 보냅니다. 이것이 없으면 시스템은 "재시작 트리거됨"을 보내고 침묵하며, 이를 본 사람은 문제가 처리되었다고 가정합니다. 조치 후 침묵하는 그 상태가 바로 최초의 6시간 중단 사태가 발생한 방식이며, 자동화는 이를 완벽하게 재현할 수 있습니다.
방금 수행한 재시작이 중단된 것처럼 보일 것입니다
검토 중에 발견한 가장 미묘한 장애는 거의 동전 던지기 확률로 발생하는 자가 유발 장애였습니다. 바로 이중 재시작입니다.
docker restart 후 컨테이너는 1분 이상 부팅하고 모델을 로드합니다. 그 시간 동안 이전 상태 레코드는 오래된 하트비트와 함께 processing이라고 표시된 채 Redis에 여전히 남아 있습니다. 종료 중인 프로세스가 이를 업데이트하지 못했기 때문입니다. 복구 루프에게는 이것이 새로운 중단과 구별할 수 없습니다. 복구 루프는 타이머를 다시 활성화하고, 지연 시간만큼 기다린 다음, 부팅 중에 머신을 다시 재시작합니다. 각 라운드는 서킷 브레이커 예산을 소모하므로, 한 번의 실제 중단이 부팅 중인 컨테이너를 반복적으로 종료시키면서 일일 허용량을 모두 소진할 수 있으며, 그러면 자동화는 바로 필요할 때 포기하게 됩니다.
해결책은 쿨다운 키입니다. 재시작 후 고정된 시간(저희는 10분을 사용합니다) 동안 재활성화를 억제하고 쿨다운이 존재할 때마다 지속성 타이머를 지웁니다. 일반적인 규칙은 다음과 같습니다. 일시적으로 시스템이 고장 난 것처럼 보이게 하는 모든 자동화된 조치는 탐지기에게 "이것은 나이며, 새로운 사고가 아니다"라고 알리는 마커를 남겨야 합니다.
같은 검토에서 나온 인접한 두 가지 함정을 언급할 가치가 있습니다.
프로세스 메모리가 아닌 공유 스토리지에서 사고 지속 시간을 측정하세요. 첫 번째 초안에서는 메모리 내 구조체에서 중단 시작 시간을 읽었습니다. 해당 필드는 재확인 주기마다 새로 고쳐졌기 때문에 측정된 지속 시간은 영원히 0에 가까웠고 재시작 임계값에 도달할 수 없었습니다. 작동하는 설계는 SETNX를 사용하여 Redis에 시작 시간을 고정합니다. 첫 번째 관찰자가 이기고, 모든 인스턴스가 동일한 시계를 보며, 다중 인스턴스 배포는 서로의 타이머를 재설정할 수 없습니다.
로컬 결론이 아닌 데이터에서 트리거 조건을 도출하세요. 두 개의 모니터 인스턴스가 있는 경우, 각 인스턴스는 "확인됨"에 대한 자체적인 시각을 가집니다. 만약 인스턴스 A가 공유 타이머를 활성화하고, 한 관찰 단계 뒤처진 인스턴스 B가 여전히 서버가 정상이라고 간주하면, B는 A의 타이머를 지울 것이고 둘은 무한정 싸우게 될 것입니다. 공유 상태 레코드에서 직접 술어(predicate)를 도출하면 모든 인스턴스가 동일한 답을 계산하게 됩니다.
일부 장애는 재시작을 트리거해서는 안 됩니다
하나의 판정을 복구 조치에 연결하는 것이 모든 판정을 연결해야 한다는 의미는 아닙니다. 저희는 의도적으로 두 가지는 그대로 두었습니다.
상태 레코드가 없다는 것은 에이전트에 연결할 수 없음을 의미합니다. 서버가 재부팅 중이거나, 네트워크 분할 상태이거나, 의도적으로 전원이 꺼져 있을 수 있으며, 현재 저희 머신 중 하나는 비용 문제로 꺼져 있습니다. 알 수 없는 전원 상태의 머신을 SSH로 재시작하는 것은 무용지물이거나 해롭기까지 하므로, 해당 판정은 경고만 발생시킵니다. 그리고 하트비트는 정상이지만 진행률 마커가 멈춘 작업도 마찬가지로 재시작되지 않습니다. 왜냐하면 장기 실행 작업이 원래 그렇게 보일 수 있고, 느린 태스크 때문에 살아있는 워커를 종료하는 것은 거짓 양성(false positive)으로 인해 실제 손상을 초래하기 때문입니다.
이로부터 도출된 경계 규칙은 다음과 같습니다. 조치가 거의 멱등성을 가지며(idempotent-ish), 폭발 반경(blast radius)이 이미 죽은 워커 하나에 국한되고, 잘못된 추측의 대가가 저렴한 장애 모드에 대해서만 복구 조치를 자동화합니다. 그 외 모든 것은 신속하고 명확하게 사람에게 에스컬레이션됩니다.
자주 묻는 질문
컨테이너 상태 확인(health check)을 사용하고 오케스트레이터가 재시작하도록 하면 안 되나요? 적절한 liveness probe가 있는 Kubernetes를 사용 중이라면, 먼저 그 방법을 사용하세요. 이 패턴은 프로세스가 여전히 TCP 연결을 수락하여 대부분의 포트 수준 상태 확인을 통과하지만, "중단(hang)" 상태가 애플리케이션 수준(Redis의 하트비트, 작업 진행률)에서만 보일 때 적용됩니다. 저희의 좀비 프로세스는 6시간 동안 연결에 응답했습니다.
3분 지연은 너무 느리지 않나요? 실제 타임라인을 모두 더해 보세요: 비활성(stale) 임계값, 두 번의 확인 주기, 지연, 그리고 재시작 및 모델 로딩. 저희의 경우 장애 발생부터 복구까지 약 10분이 걸리는데, 사람이 개입할 때는 6시간이 걸렸습니다. 지연 시간을 서비스 부팅 시간보다 짧게 줄이는 것은 얻는 이점은 거의 없으면서 거짓 양성(false-positive) 비용을 높입니다.
재시작 경로마다 서킷 브레이커를 두지 않고 왜 하나를 공유하나요? 서킷 브레이커는 물리적 머신을 보호하며, 머신은 어떤 코드 경로가 자신을 재시작했는지 신경 쓰지 않습니다. 예산을 분리하면 최악의 경우가 배가됩니다. 만약 분리한다면, 그 합계에 상한을 두세요.
재시작 명령 자체가 실패하면 어떻게 하나요? 로그 한 줄로 처리하지 말고, 일급 결과(first-class outcome)로 다루세요. 다음 주기에 재시도할 수 있도록 중복 제거 키(dedup key)를 해제하고, 즉시 사람에게 호출(page)하세요. 원격으로 재시작조차 할 수 없는 머신은 자동화가 결정할 수 있는 범위를 넘어선 것이기 때문입니다.
관련 글
효과적인 AI 페어 프로그래밍을 위한 결정적 파이프라인
AI 코더는 강력하지만 게이트가 없으면 방향을 잃습니다. 상태 파일과 계획 동결 기능을 갖추고 이를 감싸는 계획, 빌드, 검증, 배포 파이프라인이 있습니다.
404를 포함한 모든 것을 통과시킨 안전 게이트
한 콘텐츠 필터가 몇 달 동안 깨끗하다고 보고했습니다. 그것은 아무것도 읽고 있지 않았습니다. 검사가 조용히 승인으로 뒤바뀌는 네 가지 방법과 실패를 명확하게 만드는 방법.
파괴적 읽기가 파싱 실패를 영구적인 행으로 전환
워커는 `GETDEL`로 작업 결과를 읽었기 때문에, 파싱할 수 없는 페이로드는 영원히 사라졌습니다. 이를 "아직 준비되지 않음"으로 처리하는 것은 영원히 기다리는 것을 의미했습니다.
당신의 에이전트가 당신 없이 게시하는 텍스트를 위한 프라이버시 게이트
스스로 게시물을 작성하고 게시하는 에이전트는 설득으로 차단 기능을 해제할 수 없는 게이트가 필요합니다. 두 개의 레이어가 있는데, 하나는 로컬 및 결정적 레이어이고 다른 하나는 격리된 레이어입니다.
AI 에이전트는 Sudo를 실행할 수 없으며, 그것이 바로 여러분을 위한 경계선입니다
AI 에이전트가 프로덕션 서버를 정리하도록 하는 것은 `sudo`를 실행할 수 없다는 것을 알아차리기 전까지는 위험하게 들립니다. 권한 경계는 작업을 에이전트에게 안전한 작업과 사람만 할 수 있는 작업으로 저절로 나눕니다.
Redis 키스페이스 알림 및 리스를 사용한 GPU 워커 풀 로드 밸런싱
GPU는 비싸고 한 번에 하나의 무거운 작업만 실행하므로, 라운드 로빈 라우팅은 바쁜 워커 뒤에서 지연됩니다. 여기에 Redis 리스와 키스페이스 알림으로 구축된 바쁨 인지 스케줄러가 있습니다.