티스토리 뷰

반응형

월 $3.5짜리 Lightsail 인스턴스에 Spring Boot 서버를 올릴 수 있는지 알고 싶었다. 512MB 램에 2 vCPU, 스토리지 20GB짜리다. 검색으로도 LLM에 물어봐도 답은 대체로 "안 된다"였고, 이유는 한결같이 512MB에 JVM이 안 들어간다는 것이었다. 근거로 제시되는 숫자를 본 적이 없어서 직접 측정해봤다.

 

결론부터 적으면 이렇다.

 

  • 512MB라는 메모리에도 스프링부트 운영이 가능하다. OS와 Litestream까지 올린 전체 스택이 419MB 중 256MB를 쓰고 147MB가 남았다
  • 요청수가 초당 30건 이하로 유지된다면 $3.5짜리 Lightsail 인스턴스에서 운영이 가능하다. (일반적인 HTML 페이지 호출 기준. 자세한 사항은 아래 설명)
  • (순간 피크 기준) 요청수 초당 120건까지 성능저하 없이 처리한다. (일반적인 HTML 페이지 호출 기준. 자세한 사항은 아래 설명)

측정 조건

실제 Lightsail 인스턴스($3.5, 512MB, Amazon Linux 2023, x86_64)에서 쟀다. 요청 하나의 비용을 성분별로 나누는 부분만 Docker 컨테이너를 따로 썼다. 조건을 하나씩 고정해야 하는 측정이라 그렇다.

구성 요소 버전
JDK Eclipse Temurin 25.0.4
Spring Boot 4.1.0
Spring Framework 7.0.8
서블릿 컨테이너 Tomcat Embed 11.0.22
템플릿 엔진 Thymeleaf 3.1.5
DB SQLite (WAL) + sqlite-jdbc 3.51.0.0
커넥션 풀 HikariCP 7.0.2
복제 Litestream 0.5.16
부하 생성기 k6 v2.2.0
spring.threads.virtual.enabled=true
spring.datasource.hikari.maximum-pool-size=8
spring.datasource.hikari.data-source-properties.journal_mode=WAL
server.compression.enabled=false
spring.thymeleaf.cache=true

 

JVM 플래그는 하나도 주지 않았다. 힙을 지정하지 않으면 JVM이 시스템 메모리를 읽어 그 25%로 잡는다. 이 인스턴스에서는 126MB다.

가상스레드는 사용했다.

SQLite와 Litestream을 쓰는 이유

비용 때문이다. 관리형 데이터베이스는 이 가격대에서 인스턴스 값보다 비싸고, DB용 인스턴스를 하나 더 띄우면 최소 비용으로 서버를 올린다는 전제가 무너진다. SQLite는 별도 서버 프로세스 없이 애플리케이션 안에서 도는 파일 기반 데이터베이스라 추가 비용이 붙지 않는다.

 

대신 데이터가 인스턴스 디스크의 파일 하나에 들어 있다. 인스턴스가 사라지면 데이터도 사라진다.

Litestream이 그 문제를 메운다. SQLite의 변경 기록(WAL)을 계속 읽어 S3나 R2 같은 오브젝트 스토리지로 복제하는 별도 프로세스다. 애플리케이션은 평소처럼 SQLite를 쓰고, 복제는 뒤에서 알아서 돌아간다. 인스턴스를 잃어도 마지막 시점의 데이터베이스를 다시 받아올 수 있다.

 

이 조합이면 데이터베이스 몫으로 나가는 돈이 오브젝트 스토리지 요금만 남는다. 대신 인스턴스의 CPU와 메모리를 애플리케이션과 나눠 쓰게 되므로, 이 글의 측정에는 Litestream도 함께 올려서 그 몫까지 포함했다.

데이터와 시나리오

데이터 수는 운영 중인 커뮤니티 사이트의 규모에 맞췄다. 글 4,684건, 댓글 17,645건, 글당 평균 댓글 3.8개.

