Blog
trinoperformancetuningresource-groupsmemorydata-platform

Trino 메모리 관리와 Resource Groups

Trino 의 메모리 모델(per-node·cluster·heap headroom)과 spill-to-disk, 그리고 Resource Groups 로 워크로드를 격리해 한 쿼리가 클러스터 전체를 점유하지 못하게 하는 방법을 실전 설정과 함께 정리합니다.

Data Dynamics2026年6月5日13 min read
This post is not yet translated. The original Korean version is shown below.

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큐에서 다음 쿼리를 고르는 정책
schedulingWeightweighted 정책에서 그룹 간 자원 배분 가중치

셀렉터 — 쿼리를 그룹에 매칭

selectors 는 위에서부터 첫 매칭으로 쿼리를 그룹에 배정합니다. user, source(클라이언트가 보낸 소스명), clientTags, queryType 등으로 분류할 수 있습니다.

etl-svc 사용자        → global.batch    (동시성 20, 메모리 60%, 가중치 높음)
superset 대시보드     → global.dashboard(많은 동시성, 작은 쿼리)
그 외 모든 쿼리       → global.adhoc    (임시 분석, 가중치 낮음)

이렇게 하면 분석가들의 임시 쿼리(adhoc)가 아무리 몰려도 배치(batch)의 동시성·메모리를 침범하지 못합니다. SLA 가 걸린 작업을 보호하는 핵심 장치입니다.

5. 스케줄링 정책 고르기

정책동작적합
fair같은 그룹 내 쿼리를 공평하게 (FIFO 기반)일반적인 기본값
weightedschedulingWeight 비율로 하위 그룹에 자원 배분그룹 간 우선순위 차등
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-diskOOM 으로 인한 실패
워크로드 경합Resource Groups그룹 간 자원 침범, SLA 침해
폭주 사고글로벌 쿼리 제한실수성 거대 스캔·무한 실행
실행 품질ANALYZE + EXPLAIN ANALYZE나쁜 조인 순서·느린 쿼리

Trino 안정화의 핵심은 두 가지입니다. 첫째, 메모리 모델을 이해해서 힙·headroom·쿼리 상한을 일관되게 잡고 spill 로 안전망을 두는 것. 둘째, Resource Groups 로 워크로드를 격리해서 한 쿼리·한 팀이 전체를 마비시키지 못하게 하는 것. 이 둘이 갖춰지면 Trino 클러스터는 부하가 몰려도 "느려질지언정 멈추지 않는" 상태가 됩니다.


이 글은 Trino 440번대 기준으로 작성되었습니다. Trino 클러스터의 메모리·동시성 튜닝이나 워크로드 격리 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