
/health가 200을 돌려주고 컨테이너가 모두 healthy여도 배포는 깨져 있을 수 있습니다. 승인한 판 대신 옛 판이 떠 있을 수 있고, 컨테이너는 섰는데 채팅 턴(질문 하나를 받아 답을 내기까지의 한 번)마다 같은 예외가 날 수도 있습니다. DB를 재시작한 뒤 끊긴 연결을 커넥션 풀에 든 채로 둔 프로세스는, 그 연결을 꺼내 쓰는 첫 요청에서야 실패합니다. 헬스 체크는 프로세스가 살아 있는지만 묻기 때문에 이 셋을 모두 통과시킵니다.
OwlAI 구축형의 업데이트 실행기(updater)는 서비스를 교체한 뒤 기동 확인 단계에서 네 가지를 차례로 봅니다. 실행 중인 이미지의 판 라벨, 적용 전과 비교한 컨테이너 상태, 백엔드·워커 로그의 예외 표지, 적용을 시작한 뒤 실패한 턴과 잡입니다. 하나라도 걸리면 업데이트는 실패로 끝나고, 실행기는 「업데이트가 실패하면 스키마 변경에 따라 코드만 되돌리거나 덤프로 복원한다」에서 다룬 규칙대로 코드를 되돌리거나 멈춥니다. 이 글은 네 확인이 각각 무엇을 근거로 실패를 가르는지 설명합니다.
헬스 체크는 가볍게 두고, 배포 판정은 따로 한 번 돈다
백엔드의 /health는 프로세스가 돌고 있으면 언제나 ok를 돌려줍니다. 이 핸들러는 이벤트 루프에서 바로 응답합니다. 스레드 풀에서 돌게 하면 진행 중인 채팅 턴 뒤에 줄을 서다 시간을 넘기고, 멀쩡한 프로세스가 unhealthy로 찍히기 때문입니다. 준비성 프로브(readiness probe)인 /health/ready는 따로 있어서, 메타스토어(OwlAI 자체 PostgreSQL)에 SELECT 1을 보내 보고 실패하면 503을 돌려줍니다.
헬스 체크에 판을 묻고 DB와 큐까지 두드리게 하는 방법도 있습니다. 판정이 한곳에 모인다는 장점이 있습니다. 그런데 헬스 체크는 도커가 15초마다 부르는 확인이라, 무거워진 헬스 체크가 한 번 느려지면 멀쩡한 컨테이너가 unhealthy가 되고 순차 교체가 멈춥니다. 더 큰 문제는 헬스 체크가 지금 이 순간만 본다는 점입니다. 배포 뒤에 들어온 요청이 실제로 실패했는지는 프로세스 상태로 알 수 없습니다.
그래서 헬스 체크는 가볍게 두고, 배포 판정은 배포 직후 한 번 도는 기동 확인 단계로 뗐습니다. 이 단계의 기준 시각은 실행기가 적용을 시작한 시각입니다. 로그와 턴은 모두 이 시각 이후의 것만 봅니다.
떠 있는 판은 이미지 라벨로 맞춰 본다
백엔드 이미지에는 빌드할 때 릴리스 판을 OCI 표준 라벨 org.opencontainers.image.version으로 적어 둡니다. 실행기는 교체가 끝난 뒤 실행 중인 백엔드 컨테이너의 라벨을 읽어, 승인한 판과 같아야 통과시킵니다. 라벨이 없거나 다른 판이면 실패입니다. 웹과 워커는 같은 이미지를 쓰므로 백엔드 하나로 봅니다.
운영자도 같은 사실을 화면에서 확인합니다. 관리자 시스템 현황 화면의 「실행 중인 버전」 줄에 지금 도는 릴리스 판과 빌드 커밋 앞 12자가 보입니다. 태그 없이 빌드한 이미지는 판 대신 「태그 없는 빌드」로 나옵니다. 업데이트 화면의 현재 버전도 이런 빌드를 「개발 빌드 · 커밋 …」으로 보여 주고, 새 판과 비교하지 않습니다. 운영 설치에서 이 표시가 보이면 그 자체로 점검할 일입니다.
컨테이너 상태는 적용 전 재시작 횟수와의 차이로 본다
docker compose up -d는 컨테이너를 만들고 바로 돌아옵니다. 기동하자마자 죽는 서비스가 있어도 배포 스크립트는 성공으로 끝납니다. 그래서 실행기는 배포가 끝난 뒤 설치의 컨테이너를 5초 간격으로 다시 읽어 셋 중 하나로 가릅니다.
| 판정 | 조건 |
|---|---|
| 즉시 실패 | exited·dead, unhealthy, 재시작 2회 이상(크래시 루프) |
| 대기 | created·restarting, 헬스 체크 starting, 재시작 1회 |
| 통과 | 전부 running이면서 healthy이거나 헬스 체크가 없음, 그리고 판 라벨 일치 |
5분이 지나도 대기가 풀리지 않으면 남은 컨테이너 목록과 함께 실패로 끝냅니다.
재시작 횟수는 적용 전에 적어 둔 값과의 차이로 셉니다. postgres처럼 배포를 넘어 계속 사는 컨테이너는 지난 재시작 횟수를 들고 있어서, 절댓값으로 세면 멀쩡한 컨테이너가 크래시 루프로 잡힙니다. 마이그레이션처럼 한 번 돌고 종료 코드 0으로 끝나는 컨테이너는 판정에서 뺍니다.
로그에 traceback이나 AdminShutdown이 한 줄이라도 있으면 실패다
컨테이너가 healthy여도 특정 경로에서만 예외가 날 수 있습니다. 컨테이너 판정을 통과하면 실행기는 적용 시작 이후의 백엔드·워커 로그에서 두 표지를 찾습니다. 하나는 파이썬 traceback 머리(Traceback (most recent call last))이고, 다른 하나는 PostgreSQL이 관리자 명령으로 연결을 끊었을 때 드라이버가 남기는 AdminShutdown입니다. OwlAI 로그는 JSON 한 줄 형식이고 예외는 exception 칸에 traceback 원문으로 실리므로, 문자열 하나로 찾을 수 있습니다. 사내 운영 환경의 24시간 로그에서 두 표지는 모두 0건이었습니다. 그래서 한 줄만 나와도 실패로 봅니다.
AdminShutdown을 표지로 넣은 이유는 헬스 체크가 쓰지 않는 자원에 있습니다. DB가 재시작하면 서버는 붙어 있던 연결을 모두 끊습니다. 커넥션 풀이 연결을 꺼낼 때 살아 있는지 확인하지 않으면, 끊긴 연결이 풀에 그대로 남습니다. 준비성 프로브는 다른 세션으로 SELECT 1을 보내므로 계속 200을 돌려줍니다. 실패는 그 풀을 쓰는 첫 요청에서야 드러납니다.
웹 서버가 잡을 큐에 넣을 때 쓰는 커넥션 풀이 그런 자리입니다. 이 풀은 연결을 꺼낼 때마다 빈 쿼리로 살아 있는지 확인하고, 죽은 연결은 버린 뒤 다음 연결을 확인합니다. 워커의 큐 풀은 큐 라이브러리가 같은 확인을 합니다. 그래도 DB를 멈추는 작업(PostgreSQL 메이저 판 이전, 덤프 복원, 매니지드 DB의 엔진 업그레이드) 뒤에는 웹과 워커를 다시 띄우고, 로그에 AdminShutdown이 0건인지 보는 것을 운영 절차로 둡니다. 풀마다 확인을 걸어야 하는 것과 달리, 재기동은 프로세스가 쥔 연결을 모두 새로 맺습니다. 구축형의 PostgreSQL 메이저 판 이전은 배포 스크립트 안에서 웹·워커 교체보다 먼저 돌기 때문에 이 재기동이 저절로 따라옵니다.
실패한 턴은 오류 문구로 네 갈래로 나눈다
헬스 체크와 달리 실패한 턴은 사용자가 실제로 겪은 실패입니다. OwlAI는 채팅 턴마다 상태와 실패 사유 문구를 메타스토어에 기록합니다. 기동 확인은 적용 시작 이후에 시작한 턴을 상태별로 세고, 실패한 턴을 문구로 네 갈래로 나눕니다.
- 회귀: traceback, 파이썬 내장 예외 모양의 문구, 러너 결과 없이 끝나거나 스트림이 마무리 없이 끊긴 턴. 코드가 깨졌다는 뜻입니다.
- 중단: 워커 정지, 워커 유실, 시작되지 못함, 감시자가 대신 닫은 턴. 순차 교체가 길었다는 뜻이고 코드 결함으로 세지 않습니다. 교체 중에 작업이 끊기지 않게 하는 방법은 「워커를 재시작해도 돌던 턴은 답을 내거나 실패를 남긴다」에서 다룹니다.
- 정상 거절: 모델·자격 증명·쿼터 같은 사유 코드와 프로바이더의 HTTP 오류.
- 미분류: 위 어디에도 맞지 않는 문구. 판정 결과에 표본을 함께 싣습니다.
회귀가 1건 이상이거나 미분류가 3건 이상이면 실패입니다. 두 임계는 설정 파일의 deployJudge.regressionFailureLimit·unclassifiedFailureLimit로 조정합니다. 미분류 임계를 회귀보다 높게 둔 이유는 문구가 예외 종류를 잃는 경로가 있어서입니다. 러너가 예외를 문장으로만 남기면 list index out of range 같은 내장 예외 문구는 모양만으로 다 가를 수 없고, 이런 실패가 미분류로 옵니다. 한 건은 우연일 수 있어서 쌓이는 것을 신호로 봅니다.
같은 창에서 실패한 회의·플레이북 잡도 같은 네 갈래로 셉니다. 다만 잡은 창 안에 시작한 것이 아니라 창 안에 실패한 것을 셉니다. 회의나 예약 실행은 배포 전에 시작해 배포 뒤에 끝나는 일이 흔하기 때문입니다. 잡의 회귀는 턴의 회귀와 합쳐 같은 임계에 겁니다. 잡의 미분류는 건수만 판정 결과에 싣고 임계에는 넣지 않습니다. 회의는 실패 사유로 사용자에게 보일 안내 문구를 남겨서, 턴처럼 문구 모양으로 가를 수 없기 때문입니다.
판정을 내지 못한 것도 실패로 다룬다
턴 집계는 새 판의 백엔드 이미지로 띄운 일회성 도우미 컨테이너가 돌립니다. 앱과 같은 DB 접속 정보와 설정을 쓰고, 결과를 JSON 한 줄로 냅니다. 집계는 메타스토어를 읽기만 합니다.
판정을 내지 못한 경우는 세 가지로 다룹니다.
| 경우 | 처리 |
|---|---|
| 집계가 DB 오류·설정 오류로 판정 불가를 냄 | 실패 |
| 한 번의 집계가 10분을 넘김 | 실패(다시 돌리지 않음) |
| 도우미가 뜨지 못하거나 끊겨 판정 줄 자체가 없음 | 60초 뒤 다시 돌림, 최대 3번 |
다시 돌리는 것은 판정 줄이 없을 때뿐입니다. 집계는 읽기만 하므로 결과를 잃은 실행을 다시 돌려도 같은 답이 나옵니다. 판정 불가를 통과로 치면 DB에 닿지 못한 새 판이 성공으로 기록되기 때문에, 판정 불가는 회귀와 똑같이 실패로 다룹니다.
판정 창에 턴도 실패한 잡도 없으면 집계는 통과로 끝납니다. 이때 실행기는 근거가 없었다는 경고를 업데이트 진행 기록에 남깁니다. 진행 기록은 관리 화면과 호스트의 업데이트 CLI에서 읽습니다. 판 라벨·컨테이너·로그 확인은 이 경우에도 그대로 걸립니다.
OwlAI SaaS 배포도 같은 집계 명령을 같은 10분 상한과 같은 재시도 규칙으로 돌리고, 실패하면 배포를 되돌립니다. 두 배포 경로가 한 판정 기준을 씁니다.
설계에서 고른 것
- 헬스 체크를 무겁게 만드는 대신 배포 직후 한 번 도는 판정을 골랐습니다. 헬스 체크는 도커의 15초 주기에 맞춰 가볍게 남고, 배포 판정은 10분까지 걸려도 됩니다. 대가는 판정이 배포 때만 돈다는 점입니다. 배포와 상관없이 DB만 재시작했다면 끊긴 연결은 운영 절차(웹·워커 재기동과
AdminShutdown0건 확인)로 잡아야 합니다. - 예외 클래스 이름을 따로 저장하는 대신 오류 문구의 모양으로 가르고, 모르는 실패는 세어서 신호로 씁니다. 클래스 이름을 저장하면 더 정확히 가를 수 있지만, 러너가 예외를 문장으로 남기는 경로를 모두 바꿔야 하고 새 예외는 그래도 목록에 없습니다. 미분류 임계는 목록에 없는 결함도 판정에 걸리게 합니다. 판정 결과의 표본을 사람이 읽고 규칙을 보태는 것이 이 선택의 대가입니다.
- 근거가 약한 갈래는 임계를 높이거나 임계에서 뺐습니다. 판정이 틀리면 운영 업데이트가 되돌려지기 때문입니다. 한 건짜리 미분류는 통과시키고, 잡의 미분류는 건수만 싣습니다. 그래서 traceback 모양을 남기지 않는 잡 결함은 건수로만 보입니다.
- 판정 창에 턴이 없을 때 점검용 턴을 직접 돌리는 대신 경고를 남기는 쪽을 골랐습니다. 점검용 턴은 설치 모델 비용과 감사 기록의 귀속을 새로 만들고, 모델을 아직 설정하지 않은 설치에서는 돌 수도 없습니다. 사용자가 없는 시간에 업데이트하는 설치라면, 업데이트 뒤 첫 사용 시간대에 업데이트 기록과 실패한 턴을 한 번 더 보는 것이 이 선택의 대가입니다.
LLM 에이전트처럼 요청 하나가 몇 분씩 걸리고 실패 사유가 문장으로 남는 서비스라면 이 판정을 옮길 수 있습니다. 헬스 체크는 살아 있는지만 보게 두고, 배포 직후 실패 문구를 갈래로 나눠 세는 단계를 배포 절차 안에 넣으면 됩니다.
어떤 문제를 해결하나요?
설치·운영과 품질의 다른 글

엔지니어링 노트 · 설치·운영과 품질
설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다
구축형 OwlAI는 고객이 고른 호스트 한 대에 Docker Compose로 올라갑니다. 배포판과 커널, 도커 판, 보안 모듈, 인증서는 고객마다 다릅니다.

엔지니어링 노트 · 설치·운영과 품질
도커 주소 풀은 설치하는 사람이 정하고, 점검은 사내 대역과 대조한다
구축형 OwlAI를 기업망 호스트에 설치하고 몇 주 뒤, 어제까지 붙던 사내 DB나 추론 서버에 컨테이너에서 닿지 않는 날이 올 수 있습니다. 같은 호스트의 셸에서도 닿지 않습니다.

엔지니어링 노트 · 설치·운영과 품질
사내 프록시 뒤에서는 NO_PROXY를 대역 대신 이름과 IP로 적는다
사내 프록시로만 밖에 나갈 수 있는 기업망에 구축형 OwlAI를 설치하면, 클라우드 LLM으로 가는 호출은 HTTPSPROXY를 따라 프록시로 나가야 합니다.