시나리오 엔드포인트 응답 하는 일
page GET /page/post/{id} HTML 102 KB 쿼리 5회 + Thymeleaf 렌더링
json GET /json/post/{id} JSON 12 KB 같은 쿼리 5회, 템플릿 없음
blob GET /static/blob 102 KB 미리 만들어둔 문자열 반환
db GET /db/user/{id} JSON 380 B 인덱스 조회 1회

 

 

page가 주력이다. 글 상세 페이지 하나에 쿼리 5개가 나간다. (본문, 댓글 목록, 최근글, 인기글, 게시판 목록...) 나머지 셋은 비용을 성분별로 나누기 위한 것이다.

gzip 같은 압축은 껐다. Cloudflare를 앞에 두면 엣지에서 압축하므로 원본 CPU를 아낄 수 있다. 참고로 gzip을 켜도 CPU 비용은 전체의 5%뿐이라, 직접 서빙한다면 켜는 편이 낫다.

 

1. 메모리 512MB도 충분히 운영 가능

전체 스택을 단계별로 올리며 잰 값이다.

단계 used available
OS 110 MB 298 MB
OS + Spring Boot 232 MB 176 MB
OS + Spring Boot + Litestream 240 MB 168 MB
OS + Spring Boot + Litestream + 부하 256 MB 147 MB

 

프로세스별로는 java 161MB, litestream 13MB다. OOM 0건, 여유 147MB.

 

명목 512MB 중 실제로 보이는 것은 419MB였다. 커널 예약 영역과 펌웨어가 먼저 가져간다. "512MB에 들어가는가"를 따질 때 기준은 512가 아니라 419다.

 

free를 볼 때는 free 열이 아니라 available 을 봐야 한다. 리눅스는 남는 메모리를 전부 캐시로 쓰기 때문에 free는 항상 작게 나온다. available이 새 프로세스에 내줄 수 있는 실제 여유다.

2. Lightsail에서 반드시 알아야하는 '베이스라인'

Lightsail 인스턴스는 버스트형이다. "2 vCPU"는 순간 최대치이지 계속 쓸 수 있는 양이 아니다.

vCPU보다 베이스라인이 더 중요한데, 한줄로 요약하면 평균적으로 쓸수있는 CPU 성능 한계다.

순간 5%를 넘어가는 작업도 가능하지만 평균 5%를 넘기다 보면 크레딧이 고갈되어 인스턴스 성능이 저하된다.

 

문서에 플랜별 성능 베이스라인이 명시돼 있다.

플랜 vCPU 메모리 성능 베이스라인
Linux $3.5 2 512 MB 5%
Linux $5 2 1 GB 10%
Linux $10 2 2 GB 20%

 

가격은 IPv6 전용 번들 기준이다. Lightsail은 IPv4 주소를 함께 주는 번들(듀얼스택)과 IPv6 주소만 주는 번들을 따로 파는데, 후자가 더 싸다. 같은 사양이 듀얼스택으로는 $5 / $7 / $12다. 성능 베이스라인은 둘이 동일하다.

 

(IPv6 전용은 IPv4 클라이언트가 서버에 직접 접속하지 못한다는 제약이 있지만, Cloudflare를 앞에 두면 해결된다. 클라이언트는 Cloudflare에 IPv4로 붙고 Cloudflare가 원본에 IPv6로 접속한다.)

 

문서에 단서가 하나 붙어 있다. *"These performance baselines are per vCPU."* vCPU당 값이라는 뜻이고, 콘솔 그래프는 두 vCPU를 평균 내서 보여준다.

$3.5   버스트 상한 2.00 vCPU   /   지속 보장 0.10 vCPU  (2 x 5%)

 

베이스라인은 상한이 아니라 크레딧이 늘지도 줄지도 않는 경계다. 언제든 100%까지 쓸 수 있고, 그 위로 쓰면 burst capacity가 줄고 아래로 쓰면 쌓인다.

burst capacity 계산

문서에 공식이 있다.

burst capacity는 "CPU를 100%로 쓸 수 있는 시간" 을 분 단위로 센다. 한 시간(60분) 가운데 베이스라인 비율만큼이 쌓이고, 실제로 쓴 비율만큼이 빠진다.

시간당 적립 (분)  =  베이스라인%  × 60분
시간당 소모 (분)  =  실제 CPU%    × 60분
최대 적립량 (분)  =  시간당 적립  × 24시간

