적재와 운영 : COPY, VACUUM, WLM, Spectrum
저번 포스팅에서는, Redshift 의 테이블 설계를 공부하였습니다. 이번에는 데이터를 넣고 운영하는 이야기입니다. 어떻게 적재해야 하는지, 수정과 삭제는 왜 비싼지, 동시에 들어오는 쿼리를 어떻게 관리하는지, 그리고 S3 의 데이터를 어떻게 함께 다루는지를 정리하겠습니다.
1. 데이터 적재
1.1. INSERT 를 반복하면 안 된다
가장 먼저 짚어야 할 부분입니다. Redshift 에서 INSERT INTO ... VALUES 를 한 건씩 반복하는 것은 재앙에 가깝습니다. 1편에서 본 대로 Redshift 는 컬럼 지향이고 블록 단위로 저장합니다. 한 행을 넣으려면 컬럼 수만큼의 블록을 건드려야 합니다. 그리고 각 블록은 1MB 인데, 한 행을 위해 그 블록 전체를 다시 써야 합니다.
한 행을 넣는 비용이 수백만 행을 넣는 비용과 크게 다르지 않습니다. 그래서 한 건씩 넣는 방식은 처리량이 극단적으로 떨어집니다.
1.2. COPY 를 쓴다
정석은 S3 에 파일로 올려두고 COPY 로 한 번에 적재하는 것입니다.
COPY orders
FROM 's3://my-bucket/orders/'
IAM_ROLE 'arn:aws:iam::...:role/RedshiftLoad'
FORMAT AS PARQUET;
COPY 가 빠른 이유는 병렬이기 때문입니다. 모든 슬라이스가 동시에 S3 에서 자기 몫을 읽어갑니다. 여기서 중요한 요령이 있습니다. 파일을 여러 개로 나눠야 합니다. 큰 파일 하나를 두면 슬라이스 하나만 일하고 나머지는 놉니다. 슬라이스 개수의 배수로 파일을 나누는 것이 권장됩니다. 슬라이스가 32개라면 파일을 32개나 64개로 나눕니다.
파일 하나의 크기는 압축 기준 1MB에서 1GB 사이가 적당합니다. 너무 잘게 나누면 파일마다 여는 비용이 커집니다.
1.3. 그 밖의 적재 경로
- INSERT INTO ... SELECT : 이미 Redshift 안에 있는 데이터를 옮길 때는 이 방식이 효율적입니다. 한 건씩이 아니라 집합 단위이기 때문입니다.
- 자동 복사(Auto-copy) : S3 경로를 지정해두면 새 파일이 올라올 때 자동으로 적재합니다.
- 스트리밍 수집 : Kinesis 나 MSK 에서 직접 가져옵니다.
- 제로 ETL 통합 : Aurora 같은 운영 데이터베이스의 변경을 별도 파이프라인 없이 Redshift 로 흘려보냅니다. DMS 같은 복제 도구가 하던 일 중 일부를 관리형으로 대신하는 방향입니다.
1.4. 적재 중 오류 확인
COPY 가 실패하면 어느 행이 왜 문제인지 봐야 합니다. STL_LOAD_ERRORS 에 기록됩니다. 어느 파일의 몇 번째 줄에서 어느 컬럼이 문제였는지가 남습니다. MAXERROR 로 일정 개수까지 무시하고 진행할 수 있지만, 운영 파이프라인에서는 조용히 데이터가 빠지는 것이 더 위험할 때가 많습니다.
2. 수정과 삭제가 비싼 이유
2.1. 실제로는 지우지 않는다
Redshift 에서 DELETE 는 행을 실제로 지우지 않습니다. 삭제 표시만 해둡니다. UPDATE 는 더 나아가서, 기존 행을 삭제 표시하고 새 행을 뒤에 추가합니다. 컬럼 지향 저장에서 블록 중간의 값을 제자리에서 고치는 것은 매우 비싸기 때문에 고른 방식입니다.
여기서 두 가지 문제가 따라옵니다.
- 공간이 회수되지 않습니다. 삭제 표시된 행이 디스크를 계속 차지합니다.
- 정렬이 깨집니다. 새로 추가된 행은 정렬 키 순서가 아니라 끝에 붙습니다. 2편에서 본 존 맵의 효과가 떨어집니다.
2.2. VACUUM
이것을 정리하는 것이 VACUUM 입니다.
- 삭제 표시된 행의 공간을 회수합니다.
- 정렬되지 않은 영역을 정렬해 다시 끼워 넣습니다.
무거운 작업이라 예전에는 운영에서 큰 부담이었습니다. 지금은 자동 VACUUM 과 자동 정렬이 동작해 한가한 시간에 백그라운드로 처리합니다. 다만 대량 삭제 직후처럼 급할 때는 직접 실행하기도 합니다. SVV_TABLE_INFO 의 unsorted 값으로 상태를 확인할 수 있습니다. 이 비율이 높으면 정렬이 많이 깨진 상태입니다.
2.3. 그래서 UPSERT 는 이렇게 한다
한 건씩 UPDATE 하는 대신, 스테이징 테이블을 거치는 방식이 정석입니다.
BEGIN;
-- 1. 변경분을 스테이징 테이블에 COPY 로 적재
COPY staging_orders FROM 's3://...' IAM_ROLE '...';
-- 2. 기존 테이블에서 겹치는 행을 한 번에 삭제
DELETE FROM orders
USING staging_orders s
WHERE orders.order_id = s.order_id;
-- 3. 스테이징을 통째로 삽입
INSERT INTO orders SELECT * FROM staging_orders;
COMMIT;
집합 단위로 한 번에 처리하는 것이 핵심입니다. 최근에는 MERGE 문도 지원되어 더 간결하게 쓸 수 있습니다.
2.4. 삭제는 DROP 이나 TRUNCATE 가 낫다
날짜 파티션처럼 통째로 지울 수 있는 단위라면, DELETE 대신 테이블을 나눠두고 DROP TABLE 하는 편이 훨씬 쌉니다. VACUUM 이 필요 없기 때문입니다. 전체를 비울 때도 DELETE 대신 TRUNCATE 를 씁니다. 즉시 공간이 회수됩니다.
3. 동시 쿼리 관리
3.1. 문제 상황
Redshift 는 노드 수가 고정되어 있습니다. 무거운 쿼리 하나가 메모리를 크게 잡으면 다른 쿼리가 대기합니다. 대시보드의 가벼운 조회가 배치 쿼리 뒤에 줄을 서게 되는 상황이 생깁니다.
3.2. WLM
WLM(Workload Management) 은 쿼리를 큐로 나누어 관리합니다. 큐마다 메모리 비율과 동시 실행 수를 배분합니다. 하둡 시리즈에서 본 YARN 의 큐와 발상이 같습니다. 자원을 용도별로 갈라두는 것입니다.
- 수동 WLM : 큐 개수, 메모리 비율, 동시성을 직접 정합니다. 통제는 확실하지만 잘 맞추기 어렵습니다. 메모리를 크게 잡으면 동시성이 떨어지고, 작게 잡으면 디스크로 흘러넘칩니다.
- 자동 WLM (권장) : Redshift 가 쿼리의 성격을 보고 메모리와 동시성을 알아서 조정합니다. 사용자는 우선순위만 지정합니다.
자동 WLM 에서는 큐마다 우선순위(Highest ~ Lowest)를 두는 것으로 충분한 경우가 많습니다. 대시보드 큐를 높게, 배치 큐를 낮게 두는 식입니다.
3.3. 짧은 쿼리 가속
SQA(Short Query Acceleration) 는 짧게 끝날 것으로 예측되는 쿼리를 전용 공간에서 먼저 처리합니다. 긴 배치 뒤에 줄 서 있던 가벼운 조회가 먼저 나가게 됩니다.
3.4. 쿼리 모니터링 규칙
QMR(Query Monitoring Rules) 로 "이런 쿼리는 이렇게 처리하라" 를 걸 수 있습니다.
- 실행 시간이 N초를 넘으면 중단
- 스캔 행 수가 과도하면 로그만 기록
- 조건에 걸리면 다른 큐로 이동
실수로 WHERE 를 빠뜨린 전체 스캔이 클러스터를 점유하는 것을 막는 안전장치입니다.
3.5. 동시성 스케일링
부하가 몰리면 임시 클러스터를 자동으로 추가해 대기 중인 쿼리를 처리합니다. 부하가 가라앉으면 사라집니다. 읽기 쿼리가 몰리는 시간대에 효과적입니다. 다만 별도 요금이 붙으므로 무제한으로 켜두지 않고 한도를 정합니다.
4. 쿼리 성능 끌어올리기
4.1. 결과 캐시
같은 쿼리가 다시 들어오고 그 사이 데이터가 바뀌지 않았다면 리더 노드가 캐시된 결과를 바로 돌려줍니다. 실행 자체가 일어나지 않습니다. 성능 측정을 할 때 이것을 모르면 착각하기 쉽습니다. 두 번째 실행이 갑자기 수십 배 빨라지는 것은 최적화가 아니라 캐시일 수 있습니다. 순수한 성능을 재려면 캐시를 꺼야 합니다.
4.2. 구체화 뷰
Materialized View 는 쿼리 결과를 실제로 저장해둡니다. 자주 쓰는 집계를 미리 계산해두는 것입니다. Redshift 의 구체화 뷰에는 두 가지 편리한 기능이 있습니다.
- 자동 새로 고침 : 기반 테이블이 바뀌면 알아서 갱신합니다. 전체를 다시 계산하지 않고 변경분만 반영하는 증분 갱신도 지원합니다.
- 자동 쿼리 재작성 : 사용자가 원본 테이블에 대해 쿼리를 던져도, 옵티마이저가 적합한 구체화 뷰가 있으면 그것을 쓰도록 계획을 바꿉니다. 쿼리를 고치지 않아도 혜택을 받습니다.
하둡 시리즈에서 본 Apache Kylin 의 "미리 계산해두고 꺼내 쓴다" 는 접근과 같은 발상입니다.
4.3. 자주 하는 실수
SELECT *컬럼 지향의 이점을 통째로 버립니다. 필요한 컬럼만 적어야 합니다.- 날짜 컬럼에 함수를 씌우는 것
WHERE DATE_TRUNC('month', order_date) = '2025-01-01'처럼 쓰면 컬럼이 함수 안에 들어가 존 맵을 쓸 수 없습니다.WHERE order_date >= '2025-01-01' AND order_date < '2025-02-01'로 범위 조건으로 바꿔야 합니다. - 암묵적 타입 변환 문자열 컬럼에 숫자를 비교하는 식으로 두면 변환이 일어나 블록 건너뛰기가 깨집니다.
- 디스크 스필
메모리가 부족해 중간 결과가 디스크로 흘러넘치면 급격히 느려집니다.
SVL_QUERY_SUMMARY에서 확인할 수 있고, 큰 조인의 분산 방식이 잘못되었거나 WLM 메모리가 부족한 경우가 많습니다.
5. S3 와 함께 쓰기
5.1. Redshift Spectrum
S3 에 있는 데이터를 Redshift 로 적재하지 않고 그 자리에서 조회하는 기능입니다. Glue 데이터 카탈로그에 등록된 테이블을 외부 스키마로 연결하면, 일반 테이블처럼 SQL 을 던질 수 있습니다. 클러스터 안의 테이블과 조인하는 것도 가능합니다.
하둡 시리즈에서 본 Hive Metastore 와 외부 테이블 개념이 그대로 이어집니다. 실제로 Glue 카탈로그는 Hive 메타스토어와 호환됩니다. 쓰임새가 분명합니다.
- 자주 안 보는 오래된 데이터를 S3 에 두고 필요할 때만 조회합니다.
- 원본 로그 전체를 적재하지 않고 일부만 뽑아 옵니다.
- 적재 전에 데이터를 미리 살펴봅니다.
성능을 위해서는 S3 쪽 관리가 중요합니다. Parquet 같은 컬럼 포맷으로 저장하고, 날짜 등으로 파티션을 나누고, 작은 파일이 너무 많지 않게 유지해야 합니다. 파티션을 태우지 못하면 S3 전체를 스캔하게 되고, 스캔한 데이터 양만큼 과금됩니다.
5.2. UNLOAD
반대 방향입니다. Redshift 의 쿼리 결과를 S3 로 병렬 내보내기 합니다. Parquet 으로 내보낼 수 있어, 다른 분석 도구와 데이터를 주고받는 통로가 됩니다.
5.3. 데이터 공유
1편에서 본 RA3 의 저장소 분리 덕분에 가능해진 기능입니다. 데이터를 복사하지 않고 다른 클러스터에서 읽게 할 수 있습니다. 부서별로 클러스터를 분리해 워크로드를 격리하면서도, 공통 데이터는 한 벌만 유지하는 구성이 가능합니다.
6. 운영에서 보는 것들
6.1. 주기적으로 확인할 것
SVV_TABLE_INFO: 스큐, 정렬되지 않은 비율, 통계 상태STL_ALERT_EVENT_LOG: 옵티마이저 경고. 방송 조인, 통계 없음, 디스크 스필- 디스크 사용률 : RA3 가 아니라면 여유가 없을 때 급격히 느려집니다
- 큐 대기 시간 : 쿼리가 실행보다 기다리는 데 더 오래 걸리고 있지 않은지
6.2. 백업과 복구
스냅샷이 기본입니다. 자동 스냅샷이 주기적으로 생성되고, 수동으로도 만들 수 있습니다. 다른 리전으로 복사해두면 재해 복구에도 대비됩니다.
6.3. 크기 조정
- 엘라스틱 리사이즈 : 같은 노드 타입에서 개수를 바꿉니다. 빠르지만 변경 폭에 제한이 있습니다.
- 클래식 리사이즈 : 노드 타입까지 바꿀 수 있습니다. 데이터를 옮기므로 오래 걸립니다.
6.4. 비용
- 쓰지 않는 시간에 일시 중지할 수 있습니다. 개발 클러스터에 유용합니다.
- 동시성 스케일링과 Spectrum 은 별도 과금이므로 한도를 두고 사용량을 봅니다.
- 오래된 데이터를 S3 로 내리고 Spectrum 으로 조회하면 저장 비용이 줄어듭니다.
7. Redshift 가 맞지 않는 경우
마지막으로 선을 그어두겠습니다. 다음 경우에는 다른 도구를 봐야 합니다.
- 단건 조회와 갱신이 많은 경우 : OLTP 용도입니다. RDS 나 DynamoDB 가 맞습니다.
- 초당 수천 건의 동시 요청 : 웹 서비스의 백엔드로 쓰기에는 맞지 않습니다.
- 비정형 데이터 탐색 : 스키마가 정해지지 않은 데이터라면 S3 와 Athena 쪽이 유연합니다.
- 밀리초 단위 응답이 필요한 경우 : 결과 캐시에 걸리지 않으면 어렵습니다.
8. 조금 더 들어가보기 : 커밋과 가시성
적재와 정리 작업을 봤는데, 여러 작업이 동시에 돌 때 무슨 일이 벌어지는지까지 내려가 보겠습니다. 운영에서 겪는 이상한 현상들이 대개 이 층에서 설명됩니다.
8.1. 커밋은 줄을 선다
Redshift 에서 트랜잭션을 커밋하는 일은 클러스터 전체에서 직렬화됩니다. 여러 세션이 동시에 커밋을 요청해도 한 번에 하나씩 처리됩니다. 메타데이터를 일관되게 유지하려면 그래야 하기 때문인데, 여기서 실무적인 결론이 나옵니다.
커밋을 자주 하면 그 자체가 병목이 됩니다. 1,000건을 한 번에 넣고 커밋하는 것과 한 건씩 1,000번 커밋하는 것은, 쓰는 데이터는 같지만 커밋 대기가 1,000배입니다. 앞서 "집합 단위로 처리하라" 고 한 이유가 하나 더 있는 셈입니다. 블록을 다시 쓰는 비용뿐 아니라 커밋 줄서기 비용도 함께 줄어듭니다.
STL_COMMIT_STATS 같은 곳에서 커밋 대기 시간을 볼 수 있는데, 이 값이 크면 쿼리가 느린 것이 아니라 커밋을 기다리고 있는 것입니다.
8.2. 읽는 쪽은 왜 막히지 않는가
적재가 도는 중에도 조회는 됩니다. 그리고 조회하는 쪽은 적재가 끝나기 전의 상태를 봅니다. 중간에 절반만 들어간 데이터가 보이지 않습니다. 이것이 가능한 이유는 덮어쓰지 않기 때문입니다. 앞서 본 대로 삭제는 표시만 하고, 갱신은 새 행을 뒤에 붙입니다. 그래서 옛 버전이 그대로 남아 있고, 진행 중인 조회는 그것을 계속 읽으면 됩니다.
대신 대가가 있습니다. 긴 조회가 돌고 있으면 그 조회가 보는 옛 버전을 치울 수 없습니다. 정리 작업이 공간을 회수하지 못하고, 디스크 사용량이 줄지 않습니다. "삭제를 많이 했는데 공간이 안 줄어든다" 의 원인이 여기인 경우가 있습니다. 아주 오래 도는 조회나 열어둔 채 방치된 트랜잭션을 찾아봐야 합니다.
8.3. 그래서 락은 어디에 걸리는가
대부분의 조회는 서로를 막지 않습니다. 그런데 테이블 구조를 바꾸는 작업은 다릅니다.
- 컬럼을 추가하거나 이름을 바꾸는 작업
- 테이블을 지우거나 이름을 바꾸는 작업
- 일부 정리 작업
이런 것들은 테이블을 독점해야 하므로, 진행 중인 조회가 끝나기를 기다립니다. 그리고 그 뒤에 들어온 조회들은 이 작업을 기다립니다. 여기서 줄이 길게 늘어집니다. 한 시간짜리 조회 하나 뒤에 스키마 변경이 걸리면, 그 뒤의 모든 쿼리가 한 시간 넘게 대기합니다. 대시보드가 통째로 멈춘 것처럼 보이는데, 정작 원인은 아주 오래전에 시작된 조회 하나입니다.
스키마 변경은 조회가 적은 시간대에 하라는 조언이 여기서 나옵니다.
8.4. COPY 가 병렬인 이유와 그 조건
COPY 가 빠른 것은 모든 슬라이스가 동시에 S3 에서 자기 몫을 읽기 때문입니다. 그런데 이 병렬성은 조건부입니다.
- 파일이 여러 개여야 합니다. 하나면 한 슬라이스만 일합니다.
- 파일이 고르게 나뉘어야 합니다. 크기가 제각각이면 가장 큰 파일을 맡은 슬라이스가 전체를 결정합니다. 앞서 본 스트래글러입니다.
- 압축 방식이 나눠 읽을 수 있어야 합니다. 파일 중간부터 읽을 수 없는 압축 형식이라면, 하나의 파일을 여럿이 나눠 읽지 못합니다.
그래서 슬라이스 수의 배수로, 비슷한 크기로 나누라는 권고가 나옵니다. 이것은 앞서 여러 번 본 "고르게 나누기" 문제의 또 다른 모습입니다.
8.5. 결과 캐시가 성능 측정을 속이다
앞서 짚은 부분을 조금 더 보겠습니다. 결과 캐시는 쿼리 문자열과 데이터 상태가 모두 같을 때 동작합니다. 여기서 측정할 때 조심할 점이 생깁니다.
- 두 번째 실행이 100배 빠르다면 최적화가 아니라 캐시일 수 있습니다.
- 그런데 공백 하나만 달라도 다른 쿼리로 보므로, 의도치 않게 캐시를 비껴가기도 합니다.
- 테이블에 적재가 일어나면 캐시가 무효가 됩니다. 그래서 실시간 적재가 도는 테이블에서는 캐시 효과가 거의 없습니다.
성능을 비교할 때는 캐시를 끄고 재야 하고, 반대로 운영 지표를 볼 때는 캐시 적중률을 함께 봐야 합니다. 적중률이 갑자기 떨어졌다면 적재 패턴이 바뀐 것일 수 있습니다.
8.6. 대규모에서 무엇이 먼저 막히는가
- 커밋 직렬화 : 적재 작업이 많아질수록 커밋 대기가 늘어납니다. 작업을 합쳐 커밋 횟수를 줄이는 것이 직접적인 대응입니다.
- 정렬되지 않은 비율 : 갱신이 잦은 테이블에서 자동 정리가 유입을 못 따라가면 조회가 서서히 나빠집니다.
- 동시 실행 한도 : 큐마다 동시 실행 수가 정해져 있어, 넘으면 대기합니다. 대기 시간과 실행 시간을 나눠 봐야 하는 이유입니다.
- 디스크 여유 : 여유가 적어지면 정렬과 조인의 중간 결과를 쓸 공간이 부족해져 쿼리가 실패합니다.
- 오래 열린 트랜잭션 : 공간 회수를 막고 스키마 변경을 막습니다. 눈에 잘 안 띄는 대신 영향이 넓습니다.
마지막 항목은 특히 원인과 증상이 멀리 떨어져 있어서 찾기 어렵습니다. 디스크가 안 줄고, 스키마 변경이 안 끝나고, 쿼리가 대기하는 세 증상이 동시에 나타난다면 열린 트랜잭션부터 확인해볼 만합니다.
결론
운영 관점에서 Redshift 를 정리하면서 가장 크게 남은 것은, "한 건씩" 이라는 발상을 버려야 한다는 점이었습니다. INSERT 도, UPDATE 도, DELETE 도 전부 집합 단위로 생각해야 했습니다. 컬럼 지향 저장이라는 선택 하나가 데이터를 다루는 방식 전체를 바꿔놓는다는 것이 인상적이었습니다.
두 번째는 DELETE 가 실제로 지우지 않는다는 사실이었습니다. 삭제 표시만 하고 VACUUM 이 나중에 정리한다는 구조는, Iceberg 의 삭제 방식이나 LSM 트리의 컴팩션과 같은 계열의 이야기였습니다. 쓰기를 싸게 만들기 위해 정리를 뒤로 미루는 것은 여러 시스템에서 반복해서 나오는 방식이라는 생각이 들었습니다.
세 번째는 날짜 컬럼에 함수를 씌우면 존 맵이 무력화된다는 부분이었습니다. 같은 결과를 내는 쿼리인데 표현 방식 하나로 읽는 양이 수십 배 차이 납니다. 2편에서 배운 존 맵의 원리를 알고 있어야 이런 차이를 알아챌 수 있었고, 원리를 아는 것이 곧 튜닝 능력이 된다는 것을 확인한 대목이었습니다.
마지막으로 Spectrum 을 보면서 하둡 시리즈가 다시 떠올랐습니다. 외부 테이블, 메타스토어, 파티션 프루닝까지 이름만 바꿔 그대로 나왔습니다. 데이터 웨어하우스와 하둡 진영이 결국 같은 자리에서 만나고 있다는 인상을 받았습니다.