DOCS/300_architecture/370_임베딩_서비스_분리_아키텍처.md
happybell80 725ad0876c fix: 문서 파일 실행 권한 제거
- 모든 .md, .html 파일 권한을 644로 정상화
- .gitignore 파일 권한도 644로 수정
- 문서 파일에 실행 권한은 불필요하고 보안상 바람직하지 않음
- deprecated 아이디어 폴더 생성 및 레벨별 UI 변경 아이디어 이동
2025-08-18 00:37:51 +09:00

8.4 KiB

370. 임베딩 서비스 분리 아키텍처

개요

각 로빙이 독립적으로 ONNX 임베딩 모델을 로드하던 구조에서 중앙 임베딩 서비스를 공유하는 구조로 전환하여, 메모리 사용량을 극적으로 감소시키고 확장성을 확보했습니다.

"모든 로빙이 하나의 임베딩 서비스를 공유하면, 메모리는 절약되고 성능은 유지된다."

왜 임베딩 서비스를 분리했나?

임베딩이란?

임베딩 = 텍스트를 숫자(벡터)로 변환하는 과정

  • "안녕하세요" → [0.1, 0.3, -0.5, ...] (384개의 숫자)
  • 비슷한 의미의 텍스트는 비슷한 숫자 패턴을 가짐
  • 로빙이 "의미"를 이해하는 핵심 기술

분리 전 문제점

  1. 메모리 낭비: 각 로빙이 동일한 모델(449MB)을 각자 로드
    • 로빙 10개 = 4.5GB 메모리 낭비
    • 로빙 100개 = 45GB (서버 메모리 초과)
  2. 시작 시간 지연: 각 로빙이 시작할 때마다 모델 로드 (5-10초)
  3. 업데이트 어려움: 모델 업데이트 시 모든 로빙 재시작 필요

분리 후 장점

  1. 메모리 절약: 모델 하나만 로드하고 모두가 공유
  2. 빠른 시작: 로빙은 API 호출만 하면 됨 (모델 로드 불필요)
  3. 쉬운 관리: 임베딩 서비스만 업데이트하면 모든 로빙이 자동 적용

비유: 각자 사전을 들고 다니던 것 → 도서관에 사전 하나 두고 필요할 때 찾아보기

다른 선택지와 비교:

  • 각자 모델 로드: 메모리 낭비, 관리 복잡
  • 클라우드 API 사용: 비용 발생, 네트워크 의존
  • CPU 임베딩: 너무 느림 (10배 이상)

해결책: 중앙 임베딩 서비스

아키텍처

이전:                              이후:
┌─────────────┐                   ┌─────────────┐
│  rb10508    │                   │  rb10508    │
│ ONNX Model  │                   │   (118MB)   │──┐
│  (988MB)    │                   └─────────────┘  │
└─────────────┘                                     │
                                                    ▼
┌─────────────┐                   ┌─────────────┐  ┌─────────────────┐
│   rb8001    │                   │   rb8001    │  │ skill-embedding │
│ ONNX Model  │         →         │   (200MB)   │──│   (874.4MB)     │
│  (416MB)    │                   └─────────────┘  │  - ONNX Model   │
└─────────────┘                                     │  - Port 8015    │
                                                    ▼  └─────────────────┘
┌─────────────┐                   ┌─────────────┐
│  rb10408    │                   │  rb10408    │
│ ONNX Model  │                   │   (30MB)    │──┘
│   (55MB)    │                   └─────────────┘
└─────────────┘

기술 스택 선택 이유

  • FastAPI + Uvicorn:

    • 왜? Python 기반 최속 웹 프레임워크, 비동기 처리로 동시 요청 효율적 처리
    • 다른 선택지: Flask (동기 처리로 느림), Django (너무 무거움)
  • ONNX Runtime:

    • 왜? PyTorch보다 3배 빠르고 메모리 50% 절약
    • 다른 선택지: PyTorch (메모리 2배 사용), TensorFlow (구성 복잡)
    • ONNX = Open Neural Network Exchange (어떤 프레임워크에서도 사용 가능)
  • multilingual-MiniLM-L12-v2:

    • 왜? 한국어 포함 100개 언어 지원, 크기 대비 성능 우수 (134MB)
    • 다른 선택지: ko-sroberta (한국어만, 400MB), BERT-large (너무 큼, 1.3GB)

API 설계 (간단명료한 인터페이스)

# POST /embed - 텍스트를 벡터로 변환
# 요청:
{
    "texts": ["안녕하세요", "오늘 날씨가 좋네요"]
}

# 응답: (각 텍스트가 384개 숫자로 변환됨)
{
    "embeddings": [
        [0.1, 0.2, ...],  # "안녕하세요"의 벡터 표현
        [0.3, 0.4, ...]   # "오늘 날씨가 좋네요"의 벡터 표현
    ]
}

