어디서 돌고 있나, 왜 느린가 — 서빙 위치 표시와 채팅 지연 쪼개 보기

두 대(집 서버 + 노트북) 운영 사흘째. 오늘은 “지금 어느 기기가 받고 있지?”를 매번 터미널로 확인하던 일을 어드민 화면으로 옮기고, “Mova 채팅이 왜 이렇게 느리지?”를 단계별 시간으로 쪼개 원인을 나눠 고쳤다.

1. 어드민 홈에 서빙 서버·챗봇 경로 표시

  내용
상황 노트북이 꺼지거나 밖에 나가면 집 서버가 넘겨받는데, 지금 어느 쪽이 서빙 중인지 보려면 매번 두 기기에 접속해야 했다
방법 쿠버네티스가 파드에 “자기가 뜬 노드 이름”을 넣어 주게 했다(downward API). 어드민 API는 그 요청을 실제로 처리한 파드의 노드를 돌려주니 추측이 없다
덤 챗봇은 서빙 기기와 따로 움직인다(앱은 집 서버, 언어 모델은 노트북 GPU일 수 있다). 그래서 도메인 챗봇·Mova 챗봇(질문 이해·추천)이 지금 어디서 답하는지도 2초짜리 연결 확인으로 판정해 함께 보여 준다
결과 어드민 홈 오른쪽 위 “서빙 서버 · 집컴”, 아래 “챗봇 LLM 경로” 카드. 판정 규칙은 실제 폴백 코드와 같은 조건으로 쓰고 테스트 9개로 고정

같은 날 “Tailscale을 붙였는데 밖에서 GPU가 안 도는 것 같다”는 의심도 확인했다. 집 서버 운영 파드에서 노트북 GPU 경로로 실제 생성 요청을 보내고, 노트북에 모델이 GPU로 올라간 것을 확인했다 — GPU는 밖에서도 돈다. 밖에서 빠지는 건 노트북의 앱 서빙뿐이고, 그건 DB 쿼리가 매번 인터넷을 건너지 않게 일부러 정한 설계다.

2. Mova 채팅이 느렸던 이유 — 22.8초를 쪼개 보니

같은 질문이 첫 번째 22.8초, 바로 다음 4.1초. 로그 시각과 모델 서버 로그를 맞대어 보니 원인이 셋이었다.

단계 첫 요청 다음 요청 원인
질문 이해 (EXAONE 2.4B, 노트북 GPU) 4.8초 0.8초 노트북 재부팅 뒤 첫 호출 — 모델 적재
검색용 임베딩 (bge-m3) 2.2초 0.2초 임베딩 요청이 “계속 올려 둬”를 안 보내 5분 쉬면 모델이 내려갔다
추천 (EXAONE 2.4B LoRA, 집 서버 GTX 1650) 약 14초 약 3초 처음 보는 긴 프롬프트를 읽는 시간. 두 번째는 프롬프트 캐시 덕분
  • 임베딩은 고쳤다. 상주 설정을 같이 보내고, 실행 위치도 바꿨다. 질문 한 줄 임베딩을 재 보니 집 서버 CPU 직접 약 0.1초, 노트북 GPU를 거치면 약 0.35초 — 계산이 작으면 GPU보다 네트워크 왕복이 더 크다. 그래서 임베딩은 집 서버에 고정했다.
  • 추천 단계는 숫자로만 남겼다. 직접 재 보니 1,430토큰 프롬프트 처리 4.1초 + 200토큰 생성 2.8초. 줄이는 길(노트북 GPU로 옮기기, 후보 수 줄이기)은 둘 다 구성 변경이라 다음 과제로 올렸다.

3. 첫 채팅 화면의 바깥 스크롤

채팅 영역이 “화면 − 헤더”를 꽉 채우고 그 아래 푸터가 붙어, 페이지가 푸터 높이만큼 스크롤됐다(900px 화면에 문서 979px). 채팅 페이지에서만 레이아웃 높이를 화면에 고정하고 채팅이 남은 높이를 차지하게 바꿨다. 푸터는 TMDB 출처 고지가 있어 남겼다. 1280·500px 폭에서 전송 전·로딩·답변 뒤 모두 문서 높이 = 화면 높이, 긴 대화는 목록만 안에서 스크롤되는 것을 확인했다.

4. 추천 문구는 실제로 물어보고 골랐다

입력창 아래 추천 문구를 바꾸기 전에 후보 27개를 운영 API에 하나씩 보내 답을 받아 봤다. 그럴듯해 보여도 못 하는 문구가 있었다 — “지금 극장에서 볼 만한 영화”는 엉뚱한 상영관 되묻기, “이번 주 박스오피스 순위”는 인사말, “공포는 싫고 긴장감 있는 영화”는 공포물 추천. 잘 답하는 16개만 남기고, 드러난 버그는 미결 항목(B-014)으로 옮겼다.

남긴 것

  • 같은 질문이 드물게 두 번 저장되는 현상(14일 16건)을 로컬에서 경로·지연을 바꿔 가며 재현해 봤지만 재현되지 않았다. 원인을 모른 채 중복 방지 장치로 덮지 않고 관찰 사실만 기록했다(I-008).

오후 — 버그 다섯 개, 그리고 mova 랭킹

채팅 버그 5건(B-014)을 운영 로그로 원인부터 확인하고 고쳤다. 셋이 같은 유형이었다. 질문 이해 모델(2.4B)이 발화에 없는 제목을 지어냈는데(“지금 극장에서 볼 만한 영화” → 어벤져스: 엔드게임), 검증이 “카탈로그에 있는 제목인가”만 봐서 통과했다. 빠진 질문은 “사용자가 그 제목을 말했는가”였다 — 제목(또는 그 낱말)이 발화나 직전 대화에 있을 때만 쓰게 했다.

질문 전 후
지금 극장에서 볼 만한 영화 엔드게임 상영관 되묻기 이번 주 상영작 안내
이번 주 박스오피스 순위 인사말 이번 주 상영작 안내
공포는 싫고 긴장감 있는 영화 컨저링 3 포함 공포 장르 제외(스릴러 위주)
인터스텔라 같은 영화 추천 인터스텔라 자신 포함 그래비티·마션·인셉션
기생충 평점 어때? “별점 0(리뷰 0건)”을 지어냄 mova 리뷰가 없으면 별점을 말하지 않음

랭킹의 “AI 검색 TOP”을 “mova 랭킹”으로 바꿨다. 로그인 사용자의 채팅 카드 클릭만 세던 것을, 어디서 들어오든 영화 상세를 연 것(비로그인 포함)으로 센다. 같은 사람·영화·하루는 한 번만(DB 고유 제약), 봇은 제외. 랭킹 스냅샷을 만들던 스케줄러가 노트북에서만 돌아 집 밖이면 멈추던 문제도, 조회 때 최근 7일을 바로 집계하는 방식으로 함께 없앴다.

추천 단계가 느린 이유도 숫자로 확인했다. 집 서버 GTX 1650에서 llama.cpp 옵션(flash attention·배치 크기)을 바꿔 가며 같은 프롬프트로 재 보니 이미 최적이었다(차이 3% 이내). 처음 보는 질문의 추천 단계는 4~5초가 정상치이고, 아침의 22초는 모델 콜드 로딩이 겹친 결과였다.