Trino 메모리 관리와 Resource Groups
Trino 의 메모리 모델(per-node·cluster·heap headroom)과 spill-to-disk, 그리고 Resource Groups 로 워크로드를 격리해 한 쿼리가 클러스터 전체를 점유하지 못하게 하는 방법을 실전 설정과 함께 정리합니다.
Trino 운영에서 가장 흔한 장애는 "거대한 쿼리 하나가 메모리를 다 먹고 클러스터를 전체를 마비시키는 것", 그리고 "특정 팀의 임시 쿼리가 동시성을 점유해 정기 배치가 밀리는 것"입니다. 두 문제 모두 메모리 모델과 Resource Groups 를 이해하면 구조적으로 막을 수 있습니다.
이 글은 Trino 의 메모리가 어떻게 분배되는지, spill-to-disk 가 무엇을 구해주는지, 그리고 Resource Groups 로 워크로드를 격리하는 방법을 실제 설정과 함께 정리합니다.
1. Trino 메모리 모델 — 세 개의 경계
Trino 메모리는 세 가지 경계로 관리됩니다.
[ 워커 노드의 JVM 힙 ]
├── heap-headroom-per-node (Trino 가 손대지 않는 예약 영역: GC, 버퍼)
└── 쿼리용 메모리 풀
└── query.max-memory-per-node (한 쿼리가 이 노드에서 쓸 수 있는 상한)
[ 클러스터 전체 ]
└── query.max-memory (한 쿼리가 모든 워커에서 합산해 쓸 수 있는 상한)| 설정 | 범위 | 의미 |
|---|---|---|
query.max-memory-per-node | 워커 1대 | 한 쿼리가 그 노드에서 쓸 수 있는 최대 메모리 |
query.max-memory | 클러스터 | 한 쿼리가 전체 워커에서 합산해 쓸 수 있는 최대 메모리 |
memory.heap-headroom-per-node | 워커 1대 | Trino 가 쿼리에 할당하지 않고 남겨두는 힙 (GC·시스템용) |
관계식(대략):
query.max-memory-per-node < (JVM 힙 - heap-headroom-per-node)
query.max-memory ≈ query.max-memory-per-node × 워커 수 (이하)예를 들어 워커 힙이 24GB 이고 headroom 이 5GB 라면, 쿼리용 풀은 약 19GB 입니다. query.max-memory-per-node 를 12GB 로 두면, 한 쿼리가 한 노드에서 12GB 까지 쓸 수 있고 나머지는 다른 쿼리들이 나눠 씁니다.
# etc/config.properties (워커)
query.max-memory-per-node=12GB
memory.heap-headroom-per-node=5GB
# 코디네이터
query.max-memory=240GB쿼리가 이 상한을 넘으면 EXCEEDED_LOCAL_MEMORY_LIMIT 또는 EXCEEDED_GLOBAL_MEMORY_LIMIT 로 그 쿼리만 실패합니다. 상한이 있기에 한 쿼리가 클러스터 전체를 비정상 동작하지 못하도록 합니다.
2. Spill-to-Disk — OOM 대신 느려지기
메모리 상한에 걸린 쿼리를 무조건 죽이는 대신, 중간 데이터를 디스크로 흘려보내(spill) 정상적으로 실행할 수 있도록 할 수 있습니다. 조인·집계·정렬·윈도우 연산에서 동작합니다.
# etc/config.properties
spill-enabled=true
spiller-spill-path=/data1/trino-spill,/data2/trino-spill # 여러 디스크 분산 권장
max-spill-per-node=200GB
query-max-spill-per-node=100GB트레이드오프가 분명합니다.
| spill 끄기 | spill 켜기 | |
|---|---|---|
| 메모리 초과 시 | 쿼리 실패 | 디스크로 흘려 완주 |
| 성능 | 빠름(메모리 내) | 느려짐(디스크 I/O) |
| 적합 | 저지연 대화형 | 대형 배치·ETL |
spill 은 "느려지더라도 끝나게" 하는 안전장치입니다. 모든 쿼리를 메모리로 처리하는 것이 이상적이지만, 가끔 튀는 대용량 쿼리 때문에 클러스터가 죽는 것보다는 낫습니다. spill 경로는 빠른 로컬 SSD 여러 개로 분산하는 것을 추천합니다.
spill 로도 한계가 있을 때의 근본 대응은 데이터를 처리하는 쿼리를 개선하는 것입니다 — SELECT * 대신 필요한 컬럼만, 큰 조인의 build side 축소, 사전 집계, 파티션 프루닝 활용.
3. 문제: 메모리 상한만으로는 부족하다
query.max-memory 는 "한 쿼리"의 상한입니다. 하지만 현실의 문제는 보통 여러 쿼리의 경합입니다.
- 분석가 30명이 동시에 무거운 쿼리를 던지면, 각각은 상한 안이라도 클러스터 메모리가 고갈됩니다.
- 우선순위가 낮은 임시 쿼리가 동시성을 점유해 SLA 가 걸린 정기 배치가 큐에서 밀립니다.
- 한 BI 도구의 대시보드 새로고침 폭주가 전체 응답성을 떨어뜨립니다.
이를 해결하는 것이 Resource Groups 입니다. (Cloudera 를 쓰던 분이라면 Impala 의 Admission Control / 리소스 풀에 대응하는 개념입니다.)
4. Resource Groups — 워크로드 격리
Resource Groups 는 들어오는 쿼리를 그룹으로 분류하고, 각 그룹에 동시성·메모리·큐 한도를 부여합니다.
# etc/resource-groups.properties
resource-groups.configuration-manager=file
resource-groups.config-file=/etc/trino/resource-groups.json{
"rootGroups": [
{
"name": "global",
"softMemoryLimit": "80%",
"hardConcurrencyLimit": 100,
"maxQueued": 1000,
"schedulingPolicy": "weighted",
"subGroups": [
{
"name": "batch",
"softMemoryLimit": "60%",
"hardConcurrencyLimit": 20,
"maxQueued": 500,
"schedulingWeight": 3
},
{
"name": "adhoc",
"softMemoryLimit": "30%",
"hardConcurrencyLimit": 40,
"maxQueued": 200,
"schedulingWeight": 1
},
{
"name": "dashboard",
"softMemoryLimit": "20%",
"hardConcurrencyLimit": 50,
"maxQueued": 100,
"schedulingWeight": 2
}
]
}
],
"selectors": [
{ "user": "etl-svc", "group": "global.batch" },
{ "source": "superset", "group": "global.dashboard" },
{ "group": "global.adhoc" }
]
}주요 속성
| 속성 | 의미 |
|---|---|
hardConcurrencyLimit | 동시에 실행되는 쿼리 최대 수 |
maxQueued | 대기 큐 최대 길이. 초과하면 쿼리 거부 |
softMemoryLimit | 이 그룹이 쓸 수 있는 메모리(%, 또는 절대값). 초과 시 신규 쿼리 대기 |
schedulingPolicy | 큐에서 다음 쿼리를 고르는 정책 |
schedulingWeight | weighted 정책에서 그룹 간 자원 배분 가중치 |
셀렉터 — 쿼리를 그룹에 매칭
selectors 는 위에서부터 첫 매칭으로 쿼리를 그룹에 배정합니다. user, source(클라이언트가 보낸 소스명), clientTags, queryType 등으로 분류할 수 있습니다.
etl-svc 사용자 → global.batch (동시성 20, 메모리 60%, 가중치 높음)
superset 대시보드 → global.dashboard(많은 동시성, 작은 쿼리)
그 외 모든 쿼리 → global.adhoc (임시 분석, 가중치 낮음)이렇게 하면 분석가들의 임시 쿼리(adhoc)가 아무리 몰려도 배치(batch)의 동시성·메모리를 침범하지 못합니다. SLA 가 걸린 작업을 보호하는 핵심 장치입니다.
5. 스케줄링 정책 고르기
| 정책 | 동작 | 적합 |
|---|---|---|
fair | 같은 그룹 내 쿼리를 공평하게 (FIFO 기반) | 일반적인 기본값 |
weighted | schedulingWeight 비율로 하위 그룹에 자원 배분 | 그룹 간 우선순위 차등 |
weighted_fair | 가중치 + 공평성 절충 | 다수 그룹의 균형 |
query_priority | 쿼리의 query_priority 세션 값으로 우선순위 | 동적 우선순위 제어 |
위 예시는 weighted 로, batch:dashboard:adhoc = 3:2:1 비율로 자원을 우선 배분합니다.
6. 쿼리 레벨 제어 — 추가 안전장치
Resource Groups 외에도 폭주를 막는 글로벌·세션 설정이 있습니다.
# etc/config.properties — 비정상적으로 무거운/느린 쿼리 차단
query.max-execution-time=2h
query.max-scan-physical-bytes=5TB
query.max-stage-count=150-- 세션 단위 조정 (특정 쿼리만)
SET SESSION query_max_memory_per_node = '20GB';
SET SESSION query_max_run_time = '30m';query.max-scan-physical-bytes 는 실수로 거대 풀스캔을 던졌을 때 일정 바이트 이상 스캔하면 쿼리를 끊어주는 유용한 가드레일입니다.
7. 진단 — 무엇을 보고 튜닝하나
7.1 시스템 테이블
-- 현재 실행/대기 중인 쿼리와 메모리 사용
SELECT query_id, state, resource_group_id,
query, total_memory_reservation
FROM system.runtime.queries
WHERE state IN ('RUNNING', 'QUEUED')
ORDER BY total_memory_reservation DESC;
-- 리소스 그룹별 현황
SELECT * FROM system.runtime.resource_group_pools;7.2 EXPLAIN ANALYZE
실제 실행 후 단계별 시간·메모리·행 수를 확인합니다.
EXPLAIN ANALYZE
SELECT user_id, count(*)
FROM iceberg.analytics.events
WHERE event_time >= TIMESTAMP '2026-06-01 00:00:00 UTC'
GROUP BY user_id;조인 순서가 나쁘거나 한 스테이지가 메모리를 독점하는지 여기서 드러납니다. 조인 품질이 낮으면 통계를 갱신하세요.
ANALYZE iceberg.analytics.events;7.3 Web UI
코디네이터 Web UI 의 Query Detail 에서 스테이지별 메모리·실행 시간·spill 발생 여부를 시각적으로 확인할 수 있습니다. "Peak Memory" 가 상한에 근접하는 쿼리가 튜닝 1순위입니다.
8. 증상별 처방 빠른 표
| 증상 | 원인 후보 | 처방 |
|---|---|---|
EXCEEDED_LOCAL_MEMORY_LIMIT | 한 노드에서 쿼리가 상한 초과 | spill 켜기, query.max-memory-per-node 상향, 쿼리 최적화 |
EXCEEDED_GLOBAL_MEMORY_LIMIT | 클러스터 상한 초과 | query.max-memory 점검, 워커 증설, 쿼리 분할 |
| 워커 OOMKill / GC 폭주 | 힙 ≈ 컨테이너 limit, headroom 부족 | headroom 상향, 힙 < limit 보장 |
| 배치가 임시 쿼리에 밀림 | 워크로드 격리 없음 | Resource Groups 로 batch/adhoc 분리 |
| 대시보드 폭주로 전체 저하 | dashboard 동시성 무제한 | dashboard 그룹에 동시성·큐 한도 |
| 거대 풀스캔 사고 | 가드레일 없음 | query.max-scan-physical-bytes 설정 |
| 쿼리는 도는데 느림 | 조인 순서·통계 부재 | ANALYZE, EXPLAIN ANALYZE 로 진단 |
9. 튜닝 체크리스트
- JVM 힙 < 컨테이너/노드 메모리 (headroom 20~30%)
-
query.max-memory-per-node< (힙 − headroom) - spill 활성화 + 빠른 로컬 디스크 다중 경로
- Resource Groups 로 batch/adhoc/dashboard 격리
- 셀렉터로 사용자·소스별 그룹 매칭
-
query.max-scan-physical-bytes등 글로벌 가드레일 - 핵심 테이블 정기
ANALYZE - Web UI/system 테이블로 peak memory 상위 쿼리 점검
10. 정리
| 계층 | 도구 | 막아주는 것 |
|---|---|---|
| 단일 쿼리 메모리 | query.max-memory(-per-node) | 한 쿼리의 클러스터 독점 |
| 완주 보장 | spill-to-disk | OOM 으로 인한 실패 |
| 워크로드 경합 | Resource Groups | 그룹 간 자원 침범, SLA 침해 |
| 폭주 사고 | 글로벌 쿼리 제한 | 실수성 거대 스캔·무한 실행 |
| 실행 품질 | ANALYZE + EXPLAIN ANALYZE | 나쁜 조인 순서·느린 쿼리 |
Trino 안정화의 핵심은 두 가지입니다. 첫째, 메모리 모델을 이해해서 힙·headroom·쿼리 상한을 일관되게 잡고 spill 로 안전망을 두는 것. 둘째, Resource Groups 로 워크로드를 격리해서 한 쿼리·한 팀이 전체를 마비시키지 못하게 하는 것. 이 둘이 갖춰지면 Trino 클러스터는 부하가 몰려도 "느려질지언정 멈추지 않는" 상태가 됩니다.
이 글은 Trino 440번대 기준으로 작성되었습니다. Trino 클러스터의 메모리·동시성 튜닝이나 워크로드 격리 설계가 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