ChromaDB 운영 전에: v1 heartbeat 410과 v2 전환
사내 규정 검색을 붙이려는 동료에게 벡터 저장소를 소개하다 보면 설치 명령보다 먼저 받는 질문이 있습니다. GPU 없는 서버에서도 시작할 수 있는지, 프로세스가 살아 있다는 사실만으로 검색 서비스를 정상이라고 판단해도 되는지입니다. 패키지가 설치되는 순간과 운영 배포를 결정하는 순간 사이에는 확인할 항목이 남아 있습니다. 이번에는 운영 배포 전에 GPU 없는 Debian 13 서버에서 Chroma를 설치하고, 로컬 주소에 띄운 서버의 heartbeat 응답과 유휴 자원 사용량, PersistentClient로 넣은 한국어 문서의 검색 결과를 스크립트와 그 출력으로 차례로 남겼습니다.
운영 배포 전, GPU 없는 Debian 서버에서 확인할 것
검증의 목적은 저장소를 띄우는 작업과 업무에 맞는 문서를 찾는 작업을 떼어 보는 데 있습니다. 아래에서는 설치 환경, 서버 실행과 API 응답, 유휴 상태의 자원 사용, 문서를 넣고 검색하는 과정을 차례로 봅니다. 서버 실행은 ./data를 사용하고 검색 코드는 ./data_c를 사용하는 별도 실행이므로, 검색 결과를 HTTP 서버의 처리 성능으로 읽지 않도록 흐름을 나누어 설명하겠습니다.
설치와 서버 실행, heartbeat 응답을 읽는 방법
먼저 재현할 환경을 확인하고 가상 환경에 패키지를 설치합니다. 배포 준비 과정에서 운영체제와 Python 버전을 함께 남겨 두면 이후 다른 서버의 결과와 비교할 기준이 생깁니다. 다음은 실제 설치 스크립트에서 실행 추적 설정만 제외한 내용과 그 출력입니다.
#!/bin/bash
cat /etc/os-release | grep PRETTY; date '+%F %T %Z'; nproc; free -m | head -2; python3 --version
python3 -m venv v && . v/bin/activate && pip install -q chromadb 2>&1 | tail -2
pip show chromadb | grep -E '^(Name|Version)'
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
2026-10-16 05:31:26 KST
8
total used free shared buff/cache available
Mem: 16002 11137 2956 223 2704 4865
Python 3.13.5
Name: chromadb
Version: 1.5.9

실행 환경은 Debian GNU/Linux 13 (trixie), Python 3.13.5이며 설치된 chromadb 패키지는 1.5.9입니다. nproc 출력은 8이고, 메모리 출력의 total은 16002로 기록되어 있습니다. 이 메모리 표는 설치 당시 호스트 전체의 상태이지, Chroma 프로세스가 쓴 메모리가 아닙니다. 설치 확인까지 진행된 기록을 출발점으로 삼고, 다음에는 실제 서버 경로가 응답하는지 확인합니다.
서버를 실행할 때는 저장 경로와 바인딩 주소를 명시하고 heartbeat를 직접 호출합니다. 기존 배포 설정에 남아 있을 수 있는 v1 경로와 v2 경로를 같은 실행에서 비교하면, 상태 검사 실패가 프로세스 종료 때문인지 경로 변경 때문인지 판단하는 데 도움이 됩니다. 다음 스크립트는 로컬 주소에서 서버를 실행한 뒤 heartbeat와 버전 응답을 확인하고 프로세스 상태를 남긴 후 종료합니다.
#!/bin/bash
# run chroma server, check heartbeat v1/v2, RSS/CPU, then stop
. v/bin/activate; date '+%F %T %Z'
chroma run --path ./data --host 127.0.0.1 --port 29871 > server.log 2>&1 & P=$!
for i in $(seq 1 30); do curl -s -o /dev/null http://127.0.0.1:29871/api/v2/heartbeat && break; sleep 1; done
echo "--- v1 heartbeat"; curl -s -w '\nHTTP %{http_code}\n' http://127.0.0.1:29871/api/v1/heartbeat
echo "--- v2 heartbeat"; curl -s -w '\nHTTP %{http_code}\n' http://127.0.0.1:29871/api/v2/heartbeat
echo "--- v2 version"; curl -s -w '\nHTTP %{http_code}\n' http://127.0.0.1:29871/api/v2/version
SP=$(pgrep -P $P | head -1); SP=${SP:-$P}; sleep 5
echo "--- idle process stats (pid tree of chroma run)"; ps -o pid,rss,%cpu,etime,cmd -p $P $(pgrep -P $P) | cut -c1-120
kill $P; wait $P 2>/dev/null; echo "stopped"
2026-10-16 05:32:24 KST
--- v1 heartbeat
{"error":"Unimplemented","message":"The v1 API is deprecated. Please use /v2 apis"}
HTTP 410
--- v2 heartbeat
{"nanosecond heartbeat":1792096345484723225}
HTTP 200
--- v2 version
"1.0.0"
HTTP 200
--- idle process stats (pid tree of chroma run)
PID RSS %CPU ELAPSED CMD
1638222 112644 20.7 00:06 /home/user/rag/chroma/v/bin/python3 /home/user/rag/ch
stopped

