티스토리 뷰

배경

아키텍쳐 선택을 위해 동기, 비동기 파이프라인 각각에 대해 부하테스트(ramp-up test)를 수행.

new_sync 파이프라인 요약 (완전 동기)

배치 내 단일 파이프라인 내 동기, 배치 내 단일 파이프라인 간 동기

new_async 파이프라인 요약 (완전 비동기)

배치 내 단일 파이프라인 내 비동기, 배치 내 단일 파이프라인 간 비동기

결과

sync와 async가 다운된 이유(로그 분석)

1) new_sync: “모델 파일이 없어서” 바로 예외로 터짐 (NoSuchFile)

new_sync는 요청 처리 중 get_vector_search → VectorSearch → fastembed(TextEmbedding) → onnxruntime.InferenceSession으로 가다가,

  • /tmp/fastembed_cache/.../onnx/model.onnx 파일이 없어서
  • onnxruntime ... NoSuchFile 예외
  • 그 결과 POST /api/v1/sentiment/analyze/batch500으로 떨어지고
  • “Unhandled exception”으로 찍혀 있음.

이건 전형적으로 다운로드/캐시가 아직 완성되기 전에 로드하려 했거나, 캐시가 깨졌거나, 경로가 휘발(/tmp)이라 컨테이너 생명주기/동시성에 취약할 때 나옴.

2) new_async: NoSuchFile은 안 보이는데 “로드가 여러 번” 일어남 (중복 초기화/동시 초기화 정황)

new_async 로그에는 같은 임베딩 모델이

  • Dense 벡터 모델 로드 완료여러 번 반복.

즉, new_async는 “파일이 없어서 즉시 폭발”보다는,

  • 여러 요청/태스크가 동시에 들어오면서 VectorSearch(또는 임베딩 모델)가 중복 초기화되는 정황이 더 강해 보임.
  • 이 상태도 결국 캐시 다운로드/로드 race를 만들기 쉬워서, 타이밍 나쁘면 new_sync처럼 NoSuchFile로도 이어질 수 있음(이번 로그에선 그 전에 “운 좋게” 로드가 성공했을 가능성이 큼).

결론: “로그가 다르다” = 파이프라인 문제가 아니라, 공통 원인(임베딩 모델 캐시/초기화 레이스)가 다른 형태로 표출된 것

  • new_sync는 첫 로드 타이밍이 더 공격적/동시성에 더 취약해서 “파일 없음”으로 터진 흔적이 명확하고
  • new_async는 중복 로드(동시 초기화)가 보이고 , 당장 NoSuchFile은 안 찍혔지만 구조적으로 안전하다고 보긴 어려움.

아키텍쳐 선택에 대한 현실적인 답

이 로그만으로 “new_async가 더 낫다/ new_sync가 더 낫다”를 아키텍처 선택 근거로 쓰긴 약함.
대신 이 로그로는 “선택 전에 반드시 고쳐야 할 공통 결함”이 드러났다고 보는 게 정확함:

반드시 고칠 것 (둘 다 해당)

  1. VectorSearch(임베딩 모델)도 sentiment처럼 싱글톤 초기화 락(DCL) 적용
    → 초기화 구간에 동시 진입을 막는 락/세마포어로 중복 로드·캐시 race 제거.
  2. 서버 시작 시 preload/warm-up
    모델 + 토크나이저 + 첫 inference까지 강제 warm-up해서 “첫 요청에서 다운로드/로드”가 일어나지 않게.
  3. 캐시 경로를 /tmp가 아니라 볼륨으로 고정
    → HF/ONNX 캐시가 공유 볼륨(또는 이미지 bake-in)으로 안정적으로 존재하도록 (컨테이너 재시작/동시성에 덜 취약).
  4. readiness(준비됨) 되기 전까지 트래픽을 안 받음
    → K8s readinessProbe 등으로 준비 완료 시점을 명시하고, 그 전에는 LB/트래픽이 들어오지 않게.
  5. (옵션) HF 토큰 설정해서 rate limit/불안정 다운로드 가능성 줄이기 (로그에도 unauth 경고가 보임).

