Modal Servers (저지연 HTTP 서빙)

Modal Servers는 외부 클라이언트와 Modal 컨테이너 안에서 돌아가는 프로세스 사이의 저지연 HTTP 통신에 최적화된 서버리스 컴퓨트 원시 요소예요. LLM 추론 서버처럼 포트에 바인딩해서 HTTP를 네이티브로 말하는 프로세스가 있을 때, 지연을 최소화하면서 서빙하고 싶다면 이걸 쓰면 돼요. Endpoints의 바탕 인프라를 제공하기도 하면서, 완전히 커스터마이즈된 애플리케이션 로직으로 직접 배포할 수도 있죠.

출처: https://modal.com/docs/guide/servers

Functions와 무엇이 다른가

Modal Servers는 Functions와 많은 특징을 공유해요. App의 구성원이고, 일반 modal deploy 워크플로로 배포되며, baseline request + burst 리소스 의미론을 그대로 따르고, 물론 GPU도 쓸 수 있어요. 컨테이너는 전 세계 어디서나 실행되고 커스텀 Images를 쓰며, 메모리 스냅샷 덕에 동일하게 빠른 콜드 부팅을 누리죠. Secrets·Volumes을 마운트하고 안정적인 아웃바운드 IP 주소를 가질 수 있어요.

Servers는 저지연을 위해 처음부터 설계됐어요. 그래서 Functions와는 자동 스케일링, 부하 분산, 인증, 컨테이너 수명 주기에서 중요한 차이가 있고, Functions의 상태 저장 입력 시스템에 의존하는 일부 운영 기능을 갖지 않아요. 참고 문서는 @app.server() 데코레이터와 modal.Server 객체를 보면 돼요.

Server 정의하기

Modal Server는 수명 주기 데코레이터로 시작(및 선택적으로 종료) 로직을 지정한 메서드를 가진 클래스로 정의해요. 클래스 자체는 @app.server() 데코레이터로 App에 등록하고, 메인 Server 설정 인자를 받죠.

시작 로직은 0.0.0.0에 바인딩하고 기본 포트 8000에서 리슨하는 서버 프로세스를 초기화해야 해요. modal.Cls와 달리 Server 정의는 @modal.method()이나 @modal.fastapi_endpoint 같은 Web Function 데코레이터를 쓸 수 없고, 요청 처리는 클래스 메서드가 아니라 포트에서 리슨하는 프로세스가 담당해요.

Modal Server는 @modal.web_server()를 쓰는 Function과 가장 직접적으로 유사하고, 대부분의 웹 서버 Function은 이 가이드에서 설명하는 동작·구성 모델의 차이만 감안하면 Server로 옮길 수 있어요. 모든 Server는 공개 인터페이스로 URL을 배정받고, modal.Server.get_url()로 프로그래매틱하게 조회할 수 있어요.

동시성과 자동 스케일링

Modal Function 컨테이너는 명시적으로 입력 동시성을 선택하지 않으면 한 번에 하나의 입력만 처리하고, 필요에 따라 컨테이너를 자동 스케일링해요. Servers는 그 반대예요. Server 프로세스는 동시 요청을 처리하도록 기대하고, 컨테이너 자동 스케일링은 원할 때 명시적으로 선택해야 하죠.

자동 스케일링을 켜려면 @app.server() 데코레이터에 target_concurrency= 값을 넣으면 돼요. Modal은 이 목표값으로 컨테이너 풀을 관리해서, 각 컨테이너의 동시 요청 부하에 맞춰 목표 컨테이너 수 쪽으로 스케일링해요. 다만 이건 소프트 한도라서, Server 프로세스가 요청 동시성을 감당 못 하면 스스로 부하 분산이나 부하 버리기(load-shedding)를 해야 해요.

Servers는 표준 min_containers=, max_containers=, buffer_containers= 인자로 오토스케일러를 묶거나 추가 컨테이너를 웜 상태로 유지할 수 있어요. scaleup_window=scaledown_window=로 요청 변동에 대한 반응성도 조절하고, modal.Server.update_autoscaler()로 동적 튜닝도 가능하죠. Functions와 마찬가지로 동적 구성은 다음 배포 시 리셋돼요.