# GET /health
{
    "status": "healthy",
    "service": "skill-embedding",
    "model": "multilingual-MiniLM-L12-v2",
    "uptime": 3600.5
}

구현 가이드

1. 로빙 측 변경사항 (코드 주석 강화)

# 기존: ONNX 모델을 각 로빙이 직접 로드 (메모리 낭비)
from onnx_embedder import ONNXEmbedder
embedder = ONNXEmbedder("/models/onnx/...")  # 449MB 모델 로드

# 변경: 임베딩 서비스에 HTTP 요청만 (경량화)
class HTTPEmbeddingFunction(EmbeddingFunction):
    def __init__(self):
        # 임베딩 서비스 URL (환경변수로 설정 가능)
        self.url = f"{os.getenv('SKILL_EMBEDDING_URL', 'http://localhost:8015')}/embed"
    
    def __call__(self, input: List[str]) -> List[List[float]]:
        # 텍스트를 서비스로 보내고 벡터 받아오기
        response = requests.post(self.url, json={"texts": input}, timeout=30)
        return response.json()["embeddings"]  # 384차원 벡터 반환

2. ChromaDB 통합 (로빙의 기억 저장소)

# memory.py - 로빙의 기억을 저장하는 코드
self.episodic = self.client.get_or_create_collection(
    name=f"{self.robing_id}_episodic",  # 각 로빙마다 독립된 기억 공간
    embedding_function=HTTPEmbeddingFunction()  # 임베딩은 공유 서비스 사용
)
# 결과: 기억은 각자, 임베딩 엔진은 공유

3. Docker 구성 변경

# 불필요한 볼륨 제거
# - /opt/models:/models:ro  # 더 이상 필요 없음

# 환경변수 추가
environment:
  - SKILL_EMBEDDING_URL=http://localhost:8015

성능 분석

메모리 절감 (실측 데이터)

로빙 이름    | 이전 메모리 | 이후 메모리 | 절감량
------------|------------|------------|--------
rb10508     | 988MB      | 118MB      | -870MB (88%↓)
rb8001      | 416MB      | 200MB      | -216MB (52%↓)  
rb10408     | 55MB       | 30MB       | -25MB  (45%↓)

예상 절감:
- 10개 로빙: 8.7GB 절약
- 100개 로빙: 87GB 절약 (서버 1대 가격)

속도 영향

  • HTTP 통신 추가 시간: +7ms (전체의 0.2%)
  • 실제 사용자 체감: 차이 없음 (전체 응답 1-3초)

확장성

  • 단일 임베딩 서비스로 수백 개 로빙 지원
  • 수평 확장 가능 (로드밸런서 적용 시)

모니터링

# 주요 메트릭
- 임베딩 생성 요청 
- 평균 응답 시간
- 메모리 사용량
- 에러율

# 헬스체크
curl http://localhost:8015/health

다음 단계

  1. 캐싱 레이어 추가: Redis로 자주 사용되는 임베딩 캐싱
  2. 배치 처리 최적화: 대량 텍스트 임베딩 성능 개선
  3. 모델 업데이트: L12 → L6 모델로 추가 경량화 검토
  4. 다른 로빙 적용: rb8001, rb10408에 순차 적용

코드로 보는 효과

# 전체 시스템 메모리 사용량 계산
def calculate_memory_usage(num_robeings: int, shared_embedding: bool):
    """
    임베딩 서비스 공유 여부에 따른 메모리 사용량 계산
    """
    base_memory_per_robeing = 118  # MB (임베딩 제외한 기본 메모리)
    embedding_model_size = 449  # MB (임베딩 모델 크기)
    
    if shared_embedding:
        # 임베딩 서비스 공유: 모델 한 번만 로드
        total = (num_robeings * base_memory_per_robeing) + embedding_model_size
    else:
        # 각자 로드: 모든 로빙이 모델 복사
        total = num_robeings * (base_memory_per_robeing + embedding_model_size)
    
    return total

# 비교 예시
print(f"10개 로빙 (공유 X): {calculate_memory_usage(10, False)}MB")  # 5,670MB
print(f"10개 로빙 (공유 O): {calculate_memory_usage(10, True)}MB")   # 1,629MB
print(f"절약률: 71%")

교훈

  • 중복 제거의 힘: 동일한 기능을 서비스로 분리하면 극적인 효율성 향상
  • 간단한 구현: 12줄의 HTTPEmbeddingFunction으로 870MB 절약
  • 점진적 적용: 하나의 로빙에서 성공 후 확산
  • 공유와 독립의 균형: 임베딩 엔진은 공유, 기억과 개성은 독립 유지

"임베딩은 공유하되, 기억은 분리하라"