14장. Streamlit 화면과 Docker 컨테이너
API 아키텍처
큰 식당은 홀, 주방, 창고, 계산대가 나뉘어 있고, 손님이 늘면 주방장을 더 둡니다. 서비스도 화면, API, 모델 서버, 데이터를 나누면 바쁜 곳만 늘리고, 한 곳을 고칠 때 다른 곳은 그대로 둘 수 있습니다.
1번 화면은 사용자와 만납니다. 2번 API 는 인증, 입력 검사, 기록을 맡고 일을 나눠 맡깁니다. 3번 모델 서버(Ollama, 이후 장의 vLLM)는 계산이 무거운 추론만 합니다. 4번 데이터에는 벡터DB, 문서, 기록이 있습니다. 화면은 API 만 알고, 모델 서버와 데이터는 API 뒤에 숨깁니다. 여기서 홀이 화면, 계산대와 주문 관리가 API, 주방이 모델 서버, 창고가 데이터입니다.
나누는 까닭과 늘리는 법
| 층 | 무거운 것 | 사용자가 늘면 |
|---|---|---|
| 화면 | 가볍다 | 화면 서버를 여러 개로 |
| API | 가볍다(인증, 검사) | 여러 개를 띄우고 앞에 부하 분산기를 둔다 |
| 모델 서버 | GPU 와 메모리 | GPU 를 늘리거나, 묶어서 처리(16장의 vLLM) |
| 데이터 | 저장 공간 | 벡터DB 를 서버형으로(11장) |
오래 걸리는 일(문서 수백 개 요약 같은)은 API 가 바로 답하지 않고 작업 번호를 주고, 뒤에서 처리한 뒤 결과를 따로 받게 하기도 합니다(작업 큐). 대화처럼 바로 답해야 하는 일은 지금처럼 요청을 받으면 바로 답합니다.
여러 API 서버에 나눠 보내기
import itertools
replicas = ["http://api-1:8013", "http://api-2:8013", "http://api-3:8013"] # 같은 API 를 셋 띄웠다
next_server = itertools.cycle(replicas) # 돌아가며 하나씩(라운드 로빈)
for request_id in range(5):
print(request_id, "→", next(next_server))코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
import itertools | 반복(iteration)을 돕는 도구(tools) 모음이다 |
itertools.cycle(replicas) | 목록을 끝까지 돌면 다시 처음으로 돌아가며 끝없이 하나씩 내준다. cycle 은 "돌고 돈다"는 뜻이다 |
next(next_server) | 다음 차례 하나를 꺼낸다 |
replicas | replica(복제본)들이다. 같은 API 를 여러 개 띄운 것을 가리킨다 |
이 코드가 뜻하는 것
은행 창구가 셋일 때 번호표를 1번 창구, 2번 창구, 3번 창구, 다시 1번 창구 순으로 나눠 주는 것과 같습니다. 여기서 창구가 API 서버 하나, 번호표를 나눠 주는 사람이 부하 분산기입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
replicas = [...] | 같은 API 를 셋 띄웠다고 적는다 | 창구 셋 |
next_server = itertools.cycle(replicas) | 셋을 돌아가며 내주는 줄을 만든다 | 번호표를 차례로 나눠 줄 준비 |
for request_id in range(5): | 요청 다섯 개를 받는다 | 손님 다섯 명 |
next(next_server) | 이번 요청을 받을 서버를 고른다 | 이번 손님을 몇 번 창구로 보낼지 |
결과는 0, 1, 2 번 요청이 api-1, api-2, api-3 으로, 3, 4 번 요청이 다시 api-1, api-2 로 갑니다. 비유와 다른 점은, 진짜 은행은 빈 창구로 보내지만 라운드 로빈은 서버가 바쁜지 보지 않고 차례만 따진다는 것입니다.
작게 줄여서 눈으로 보기
요청이 많아지면 서버마다 몇 개씩 가는지 세어 봅니다.
import itertools
from collections import Counter
replicas = ["api-1", "api-2", "api-3"]
next_server = itertools.cycle(replicas)
sent = Counter(next(next_server) for _ in range(10)) # 요청 10개를 돌아가며 보낸다
print(sent)Counter({'api-1': 4, 'api-2': 3, 'api-3': 3})| 결과 | 읽는 법 |
|---|---|
'api-1': 4 | 10은 3으로 나누어떨어지지 않아, 처음 차례인 api-1 이 하나 더 받았다 |
'api-2': 3, 'api-3': 3 | 나머지는 셋씩 받았다. 요청이 많을수록 거의 고르게 나뉜다 |
Counter 는 같은 것이 몇 번 나왔는지 세어 주는 도구입니다. counter 는 "세는 것"이라는 뜻입니다.
주의할 점
- 나눌수록 고장 날 곳과 지켜볼 곳이 늘어납니다. 처음에는 화면과 API 둘로 시작하고, 필요할 때 나눕니다.
- 층마다
/health같은 상태 확인 주소를 두면, compose 나 감시 도구가 죽은 서비스를 알아채고 다시 띄울 수 있습니다(20장).