캡처 b의 프로세스 표에는 실제 작업 폴더 경로가 보이고, 위 코드블록은 같은 명령과 출력을 /home/user/rag로 줄여 적었습니다.
v1 heartbeat는 API가 폐기되었으며 v2를 사용하라는 메시지와 HTTP 410을 반환했고, v2 heartbeat는 HTTP 200을 반환했습니다. 같은 서버에서 확인한 응답이므로 이 기록의 v1 실패 원인은 서버가 내려간 상황이 아니라 요청한 API 경로에 있습니다. /api/v2/version의 응답은 “1.0.0”으로, 설치 확인에서 나온 패키지 버전 1.5.9와 표시가 다르므로 버전 기록에는 확인 위치도 함께 남겨야 합니다. 프로세스 표의 RSS 112644와 CPU 20.7은 기동 직후의 관측값이며, 이를 유휴 구간의 CPU 사용률로 해석해서는 안 됩니다.
배포 전 자원 예산을 생각하려면 기동 직후 표와 별도로 유휴 구간을 살펴볼 필요가 있습니다. 다음 스크립트는 서버 실행 후 대기하고, /proc에 기록된 CPU 시간을 구간 앞뒤에서 읽어 사용률을 계산합니다. RSS는 프로세스 상태 파일에서 읽으며, 실제 요청을 처리하는 부하를 넣는 과정은 포함되어 있지 않습니다.
#!/bin/bash
# idle server: RSS and CPU over a 30s window
. v/bin/activate; date '+%F %T %Z'
chroma run --path ./data --host 127.0.0.1 --port 29871 > server_d.log 2>&1 & P=$!
for i in $(seq 1 30); do curl -s -o /dev/null http://127.0.0.1:29871/api/v2/heartbeat && break; sleep 1; done
sleep 10; T=$(getconf CLK_TCK)
a=$(awk '{print $14+$15}' /proc/$P/stat); sleep 30; b=$(awk '{print $14+$15}' /proc/$P/stat)
echo "rss_kB $(awk '/VmRSS/{print $2}' /proc/$P/status)"
echo "cpu_pct_30s_idle $(python3 -c "print(round(($b-$a)/$T/30*100,2))")"
kill $P; wait $P 2>/dev/null; echo stopped
2026-10-16 05:33:04 KST
rss_kB 111064
cpu_pct_30s_idle 0.0
stopped
유휴 측정에서 RSS는 111064 kB, 30초 구간의 CPU 사용률은 0.0%로 기록되었습니다. 이는 해당 실행에서 요청 부하가 없는 동안 관측한 값으로, 서버를 띄워 둔 상태의 자원 규모를 살펴보는 근거가 됩니다. 앞선 프로세스 표와 측정 시점 및 계산 방식이 다르므로 CPU 수치를 나란히 놓고 성능 향상이라고 설명하지 않습니다. 서버 용량을 정할 때는 이 값을 초기 기준으로 남기고 실제 문서 적재와 검색 요청이 들어오는 조건에서 필요한 자원을 이어서 측정하는 구성을 권합니다.
API 경로와 한국어 검색 순위가 만드는 운영 함정
기존 컨테이너 설정을 재사용한다면 Docker healthcheck와 k8s livenessProbe/readinessProbe의 경로부터 확인해야 합니다. 이번 실행 결과를 적용하는 구성에서는 이 경로를 /api/v2/heartbeat로 맞춥니다. v1을 계속 호출하는 검사는 살아 있는 서버에서도 실패 응답을 받으므로, 점검 대상과 점검 설정이 어긋난 상태가 생기기도 합니다. 경로를 바꾸면서 배포 설정의 실제 호출 주소도 확인하고, 응답을 직접 대조하는 절차를 둡니다.
다만 heartbeat가 응답한다는 사실을 업무 검색까지 준비되었다는 의미로 확장하지는 않습니다. livenessProbe는 프로세스 상태를 확인하는 목적을 분명히 하고, readinessProbe에서는 서비스가 요청을 받을 준비가 되었는지 별도 기준을 정해야 합니다. 이번 기록에서 검증한 경로는 heartbeat이므로 문서 적재 완료 여부나 검색 품질까지 보장하는 검사로 설명할 수는 없습니다. 인프라 상태 확인과 애플리케이션 준비 상태 확인의 책임을 나누면, 경로 변경 대응과 검색 장애 대응을 각각의 근거로 진행할 수 있습니다.
이어서 한국어 업무 문서를 기본 임베딩 설정으로 검색했을 때 어떤 문서가 나오는지 확인합니다. c.py는 임베딩 함수를 따로 지정하지 않고 PersistentClient를 생성하며, 출장비·연차·재택근무 문서를 upsert한 뒤 출장 경비 정산 기한을 질문합니다. HTTP 서버로 요청을 보내는 코드가 아니라 로컬 경로를 쓰는 코드라서, 여기서 나온 결과는 서버 API 검증과 별개의 기록입니다.
import chromadb, time, resource, sys
print("chromadb", chromadb.__version__, "python", sys.version.split()[0])
c=chromadb.PersistentClient(path="./data_c")
col=c.get_or_create_collection("docs")
t=time.time()
col.upsert(ids=["d1","d2","d3"],documents=["출장비는 영수증 첨부 후 7일 이내 정산합니다.","연차는 입사일 기준으로 15일이 부여됩니다.","재택근무 신청은 전날까지 팀장 승인을 받습니다."],metadatas=[{"src":"travel"},{"src":"leave"},{"src":"remote"}])
print("upsert_sec", round(time.time()-t,2))
r=col.query(query_texts=["출장 경비 정산 기한"],n_results=2)
print("ids", r["ids"][0]); print("distances", [round(x,4) for x in r["distances"][0]])
print("count", col.count())
print("maxrss_MB", resource.getrusage(resource.RUSAGE_SELF).ru_maxrss//1024)
2026-10-16 05:32:59 KST
chromadb 1.5.9 python 3.13.5
upsert_sec 0.38
ids ['d3', 'd2']
distances [0.3266, 0.7961]
count 3
maxrss_MB 275

count는 3으로 기록되었고 upsert_sec는 0.38이지만, 출장비 질문의 검색 결과는 기대한 d1 대신 d3와 d2였습니다. 반환된 거리 값은 각각 0.3266과 0.7961이며, 이 값만으로 업무상 정답의 신뢰도를 판단해서는 안 됩니다. maxrss_MB 275는 이 Python 프로세스의 최대 RSS 기록이므로 앞서 측정한 유휴 서버 RSS와 같은 조건의 수치로 비교하지 않습니다. 문서 적재와 질의 실행이 성공했더라도 이 질문에서 필요한 출장비 문서가 반환되지 않았다는 사실이 검색 구성 검토의 출발점입니다.
이 결과는 기본 설정을 그대로 사용한 한국어 업무 검색에서 순위를 별도로 검증해야 한다는 신호입니다. 기록에는 임베딩 모델을 바꾸어 비교한 실험이 없으므로 특정 모델로 교체하면 해결된다고 단정할 근거는 없습니다. 대신 실제 사용자가 묻는 질문과 기대 문서를 함께 관리하고, 임베딩 구성을 선택할 때 그 질문으로 검색 순위를 확인하는 절차를 권합니다. 출장비 문서가 들어 있다는 사실과 출장비 질문에서 그 문서를 찾는다는 사실을 따로 검사해야 운영자가 성공 여부를 설명할 수 있습니다.
아키텍처도 이 책임을 드러내도록 나누는 편이 좋습니다. 문서를 읽고 임베딩을 만드는 처리, 벡터를 저장하고 조회하는 저장소, 질문을 받아 결과를 사용하는 애플리케이션의 역할을 분리하는 구성입니다. 임베딩 구성이 달라질 때는 문서와 질의에 적용되는 구성을 함께 검토하고, 저장된 문서를 다시 처리할 필요가 있는지도 확인합니다. 이렇게 하면 API 경로를 고치는 배포 작업과 한국어 검색 순위를 개선하는 작업을 각각 검증할 수 있습니다.
필자가 권하는 배포 준비 구성
필자는 이번 결과를 바탕으로 Chroma를 운영 전 검증용 저장소로 시작하되, 배포 판단에는 서버 응답과 검색 결과를 함께 제출하는 구성을 권합니다. GPU 없는 Debian 서버에서 설치와 실행, 문서 적재 및 질의까지 진행한 기록이 있으므로 같은 흐름으로 검증을 시작할 수 있습니다. 운영 배포 여부는 실제 업무 질문에서 필요한 문서가 반환되는지 확인한 뒤 판단하겠습니다.
서버 구성에는 /api/v2/heartbeat를 사용하는 상태 검사를 반영하고, 패키지 버전과 API 버전 응답을 각각 기록합니다. 기존 Docker 및 k8s 설정을 가져올 때는 상태 검사 경로를 배포 검토 항목으로 두겠습니다. 경로가 맞아야 장애 알림이나 트래픽 수용 판단에 쓰는 응답을 해석할 수 있습니다.
데이터 구성에서는 문서 적재와 검색 평가를 이어서 실행하겠습니다. 이번 출장비 질문처럼 기대 문서가 분명한 사례를 남기고, 반환된 문서 식별자까지 확인하는 방식입니다. 적재 개수만 확인하고 넘어가면 업무상 필요한 검색 결과가 빠진 상태를 놓칠 수 있습니다.
서비스 구성에서는 임베딩 처리와 저장소 운영의 책임을 나누겠습니다. 애플리케이션은 어떤 문서가 반환되었는지 추적할 수 있게 하고, 임베딩 변경은 같은 업무 질문으로 비교할 수 있게 준비합니다. PersistentClient로 확인한 결과를 서버 연결 검증으로 옮겨 갈 때도 클라이언트 연결 방식과 데이터 경로를 다시 확인하겠습니다.
자원 계획에는 유휴 RSS 111064 kB와 30초 CPU 0.0%를 측정 조건과 함께 남기겠습니다. 검색 코드의 최대 RSS 275 MB 역시 별도 프로세스의 기록으로 관리합니다. 필자가 추천하는 출발점은 상태 검사 경로를 맞춘 서버, 책임을 분리한 임베딩 처리, 기대 문서를 확인하는 검색 평가를 함께 갖춘 구성입니다.
| 확인 항목 | 실행 기록 | 배포 준비에 반영할 내용 |
|---|---|---|
| 설치 환경 | Debian 13, Python 3.13.5, chromadb 1.5.9 | 환경과 패키지 버전을 함께 기록합니다. |
| 서버 API | v1 heartbeat 410, v2 heartbeat 200, v2 version “1.0.0” | Docker healthcheck와 k8s probe 경로를 /api/v2/heartbeat로 맞춥니다. |
| 유휴 자원 | RSS 111064 kB, 30초 CPU 0.0% | 요청 부하가 없는 조건의 측정값으로 관리합니다. |
| 문서 적재 | count 3, upsert_sec 0.38 | 적재 확인 뒤 업무 질문으로 검색을 검증합니다. |
| 한국어 검색 | ids [‘d3’, ‘d2’], distances [0.3266, 0.7961] | 출장비 문서 d1이 빠진 결과를 임베딩 구성 평가에 반영합니다. |
| 검색 프로세스 메모리 | maxrss_MB 275 | 유휴 서버 RSS와 구분하여 기록합니다. |
이 글의 명령과 응답은 아래 1차 자료와 함께 확인했습니다.
- Chroma 문서 Introduction: 설치와 클라이언트·서버 실행 방식 (확인일 2026-10-16)
- PyPI chromadb 패키지 페이지: 배포 버전 1.5.9 확인 (확인일 2026-10-16)
- chroma-core/chroma GitHub 저장소: 소스와 릴리스 기록 (확인일 2026-10-16)






