‘폰 서버 안정화 일지’ 네 번째 글이다. 워치독 로그를 집계해 보니 블로그 터널이 5일 동안 17번 재시작돼 있었다. 터널은 멀쩡했다. 재시작한 건 워치독이었다.
집계부터
워치독 로그에서 blogtunnel 재시작 줄만 뽑아 보니 10월 7일부터 11일까지 17건이었다. 같은 시각에 Caddy나 관리 서버가 재시작된 기록은 한 건도 없었다. 터널만 죽었다는 뜻이거나, 터널만 죽은 것처럼 보였다는 뜻이다. 바깥에서 블로그에 요청을 보내면 0.2~0.6초에 응답하고 있었으니 후자가 유력했다.
재시작 시각의 규칙성, cloudflared 로그의 직전 오류, tmux 세션 재생성 흔적, 폰의 절전·네트워크 전환 단서를 차례로 확인했다. cloudflared 로그의 오류는 재시작 순간에만 찍혀 있었고 직전까지는 정상 연결이었다. 폰 쪽 단서는 su가 출력을 주지 않아 읽지 못했다. 남는 가설은 하나였다. 점검 자체가 경계에 걸린다.
재현: 8.000초
워치독의 블로그 터널 점검은 공용 헬퍼 check_http를 썼다. curl --max-time 8로 공개 주소를 호출하고 응답 코드가 502, 503, 504, 530, 000이면 비정상으로 본다. 똑같은 명령을 세 번 돌려 봤다.
http_code=200 time_total=8.000 exit=28
http_code=200 time_total=8.001 exit=28
http_code=200 time_total=8.000 exit=28
응답은 200인데 걸린 시간이 정확히 8초, curl 종료 코드는 28(타임아웃)이었다. 이 폰에서 공개 주소를 거쳐 돌아오는 응답은 평소 6~9초 사이에 분포했다(이유는 다음 글에서 밝혀진다. DNS였다). 8초 한도는 그 분포의 가운데를 자르는 선이었고, 느리기만 한 순간마다 “죽음"으로 판정돼 재시작을 당한 것이다. 재시작 한 번에 터널은 3~5초 끊긴다. 워치독이 없던 불가용을 하루 세 번 만들고 있었다.
수정: 한도 20초, 연속 3회
바꾼 건 두 가지다. 타임아웃을 8초에서 20초로 늘렸고, 연속 3번 실패해야 재시작하도록 했다. 1~2번 실패는 로그만 남긴다. 실패 횟수는 /root/.blogtunnel-fail-count 파일에 기록하고 정상 응답이 한 번 나오면 0으로 되돌린다. 5분 간격 점검이니 약 15분 연속으로 실패해야 재시작이 일어난다.
공용 check_http 함수는 건드리지 않았다. 이 함수에 의존하는 점검이 Caddy, 관리 서버, 메인 터널, 기록기, 시세 API, 게임 서버 등 여섯 개 넘게 있어서, 한 곳의 임계값을 바꾸려다 전부 바뀌는 걸 피하려고 블로그 터널 점검만 전용 인라인 블록으로 다시 썼다. 구조는 이렇다.
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$BLOG_URL")
case "$code" in
502|503|504|530|000)
n=$(( $(cat "$F" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$F"
if [ "$n" -ge 3 ]; then restart_pane blogtunnel "$CMD"; echo 0 > "$F"; fi ;;
*) echo 0 > "$F" ;;
esac
수정 전 cp -p watchdog.sh watchdog.sh.bak-20261011로 백업했고, bash -n으로 문법을 확인한 뒤 실제로 한 번 돌려 종료 코드 0과 “강제 재시작 없음"을 확인했다. 백업과의 diff로 바뀐 줄이 블로그 터널 블록에만 있는 것도 대조했다.
결과
수정 뒤 블로그 터널 재시작은 0회가 됐다. 로컬 점검으로 바꾸는 선택지도 검토했지만, 터널 프로세스는 로컬에 열린 포트가 없어 그때는 불가능했다. cloudflared의 metrics 엔드포인트를 쓰는 방법은 메모만 남겼는데, 이게 나중에 시리즈 여섯 번째 글의 7시간 장애로 이어진다.
정리
- 감시자의 임계값은 정상 응답 시간의 분포를 먼저 재고, 그 꼬리 바깥에 둔다. 분포 가운데를 자르는 한도는 오탐 생성기다.
- 재시작은 비용이 큰 행동이다. 연속 실패 조건(히스테리시스)을 두면 일시적인 느림과 진짜 죽음을 가를 수 있다.
- 공용 함수의 임계값을 한 서비스 때문에 바꾸지 않는다. 그 서비스만 전용 블록으로 뺀다.
🤖 이 포스팅은 AI의 도움을 받아 작성되었습니다.


💬 댓글