$3.5 플랜이면 5% × 60분 = 시간당 3분이 쌓이고, 24시간이 지나면 72분에서 멈춘다.

CPU를 20%로 쓰면 20% × 60분 = 시간당 12분이 나간다. 버는 3분을 빼면 순소모가 9분이고, 72분을 8시간에 다 쓴다.

사용률 소진까지
5% 줄지 않음
10% 24시간
20% 8시간
50% 2.7시간
100% 1.3시간

 

burst capacity가 바닥나도 추가 과금은 없다. 최악의 경우가 요금 폭탄이 아니라 성능 저하라는 뜻이다.

 

이 측정에서 page 요청은 초당 120건에 CPU 20%를 썼다. 역산하면 크레딧이 줄기 시작하는 지점은 초당 약 30건이다. 그 아래로 운영하면 잔량이 마르지 않는다.

3. 실제로 어디까지 견디는가

앞의 표대로면 초당 30건을 넘는 순간부터 크레딧이 줄고, 다 쓰면 성능이 떨어져야 한다. 그래서 크레딧을 0까지 소진시킨 상태에서 요청률을 올려가며 쟀다.

 

page 시나리오(102 KB HTML, 쿼리 5회).

목표 rps 실제 처리 rps 실패율 p95 레이턴시 인스턴스 CPU
8 8.0 0% 44 ms
16 16.0 0% 42 ms
24 24.0 0% 41 ms
40 40.0 0% 39 ms
60 60.0 0% 38 ms 약 10%
90 90.0 0% 37 ms
120 119.9 0% 43 ms 약 20%
150 JVM 힙 부족으로 프로세스 사망

 

CPU는 인스턴스 안에서 vmstat으로 관측한 사용자·시스템 시간의 합이고, 두 지점에서만 쟀다.

크레딧이 하나도 없는 상태에서 초당 120건을 p95 43ms에 처리한다.

 

초당 150건에서 프로세스가 죽었다. 로그에 남은 것은 CPU 관련 오류가 아니었다.

Exception in thread "tomcat-handler-20963" java.lang.OutOfMemoryError: Java heap space

 

스레드 번호가 2만 번대까지 갔다. 처리 속도보다 요청이 빨리 들어오면서 가상 스레드가 계속 생성됐고, 각각이 102KB 응답 버퍼를 들고 있으니 126MB 힙이 버티지 못했다.

4. 운영중 크레딧 알람은 필수로 설정

크레딧 소진 자체는 이번 측정에서 큰 성능 저하로 이어지지 않았지만, 그 상태를 알고 있느냐는 별개 문제다. 이 상태에는 흔적이 거의 없다.

 

  • 요금이 늘지 않는다. Lightsail은 CPU 초과분을 과금하지 않는다
  • CPU 그래프만 봐서는 구분이 안 된다. 정상적으로 버스트 중인지 잔량을 태우는 중인지 같은 그래프로 보인다
  • 헬스체크를 통과한다. 가벼운 엔드포인트는 계속 200을 반환한다

 

Lightsail은 세 가지 CPU 지표에 알람을 걸 수 있다.

지표 의미
CPUUtilization CPU 사용률 (두 vCPU 평균)
BurstCapacityPercentage 남은 burst capacity 비율
BurstCapacityTime 남은 burst capacity를 분으로 환산

 

걸어야 할 대상은 CPUUtilization이 아니라 BurstCapacityPercentage다. 40%라는 CPU 값 하나로는 "지금 트래픽이 많다"와 "잔량이 곧 바닥난다"를 가릴 수 없다.

 

5. CPU는 어떤 과정에서 많이 쓸까

page 요청의 비용을 성분별로 나누기 위해 조각을 하나씩 뺀 엔드포인트를 비교했다. 이 부분은 조건을 정확히 고정해야 해서 컨테이너에서 쟀다.

엔드포인트 응답크기 DB 쿼리 요청수 렌더링 100 rps 콘솔 CPU
/static/blob 102 KB 0회 없음 5.24%
/json/post/{id} 12 KB 5회 Jackson 8.50%
/page/post/{id} 102 KB 5회 Thymeleaf 12.12%

 

