작성일
Gem3Park 개발기 4 — SIP 등록 장애, watchdog, 그리고 최후검증
프로세스는 살아 있는데 전화가 바로 끊기던 5월의 SIP 등록 장애와 그 뒤 만든 3단계 watchdog, 그리고 통화 녹음을 근거로 상담원 전환 직전에 방문 목적을 다시 판정하는 최후검증을 정리했다.
Gem3Park 개발기 시리즈 4/8편. 전체 목록은 gem3park 태그에서 볼 수 있다.
모든 것이 떠 있는데 전화가 끊긴다
2026년 5월 4일, 인입 전화가 연결되자마자 끊긴다는 걸 알았다. 확인해 보니 겉으로는 전부 정상이었다.
systemctl로 보면 Asterisk는 active다.- PM2로 보면 릴레이와 모니터가 모두 online이다.
- AudioSocket 포트도 열려 있다.
그런데 외부 SIP 서버에 거는 아웃바운드 등록 두 개가 Rejected 상태였다. Asterisk 로그에는 이런 문장이 있었다.
Fatal response '401' received from '***'
on registration attempt to '***', stopping outbound registration
원인은 설정 기본값과 일시적 인증 실패가 겹친 것이었다. 등록 블록의 런타임 설정이 아래와 같았다.
auth_rejection_permanent : true
fatal_retry_interval : 0
forbidden_retry_interval : 0
retry_interval : 30
401 한 번을 영구 거절로 보고, 치명 응답 뒤 재시도 간격도 0이었다. 그래서 일시적인 인증 실패 한 번이 등록을 Rejected에 붙잡아 두었다.
복구 자체는 쉬웠다. dialplan과 PJSIP를 reload하자 두 등록이 모두 Registered로 돌아왔다. 자격 증명이 영구적으로 틀렸다면 reload로 바로 살아날 수 없으므로 일시적 인증 실패로 판단했다. 영구 설정으로 auth_rejection_permanent=no와 두 재시도 간격 30초를 넣고, 증상·확인 명령·복구 순서를 runbook으로 정리했다.
그때까지 있던 watchdog은 4월 24일에 만든 cron 스크립트(1분마다)였고, Asterisk 프로세스와 CLI 응답만 확인했다. 이번 장애는 그 점검을 모두 통과했다.
모델 쿼터가 바닥나면
6월 5일에는 다른 종류의 장애에 대비했다. Gemini가 RESOURCE_EXHAUSTED나 429를 돌려주면 guided 흐름에서 에러만 올리고 통화는 멈췄다. 이제는 "시스템 사용량 초과로 상담원에게 연결해 드리겠습니다"라고 안내하고 apt_agent_redirect를 보낸다. 외부 모델이 죽어도 방문 차량이 인터폰 앞에 갇히지 않게 하는 것이 기준이었다.
export function isGeminiResourceExhaustedError(error: Error): boolean {
const message = error.message.toLowerCase();
return message.includes("resource_exhausted")
|| message.includes('"code":429')
|| message.includes("code: 429");
}
3단계 watchdog
6월 12일에 watchdog을 고쳐 systemd timer로 옮겼다. 점검 단계는 셋이다.
- Asterisk 프로세스가 active인가
- Asterisk CLI가 응답하는가 (
core show version) - PJSIP 등록이 정상인가
등록이 비정상이면 전체를 재시작하기 전에 pjsip reload를 먼저 한다. 그래도 안 풀릴 때만 Asterisk를 재시작하고, 재시작 시도는 최대 3회로 제한했다. timer는 부팅 40초 뒤에 시작해 40초마다 돈다.
[Timer]
OnBootSec=40s
OnUnitActiveSec=40s
AccuracySec=1s
Unit=gem3park-asterisk-watchdog.service
스크립트에는 MAX_RESTART_ATTEMPTS=3, 재시작 사이 RESTART_COOLDOWN_SEC=20, reload 뒤 대기 PJSIP_RELOAD_WAIT_SEC=30이 상수로 있다. 비정상 판정은 등록 상태 문자열(Rejected, Unregistered, No response, Maximum retries)을 파싱해서 한다. 이 파서는 따로 테스트하는 스크립트를 두었다. 판정이 틀려 정상 등록을 비정상으로 읽으면 40초마다 reload와 재시작이 돌면서 받고 있던 통화를 끊기 때문이다.
녹음을 남기고, 마지막에 다시 듣는다
6월 12일에는 디버깅용 입력 음성 녹음을 넣었다. SAVE_CALL_INPUT_AUDIO=true일 때 STT로 보내는 16kHz PCM을 통화 폴더에 WAV로 저장하고, 통화 종료 시 헤더에 실제 길이를 써 넣는다. 통화 요약 JSON에는 녹음 경로와 바이트 수, 샘플레이트, 녹음 오류를 같이 기록해서 사후에 로그와 음성을 맞춰 볼 수 있다.
이 녹음에 다른 용도가 생겼다. 상담원 전환 직전에도 방문 목적이 정해지지 않은 통화는, 전사문에서 목적을 못 찾았을 뿐 녹음에는 답이 있을 수 있다. 그래서 전환 전에 오디오 모델에 목적 분류를 한 번 더 맡기는 최후검증을 넣었다(6월 12일). 처음에는 최근 12초를 담는 버퍼를 썼다. 6월 15일에 통화 전체 녹음을 우선 쓰도록 바꿨고, 통화 중에도 그때까지의 녹음을 온전한 WAV로 떼어 낼 수 있게 레코더에 기능을 더했다. 전체 통화는 길어서 오디오 판독 timeout도 WAV 길이에 따라 10초에서 30초 사이로 계산한다. 녹음이 꺼져 있으면 12초 버퍼로 물러나고, 어느 쪽이든 마지막에도 unknown이면 상담원이 받는다.
같은 6월 15일에는 동·호수만 빠진 통화에서 모델을 부르지 않고 코드가 바로 "몇 동 몇 호 배달이세요?"라고 묻게 했다. 단순한 질문에 모델을 쓸 이유가 없었고, 지연을 줄이면서 Gemini 503의 영향도 받지 않는다.