본 글은 GPT-4로 작성 후 수정한 글입니다.
현상 요약
- 상황: 하나의 API 서버에서 여러 프로세스/스레드가 동시에 인덱스에 접근(조회/추가).
- 증상: 요청 증가 구간에서 프로세스가 즉시 종료되며 표준 에러에 double free or corruption (!prev)가 출력.
- 영향: API 서버 다운타임 및 인덱스 상태 불명확.
double free or corruption (!prev)
Fatal Python error: Aborted
Thread 0x0000720960aa56c0 (most recent call first):
File "/usr/lib/python3.12/threading.py", line 359 in wait
File "/usr/lib/python3.12/queue.py", line 180 in get
File "/home/jin/Eirene/.venv/lib/python3.12/site-packages/posthog/consumer.py", line 107 in next
File "/home/jin/Eirene/.venv/lib/python3.12/site-packages/posthog/consumer.py", line 76 in upload
File "/home/jin/Eirene/.venv/lib/python3.12/site-packages/posthog/consumer.py", line 65 in run
File "/usr/lib/python3.12/threading.py", line 1073 in _bootstrap_inner
File "/usr/lib/python3.12/threading.py", line 1030 in _bootstrap
Thread 0x00007209612a66c0 (most recent call first):
File "/usr/lib/python3.12/concurrent/futures/thread.py", line 89 in _worker
File "/usr/lib/python3.12/threading.py", line 1010 in run
File "/usr/lib/python3.12/threading.py", line 1073 in _bootstrap_inner
File "/usr/lib/python3.12/threading.py", line 1030 in _bootstrap
Thread 0x0000720961aa76c0 (most recent call first):
File "/home/jin/Eirene/.venv/lib/python3.12/site-packages/faiss/swigfaiss_avx512.py", line 11635 in write_index
File "/home/jin/Eirene/eirene/mem0_naver/vector_stores/faiss.py", line 108 in _save
File "/home/jin/Eirene/eirene/mem0_naver/vector_stores/faiss.py", line 222 in insert
File "/usr/lib/python3.12/concurrent/futures/thread.py", line 58 in run
File "/usr/lib/python3.12/concurrent/futures/thread.py", line 92 in _worker
File "/usr/lib/python3.12/threading.py", line 1010 in run
File "/usr/lib/python3.12/threading.py", line 1073 in _bootstrap_inner
File "/usr/lib/python3.12/threading.py", line 1030 in _bootstrap
Current thread 0x00007209622a86c0 (most recent call first):
File "/home/jin/Eirene/.venv/lib/python3.12/site-packages/faiss/swigfaiss_avx512.py", line 2623 in add
File "/home/jin/Eirene/.venv/lib/python3.12/site-packages/faiss/class_wrappers.py", line 230 in replacement_add
File "/home/jin/Eirene/eirene/mem0_naver/vector_stores/faiss.py", line 215 in insert
File "/usr/lib/python3.12/concurrent/futures/thread.py", line 58 in run
File "/usr/lib/python3.12/concurrent/futures/thread.py", line 92 in _worker
File "/usr/lib/python3.12/threading.py", line 1010 in run
File "/usr/lib/python3.12/threading.py", line 1073 in _bootstrap_inner
File "/usr/lib/python3.12/threading.py", line 1030 in _bootstrap
...
Extension modules: websockets.speedups, zstandard.backend_c, charset_normalizer.md, requests.packages.charset_normalizer.md, requests.packages.chardet.md, yaml._yaml, regex._regex, numpy._core._multiarray_umath, numpy.linalg._umath_linalg, _jpype, faiss._swigfaiss_avx512 (total: 11)
[Thu Aug 28 01:32:31 KST 2025] uvicorn exited with code 0
[Thu Aug 28 01:32:31 KST 2025] Core dump: /home/jin/Eirene/coredumps/core.uvicorn.11571.1756312351
배경 지식
- FAISS는 라이브러리다. 내부는 C++/OpenMP로 작성되어 있고, CPU 경로에서 검색/학습/추가 자체가 멀티스레드로 실행될 수 있다. 스레드 수는 OMP_NUM_THREADS 또는 faiss.omp_set_num_threads()로 제어된다. (GitHub, openmp.org)
- 스레드 안전성: 동일 인덱스에 대한 동시 검색은 안전하도록 설계되어 있으나, 인덱스 구조를 바꾸는 연산(추가/학습/삭제/재구성)은 서로 배타적으로 실행되어야 한다. 즉, “읽기-쓰기 동시” 또는 “쓰기-쓰기 동시”는 금지다. (Stack Overflow)
- 프리포크(예: Gunicorn): --preload는 마스터에서 애플리케이션을 먼저 로드한 뒤 fork한다(메모리는 Copy-on-Write 공유). 이 모드는 fork 이후의 런타임 상태/스레드풀/네이티브 핸들과 상호작용 시 주의가 필요하다. (docs.gunicorn.org)
- fork × OpenMP 계열 라이브러리: 포크된 자식 프로세스에서 스레드 런타임 상태가 일관되지 않아 행이 꼬이거나 크래시가 보고된 사례가 널리 존재한다(동일 클래스의 문제로 PyTorch 이슈 다수). (GitHub)
근본 원인
아래 요인이 단독/복합으로 힙 메타데이터 손상 → glibc malloc 검증 실패 → 프로세스 abort를 유발했을 가능성이 높다.
- 인덱스 동시 변형
- 한 워커(또는 스레드)가 add()/train()/reset()/merge_from() 등으로 구조를 바꾸는 동안, 다른 워커/스레드가 동일 인덱스에 대해 search()를 수행.
- 결과: 인덱스 내부 버퍼 재할당/해제 타이밍이 얽히며 이중 해제/메모리 경계 손상 발생.
- 근거: FAISS 문서의 “변형 연산은 상호 배타적” 원칙. (Stack Overflow)
- 프리포크 + 네이티브 스레드 런타임 상태 공유
- --preload로 마스터에서 인덱스를 로드한 뒤 워커를 fork하면, 각 워커는 COW로 같은 초기 메모리 상태를 출발점으로 가짐.
- 이후 각 워커가 자체 OpenMP 스레드를 올리고 동일 인덱스 오브젝트를 변형하면, 복제/해제 타이밍이 어긋나 불안정성이 커진다. (일반적인 주의사항) (docs.gunicorn.org, GitHub)
결론: 동일 인덱스 객체에 대한 동시 쓰기 또는 쓰기-읽기 병행이 핵심 원인이다. 여기에 프리포크 런타임 상태와 OpenMP 내부 스레딩이 결합되면 크래시 확률이 상승한다.
재현 조건
- 전역 싱글턴으로 faiss.Index를 로드.
- FastAPI/WSGI에서 여러 워커 프로세스 또는 스레드가 동시에 다음을 수행:
- 일부 요청: index.add(x)
- 다른 요청: index.search(q, k)
- 부하(동시성)가 특정 임계치를 넘으면, 워커 중 하나에서 힙 손상 → abort.
즉시 조치
목표: “동일 인덱스”에 대해 쓰기는 항상 단일화, 검색은 자유롭게.
A. 쓰기 전용 임계구역(글로벌 단일화)
- 동일 프로세스 내에서는 Writer 락으로 add()/train()/reset()을 감싼다.
- Writer 수행 중에는 모든 검색을 잠시 차단(RWLock)하여 “쓰기-읽기 동시”를 제거.
- 근거: 변형 연산 상호배타 원칙. (Stack Overflow)
B. 프리포크 사용 시 초기화 시점 수정
- --preload 비활성화 또는 **워커 시작 훅(예: FastAPI startup 이벤트)**에서 각 워커가 자체적으로 인덱스를 로드하도록 전환.
- 목적: 포크 이후 깨끗한 스레드/런타임 상태에서 시작. (docs.gunicorn.org)
C. OpenMP 스레드 정리
- 과다한 내부 스레딩과 포크 상호작용을 줄이기 위해 우선 OMP_NUM_THREADS=1로 설정(문제 안정화 후 단계적으로 조정). 필요 시 faiss.omp_set_num_threads(n)으로 명시 제어. (GitHub, openmp.org)
D. 프로세스 구성 점검
- 가능하면 단일 워커 + 다중 스레드보다 여러 워커(프로세스) + 각 워커 내 읽기 전용이 안정적이다(쓰기 경로는 별도 단일화).
- 크래시 발생 워커만 재시작되도록 프로세스 매니저 설정 점검.
권장 구조
단일-작성자(Single-Writer) 아키텍처
- API 서버(여러 워커): 검색 전용. 인덱스는 읽기만.
- 업데이트 워커(1개 프로세스): 큐(Redis/SQS 등)에서 추가 요청을 받아 인덱스에만 쓰기 수행.
- 파일 교체는 원자적 스왑으로 배포:
- 기존 인덱스 로드 → 2) 데이터 추가 → 3) index_tmp.faiss로 저장 → 4) os.rename()로 원자 교체 → 5) 읽기 워커는 주기적으로 mtime 확인 후 핫 리로드.
이 구조는 읽기와 쓰기를 프로세스 레벨에서 분리하여, 라이브러리 수준 스레드 안전성 제약을 우회한다.
내부 락으로 일시 대응
- 같은 프로세스에서 검색/추가를 함께 처리해야 한다면, 강제적인 RWLock(다중 읽기, 단일 쓰기)을 적용.
- 단, 쓰기 동안 검색 지연이 발생하므로 QPS/지연시간 트레이드오프를 수용해야 한다.
장기 대안: 벡터 DB 도입
- Milvus / Pinecone / Weaviate 등 동시성/복제/업데이트가 기본 제공되는 솔루션 고려(운영 복잡성 ↓).
운영 체크리스트
- 코드 어디서든 인덱스를 변형할 수 있는 경로가 없는지 정적 점검.
- Gunicorn은 --preload 미사용 혹은 워커 시작 후 로딩으로 전환. (docs.gunicorn.org)
- OMP_NUM_THREADS/faiss.omp_set_num_threads() 값 명시 관리. 초기에 1로 고정 후 관측 기반으로 상향. (GitHub, openmp.org)
- 추가 요청은 큐잉하여 단일 작성자에게 위임(가능하면 별도 프로세스/서비스).
- 헬스체크/메트릭: 락 대기시간, 추가 큐 적체, 검색 QPS/지연, 워커 크래시율.
결론
- 현 크래시는 동일 인덱스에 대한 동시 변형(쓰기)과 검색이 겹치면서 발생하는 힙 손상으로 해석된다.
- 단일-작성자 원칙과 프리포크 안전 초기화, 내부 스레드 수 명시로 즉시 안정화 가능하다.
- 중장기적으로 읽기 전용 다중 워커 + 별도 업데이트 워커(원자 스왑) 또는 벡터 DB 도입을 권장한다.
참고 문헌
- FAISS, Threads and asynchronous calls — 내부 스레딩/스레드 안전성/스레드 수 제어. (Stack Overflow, GitHub)
- OpenMP 사양, OMP_NUM_THREADS / omp_set_num_threads. (openmp.org)
- Gunicorn 문서, preload_app(–-preload) — 프리포크 로딩 동작. (docs.gunicorn.org)
- PyTorch 이슈 예시 — fork × OpenMP 런타임 상호작용의 일반적 위험. (GitHub)
가정과 한계
- 서버 런타임/배포(예: Gunicorn/UVicorn, 워커 수, 스레드 모델)가 표준 구성이라는 가정 하에 정리했다.
- GPU 인덱스 사용 시 가정이 달라질 수 있으며, 본 문서는 CPU 인덱스를 전제로 했다(동일 원칙 적용 가능).
- 실제 크래시 지점은 빌드/플랫폼별로 상이할 수 있으므로, 재현 로그/콜스택 기반 세부 튜닝이 추가로 유효하다.