target_concurrency=를 안 두면서 min_containers=로 여러 컨테이너를 잡으면 요청이 풀 전체에 분산돼요. 단일 컨테이너를 원하면 max_containers=1보다는 target_concurrency=를 안 두는 쪽이 나아요. max_containers=1은 롤링 재배포 때 트래픽을 부드럽게 옮길 교체 컨테이너를 Modal이 못 올리게 하거든요.

제로에서 원으로 스케일링

Servers는 클라이언트와 컨테이너 사이에 무상태 역방향 프록시를 두기 때문에, Function 입력처럼 컨테이너를 기다리며 요청이 큐에 쌓이지 않아요. 그래서 제로에서 원(zero-to-one) 스케일링에 중요한 차이가 있어요. Server에 활성 컨테이너가 없으면 요청은 503 Service Unavailable로 거부되고, 클라이언트가 이를 처리해야 해요. 제로투원 스케일링 자체는 자동이라, 첫 요청이 컨테이너 콜드 스타트를 트리거하고 Server가 준비되는 즉시 이후 요청을 처리해요.

컨테이너 수명 주기

Server 컨테이너는 시작 메서드가 반환됐더라도 설정된 포트에서 프로세스가 리슨하기 전까지 준비된 것으로 간주하지 않아요. 준비 전의 요청은 다른 컨테이너로 보내지거나 503으로 거부되죠. startup_timeout= 초 안에 준비되지 못한 컨테이너는 종료·실패 처리돼요.

활성 컨테이너에는 포트 리슨 여부를 확인하는 헬스 체크가 전송되고, 연속 실패가 기준을 넘으면 종료·교체돼요. 스케일 다운되면 새 요청 수신은 멈추지만 exit_grace_period= 초까지 진행 중 요청을 처리하다가, 이후 SIGTERM을 받아 종료 핸들러(@modal.exit())를 실행해요. 프로세스 종료와 핸들러에는 추가로 30초가 주어지고, 그래도 살아 있으면 강제 SIGKILL을 받아요.

요청 인증

Web Functions와 달리 Servers는 기본적으로 요청 인증을 요구해요. 공개 웹 트래픽을 받으려면 Server 구성에서 unauthenticated=True를 설정해야 하고, 아니면 Modal 프록시가 인증 없는 요청을 401로 거부하고(오토스케일러 집계에도 포함 안 됨) 전해요.

인증하려면 Authorization: Bearer wk-<id>.ws-<secret> 헤더 하나로 Proxy Token을 넘기거나, Modal-Key·Modal-Secret 별도 헤더로 넘기면 돼요. RBAC가 켜진 Workspace에서는 Proxy Token이 Server App이 배포된 Environment에 스코프되어야 하며, 그렇지 않은 유효 토큰은 403으로 거부돼요.

요청 라우팅

Server 구성에는 컨테이너로 요청을 라우팅하는 프록시의 지역 지정(routing_region=)이 포함돼요. 지원 라우팅 지역은 us-east(기본), us-west, ca-central, eu-west, ap-south, ap-southeast-2이고, 가급적 클라이언트와 가장 가까운 지역을 고르는 게 좋아요. compute_region=으로 같은 지역 안에 컨테이너 스케줄링을 제한할 수도 있는데, 이 경우 비용 승수가 붙어요.

라우팅 프록시는 '스티키 세션'도 지원해요. 요청에 Modal-Session-ID 헤더(임의 문자열 허용)가 있으면 같은 세션 ID를 공유하는 별개 요청이 같은 컨테이너에서 처리돼요.

운영 특징

Servers는 요청 큐잉 외에도 Functions의 상태 저장 입력 시스템이 주는 몇몇 운영 기능이 빠져 있고, 필요하면 클라이언트나 서버 애플리케이션 계층에서 직접 구현해야 해요. 요청 데이터의 직렬화·역직렬화는 애플리케이션 계층에서 처리해야 해요. 실패한 요청(컨테이너 선점·크래시 포함)에 대한 내장 재시도가 없고, Modal 안에서 요청 타임아웃을 커스터마이즈할 수 없으니 클라이언트나 서버 코드에서 설정해야 해요.

더 알아보기