이걸 한 뒤에야 “sync vs async” 성능/안정성 비교가 의미 있게 됌.


코드로 전부 반영했을 때의 이점

1. 안정성 (ramp-up·재시작)

조치 이점
싱글톤 락 + 초기화 락 ramp-up 시 여러 요청이 동시에 임베딩/모델 로드를 트리거하지 않음 → 중복 로드·캐시 race 제거 → NoSuchFile/ONNX 에러 감소.
warm-up(모델+토크나이저+첫 inference) 첫 실제 요청에서 로드가 일어나지 않음 → 첫 요청 타임아웃/500 가능성 감소.
캐시 볼륨/이미지 고정 /tmp 휘발·재시작 시 캐시 소실 방지 → 재시작 후에도 즉시 서빙, 다운로드 재시도로 인한 불안정 감소.
readiness 전까지 트래픽 차단 모델/캐시 준비 전에 요청이 들어오지 않음 → "아직 준비 안 된 서버"에 부하가 걸리는 상황 자체가 사라짐.

→ ramp-up/재시작 시 크래시·soft-death 확률 감소, 로그에서 보이던 실패 패턴이 재현되기 어려워짐.

2. 실서비스 측면 이점

관점 이점
가용성·SLA 롤링 배포·스케일아웃 시 “준비 안 된 인스턴스”에 트래픽이 가지 않음 → 500/타임아웃 감소. 피크/갑작스런 트래픽에서도 중복 초기화로 인한 크래시 가능성 감소.
사용자 체감(UX) 첫 요청에서 cold start가 발생하지 않음 → 첫 요청 지연·간헐적 500·재시도 감소.
운영·배포 “언제 트래픽 받을 수 있는지”가 명확해져 롤링 업데이트·오토스케일 시 동작이 예측 가능. 재시작 후 캐시 유지로 복구 시간(RTO) 단축. 레이스 컨디션 성 장애 감소.
비용·리소스 캐시 재다운로드 감소 → 네트워크·스토리지·기동 시간 절감. 준비 전 트래픽 차단으로 불필요한 재시도·CPU 사용 감소.

실서비스에서도 가용성, 체감 품질, 운영성, 비용 측면에서 이득이 있음.


new_async가 다운된 이유 (요약)(로그+코드 포함 분석)

원인: new_sync와 같은 임베딩 모델 캐시/초기화 레이스인데, async 구조 때문에 동시 진입이 더 많이 일어나서 더 빨리/같이 죽은 것.


1) 로그에서 보인 것 (new_sync와의 차이)

  • new_sync: NoSuchFile (onnx 파일 없음) → 바로 크래시.
  • new_async:
    • NoSuchFile안 보이거나,
    • Dense 벡터 모델 로드 완료가 여러 번 반복 (같은 모델이 중복 로드되는 정황).

즉, new_async는 “한 번에 파일 없음으로 터진다”기보다 여러 요청이 동시에 get_vector_search / VectorSearch 초기화를 타면서 중복 초기화·캐시 race가 난 상황.


2) 왜 async에서 더 심해졌나

  • async → ramp-up 시 여러 요청이 동시에 들어옴.
  • 그때 VectorSearch 싱글톤에 락이 없었음 → 여러 코루틴이 동시에 “아직 없네?” 하고 여러 번 생성 시도 또는 동시에 같은 캐시/모델 로드.
  • 그 결과:
    • 캐시 다운로드/캐시 쓰기 race 발생,
    • 타이밍 나쁘면 new_sync처럼 NoSuchFile/로딩 실패 → 프로세스 크래시로 이어짐.

문서 표현 그대로면, “구조적으로 async가 더 빨리 죽었을 가능성 높음”.


3) 한 줄로 정리

new_async가 다운된 이유
= 임베딩(및 HF/ONNX) 캐시·초기화 레이스가 근본 원인이고,
async라서 동시에 많은 요청이 초기화를 트리거해 그 레이스가 더 잘 걸리고, 결국 로딩 실패/크래시(hard-death) 로 이어진 것.

