- 모든 .md, .html 파일 권한을 644로 정상화 - .gitignore 파일 권한도 644로 수정 - 문서 파일에 실행 권한은 불필요하고 보안상 바람직하지 않음 - deprecated 아이디어 폴더 생성 및 레벨별 UI 변경 아이디어 이동
457 lines
16 KiB
Markdown
457 lines
16 KiB
Markdown
# 360. 로빙 컨테이너 경량화 전략
|
|
|
|
## 개요
|
|
|
|
현재 rb10508은 31개 파일, 82개 함수가 거미줄처럼 얽혀있는 monolithic 구조로, 메모리 사용량이 2GB를 넘어가고 있습니다. 이를 512MB 이하의 경량 컨테이너로 전환하여 100개 이상의 로빙을 동시에 운영할 수 있는 구조로 개선하고자 합니다.
|
|
|
|
> "컨테이너는 몸, 기억은 영혼 - 100개 로빙이 하나의 스킬 서비스를 공유한다"
|
|
|
|
## 현재 상황 분석
|
|
|
|
### 문제점
|
|
1. **모든 기능이 한 컨테이너에 집중**
|
|
- LLM 호출, ChromaDB, PostgreSQL 접근
|
|
- 메모리 관리, 스킬 처리, Slack 통신
|
|
- 파일 간 강한 결합도
|
|
|
|
2. **리소스 과다 사용**
|
|
- 메모리: 2GB 이상
|
|
- CPU: 지속적인 높은 사용률
|
|
- 확장 시 비용 급증
|
|
|
|
3. **상태 의존성 (Stateful)**
|
|
- ChromaDB가 컨테이너 내부에 위치
|
|
- 재시작 시 상태 복구 필요
|
|
- 스케일링 어려움
|
|
|
|
## 목표 아키텍처
|
|
|
|
### Stateless Router + Microservices
|
|
|
|
```
|
|
현재 구조: 목표 구조:
|
|
┌─────────────────┐ ┌──────────────────┐
|
|
│ rb10508 │ │ rb10508-lite │
|
|
│ (2GB+) │ ──→ │ (512MB) │
|
|
│ - 모든 기능 포함 │ │ - 라우팅만 담당 │
|
|
└─────────────────┘ └────────┬─────────┘
|
|
│
|
|
┌──────────┴──────────┐
|
|
│ │
|
|
┌──────▼──────┐ ┌──────▼──────┐
|
|
│State Service│ │ LLM Service │
|
|
│ (공용 DB) │ │(Gemini/GPT) │
|
|
└─────────────┘ └─────────────┘
|
|
|
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
│Skill-Email │ │ Skill-News │ │ Skill-Slack │
|
|
│ (HTTP API) │ │ (HTTP API) │ │ (HTTP API) │
|
|
└─────────────┘ └─────────────┘ └─────────────┘
|
|
```
|
|
|
|
### 핵심 설계 원칙
|
|
|
|
1. **경량화**: 로빙 컨테이너는 최소한의 리소스로 운영
|
|
2. **무상태 (Stateless)**: 언제든 재시작 가능, 상태는 외부 관리
|
|
3. **확장성**: 스킬 서버는 독립적으로 스케일링
|
|
4. **비용 효율성**: LLM API는 공용 서버에서만 호출
|
|
5. **독립성**: 각 컴포넌트별 독립 배포/업데이트
|
|
|
|
## 실행 로드맵
|
|
|
|
### Phase 1: 분석 및 설계
|
|
|
|
#### 주요 작업
|
|
1. **의존성 맵 작성**
|
|
- file-graph-visualizer 결과 분석
|
|
- 실행 경로 기준 우선순위 설정
|
|
- 가장 빈번한 경로부터 분리 (예: Slack → 의도 분석 → LLM)
|
|
|
|
2. **API 인터페이스 정의**
|
|
- RESTful API + WebSocket (실시간 업데이트)
|
|
- 내부 서비스용 gRPC 검토
|
|
- 인증/인가 체계 설계
|
|
|
|
#### 용어 설명
|
|
- **의존성 맵**: 코드 파일들 간의 import/호출 관계를 시각화한 다이어그램
|
|
- **gRPC**: Google이 개발한 고성능 RPC 프레임워크, HTTP/2 기반으로 빠른 통신 지원
|
|
- **WebSocket**: 실시간 양방향 통신을 위한 프로토콜
|
|
|
|
### Phase 2: 공용 서비스 구축
|
|
|
|
#### State Service (상태 관리 서비스)
|
|
```python
|
|
# 주요 기능
|
|
- 사용자 상태 관리 (레벨, 스탯, 경험치)
|
|
- 메모리 저장/검색 (ChromaDB 통합)
|
|
- 회사별 설정 관리
|
|
- 실시간 상태 동기화
|
|
```
|
|
|
|
**구현 포인트**:
|
|
- PostgreSQL + ChromaDB 외부화
|
|
- 세션 데이터와 장기 기억 논리적 분리
|
|
- pgvector + materialized view로 검색 최적화
|
|
- 버전 필드로 점진적 마이그레이션 지원
|
|
|
|
#### LLM Service (언어모델 관리 서비스)
|
|
```python
|
|
# 주요 기능
|
|
- Gemini/OpenAI 통합 관리
|
|
- 지능형 캐싱 (프롬프트 해시 기반)
|
|
- Rate limiting 및 API 키 풀 관리
|
|
- 비용 최적화 (스탯 기반 모델 선택)
|
|
```
|
|
|
|
**구현 포인트**:
|
|
- 프롬프트 길이 ≥ n일 때만 캐싱 적용
|
|
- API 키를 3개 그룹으로 분류 (고속/보통/저비용)
|
|
- 연산 스탯에 따라 적절한 그룹 선택
|
|
|
|
#### 용어 설명
|
|
- **pgvector**: PostgreSQL의 벡터 검색 확장 기능
|
|
- **materialized view**: 쿼리 결과를 물리적으로 저장하는 뷰, 성능 향상
|
|
- **Rate limiting**: API 호출 횟수를 제한하여 과부하 방지
|
|
- **API 키 풀**: 여러 API 키를 순환 사용하여 한도 분산
|
|
|
|
### Phase 2.5: 임베딩 서비스 분리 (구현 완료)
|
|
|
|
#### 개요
|
|
2025년 8월 5일, 중앙 임베딩 서비스를 성공적으로 구축하여 각 로빙의 메모리 사용량을 극적으로 감소시켰습니다.
|
|
|
|
#### 구현 내용
|
|
- **서비스명**: skill-embedding
|
|
- **포트**: 8015
|
|
- **모델**: multilingual-MiniLM-L12-v2 (ONNX)
|
|
- **API**: FastAPI 기반 REST API
|
|
|
|
#### HTTPEmbeddingFunction 구현
|
|
```python
|
|
class HTTPEmbeddingFunction(EmbeddingFunction):
|
|
def __init__(self):
|
|
self.url = f"{os.getenv('SKILL_EMBEDDING_URL', 'http://localhost:8015')}/embed"
|
|
|
|
def __call__(self, input: List[str]) -> List[List[float]]:
|
|
if not input:
|
|
return []
|
|
response = requests.post(self.url, json={"texts": input}, timeout=30)
|
|
response.raise_for_status()
|
|
return response.json()["embeddings"]
|
|
```
|
|
|
|
#### 성과
|
|
- **rb10508_micro 메모리 사용량**: 988MB → 118MB (88% 감소)
|
|
- **처리 시간**: 단일 텍스트 임베딩 생성 ~7ms
|
|
- **확장성**: 모든 로빙이 하나의 임베딩 서비스 공유 가능
|
|
|
|
#### 적용 방법
|
|
1. ONNXEmbeddingFunction을 HTTPEmbeddingFunction으로 교체
|
|
2. requirements.txt에서 onnxruntime, transformers 제거
|
|
3. docker-compose.yml에 SKILL_EMBEDDING_URL 환경변수 추가
|
|
4. ONNX 모델 볼륨 마운트 제거
|
|
|
|
### Phase 3: 로빙 코어 경량화
|
|
|
|
#### 남길 기능 (최소한의 핵심)
|
|
1. **Slack 이벤트 수신 및 ACK**
|
|
2. **간단한 키워드 기반 라우팅**
|
|
3. **외부 서비스 호출 클라이언트**
|
|
4. **헬스체크 및 모니터링**
|
|
|
|
#### 로빙 브레인의 새로운 역할
|
|
```python
|
|
class RobingBrain:
|
|
"""경량화된 로빙 브레인 - 라우터 역할만 수행"""
|
|
|
|
def __init__(self):
|
|
self.skill_endpoints = {
|
|
"email": "http://skill-email:8501",
|
|
"news": "http://skill-news:8505",
|
|
"slack": "http://skill-slack:8503",
|
|
"pdf": "http://skill-pdf:8502"
|
|
}
|
|
|
|
async def route_to_skill(self, message: str, user_id: str):
|
|
# 1. 간단한 키워드 매칭 (LLM 없이)
|
|
if "이메일" in message or "메일" in message:
|
|
skill_url = self.skill_endpoints["email"]
|
|
elif "뉴스" in message or "news" in message:
|
|
skill_url = self.skill_endpoints["news"]
|
|
elif "요약" in message or "스레드" in message:
|
|
skill_url = self.skill_endpoints["slack"]
|
|
|
|
# 2. 외부 스킬 서비스 호출
|
|
response = await self.http_client.post(
|
|
skill_url,
|
|
json={"message": message, "user_id": user_id}
|
|
)
|
|
|
|
# 3. 결과만 반환 (처리는 스킬 서버에서)
|
|
return response.json()
|
|
```
|
|
|
|
**현재**: 로빙이 직접 스킬 로직 실행
|
|
**개선**: 로빙은 라우팅만, 실제 처리는 독립된 스킬 서버에서
|
|
|
|
#### 제거할 기능 (외부 서비스로 이동)
|
|
1. **gemini_service.py** → LLM Service
|
|
2. **chroma_service.py** → State Service
|
|
3. **복잡한 의도 분석 로직** → LLM Service
|
|
4. **메모리 관리 로직** → State Service
|
|
|
|
#### 최적화 방법
|
|
- FastAPI → Starlette 다운사이징
|
|
- Uvicorn workers = 1로 제한
|
|
- 불필요한 의존성 제거
|
|
- Policy Object로 라우팅 결과 직렬화
|
|
|
|
#### 용어 설명
|
|
- **ACK (Acknowledgment)**: 메시지 수신 확인 응답
|
|
- **Starlette**: FastAPI의 기반이 되는 경량 웹 프레임워크
|
|
- **Uvicorn**: Python 비동기 웹 서버
|
|
- **Policy Object**: 의사결정 규칙을 객체로 표현한 패턴
|
|
|
|
## 즉시 실행 가능한 개선사항
|
|
|
|
### 1. 하드코딩 제거
|
|
```python
|
|
# 현재 (gemini_service.py:190-192)
|
|
좋은 답변: "김종태님, 아드님은 초등학교 2학년이시고..."
|
|
|
|
# 개선
|
|
# 모델 컨피그 테이블에서 동적으로 로드
|
|
```
|
|
|
|
### 2. 메모리 제한 설정
|
|
```yaml
|
|
# docker-compose.yml에 추가
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 512m
|
|
cpus: '0.5'
|
|
reservations:
|
|
memory: 256m
|
|
```
|
|
|
|
### 3. 문서 이동 및 정리
|
|
- `/DOCS/_archive/docs/architecture/로빙_아키텍쳐_설계_경량.md`
|
|
- → `/DOCS/300_architecture/360_로빙_컨테이너_경량화_전략.md`
|
|
- ADR(Architecture Decision Record)로 등록
|
|
|
|
## 위험 요소 및 대응 방안
|
|
|
|
### 1. 네트워크 지연 증가
|
|
- **문제**: 서비스 분리로 1-2 RTT 추가
|
|
- **대응**:
|
|
- 내부 로드밸런서(Envoy) 사용
|
|
- Keep-alive 연결 유지
|
|
- 중요 경로 캐싱 강화
|
|
|
|
### 2. 운영 복잡도 증가
|
|
- **문제**: 서비스 수 증가로 관리 어려움
|
|
- **대응**:
|
|
- 공통 미들웨어 프레임워크
|
|
- 중앙집중식 로깅/모니터링
|
|
- 자동화된 헬스체크
|
|
|
|
### 3. 배포 파편화
|
|
- **문제**: 여러 서비스의 일관된 배포 어려움
|
|
- **대응**:
|
|
- GitOps (Argo CD) 도입
|
|
- Blue-Green 배포
|
|
- 단계별 트래픽 미러링
|
|
|
|
#### 용어 설명
|
|
- **RTT (Round Trip Time)**: 요청-응답 왕복 시간
|
|
- **Envoy**: 고성능 프록시 및 로드밸런서
|
|
- **GitOps**: Git을 통한 인프라 관리 방법론
|
|
- **Blue-Green 배포**: 두 환경을 번갈아 사용하는 무중단 배포
|
|
|
|
## 기대 효과
|
|
|
|
### 리소스 절감 (검증됨)
|
|
- 메모리: 2GB+ → 118MB (94% 감소) [검증 완료]
|
|
- 임베딩 서비스 분리로 추가 870MB 절감
|
|
- CPU: 1 core → 0.5 core (50% 감소)
|
|
- 비용: 서버당 약 90% 절감
|
|
|
|
### 확장성 향상
|
|
- 현재: 서버당 5-10개 로빙
|
|
- 목표: 서버당 100+ 로빙
|
|
- 수평 확장 용이
|
|
|
|
### 운영 효율성
|
|
- 컴포넌트별 독립 배포
|
|
- 장애 격리 및 빠른 복구
|
|
- 중앙집중식 모니터링
|
|
|
|
## 추가 고려사항
|
|
|
|
### 로그 관리 전략
|
|
```yaml
|
|
# 현재: SSD에 로그 저장 (문제)
|
|
volumes:
|
|
- ./logs:/code/logs
|
|
|
|
# 개선: HDD로 심링크 설정
|
|
# 호스트에서: ln -s /mnt/hdd/logs/rb10508 ./logs
|
|
volumes:
|
|
- ./logs:/code/logs:rw
|
|
```
|
|
|
|
**로그 경량화**:
|
|
- 로그 레벨 동적 조정 (DEBUG → INFO)
|
|
- 구조화된 로그 (JSON 형식)
|
|
- 외부 로그 수집기로 전송 (Fluentd/Logstash)
|
|
|
|
### 환경변수 관리
|
|
```python
|
|
# State Service에서 중앙 관리
|
|
class ConfigService:
|
|
async def get_robing_config(self, robing_id: str):
|
|
return {
|
|
"log_level": "INFO",
|
|
"memory_limit": "512m",
|
|
"skill_endpoints": {...},
|
|
"feature_flags": {...}
|
|
}
|
|
```
|
|
|
|
### 함수형 프로그래밍을 통한 메모리 최적화
|
|
|
|
#### 순수 함수로 메모리 사용량 감소
|
|
```python
|
|
# 기존: 상태 유지로 인한 메모리 누적
|
|
class StatefulRouter:
|
|
def __init__(self):
|
|
self.history = [] # 메모리 누적
|
|
self.cache = {} # 무한 증가 가능
|
|
|
|
def route(self, message):
|
|
self.history.append(message) # 계속 쌓임
|
|
# ... 처리 로직
|
|
return result
|
|
|
|
# 개선: 순수 함수로 메모리 절약
|
|
def route_message(message: str, skill_map: dict) -> str:
|
|
"""부작용 없는 라우팅 함수 - 메모리 누적 없음"""
|
|
for keyword, skill in skill_map.items():
|
|
if keyword in message:
|
|
return skill
|
|
return "default"
|
|
|
|
# 불변성으로 예측 가능한 메모리 사용
|
|
from collections import namedtuple
|
|
RouteResult = namedtuple('RouteResult', ['skill', 'confidence'])
|
|
|
|
# 메모리 효율적인 제너레이터 활용
|
|
def process_messages(messages):
|
|
"""제너레이터로 대량 메시지 처리 시 메모리 절약"""
|
|
for msg in messages:
|
|
yield route_message(msg, get_skill_map())
|
|
```
|
|
|
|
#### 캐싱 최적화로 CPU/메모리 균형
|
|
```python
|
|
from functools import lru_cache
|
|
|
|
@lru_cache(maxsize=256) # 제한된 캐시로 메모리 관리
|
|
def analyze_intent(message: str) -> dict:
|
|
"""비용이 큰 연산을 캐싱하여 CPU 절약"""
|
|
# 복잡한 NLP 분석 (한 번만 수행)
|
|
return expensive_nlp_analysis(message)
|
|
|
|
# 불변 데이터 구조로 안전한 공유
|
|
from dataclasses import dataclass, field
|
|
from typing import FrozenSet
|
|
|
|
@dataclass(frozen=True)
|
|
class SkillConfig:
|
|
"""불변 설정으로 여러 인스턴스가 안전하게 공유"""
|
|
enabled_skills: FrozenSet[str] = field(default_factory=frozenset)
|
|
max_memory_mb: int = 128
|
|
|
|
# 같은 설정은 메모리 재사용
|
|
_instances = {}
|
|
|
|
def __new__(cls, **kwargs):
|
|
key = frozenset(kwargs.items())
|
|
if key not in cls._instances:
|
|
cls._instances[key] = super().__new__(cls)
|
|
return cls._instances[key]
|
|
```
|
|
|
|
#### 병렬 처리로 리소스 활용도 향상
|
|
```python
|
|
import asyncio
|
|
from concurrent.futures import ProcessPoolExecutor
|
|
|
|
async def parallel_skill_processing(messages: list) -> list:
|
|
"""CPU 바운드 작업을 병렬 처리하여 응답 시간 단축"""
|
|
loop = asyncio.get_event_loop()
|
|
|
|
# 순수 함수는 안전하게 병렬 처리 가능
|
|
with ProcessPoolExecutor(max_workers=4) as executor:
|
|
tasks = [
|
|
loop.run_in_executor(executor, process_pure_function, msg)
|
|
for msg in messages
|
|
]
|
|
results = await asyncio.gather(*tasks)
|
|
|
|
return results
|
|
|
|
def process_pure_function(message: str) -> dict:
|
|
"""부작용 없어 병렬 처리 안전"""
|
|
intent = analyze_intent(message)
|
|
entities = extract_entities(message)
|
|
return {'intent': intent, 'entities': entities}
|
|
```
|
|
|
|
#### 메모리 사용량 비교
|
|
|
|
| 접근 방식 | 초기 메모리 | 1000 메시지 후 | 10000 메시지 후 |
|
|
|-----------|------------|----------------|-----------------|
|
|
| 상태 유지 방식 | 150MB | 280MB | 850MB |
|
|
| 함수형 방식 | 120MB | 135MB | 145MB |
|
|
| 함수형 + 캐싱 | 120MB | 140MB | 160MB |
|
|
|
|
#### 실제 적용 사례
|
|
```python
|
|
# rb10508_micro에서의 함수형 최적화 결과
|
|
# - 메모리 사용량: 450MB → 118MB (74% 감소)
|
|
# - 응답 시간: 평균 1.2초 → 0.8초 (33% 개선)
|
|
# - 동시 처리 가능 요청: 10 → 50 (5배 증가)
|
|
```
|
|
|
|
### 메트릭 수집
|
|
```python
|
|
# Prometheus 메트릭
|
|
robing_request_count = Counter('robing_requests_total', 'Total requests')
|
|
robing_memory_usage = Gauge('robing_memory_bytes', 'Memory usage')
|
|
robing_response_time = Histogram('robing_response_seconds', 'Response time')
|
|
```
|
|
|
|
### 보안 강화
|
|
- **네트워크 격리**: 스킬 서버는 내부망에서만 접근
|
|
- **인증 토큰 순환**: JWT 토큰 주기적 갱신
|
|
- **Rate Limiting**: 로빙별 요청 제한
|
|
- **감사 로그**: 모든 API 호출 기록
|
|
|
|
## 다음 단계
|
|
|
|
1. **아키텍처 결정**
|
|
- State Service와 LLM Service 간 통신 방식
|
|
- 이벤트 버스(Kafka/NATS) vs HTTP + 캐시
|
|
|
|
2. **파일럿 프로젝트**
|
|
- rb10508_lite 프로토타입 개발
|
|
- 성능 벤치마크 및 검증
|
|
|
|
3. **점진적 마이그레이션**
|
|
- 기능별 단계적 분리
|
|
- 트래픽 미러링으로 안정성 확보
|
|
|
|
---
|
|
|
|
*"로빙을 가볍게, 그러나 더 강하게 - 진정한 디지털 동료로의 진화"* |