여기서 성분이 분리된다.

 

DB가 가장 큰 몫이다. 템플릿 렌더링은 15%뿐이고, 102KB를 그냥 내보내는 것도 24%에 그친다.

응답 크기의 영향도 생각보다 작다. 240KB짜리 페이지를 따로 쟀는데 크기가 2.3배인데 비용은 13%만 늘었다. 바이트 수보다 요청당 고정 작업이 비용을 지배한다.

 

VPS 하나를 최대한 쓴다는 것은 DB도 같은 인스턴스에 둔다는 뜻이고, 그러면 쿼리 CPU가 애플리케이션과 같은 예산에서 나간다. 위 비중이 그 크기를 보여준다. 저사양 인스턴스에서 EXPLAIN QUERY PLAN을 확인하는 것이 다른 어떤 최적화보다 효과가 크다.

DB Write와 Litestream은 영향이 크지 않음

조회할 때마다 조회수를 올리는 패턴, 그리고 SQLite를 복제하는 Litestream을 차례로 얹고 7 rps에서 쟀다.

구성 콘솔 CPU
읽기 전용 1.99%
+ 조회수 UPDATE 1.85%
+ Litestream 2.20%

 

셋 다 노이즈 범위 안이다. WAL 모드에서 인덱스 단건 UPDATE를 초당 몇 건 하는 것은 SQLite에 부담이 아니고, 그 정도 쓰기가 만드는 WAL은 몇 KB라 Litestream이 읽어 올릴 것도 거의 없다.

 

메모리는 다르다. Litestream은 실제 인스턴스에서 13MB를 상주 사용했다. CPU는 무시할 수 있지만 메모리는 여유 147MB의 9%다.

이 수치는 로컬 파일로 복제하고 쓰기가 초당 몇 건일 때 그렇다는 뜻이다. S3나 R2로 올리면 네트워크와 TLS 비용이 붙고, 쓰기가 많아지면 WAL도 그만큼 커진다. 전체 테이블 COUNT를 쓰기 락 안에서 재계산하는 패턴처럼 무거운 쓰기는 비용 구조가 다르고 여기서 재지 않았다.

 

6. 정리

월 $3.5짜리 Lightsail 인스턴스에서 Spring Boot + SQLite + Litestream을 돌린 결과다.

메모리 419MB 중 256MB 사용, 147MB 여유. OOM 없음
지속 가능한 평균 초당 약 30건 (이 아래면 크레딧이 쌓인다)
크레딧 소진 상태 처리량 초당 120건, p95 43ms
붕괴 지점 초당 150건, JVM 힙 부족

 

"512MB에 JVM이 안 들어간다"는 말은 사실이 아니었다. 플래그를 주지 않으면 JVM이 시스템 메모리의 25%로 힙을 잡기 때문에 오히려 작게 시작한다. 부하가 몰려 프로세스가 죽은 순간에도 인스턴스에는 78MB가 남아 있었다.

 

CPU 베이스라인은 알아둬야 하지만 두려워할 것은 아니다. $3.5 인스턴스 베이스라인 5%라면 평균 CPU 사용량이 5%로 지속되면 된다.

 

GC 튜닝이나 네이티브 이미지 (GraalVm)는 이 규모에서 필요 없다. 기본 설정으로 충분했고, 별도로 잰 튜닝 구성은 저부하에서 기본값과 차이가 없거나 오히려 메모리를 더 썼다.

한계

  • 관측 구간이 짧다. 요청률당 1분씩 쟀다. 크레딧 초과 상태를 수 시간 지속했을 때도 같은 성능이 나오는지는 확인하지 않았다
  • 비용 분해는 컨테이너 기준이다. 성분별 비중은 상대값이라 그대로 옮겨가지만, 절대 CPU 수치는 인스턴스와 다르다
  • 무거운 쓰기 패턴을 안 쟀다. 단건 UPDATE는 측정되지 않았지만, 전체 테이블 COUNT나 같은 행에 쓰기가 몰리는 경우는 비용 구조가 다르다

참고

반응형
댓글