오류도 없고 충돌도 없었다. 내 CPU는 한 시간 동안 390%에 머물렀다.
요약
RAG 파이프라인 구축 중 설정 누락으로 인해 비전 파싱 작업이 외부 API 대신 CPU 기반 로컬 모델로 실행되어 성능 저하가 발생한 사례를 다룹니다. 폴백(fallback) 메커니즘이 오류를 발생시키지 않고 잘못된 환경에서 작업을 수행할 때 발생하는 위험성을 경고합니다.
핵심 포인트
- 설정 누락으로 인한 의도치 않은 로컬 모델 폴백 발생
- 성공적인 폴백이 오히려 시스템 성능 저하를 은폐할 수 있음
- API와 백그라운드 워커 간의 설정 동기화 중요성
- 단순한 오류보다 '조용히 작동하는 잘못된 로직'이 디버깅하기 더 어려움
이 글은 DEV's Summer Bug Smash: Smash Stories를 위한 제출물이며, Sentry의 지원을 받았습니다.
사용자가 106페이지짜리 PDF 파일을 업로드하고 '이 책에 대해 질문하기(Ask this book)' 버튼을 클릭했습니다.
화면에는
즉, 비전 파싱 (vision parsing)이 로컬 모델에서, 그것도 CPU에서 실행되고 있었습니다. 나중에 측정해 보니 페이지당 약 42초가 걸렸습니다. 책은 106페이지였습니다. 책 한 권을 파싱하는 데 대략 74분이 소요되었으며, 이 모든 과정은 인덱싱 워커 (indexing worker)에 의해 인라인 (inline)으로 대기 상태였습니다. 전체 RAG 파이프라인 (RAG pipeline)이 그 뒤에 멈춰 있었습니다. 외부에서 보기에는 "Preparing… 0/0" 상태로 얼어붙은 것처럼 보였습니다. 내부적으로는 잘못된 하드웨어에서 매우 열심히 작동하고 있었습니다.
아무것도 실패하지 않았습니다. 그것이 문제였습니다. 충돌 (crash)이 발생했다면 1분 안에 저에게 알림이 왔을 것입니다. "작동하는" 폴백 (fallback)은 한 시간 동안이나 타오를 수 있습니다.
근본 원인은 조금 아팠다
며칠 전, 저는 신뢰성 수정 사항 (reliability fix)을 배포했습니다. 이전에는 인덱싱 (indexing)이 HTTP 요청 내부에서 청킹 (chunking)을 수행했는데, 요청이 종료되면 영구적으로 멈출 수 있었습니다. 그래서 저는 이 전체 과정을 백그라운드 워커 (background Worker)로 옮겼습니다. 좋은 변화였습니다. 다시 해도 그렇게 할 것입니다.
하지만 API와 워커는 별개의 프로세스이며, 각각 고유한 appsettings.json을 가지고 있습니다. API 설정에는 다음과 같은 라우트 (route)가 있었습니다:
"Routes": { "pdf.parse": "openai-pdf" }
워커 설정에는 없었습니다.
따라서 파싱 작업이 API에서 워커로 이동했을 때, 라우터 (router)가 pdf.parse를 조회했으나 아무것도 찾지 못했고, 폴백 (fallback)이 하는 대로 행동했습니다. 106페이지 분량의 책에 대한 비전 파싱 작업을 CPU 전용 컨테이너 (container)에 있는 로컬 모델로 조용히 넘겨버린 것입니다.
작업은 이동했지만, 설정은 이동하지 않았습니다. 저의 신뢰성 수정 사항이 성능 저하 (regression)를 일으켰습니다.
해결책
API를 미러링(mirroring)하여 워커의 appsettings.json에 다음과 같은 설정 블록 하나를 추가했습니다:
"Routes": { "pdf.parse": "openai-pdf" }
여기에 일치하는 Ai:Pdf 설정 블록을 추가했습니다. 코드 변경은 없었습니다. 컨테이너의 점유율은 390%에서 유휴 (idle) 상태로 떨어졌고, OpenAI 트래픽이 복구되었으며, 책은 몇 분 만에 인덱싱되었습니다.
작성하는 데는 5분이 걸렸지만, 찾아내는 데는 훨씬 더 오래 걸렸습니다.
왜 이 버그가 이토록 잘 숨어 있었는가
세 가지 이유가 있으며, 이는 일반적인 사례라고 생각합니다:
폴백 (fallback)은 실패하지 않습니다. 잘못된 도구로 성공할 뿐입니다. 모든 상태 확인 (health check)은 녹색(정상)이었습니다. 작업은 진행 중이었습니다. 느리고 비용이 많이 들었지만, 진행은 되고 있었습니다. 시스템이 "이것은 잘못되었다"라고 말할 수 있는 순간은 없었습니다. 시스템 자체의 규칙에 따르면 아무것도 잘못된 것이 없었기 때문입니다.
침묵에 대해서는 아무도 경고를 보내지 않습니다. 저는 API의 오류(error)와 높은 지연 시간(latency)에 대한 경고(alert)는 설정해 두었습니다. 하지만 "우리가 의존하는 비용이 많이 드는 외부 제공업체(external provider)에 오늘 호출이 전혀 발생하지 않았다"는 상황에 대한 경고는 없었습니다. 신호의 부재(Absence of a signal)는 가리킬 대상 자체가 없기 때문에 알아차리기가 가장 어렵습니다.
설정(Config)은 프로세스(process)별로 존재하지만, 저의 멘탈 모델(mental model)은 그렇지 않았습니다. 제 머릿속에는 하나의 라우팅 테이블(routing table)이 있었습니다. 하지만 실제로는 두 개의 파일이 존재했고, 그 파일들은 우연히 일치하고 있었을 뿐, 결국 일치하지 않게 되었습니다.
업무 방식에서 바꾼 점
이 경험을 통해 얻은 세 가지 규칙입니다:
- 작업이 프로세스 간에 이동할 때, 설정(config)도 함께 이동해야 합니다. 이제 "X를 워커(Worker)로 이동"하는 모든 변경 사항에는 체크리스트 항목이 추가되었습니다: 배포 전 두 프로세스 간의 관련 설정 섹션을 비교(diff)할 것.
- 라우팅(routing)을 명확하게 드러내세요. 각 작업(job)마다 어떤 제공업체(provider)가 결정되었고 그 이유가 무엇인지 알려주는 로그 라인을 남기세요:
pdf.parse -> ollama (no route, DefaultProvider fallback). 이 로그 한 줄만 있었어도 한 시간 동안의 혼란을 30초 만의 해결로 바꿀 수 있었을 것입니다. - 비용이 많이 드는 작업의 경우, 폴백(fallback)보다는 페일 패스트(fail-fast)를 선호하세요. 저렴한 작업의 라우팅이 누락된 경우에는 폴백(fallback)을 수행할 수 있습니다. 하지만 책 한 권 전체를 비전 파싱(vision parsing)하는 작업의 라우팅이 누락되었다면 오류를 발생시켜야(throw) 합니다. 조용한 폴백(silent fallback)은 시스템이 사용자에게 알리지 않고 내리는 결정입니다. 제공업체의 선택이 중요하다면, 그것은 조용히 이루어져서는 안 됩니다.
가장 뼈아픈 교훈은 이것입니다: 이 버그는 제가 자랑스러워했던 수정 사항(fix)에 의해 도입되었습니다. 신뢰성(reliability) 작업을 하다 보면 코드가 실행되는 위치가 바뀌게 되는데, 그 "위치"가 바로 설정(config)이 다루는 핵심입니다.
여러분의 시스템에서는 어떤 부분에서 폴백(fallback)이 조용히 작동할 수 있으며, 이를 알아차리는 데 얼마나 걸릴까요? 저는 106페이지짜리 PDF와 CPU 그래프를 보고서야 알 수 있었습니다. 여러분의 경험을 댓글로 들려주세요.
저는 .NET으로 기술 서적을 위한 오픈 소스 리더인 TextStack을 만들고 있습니다. 이 버그는 해당 서비스의 "이 책에 대해 질문하기" 기능 뒤에 있는 RAG 인덱싱 파이프라인(indexing pipeline)에서 발생했습니다. github.com/mrviduus/textstack
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기