OLAP 환경에서 Trino를 메인 쿼리 엔진으로 쓰고 있었다. ClickHouse 도입을 검토하던 중 기술 스택 단일화 측면에서 Trino를 개선하는 게 낫겠다는 판단이 섰다. 그래서 Trino 버전을 v433에서 v451로 올렸다. 주목적은 Alluxio 기반 캐시 기능 활용이었다.
이전에 쓰던 Rubix 캐시는 deprecated 상태였고 Alluxio 캐시는 v439에서 추가된 뒤 v445에서 race condition 버그가 수정되면서 쓸 만한 상태가 됐다.
이 글은 Blue/Green 배포 환경에서 Alluxio 캐시를 PoC한 과정과 그 과정에서 마주친 병목을 정리한 기록이다.
Alluxio 캐시가 뭔가#
Trino가 S3에서 데이터를 읽을 때마다 네트워크를 타야 한다. 같은 파일을 여러 번 읽어도 매번 S3 API를 호출한다. Alluxio 캐시는 한 번 읽은 데이터를 워커 노드의 로컬 디스크에 저장해서 두 번째부터는 로컬에서 읽는 방식이다.
카탈로그 설정에 몇 줄 추가하면 동작한다.
fs.cache.enabled=true
fs.cache.max-sizes=50GB
fs.cache.directories=/mnt/cacheHive, Iceberg, Delta Lake 커넥터에서 사용할 수 있다. 우리는 hive_zeppelin과 iceberg 두 카탈로그에 적용했다.
제약사항#
PoC를 시작하기 전에 알아둬야 할 제약이 있었다.
캐시가 카탈로그/클러스터 간에 공유되지 않는다#
같은 S3 버킷을 바라보는 카탈로그가 여럿 있어도 캐시는 각 카탈로그별로 따로 관리된다. 커넥터 구조상 파일 시스템을 카탈로그마다 독립적으로 인스턴스화하기 때문이다. 클러스터 간 공유는 당연히 안 된다.
이 문제를 해결하려는 네이티브 Alluxio 파일 시스템 PR이 2024년 9월에 머지됐다(Trino 460). 카탈로그 간, 클러스터 간 캐시 공유가 가능해진다. 다만 공유 캐시 파일 시스템 접근이 S3 직접 접근보다 빠를지는 별도 검증이 필요하다.
스팟 인스턴스와 캐시는 궁합이 안 맞다#
워커의 캐시 데이터 생애주기는 해당 워커 노드의 생애주기와 같다. 노드가 사라지면 캐시도 증발한다.
문제는 우리 환경이 거의 99% 스팟 노드라는 거다. 스팟 회수가 일어나면 해당 워커의 캐시가 통째로 날아간다. 오토스케일링으로 스케일인될 때도 마찬가지다. 캐시 웜업에 시간이 걸리는데 노드가 자주 바뀌면 웜업 효과를 보기 어렵다.
스키마 단위 제어가 안 된다#
캐시 활성화는 카탈로그 단위로 all-or-nothing이다. temp 스키마처럼 재사용 빈도가 거의 없는 테이블도 전부 캐싱된다. 주요 스키마만 선택적으로 캐싱할 수 있으면 좋겠지만 현재는 지원하지 않는다.
fs.cache.skip-paths 옵션을 추가하는 PR이 있었지만 설계 방향에 대한 의견 차이로 닫혔다. 파일 경로 기반이 아니라 스키마/테이블 단위로 제어해야 한다는 게 리뷰어의 의견이었다.
PoC 구성#
Blue/Green 중 한쪽에만 적용#
비용 문제로 스테이지 환경을 프로덕션과 동일하게 구축하기 어려웠다. 그래서 OLAP과 BI 클러스터의 Blue/Green 중 Blue 클러스터에만 캐시를 적용하고 1~2주간 쿼리 성능 지표를 비교하기로 했다.
앞단에 Trino Gateway가 있어서 각 백엔드의 현재 쿼리 수를 기준으로 부하를 분산한다. 유입되는 쿼리가 완전히 동일하진 않지만 2주 정도 데이터가 쌓이면 유의미한 비교가 된다고 판단했다.
볼륨 구성#
워커 팟과 노드를 1:1로 매핑하는 게 Trino 베스트 프랙티스다. StatefulSet + volumeClaimTemplate으로 팟 단위 볼륨을 구성할 수도 있지만 우리는 노드에 캐시 전용 EBS 볼륨을 마운트하고 워커 팟에서 hostPath로 접근하게 구성했다.
초기 캐시 볼륨은 워커당 50GB. hive_zeppelin 카탈로그에 60%, iceberg 카탈로그에 30%를 할당했다.
적용 순서#
- OLAP Blue 클러스터에 선적용 (2024-07-30)
- BI Blue 클러스터에 선적용 (2024-07-31)
- Grafana에 p25/p50/p75/p99/avg/max 쿼리 수행시간 비교 패널 추가
- JMX 메트릭으로 캐시 사용 현황 모니터링
BI 클러스터는 같은 대시보드 내 여러 차트가 동일 테이블의 같은 날짜 범위를 조회하는 경우가 많아서 캐시 효과가 더 클 거라 기대했다.
EBS 스루풋 병목#
캐시를 켜고 며칠 지켜봤다. 드라마틱한 차이는 없었다. 캐시 적중률은 괜찮은데 왜 성능 차이가 안 나지?
캐시 볼륨 메트릭을 까봤더니 원인이 보였다. EBS 쓰기 스루풋이 설정된 최대치인 125MiB/s에 계속 도달하고 있었다. gp3 볼륨의 기본 스루풋이 125MiB/s인데 캐시 쓰기가 이 한도를 꽉 채우고 있던 거다.
캐시가 아무리 빨라도 디스크 쓰기가 병목이면 소용이 없다.
스루풋 상향#
gp3에서 설정 가능한 최대 스루풋인 1000MiB/s로 올렸다.
- BI 클러스터: 오후 2시 45분경 적용
- OLAP 클러스터: 오후 3시 45분경 적용
결과가 바로 나타났다. 스루풋 상향 후 캐시 적용 클러스터(Blue)의 쿼리 수행 지표가 확실히 좋아졌다. 쓰기 스루풋이 200MiB 정도까지 올라가도 최대치(1000MiB)에는 한참 여유가 있었다. 읽기까지 합치면 대략 300MiB 수준이었다.
gp3 스루풋만 올리면 됐지 io2 같은 고성능 볼륨 타입은 필요 없었다. io2의 최대 스루풋도 1000MiB/s로 동일하고 높은 IOPS가 필요한 워크로드도 아니었으니까.
캐시 볼륨 사이징#
초기 50GB는 금방 꽉 찼다. 허용된 최대 캐시 사이즈(hive 60% + iceberg 30% = 45GB)를 모두 채워 쓰고 있었다. 캐시 공간이 부족하면 오래된 데이터부터 밀려나는데 너무 빨리 밀려나면 캐시 효과가 떨어진다.
볼륨 사이즈를 키웠다.
| 클러스터 | 변경 전 | 변경 후 |
|---|---|---|
| BI 워커 | 50GB | 150GB → 1.5TB |
| OLAP 워커 | 50GB | 2TB |
| OLAP 코디네이터 | - | 1TB (iceberg 메타데이터 캐시용) |
카탈로그별 할당 비율도 실제 사용량을 보고 조정했다. hive가 iceberg보다 캐시를 훨씬 많이 쓰고 있어서 hive 60% → 85%, iceberg 30% → 10%로 재배분했다.
EBS 스토리지 비용이 걱정됐지만 확인해보니 월 비용이 크지 않았다. 캐시 덕분에 워커 수가 20% 줄어든 효과가 EBS 비용 증가분을 상쇄하고도 남았다.
필요 이상으로 크게 잡아놓고 Grafana에 각 노드의 캐시 볼륨 사용률 차트를 추가해서 지켜본 뒤 낭비되면 줄이는 방식으로 접근했다.
FIFO가 LRU보다 나을 수 있다#
Trino의 Alluxio 캐시는 LRU 같은 정교한 교체 알고리즘이 아니라 FIFO 방식으로 동작한다. 가장 먼저 캐시된 데이터를 먼저 내보낸다.
직관적으로는 LRU가 나아 보이지만 FIFO가 유리한 경우도 있다.
- One-hit wonder 제거: 한 번만 읽히고 다시 안 읽히는 데이터를 빠르게 밀어낸다. LRU는 최근 접근 시간을 갱신하느라 이런 데이터가 캐시에 더 오래 남을 수 있다.
- SSD 친화적: FIFO는 랜덤 액세스를 최소화한다. SSD나 플래시 메모리 기반 스토리지에서 쓰기 증폭(write amplification)이 적다.
초기 단계 기능이라 교체 알고리즘이 단순한 건 맞지만 OLAP 워크로드에서 FIFO가 반드시 나쁜 선택은 아니다.
결과#
쿼리 성능#
캐시 활성화 + EBS 스루풋 상향 이후 Blue 클러스터가 Green 대비 일관되게 빠른 경향을 보였다.
흥미로운 건 Blue가 더 적은 워커로 더 높은 쿼리 스루풋을 기록했다는 점이다. 쿼리를 빨리 처리하니까 앞단 게이트웨이가 더 많은 쿼리를 라우팅해줬고 그 결과 Green보다 워커 수가 20% 적은 상태에서도 처리량이 더 높았다.
캐시 웜업에는 약 3시간이 걸렸다. 워커가 새로 뜨고 캐시가 충분히 찰 때까지 이 시간이 지나야 성능 차이가 드러났다.
S3 API 비용 절감#
캐시 적용 전후로 S3 GetObject 비용이 월 약 440만 원 줄었다. 캐시 적용 외에도 쿼리 수 변동 같은 다른 요인이 영향을 줬을 수 있지만 비용 절감 규모가 꽤 컸다.
비용 정리#
| 항목 | 변화 |
|---|---|
| S3 API 비용 | 월 ~440만 원 절감 |
| EBS 스토리지 비용 | 소폭 증가 (캐시 볼륨) |
| 노드 비용 | 워커 수 ~20% 감소로 절감 |
인스턴스 스토어 검토#
EBS는 리모트 블록 스토리지다. S3 직접 접근 대비 캐시 성능 개선이 드라마틱하지 않을 수 있다. 만약 EBS 캐시로 충분한 효과가 없었다면 NVMe SSD 인스턴스 스토어가 달린 노드 타입으로 교체할 계획이었다.
| 인스턴스 타입 | 인스턴스 스토어 |
|---|---|
| r7gd.4xlarge | 1 x 950GB NVMe SSD |
| m7gd.8xlarge | 1 x 1,900GB NVMe SSD |
두 타입의 인스턴스 스토어 크기가 다르지만 Trino의 캐시 설정이 전체 디스크 용량 대비 퍼센트로도 지정할 수 있어서 문제가 되지 않는다. 다만 EBS 스루풋 상향만으로도 충분한 효과를 봤기 때문에 인스턴스 스토어로 교체는 진행하지 않았다.
같이 알아두면 좋은 것: Project Hummingbird#
Trino v451로 올리면서 눈여겨본 게 하나 더 있다. Project Hummingbird라는 성능 개선 프로젝트다.
Java 22의 Vector API를 활용한 벡터화 연산을 Trino에 적용하는 작업이다. Parquet 파일 읽기에 벡터화 디코딩이 v448부터 반영됐다. 단 256비트 이상 벡터 레지스터가 필요해서 Graviton 2 인스턴스(r6g, m6g)에서는 비활성화된다. Graviton 3 이상이면 자동으로 켜진다.
Trino가 v447부터 Java 22를 필수로 요구하게 된 것도 이 프로젝트의 일환이다.
마치며#
Alluxio 캐시 PoC에서 배운 건 단순하다.
캐시를 붙이는 것만으로는 안 된다. 디스크 I/O가 병목이면 캐시 적중률이 아무리 높아도 체감 성능이 안 나온다. EBS gp3 기본 스루풋 125MiB/s가 캐시 성능의 천장이었다. 1000MiB/s로 올리자마자 차이가 드러났다.
BI 워크로드에 캐시가 잘 먹힌다. 같은 테이블을 반복 조회하는 대시보드 쿼리는 캐시 히트율이 높다.
스팟 환경에서 캐시는 타협이 필요하다. 노드가 수시로 바뀌면 캐시 웜업 시간이 사실상 손실이다. 그래도 3시간 웜업 후에는 효과가 나타났으니 완전히 무용한 건 아니다.
앞으로 네이티브 Alluxio 파일 시스템(Trino 460)이 안정화되면 카탈로그/클러스터 간 캐시 공유가 가능해진다. 스팟 회수 시 캐시 손실 문제도 완화될 수 있을 거다.
참고 자료: