11. 모니터링과 설명가능성
10장의 두 서비스는 요청에 답하지만, 응답이 온다는 것과 답이 맞다는 것은 별개다. 흐린 사진을 보내자 탐지 서비스는 모든 요청에 200을 돌려주면서 카드의 87%를 놓쳤다. 11장은 지표, 로그, 경보, 드리프트 보고서, 설명으로 이 간극을 메운다. 이 신호들은 모두 “모니터링"으로 묶이지만 답하는 질문이 서로 달라서, 그 구분이 먼저다.
서비스, 데이터, 모델은 따로 관측해야 한다
| 층 | 묻는 것 | 이번 실습의 신호 | 얻는 시점 |
|---|---|---|---|
| 서비스 | 응답하는가, 얼마나 빠르고 얼마나 실패하나 | 요청 수, 응답 코드, 지연, 진행 중 요청 수 | 바로 |
| 데이터 | 입력이 학습 때와 닮았나 | 입력 밝기, 이미지 속성 분포, 평점 분포 | 표본이 모인 뒤 |
| 모델 | 답이 맞나 | 빈 응답 비율, confidence 분포, 정답과 비교한 정확도 | 대리 지표는 바로, 정답은 늦게 |
서비스 층은 SRE의 네 가지 황금 신호인 지연, 트래픽, 오류, 포화로 정리되고, BentoML 기본 지표가 대부분을 덮는다. 세 층은 서로를 대신하지 못한다. 흐린 사진을 4분 동안 보냈을 때 응답은 모두 200이고 p95도 100ms 안쪽이었지만, 카드가 있는 사진 60장 중 52장에서 박스가 나오지 않았다. 반대로 사진을 어둡게만 바꾸자 입력 밝기가 138에서 48로 떨어지고 드리프트 점수는 최대가 됐는데, 모델은 60장 중 59장을 찾았다.
모델 층의 진짜 지표는 정답과 비교한 정확도지만 서빙 중에는 정답이 없다. 사람이 검수하거나 사용자가 반응해야 정답이 생기므로, 그 사이는 confidence 분포나 빈 응답 비율 같은 대리 지표로 메운다. 대리 지표는 정답과 상관이 있을 때만 쓸모가 있으니 정답이 모이면 둘의 관계를 다시 확인해야 한다.
Prometheus 지표는 라벨과 프로세스 구조를 따른다
Prometheus는 서비스의 /metrics를 정해진 간격으로 가져간다. Counter는 rate()로 증가율을 보고, Histogram은 구간별 누적 개수로 분위수를 추정한다(3장 3.5). Histogram은 개별 값을 남기지 않아 마지막 구간을 넘는 값을 표현하지 못한다. BentoML 1.1의 지연 구간은 10초에서 끝나서, 요청이 30초 넘게 걸려도 p95는 10초에 붙어 있었다.
라벨은 같은 지표를 나눠 보는 축이다. 엔드포인트, 응답 코드, 모델 버전처럼 값이 몇 가지인 것만 라벨로 쓰고 사용자 ID나 요청 ID는 로그에 둔다. 값마다 시계열이 생겨 cardinality가 폭증하기 때문이다. 수집하는 쪽이 붙이는 job, endpoint 같은 라벨과 이름이 겹치면 서비스 라벨에 exported_ 접두사가 붙고, 책의 대시보드도 exported_endpoint로 질의한다.
BentoML 1.1은 API 서버 여러 개와 Runner가 따로 돌지만, prometheus_client의 다중 프로세스 모드로 모든 프로세스의 값을 한 폴더에 모은다. Runner에서 기록한 confidence가 API 서버의 /metrics에 나타나는 이유다. 지표 이름은 버전마다 다르다. 1.1은 bentoml_api_server_request_total을, 1.2 이후는 bentoml_service_request_total을 내보내서 1.4로 띄운 추천 서비스는 책의 대시보드에 나타나지 않는다. 10장에서 두 서비스의 버전이 갈린 대가다.
경보는 SLO에서 출발한다
대시보드는 사람이 볼 때만 의미가 있고, 경보는 아무도 보지 않을 때 누군가에게 지금 움직이라고 요구한다. SLO는 얼마나 실패해도 되는지 미리 합의한 목표이고 경보를 언제 울릴지 정하는 근거다. 이번에는 “30일 동안 /invocation 요청의 99%가 1초 안에 5xx 없이 응답"을 목표로 잡았다.
SLO가 99%면 요청의 1%가 오류 예산이고, 소진 속도(burn rate)는 지금 실패율이 그 예산의 몇 배인지를 나타낸다. 실패율이 10%면 30일 예산을 3일에 쓴다. 고정 임계치 경보는 짧은 튐에 울리고 오래가는 1% 오류는 놓치므로 긴 창과 짧은 창을 함께 본다. 운영에서는 1시간과 5분을 흔히 짝짓는다. Alertmanager는 경보를 라벨로 묶고, 심각도에 따라 수신자를 고르고, 서비스 중단에 딸린 부수 경보를 억제한다.
서버 지표에는 서버가 끝낸 요청만 남는다. 처리가 밀렸을 때 클라이언트는 30초 만에 연결을 끊었는데, 서버는 일부 요청을 49초 뒤에야 503으로 끝냈다. 사용자 경험을 SLO로 삼으려면 클라이언트나 로드밸런서 쪽에서도 재야 한다. 해소(resolved) 알림은 마지막 firing 때의 주석을 그대로 싣기 때문에 그 숫자를 현재 값으로 읽으면 안 된다.
로그는 지표가 답하지 못하는 질문에 답한다
지표는 “얼마나"를 싸게 알려 주지만 원인이 대기열 포화인지 Runner 예외인지는 로그를 봐야 안다. Loki는 본문이 아니라 라벨만 색인하므로 라벨에는 Prometheus와 같은 규칙이 적용되고, 지표와 같은 값을 쓰면 Grafana에서 지표 패널에서 로그로 바로 넘어갈 수 있다.
책의 리스팅 11.1은 BentoML이 이미 콘솔 처리기를 달아 둔 bentoml 로거에 처리기를 하나 더 붙여, 모든 로그가 두 번 찍힌다. 저자 코드는 탐지 결과가 비면 ERROR 로그와 함께 200을 돌려줘 같은 사건이 로그에는 오류로, 지표에는 성공으로 남는다. 이 실습은 빈 결과를 모델의 답으로 보고 no_detection_total 카운터와 WARNING 로그로 남겼다.
드리프트와 설명은 경보 뒤에 원인을 좁힌다
지표와 경보는 서비스가 도는 동안 계속 흐르는 신호다. 드리프트 보고서와 히트맵은 누군가 표본을 모아 따로 돌리는 분석이라 실행 주기도 책임지는 사람도 다르다.
이미지와 추천의 드리프트
deepchecks의 ImagePropertyDrift는 밝기, 대비, 색 강도 같은 이미지 속성의 분포를 기준 묶음과 현재 묶음 사이에서 KS 통계로 비교한다. 개념은 6장 드리프트와 같다. 아무것도 바꾸지 않은 새 표본도 밝기 KS 0.34를 받았다. 64장이 10개 클립에서 나와 같은 배경끼리 뭉치기 때문이다. deepchecks가 주황으로 칠하는 0.2를 기준으로 쓰면 대조군부터 경보가 되니, 임계치는 변화가 없을 때 점수가 얼마나 흔들리는지부터 재고 정한다. 흐린 사진은 밝기 점수가 대조군과 같고 대비 점수만 0.52로 올랐는데 모델은 87%를 놓쳤다. 속성 드리프트는 학습 때 보지 못한 조건이 들어왔다는 신호이고 모델이 실패한다는 증거는 되지 못한다. 그래서 경보는 빈 응답 비율에 걸었다.
책의 추천 드리프트 실험은 영화 1~100번의 평점을 올린 데이터로 다시 학습한 모델의 임베딩을 비교하는데, 실제로 잰 것은 초기값이다. 노트북이 원본 데이터프레임을 제자리에서 고쳐 두 모델이 같은 데이터로 학습되고, 학습률 0.001로 5 epoch를 돈 임베딩은 초기값에서 거의 움직이지 않는다. seed만 다른 두 모델도 같은 KS 0.25가 나왔다. 입력을 직접 비교하면 해당 영화들의 평균 평점이 3.76에서 4.49로 오른 변화가 분명하다. 추천의 드리프트는 평점 분포나 새 사용자 비율 같은 입력 쪽부터 봐야 한다.
Eigen-CAM
Eigen-CAM은 지정한 층의 활성값에서 첫 번째 주성분을 구해 히트맵으로 그린다. 기울기도 대상 클래스도 쓰지 않으니 같은 이미지라면 박스가 둘이든 없든 히트맵은 하나다. 히트맵이 보여 주는 것은 특정 박스를 찾은 이유가 아니라 그 층에서 가장 크게 변하는 패턴의 위치다. 이미지마다 0~1로 정규화해서 사진끼리 열의 크기를 비교할 수 없고, 해상도도 입력의 1/32이다.
카드를 confidence 0.93으로 찾은 사진에서 카드 영역에 들어간 열은 13%였고, 가장 뜨거운 곳은 배경의 빨간 원이었다. 히트맵은 “배경 무늬가 표현을 지배한다” 같은 가설을 세우는 데 쓴다. 확인은 얼굴을 가린 표본처럼 조건을 바꾼 데이터로 실제 탐지율을 재서 한다.
EMF 추천 설명
EMF는 비슷한 사용자들이 좋게 평가한 아이템 쪽으로 사용자 벡터를 끌어당기는 벌점 항을 더한 행렬 분해이고, “비슷한 사용자 10명 중 이 영화를 평가한 사람들의 평점 분포"를 설명으로 붙인다. 저자 코드의 리스팅 11.8은 그 벌점 계수가 0이라 학습은 보통의 행렬 분해와 같고, 이웃은 원래 평점 행렬에서 고른다. 설명이 모델의 계산 경로와 따로 만들어지니, 모델이 왜 그 순위를 냈는지 보여 주기보다 추천을 납득시키는 정당화에 가깝다.
디버깅용 설명은 모델의 실제 계산을 충실히 반영해야 하고, 사용자에게 보이는 설명은 이해하기 쉽고 일관되어야 한다. 둘을 섞으면 그럴듯한 이유가 실제 이유를 가린다.
책 본문과 저자 코드의 차이
탐지 쪽 11장 코드는 object-detection-project의 main이 아니라 chapter_11_changes 브랜치에 있다. 추천 쪽은 movie-recommender-project에 있다. 이해를 바꾸는 차이는 다음과 같다.
- 책이 3장에서 설치한 Prometheus 차트에는 PodMonitor가 없고, 경보 규칙의
job="yatai/bento-deployment"도 그 설치에서는 나오지 않는다. 가져오라는 대시보드의 자원 패널도 비어 있다. from bentoml.metrics import Histogram으로 두 지표를 만들면 BentoML 1.1에서 둘이 같은 지표가 되어, confidence가 다른 Histogram에 기록된다.modify_brightness.py는 책과 달리 밝기를 1.5배로 올리고, 같은 이미지를 기준과 현재로 나눠 비교한다.- 추천 드리프트 노트북은 원본을 고쳐 두 모델이 같은 데이터를 본다.
- EMF는
expl_reg_term=0.0이고, 이웃 점수를 한 칸 어긋난 열에 쓴다.
실습
책 코드로 직접 실행한 순서와 결과 화면은 11장 실습 기록에 따로 모았다.
정리
흐린 사진은 서비스 지표를 움직이지 않은 채 카드 대부분을 놓치게 했고, 어두운 사진은 드리프트 점수만 올렸다. 경보는 사람이 움직여야 하는 신호인 서비스 중단, 오류 예산 소진, 빈 응답 급증에 걸고, 드리프트와 히트맵은 그 뒤에 원인을 좁히는 데 쓴다. 어느 신호든 대조군과 정답이 있는 표본으로 다시 확인할 때 근거가 된다. 12장은 같은 원칙을 요청 하나가 여러 번의 모델 호출과 검색으로 이어지는 LLM 시스템에 적용한다.