서비스 인프라 확장성과 모니터링 설계 - 100-hours-a-week/KTB4-13th-wiki GitHub Wiki
북적북적 AI 서버를 운영 환경에 배포한다고 할 때의 배포 단위, 확장 방식, 장애 대응, 모니터링 계획이다.
설계의 출발점은 둘이다.
AI 서버는 무상태다. 대화도, 추천 카드도, 추천 목록도 저장하지 않는다. 추천 조건과 최근 대화를 요청마다 받아 처리하므로 어느 인스턴스로 요청이 가도 결과가 같다. 트래픽이 늘 때 바꿀 것은 EC2 대수뿐이고 세션 고정도 필요 없다.
임베딩은 자체 서빙한다. 외부 임베딩 API가 검색 지연의 대부분을 차지하고 처리량이 쿼터에 묶이는데, 자체 서빙으로 바꾸면 지연이 99배 빨라진다(4-1). 한국어 검색 품질이 앞서는 오픈 모델을 API로는 살 수 없기도 하다. 그래서 임베딩 컨테이너는 후속 과제가 아니라 V1 배포 구성에 포함된다. 모델은 e5-small을 잠정으로 두고 bge-m3와 비교해 확정한다.
(예: 여러 모델 서비스 인스턴스와 로드밸런서 구성, 오토스케일링 정책 등)
| 성격 | 경로와 하는 일 | 부하 특징 | 컨테이너 |
|---|---|---|---|
| 조회 | ① 검색(검색어 임베딩 1회 + 키워드·벡터 조회 + RRF 병합), ④ 홈 피드, ⑥ 취향 프로필, ⑧ 상태 점검 | 요청이 짧아 CPU 사용률이 부하를 그대로 반영한다 | api-sync |
| 생성 | ③ 챗봇 한 턴(LLM 2회 + 임베딩 + 후보 조회, 생성 30초 상한). V2의 ⑤·⑦도 여기로 온다 | 요청 하나가 수 초 동안 외부 LLM을 기다린다. CPU는 낮은데 동시 연결만 쌓인다 | api-gen |
| 임베딩 | ② 텍스트를 벡터로 변환. ①③과 적재 배치가 내부에서 부른다 | 순수 CPU 추론이다. 파이썬 추론은 스레드로는 처리량이 늘지 않아 프로세스 수로 늘린다(4-1) | embedding |
한 컨테이너에 섞으면 챗봇의 대기가 검색 처리를 밀어내고, 임베딩 추론이 검색 응답을 잡아먹는다. 스케일 신호도 셋 다 달라서(1-6) 셋을 나눈다.
규모와 피크 산식은 클라우드 파트 문서를 따르고, 방문당 AI 호출 수만 우리 API 기준으로 넣었다. 실사용 데이터가 생기면 교체한다.
| 항목 | 가정 |
|---|---|
| 기준 규모 | V1 평균 DAU 36 |
| 설계 상한 | 3년 차 목표 평균 DAU 8,000 (V1의 약 220배) |
| 방문당 AI 호출 | 검색 3회, 홈 피드 2회. 방문의 30%가 챗봇을 쓰고 평균 3턴 |
| 피크 |
DAU × 50% × 방문당 호출 ÷ 1,200초 (DAU의 절반이 20분 안에 몰린다) |
| 경로 | 1회 처리 시간 | V1 피크 | 3년 차 피크 | 3년 차 동시 진행 |
|---|---|---|---|---|
| 검색 | 약 50ms (임베딩 5.8ms 포함) | 0.045 rps | 10 rps | 1 |
| 홈 피드 | 약 300ms (DB만) | 0.03 rps | 6.7 rps | 2 |
| 챗봇 | 약 6초 (카드 생성 4,858ms 포함) | 0.014턴/s | 3턴/s | 18 |
3년 차 피크에서도 조회 경로는 10 rps 안팎이다. 챗봇은 다르다. 초당 3턴이면 턴당 LLM 2회라 호출이 분당 360회 나가므로, 인스턴스 수가 아니라 외부 LLM API 쿼터와 예산(3개월 36만 원)이 실제 상한이다. 인프라가 할 일은 그 상한을 넘기 전에 429로 막고 예산 소진 속도를 보이게 하는 것이다.
클라우드 파트의 1단계 구성에 맞춘다. 오케스트레이터 없이 AI EC2 한 대에서 Docker Compose로 컨테이너를 띄운다.
flowchart TB
subgraph APP["App EC2 · 클라우드 파트"]
BE["Nginx · Backend · MySQL"]
end
subgraph AIEC2["AI EC2 1대 · Docker Compose"]
NGX["nginx<br/>경로 라우팅"]
SYNC["api-sync<br/>① /search · ④ /feed<br/>⑥ /profile · ⑧ /health"]
GEN["api-gen<br/>③ /chat"]
EMB["embedding<br/>② /embeddings · CPU"]
PG[("postgres + pgvector<br/>데이터는 EBS")]
REP["replicator"]
ING["ingest job<br/>실행할 때만"]
end
LLM["외부 LLM API"]
BE -->|"ai.internal · 서비스 토큰"| NGX
NGX -->|"③ 경로"| GEN
NGX -->|"나머지 경로"| SYNC
SYNC --> EMB
GEN --> EMB
ING --> EMB
SYNC --> PG
GEN --> PG
ING --> PG
REP -->|"읽기 전용"| BE
REP -->|"단방향 복제"| PG
GEN --> LLM
- BE는 사설 IP 대신 고정 내부 도메인(
ai.internal)으로 부른다. V2에서는 같은 이름이 내부 ALB를 가리키므로, 호스트를 바꾸거나 늘려도 BE 설정이 바뀌지 않는다. - AI EC2는 외부에 공개하지 않고, 보안 그룹은 BE(V2에는 내부 ALB) 요청만 받는다.
/embeddings는 내부 전용이라nginx에 열지 않고, 인증이 없는/health도 이 네트워크 제한으로 막는다.
| 컨테이너 | 담당 | 왜 따로 두나 |
|---|---|---|
nginx |
경로 라우팅 | BE에게 주소 하나만 노출한다. V2에 로드밸런서를 붙여도 BE 쪽 계약이 안 바뀐다 |
api-sync (조회) |
①④⑥⑧ | LLM 장애와 무관하게 살아 있어야 하는 경로다. 검색·홈 피드가 죽으면 커머스 화면이 빈다 |
api-gen (생성) |
③ (V2 ⑤⑦) | 요청 하나가 수 초를 점유한다. 같이 두면 검색 처리를 밀어낸다 |
embedding |
② | 순수 CPU 추론. 모델 가중치를 이미지에 포함해 시작 시 내려받지 않는다 |
postgres |
AI 소유 3종 + 복제 5종 | 데이터는 EBS에 둔다. 확장할 때 가장 먼저 호스트에서 떼어낼 대상이다 |
replicator |
BE MySQL → AI Postgres | 중복 실행 금지. BE MySQL 접속 정보는 이 컨테이너만 가진다 |
ingest job |
초기 적재·신간 임베딩 | 실행할 때만 뜬다. 복제 완료 후 book_id 순 256건씩 ② 호출 → dim 확인 → upsert, 중단되면 빈 곳부터 |
상시 컨테이너의 메모리 합계는 약 2.8GB로 추정한다(embedding 1.4GB만 실측). 그래서 AI EC2는 메모리 4GB 이상, 디스크 gp3 30GiB로 잡는다. GPU는 두지 않는다. e5-small은 CPU로 151 docs/s가 나와 CPU 컨테이너로 충분하다.
V1은 AI EC2 한 대라 분산할 대상이 없고, 확장은 V2에서 한다. 먼저 postgres와 replicator를 AI EC2에서 떼어내 각각 한 대씩 둔다. DB는 한 벌이어야 하고 복제기는 중복 실행이 금지라, 호스트째 복제하면 안 되는 컨테이너다. 그다음 내부 ALB와 Auto Scaling Group을 붙인다. 넓히는 방향은 두 가지다.
-
A. 같은 구성을 여러 대: AI EC2를 통째로 복제하고 앞에 내부 ALB를 둔다. 경로 판단은 각 호스트의
nginx가 계속 맡는다. -
B. 역할별로 나눠서: 조회군과 생성군을 다른 EC2에 두고 ALB가 경로를 보고 갈라 보낸다. 두 군이 각각 Auto Scaling Group을 갖고,
embedding은 양쪽에 둔다.
방향 A 구성:
flowchart TB
BE["백엔드"]
ALB["내부 ALB<br/>ai.internal"]
subgraph ASGA["AI EC2 Auto Scaling Group · 같은 구성 n대"]
A1["EC2 1<br/>nginx · 조회 · 생성 · 임베딩"]
A2["EC2 2<br/>같은 구성"]
end
DATA[("데이터 EC2 1대<br/>postgres + pgvector")]
REP["복제기 EC2 1대 · 고정"]
BE --> ALB
ALB --> A1
ALB --> A2
A1 --> DATA
A2 --> DATA
REP -->|"읽기 전용"| BE
REP -->|"단방향 복제"| DATA
| 항목 | A. 같은 구성을 여러 대 | B. 역할별로 나눠서 |
|---|---|---|
| 푸는 문제 | 호스트 한 대가 죽으면 전부 멈추는 것 | 조회와 생성이 한 호스트에서 자원을 뺏는 것 |
| 최소 EC2 대수 | 2대 (+ 데이터 1 + 복제기 1) | 4대 (역할군 2 × 2대, + 데이터 1 + 복제기 1) |
| 스케일 신호 | 호스트 평균 CPU 하나 | 조회군은 CPU, 생성군은 진행 중 요청 수 |
| 늘릴 때 | 필요 없는 역할까지 함께 늘어난다 | 부족한 쪽만 늘린다 |
지금 우리 규모에서는 A가 맞는다. 부족한 것이 용량이 아니라 이중화이기 때문이다(4-2).
V1은 고정 구성이다. 자동 확장 없이, 부하가 몰리면 앱이 서비스 토큰 단위로 429와 Retry-After를 돌려준다. DB와 LLM 예산을 지키는 최후 방어선이다. V1 피크는 AI 경로를 모두 합쳐 0.1 rps 안팎이라(1-2) 이 구성으로 충분하다.
V2 확장 후에는 컨테이너가 아니라 EC2 대수를 늘린다.
| 대상 | 스케일 신호 | 목표 | 최소/최대 |
|---|---|---|---|
| AI EC2 ASG (방향 A) | 호스트 평균 CPU 사용률 | 60% | 2 / 6 |
| 조회군 ASG (방향 B) | 호스트 평균 CPU 사용률 | 60% | 2 / 6 |
| 생성군 ASG (방향 B) | 호스트당 진행 중 요청 수 | 50 | 2 / 4 |
| 데이터 EC2 · 복제기 EC2 | 없음. 각각 1대 고정 | — | 1 / 1 |
- 생성군에 CPU를 쓰지 않는 이유: 요청이 외부 API 응답을 기다리는 동안 CPU는 놀기 때문에, CPU가 60%에 닿기 전에 동시 연결이 먼저 찬다.
- 생성군 최대 4는 서버 한계가 아니라 LLM API 쿼터에 맞춘 값이다. 쿼터를 넘으면 EC2를 늘려도 503만 늘어난다.
무상태라도 EC2 사이에 공유되어야 하는 것이 있다.
| 공유 대상 | 어디에 둘 것인가 |
|---|---|
| 멱등 키 기록(24시간, ⑥⑦) | AI Postgres |
| 커서 서명 키(①④) | 모든 EC2에 같은 값 주입 |
⑥의 user_id 단위 직렬 처리 |
AI Postgres 행 잠금 |
| 임베딩 모델 버전 | 모든 호스트가 같은 이미지 태그 |
장애가 나도 에러를 내는 대신 기능을 줄여 정상 응답하는 축소 동작이 API마다 정해져 있다. 인프라는 그 동작이 실제로 나오게 하고, 얼마나 자주 나오는지를 지표로 만든다.
| 장애 | 자동 동작 | 사용자 영향 | 감지 지표 | V1 복구 |
|---|---|---|---|---|
| 외부 LLM 장애(오류·타임아웃) | ③은 규칙만으로 3권, degraded: true, 긴 이유 null. ⑤⑦은 503 + Retry-After
|
상세 페이지의 긴 이유가 사라지고 카드의 한 줄 이유는 규칙 문장으로 대체. 검색·홈 피드·구매는 정상 | LLM 오류율, degraded 비율 |
업스트림 복구 시 자동 |
| 외부 LLM 한도 초과 | ③⑤⑦ 모두 503 + Retry-After
|
챗봇이 잠시 응답하지 않고 BE가 재시도를 안내 | 503 비율 | 쿼터가 풀리면 자동 |
embedding 중단 · 벡터 인덱스 이상 |
①은 키워드 검색만(X-Degraded: keyword-only). 인덱스 이상이면 ④도 규칙 점수만(rule-only) |
의미 검색·개인화 약화 | 축소 응답 비율 | Docker 재시작 정책으로 재기동. 인덱스 이상은 재색인 |
postgres 중단 |
①③④⑥ 전면 500 | 검색·추천 불가. 구매 경로는 정상 | 5xx 비율, DB 연결 실패 | 컨테이너 재기동. 데이터는 EBS에 남는다 |
| 복제 중단 | 응답은 전부 정상(낡은 값) | 산 책이 다시 추천에 뜰 수 있다 | replication_lag_seconds뿐 |
복제기 재기동 |
| AI EC2 자체 장애 | AI 경로 전면 중단 | 검색·추천 모두 불가 | 상태 검사 실패 | 대신 받을 호스트가 없다. V2 확장에서 해소된다 |
| 트래픽 급증 | 초과분은 429 + Retry-After
|
일부 429 후 재시도 성공 | 429 비율 | 자동 확장 없음 |
| LLM 예산 소진 | 80%에서 알림, 100%에서 저가 모델로 전환. 저가 모델도 쓸 수 없으면 규칙 응답(degraded: true) |
이유 문장 품질 저하 | 예산 소진율 | 예산 재배분 판단 |
V1에 남는 약점은 두 가지다. AI EC2가 죽으면 대신 받을 호스트가 없고, 배포 중에 AI 요청이 끊긴다. 둘 다 V2에서 확장하면 해소된다. V1은 3주짜리 초기 운영이라 이 위험을 안고 간다.
상태 검사. GET /health의 status가 ok·degraded면 정상으로 보고, down(503)일 때만 트래픽에서 뺀다. degraded는 일부 구성 요소만 죽은 상태라 호스트를 빼면 줄어든 기능조차 못 준다. 복제 지연은 판정에 쓰지 않는다. 복제가 밀리면 멀쩡한 호스트가 전부 동시에 빠지기 때문이다.
배포. V1은 바뀐 컨테이너만 다시 만든다(Recreate). 그 사이 해당 경로가 잠깐 끊긴다. 이미지 태그는 Git 커밋 해시이고, 롤백은 이전 태그로 다시 올린다. V2에서 확장한 뒤에는 호스트를 하나씩 교체하는 롤링으로 바꿔 이 단절을 없앤다.
| 영역 | 선택 | 이유 | 안 고른 대안 |
|---|---|---|---|
| 호스트·배포 | AWS AI EC2 한 대(메모리 4GB 이상, gp3 30GiB)에 Docker Compose. 오케스트레이터는 없다 | 클라우드 파트 1단계 구성과 같다. 호스트가 한 대라 오케스트레이터가 할 일이 없다 | ECS·쿠버네티스: 호스트가 늘어 손으로 감당하기 어려워지면 검토한다 |
| 앞단 | V1은 nginx 컨테이너, V2는 내부 ALB. BE는 고정 내부 도메인(ai.internal)으로 부른다 |
주소와 경로 계약을 먼저 고정해 두면 앞단을 바꿔도 BE에 안 보인다 | API Gateway: 호출자가 BE뿐이라 필요 없다 |
| 확장 | V1 고정. V2에 EC2 Auto Scaling Group | 확장 단위가 EC2 대수다 | 한 호스트에서 컨테이너만 늘리기: 호스트 한 대의 자원을 넘지 못한다 |
| 임베딩 | 자체 서빙 CPU 컨테이너. e5-small(384차원) 잠정, bge-m3와 비교해 확정(4-1) | 외부 API보다 지연 99배·처리량 20배 개선. 쿼터에서 벗어난다 | 외부 임베딩 API: 지연 573ms, 쿼터 제한. GPU: CPU로 충분하다 |
| DB | PostgreSQL + pgvector. V1은 AI EC2의 컨테이너, 데이터는 EBS | 가격·재고 필터(WHERE)와 의미 유사도 정렬(ORDER BY)을 한 SQL로 처리한다 | 전용 벡터 DB: 필터와 벡터 정렬을 한 SQL로 할 수 없다. RDS: V2에서 DB를 떼어낼 때 검토한다 |
| 복제 |
replicator 컨테이너. 클라우드가 발급한 BE MySQL 읽기 전용 계정으로 AI Postgres에 단방향 복제한다. 동기화 방식·주기는 BE·AI·클라우드가 함께 정한다. 복제 범위는 보류: 도서 외에 구매·나의 도서관·리뷰·인기 집계까지 필요하다고 회신했고 클라우드 답변을 기다린다 |
커머스 데이터의 원본은 BE가 갖고, AI는 허용 지연(3-1) 안의 사본을 읽는다 | 양방향 복제: 원본 주인은 끝까지 BE다 |
| LLM | 외부 API를 게이트웨이 어댑터 뒤에 둔다 | 모델 교체가 설정 변경으로 끝난다 | — |
| (V2) 표지 이미지 | S3. 버킷과 권한은 클라우드가 관리하고, BE가 Pre-signed URL로 넘긴다. 전달 방식은 보류: 클라우드 제안(Object Key + s3:GetObject)과 비교해 구현 때 BE와 정한다 |
1단계 명세와 멘토 권고가 Pre-signed URL이다 | — |
| 시크릿 | SSM Parameter Store SecureString
|
컨테이너별로 필요한 것만 주입한다. MySQL 접속 정보는 replicator에만 준다 |
Secrets Manager: 자동 교체가 필요해지면 옮긴다 |
| CI/CD | GitHub Actions → ECR → SSM → AI EC2. AI 파트는 이미지·테스트, 클라우드 파트는 권한과 배포·롤백 환경을 맡는다 | 이미지 태그(Git 커밋 해시)가 배포 단위라 버전이 어긋나지 않는다 | — |
| 모니터링 | CloudWatch 지표·대시보드·알람, CloudWatch Logs(14일), 알림은 Discord | 팀 공통 도구다. 수집 서버를 따로 운영하지 않는다 | Prometheus + Grafana: 지표 규모가 커지면 검토한다 |
(예: 응답 시간, Throughput, 에러 로그 등과 Grafana 대시보드 구성안).
| 축 | 판정 기준 |
|---|---|
| 응답 시간 | 검색 p95 200ms, 홈 피드 p95 300ms, 챗봇 턴 p95 8초. 생성 30초 초과는 504로 끊는다 |
| 데이터 신선도 | 아래 표의 허용 지연 |
| LLM 예산 | 3개월 36만 원. 실측 기준 턴당 약 $0.003, 약 9만 턴 |
목표는 실측에서 역산했다. 검색 200ms는 임베딩 자체 서빙(5.8ms)을 전제로 한 값이고, 챗봇 8초는 실측 한 턴 약 6초에 여유를 둔 값이다.
BE 원본에서 AI DB로 넘어오는 데이터의 허용 지연이다. 이 값이 복제 감시의 판정선이다.
| 대상 | 허용 지연 |
|---|---|
| 도서 가격·재고 | 5분 |
| 이력 3종(구매·나의 도서관·리뷰) | 5분 |
| 신간 입고(임베딩 포함) | 1시간 |
| 인기 집계 | 24시간 |
복제가 끊겨도 응답은 모두 정상 200으로 나가서, /health의 replication_lag_seconds 말고는 감지할 방법이 없다.
모든 지표는 CloudWatch로 모은다(3-3).
| 계층 | 지표 | 출처 | 왜 보나 |
|---|---|---|---|
| 요청 | 엔드포인트별 RPS(Throughput), p50 / p95 / p99 응답 시간 | 앱 | 챗봇은 사용자가 기다리는 실시간 경로라 평균이 아니라 꼬리 지연이 체감을 결정한다 |
| 상태 코드 비율, 특히 429 · 503 · 504 | 앱 | 429는 우리 한도, 503은 업스트림, 504는 30초 초과. 원인이 다르다 | |
축소 응답 비율 — keyword-only, rule-only, ③ degraded: true
|
앱 | 장애가 사용자에게 어디까지 닿았는지. 0이 아니면 어딘가 죽어 있다 | |
① 0건 비율, ④ cold_start 비율, ③ 카드 3장 미만 비율 |
앱 | 품질 신호. 0건이 갑자기 늘면 적재·복제 문제일 수 있다 | |
| 임베딩 | 요청 지연 p95, 처리량(docs/s), 컨테이너 CPU·메모리 | 임베딩 컨테이너 | 검색 지연의 가장 큰 항이자 스케일 신호 |
| LLM | 호출 수·지연·오류·타임아웃 | 게이트웨이 | 챗봇 지연의 대부분이 여기다 |
| 토큰 사용량과 비용 환산, 예산 소진율 | 게이트웨이 | 36만 원 상한 관리 | |
| 데이터 |
replication_lag_seconds, 테이블별 지연 |
/health, 복제기 |
복제 중단의 유일한 감지 채널. 허용 지연이 테이블마다 다르다 |
| AI Postgres 연결 수·쿼리 p95, 벡터 검색 지연 |
postgres, 앱 |
커넥션 상한과 인덱스 이상을 일찍 잡는다 | |
| 자원 | 호스트 CPU·메모리·디스크, 컨테이너 상태 | CloudWatch 에이전트 · Docker | 용량과 디스크(30GiB) 여유 |
| 생성 컨테이너의 진행 중 요청 수 | 앱 | 생성군 스케일 신호(1-6) | |
| 배치 | 초기 적재 진행률 | ingest job |
적재 중에는 검색이 keyword-only다 |
flowchart LR
APP["AI EC2의 컨테이너들"] -->|"사용자 정의 지표 · 요청마다 값"| CW["CloudWatch"]
AGENT["CloudWatch 에이전트"] -->|"호스트 CPU · 메모리 · 디스크"| CW
APP -->|"JSON 로그 · X-Request-Id"| LOG["CloudWatch Logs<br/>14일 보관"]
CW --> DASH["대시보드 4장"]
CW --> ALARM["CloudWatch Alarm → Discord"]
- V1은 팀 공통 모니터링인 CloudWatch 하나로 모은다. 별도 수집 서버가 없으므로 앱이 지표를 CloudWatch로 직접 보내고, 호스트 자원은 CloudWatch 에이전트가 모은다.
- p95·p99를 보려면 요청마다 값을 올려야 한다. 평균이나 합계로 줄여 올리면 CloudWatch가 백분위를 계산하지 못한다.
- 로그는 JSON 한 줄로 남긴다. 필드는 시각,
X-Request-Id, 엔드포인트, 상태 코드, 지연, 축소 여부, 임베딩·LLM 소요 시간, 토큰 수다. 대화 원문·검색어·이미지는 남기지 않는다. -
엔드포인트는 경로만 남기고 쿼리스트링은 버린다. ④ 피드가 GET이라
user_id와category·match_score_min같은 취향 필터가 URI에 실린다. 애플리케이션 로그는 경로만 기록하고, ALB 액세스 로그처럼 URI 전체를 남기는 계층은 쿼리 마스킹을 켜거나 보관 기간을 따로 잡는다. 검색어를 남기지 않기로 한 것과 같은 이유다. -
X-Request-Id는 BE가 붙여 보내고, 없으면 AI가 만들어 응답 헤더로 돌려준다. BE 로그와 AI 로그를 이 키로 잇는다.
| 대시보드 | 패널 | 주 사용자 |
|---|---|---|
| ① 서비스 개요 | 응답 시간(검색·피드·챗봇 p95), 엔드포인트별 RPS·오류율, 축소 응답 비율, 429·503·504 추이 | 전체. 장애·시연 때 첫 화면 |
| ② 모델과 비용 | 임베딩 지연·처리량, LLM 지연·오류, 토큰 사용량, 예산 소진율(누적) | AI 파트. 주간 비용 점검 |
| ③ 데이터·복제 |
replication_lag_seconds, 테이블별 지연 대비 허용 지연, DB 연결, 적재 진행률 |
AI·BE 공동 |
| ④ 자원 | 호스트 CPU·메모리·디스크, 컨테이너 상태, 진행 중 요청 수 | 클라우드 파트 |
알림은 CloudWatch Alarm으로 만들어 Discord로 보낸다. 채널과 임계값은 보류(팀 논의 후 확정)이고, 아래 수치는 AI 파트 잠정안이다.
| 심각도 | 조건 | 뜻 |
|---|---|---|
| 즉시 | 상시 컨테이너 중 하나가 내려감 | 가용성 위협 |
| 즉시 | 5xx 비율 > 1% (5분) | 설계에 없는 오류가 나고 있다 |
| 즉시 |
replication_lag_seconds가 null로 10분 지속 |
복제 중단. 응답으로는 보이지 않는다 |
| 즉시 | LLM 예산 소진율 100% | 저가 모델 전환이 켜졌는지 확인 |
| 10분 내 |
replication_lag_seconds > 300초 |
가격·재고·이력의 5분 허용 지연 위반 |
| 10분 내 | 검색 p95 > 200ms, 챗봇 턴 p95 > 8초 (5분) | 응답 시간 목표 위반 |
| 10분 내 | 축소 응답 비율 > 5% (10분) | 임베딩 컨테이너나 LLM 중 하나가 죽어 있다 |
| 10분 내 | 504 발생 | 생성 30초 초과 |
| 당일 | LLM 예산 소진율 80% | 남은 기간 대비 소진 속도 점검 |
| 당일 | 429 비율 > 2% | 한도 재검토 |
| 당일 | 호스트 디스크 여유 < 20% | 이미지·데이터 정리 또는 볼륨 확장 |
(가능하다면)
2026-09-09 개발기(Apple Silicon 10코어, CPU, torch 스레드 4)에서 고정 질의 30개 × 3회, 도서 256건으로 쟀다. 예열은 제외했다.
| 모델 | 서빙 | 차원 | 쿼리 1건 p95 | 도서 임베딩 처리량 | 동시 4 워커 | 프로세스 메모리 | 5만 권 적재 |
|---|---|---|---|---|---|---|---|
| gemini-embedding-001 | 외부 API | 3,072 | 573ms | 7.6 docs/s (동시 8) | — | — | 1.8시간 |
| multilingual-e5-small (118M) | 자체 · CPU | 384 | 5.8ms | 151 docs/s | 230 RPS | 1.4GB | 5.5분 |
| bge-m3 (568M) | 자체 · CPU | 1,024 | 41ms | 18 docs/s | 38 RPS | 1.7GB | 45분 |
LLM(외부 API)은 카드 3장 생성이 p95 4,858ms이고, 턴당 토큰은 입력 398·출력 516이다.
- gemini-embedding-001은 제외한다. 3,072차원은 pgvector 인덱스 한도(2,000)를 넘고, 쿼리 왕복만으로 검색 목표 200ms를 넘긴다.
- e5-small은 지연·처리량·메모리 모두 여유가 크다. 다만 동시성을 1→4→8로 올리면 175→230→159 RPS로 4에서 꺾여, 컨테이너 수로 늘려야 한다.
- bge-m3는 품질 기대가 높지만 처리량이 8분의 1이다. 결정은 실제 카탈로그의 recall@10으로 하고, 그때까지 e5-small을 잠정값으로 쓴다.
1-2의 3년 차 피크와 위 실측으로 계산했다.
| 경로 | 3년 차 피크 | 동시 진행 | 예상 자원 사용 | 먼저 닿는 상한 |
|---|---|---|---|---|
| 검색 | 10 rps | 1 | 조회·임베딩 컨테이너 각 한 벌로 충분. 임베딩 한 벌이 175 RPS를 낸다 | DB 연결 수 |
| 홈 피드 | 6.7 rps | 2 | 조회 컨테이너와 공유 | DB CPU |
| 챗봇 | 3턴/s | 18 | 생성 컨테이너 한 벌. CPU는 낮고 연결만 점유 | LLM API 분당 360회, 예산 |
| 초기 적재 5만 권 | — | 순차 |
ingest job + 임베딩 컨테이너, 약 5.5분 |
임베딩 처리량 |
결론: 처리량만 보면 3년 차 목표(V1의 약 220배)까지도 AI EC2 한 대의 컨테이너 한 벌로 감당된다. 먼저 닿는 한계는 LLM API 쿼터·예산과 DB이고, 진짜 상한은 처리량이 아니라 가용성이다. 호스트가 한 대라 그 한 대가 죽으면 전부 멈추므로, 확장(1-5)의 값어치는 용량이 아니라 이중화에 있다.
위 실측은 개발기 기준이라, 배포 환경 값은 MVP 배포 전에 이 테스트로 다시 잰다. 도구는 k6를 쓰고, 스테이징은 운영과 같은 EC2 사양·컨테이너 구성으로 맞춘다. LLM은 모의 서버(앱 자체 한계 측정)와 실제 API(응답 시간 측정, 예산 소량 사용) 두 번 돌린다.
| 시나리오 | 부하 | 측정 | 통과 기준 |
|---|---|---|---|
| A. 검색 단독 | 동시 사용자 10 → 50 → 200 → 500 | p95·p99, 오류율, 임베딩 컨테이너 CPU, DB 연결 수, 호스트 CPU | p95 ≤ 200ms, 5xx 0건, 호스트 CPU가 한계에 닿는 지점 확인 |
| B. 홈 피드 | 동시 10 → 200 | A와 동일 + cold_start 비율 |
p95 ≤ 300ms, 5xx 0건 |
| C. 챗봇 | 동시 5 → 20 → 50턴 | 턴 p95, 504·503·429 건수, 진행 중 요청 수 | 턴 p95 ≤ 8초, 504 0건 |
| D. 혼합 + 장애 주입 | 검색 10 · 피드 7 · 챗봇 3 rps(3년 차 피크)를 30분 유지하며 임베딩 컨테이너를 강제 종료 | 오류율, 축소 응답 비율, 복구 시간 |
keyword-only로 응답 유지, 자동 재기동 |
| E. 적재 중 검색 | 카탈로그 전량 적재 중에 시나리오 A | 적재 소요 시간, 검색 p95, keyword-only 비율 |
적재가 검색 지연을 목표 밖으로 밀지 않음 |
E는 적재 배치와 검색이 같은 임베딩 컨테이너를 쓰기 때문에 넣었다. 밀어낸다면 적재에 속도 제한을 걸거나 트래픽이 적은 시간대로 옮긴다.
(예: "사용자 100배 증가 시에도 대응 가능하도록 OOO를 구성하여 서비스 중단 없이 확장 가능").
| 상황 | 이 구성에서 일어나는 일 |
|---|---|
| 사용자 증가 (3년 차 목표, V1의 약 220배) | 처리량은 AI EC2 한 대로도 감당된다(4-2). 부족해지면 무상태라 EC2 대수만 늘리면 되고 코드나 DB 스키마를 고칠 일이 없다. V2에서 DB·복제기를 떼어내고 내부 ALB와 Auto Scaling Group을 붙이며, BE가 부르는 내부 도메인만 ALB로 돌린다(1-5). 실제 상한은 LLM API 쿼터와 예산이고, 429와 예산 알림이 그 선을 넘지 않게 막는다 |
| LLM 장애 | 생성 컨테이너만 축소 응답으로 내려가고 검색·홈 피드·구매는 그대로 된다. 대시보드 ①의 축소 응답 비율로 영향 범위를 바로 안다 |
| 임베딩 모델 교체 | 임베딩 컨테이너의 이미지 태그를 바꾸고 전량 재색인한다(5만 권 약 5.5분). 자체 서빙이라 재색인에 호출 과금이 없다. 취향 벡터는 BE가 전 사용자에 대해 ⑥을 다시 불러 새로 계산한다. 이때 BE가 보관한 기억 벡터는 옛 모델 값이라 그대로 쓸 수 없다(차원이 다르면 400, 같으면 조용히 섞인다). 기억 벡터를 다시 만드는 경로는 ERD §7에서 정한다 |
| 복제 중단 | 응답이 전부 정상이라 사용자 신고로는 늦다. replication_lag_seconds 알림이 5분 허용 지연 안에서 잡는다 |
| 예산 소진 | 임베딩이 자체 서빙이라 호출량에 비례하는 비용이 LLM 하나뿐이다. 80%·100% 알림과 저가 모델 전환으로 3개월 36만 원을 넘기지 않는다 |
| 잦은 배포 | 이미지 태그가 배포 단위라 롤백은 이전 태그를 다시 올리면 끝난다(1-8) |
이 정도 수준으로 두는 이유는 규모와 팀에 맞추기 위해서다. 3년 차 목표에서도 AI 피크가 20 rps 안팎이라 쿠버네티스·멀티 리전·상시 GPU를 지금 들이면 얻는 것보다 운영 부담이 크다. 대신 응답 시간의 병목이던 외부 임베딩 호출을 우리 호스트 안으로 들여 우리가 늘릴 수 있는 자원으로 바꾸고, 각 API의 축소 동작·복제 허용 지연·예산 상한을 전부 지표로 만들어, 문제가 생겼을 때 사용자 응답이 아니라 대시보드에서 먼저 보이게 하는 데 집중했다.