그래서 문서에서 VectorSearch DCL 락, warm-up, 캐시 경로, readiness를 “반드시 고칠 것”으로 넣고, 그걸 코드에도 반영한 상태.


문서와 로그 해석 기준으로 보면, “왜 new_async에서는 NoSuchFile이 안 떴는지”는 두 가지로 설명할 수 있음.


1. 타이밍/운 — “그 전에 로드가 성공했을 가능성”

문서에 나온 대로:

이 상태도 결국 캐시 다운로드/로드 race를 만들기 쉬워서, 타이밍 나쁘면 new_sync처럼 NoSuchFile로도 이어질 수 있어(이번 로그에선 그 전에 "운 좋게" 로드가 성공했을 가능성이 큼).

  • new_sync: 요청이 순차로 들어와서, 첫 요청이 너무 일찍 모델 로드를 시도 → 캐시 다운로드가 끝나기 전에 model.onnx를 열려다 → NoSuchFile.
  • new_async: 요청이 동시에 많이 들어와서,
    • 여러 코루틴이 동시에 초기화를 트리거하고
    • 그중 어느 하나가 먼저 다운로드/로드를 끝내고
    • 나머지는 그 뒤에 로드를 시도해, 그때는 이미 파일이 있어서 NoSuchFile이 안 나온 경우.

즉, 같은 race가 있어도 “파일 없을 때 로드”를 누가 먼저 겪느냐가 sync/async 타이밍에 따라 달라져서,
이번 new_async 로그에서는 NoSuchFile이 발생하기 전에, 어떤 요청이 “운 좋게” 로드에 성공한 타이밍이었을 수 있다는 뜻.


2. 다른 이유로 먼저 죽었을 수 있음

  • new_async는 중복 초기화(Dense 벡터 모델 로드 완료 여러 번)가 보이고, 그 과정에서:
    • 다른 예외(중복 로드, 리소스 부족, 레이스로 인한 다른 에러)로 프로세스가 먼저 죽었거나
    • NoSuchFile이 나올 경로(예: 나중에 오는 요청이 아직 캐시가 안 된 상태에서 로드 시도)까지 가지 못하고 크래시했을 수 있음.
  • 그러면 로그에는 NoSuchFile이 안 찍히고, “여러 번 로드 완료”만 보이다가 그다음에 죽은 형태가 됨.

정리

  • 구조적으로는 new_async도 new_sync와 같은 캐시/초기화 레이스 때문에 NoSuchFile이 날 수 있는 상황.
  • 이번 로그에서 NoSuchFile이 안 보인 이유는:
    1. 타이밍 — 동시 요청 중 일부가 “캐시 준비된 뒤” 로드를 시도해서, 이번 런에서는 NoSuchFile이 안 나왔을 가능성(문서의 “운 좋게 로드 성공”),
    2. 또는 그 전에 중복 초기화/다른 예외로 프로세스가 죽어서, NoSuchFile이 발생할 기회가 없었을 가능성,

둘 다 설득력 있습니다. 그래서 문서에서 “당장 NoSuchFile은 안 찍혔지만 구조적으로 안전하다고 보긴 어려워”라고 한 것.


new_sync vs new_async — 죽는 방식 비교

구분 new_sync new_async
로그에 보이는 것 NO_SUCHFILE, Load model ... onnx/model.onnx failed. File doesn't existUnhandled exception Dense 벡터 모델 로드 완료여러 번 반복 (중복 초기화). NoSuchFile은 안 보일 수 있음
트리거 (상대적으로) 순차 요청 중, 너무 이른 요청이 모델 로드를 시도 동시에 많은 요청이 들어와서 여러 코루틴이 한꺼번에 VectorSearch/모델 로드 트리거
직접 원인 캐시 다운로드 끝나기 전에 한 요청이 model.onnx 로드 시도 → 파일 없음 → 예외 여러 요청이 동시에 초기화 → 중복 로드·캐시 race → 그 과정에서 실패/크래시
결과 Hard-death: unhandled exception → 프로세스 종료 → 컨테이너 사라짐 Hard-death: 같은 근본 원인(캐시/초기화 레이스)으로 역시 프로세스 종료. “더 빨리” 죽었을 가능성

한 줄로 정리

  • new_sync:
    한 요청이 너무 일찍 로드 시도 → NoSuchFile로 바로 터짐”이 로그에 그대로 찍힌 명백한 크래시.
  • new_async:
    여러 요청이 동시에 초기화를 타서 로드가 여러 번 돌고, 그 레이스로 같은 종류의 크래시”가 나지만,
    로그에는 중복 로드가 먼저 보이고, NoSuchFile은 안 보이거나 다른 예외로 죽을 수 있음.

즉, 근본 원인(캐시/초기화 레이스)은 같고,

  • new_sync = “너무 이른 한 번의 로드” → NoSuchFile로 확실히 죽음.
  • new_async = “동시에 여러 번 로드” → 중복 초기화 정황으로 죽음.
  • 죽는 방식(로그·트리거)만 다르고, 둘 다 hard-death*.

최종 결론

1. 아키텍쳐 선택

new_async 선택. 왜냐면, 비동기 아키텍쳐가 프로세스가 OOM으로 죽지 않게 해주기 때문에 비동기 아키텍쳐를 선택했다. 그러고, 위에서 말한 현재 비동기 아키텍쳐의 문제점을 해결하면 된다.

 

1. new_async 메커니즘 변경

Race Condition이 원인이라는 걸 알았으니 결론은 하나, 위에서 말했듯, 모델을 start-up 단계에서 로드하는 식으로 변경해. 앱이 모델 다운로드를 먼저 진행하고, 켜지는 방식이다. 이를 통해 콜드 스타트로 인한 Race Condition을 막는다.

2. 추가적으로 비동기에서 최대한 OOM으로 죽지 않게 하려는 여러 조치

우선 비동기 방식으로 이벤트 루프가 돌 때, 동기 호출이 안에 있다면, 루프는 반드시 막히고, 결국 그게 OOM으로 이어진다.

따라서 추가적인 조치를 취했다.

 

a. 스레드 블로킹을 LLM에게도 적용.

기존엔 스레드 블로킹이 sentiment 모델만 필요한 줄 알았지만, 모델은 기본적으로 sync 연산이다. 따라서, LLM도 마찬가지고, 따라서, LLM에 대해서도 to_thread()를 적용해 스레드 블로킹을 적용한다.

 

b. 비동기큐 설정과 워커 설정과 세마포 설정

현재 세마포는 설정이 안되어 있었다. 비동기 루프로 돌거면, 세마포는 사실 필수다. 왜냐면, 만약 세마포가 없다면, 동시처리 수가 늘어나는데, 그 처리가 감당 가능한 양을 벗어난다면, OOM이 발생한다. 비동기큐는 개인적으로는 요청을 비동기 루프에서 명시적으로 관리할 수 있기에 필수라고 생각한다. 또한, 비동기 루프 안에 sync 연산인 sentiment 모델이나, LLM이 있다면, async 연산처럼 볼수가 없기 때문에, 처리 성능에 대한 명시적 관리가 필요하다. 그 역할을 비동기큐가 한다고 생각한다. 구체적으로 말하면, 현재 LLM의 태스크는 리뷰 요약 태스크이기 때문에, 길이에 따라 연산의 양이 달라진다. 그럼, 큐이기 때문에 Starvation 문제가 발생할 가능성이 높다. 이에 대해 기본 셋팅인 FIFO말고 라운드로빈 방식을 적용해 볼수도 있고, 필요하면 리뷰 길이에 따라 큐의 순서를 다르게 하는 커스텀 메커니즘을 짜는 것도 가능하다. 워커는 동시처리를 위해 존재한다. 워커가 있어야 sync 연산인 모델 연산도 동시처리가 가능해진다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/09   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함