PostgreSQL Materialized View를 반정규화 테이블로 사용하며 겪은 문제와 해결 기록

H_M·2023년 9월 30일
post-thumbnail

PostgreSQL의 Materialized View(이하 Mview)를 반정규화 테이블 용도로 사용하며 겪었던 이슈들과 해결 방법을 정리합니다.

Mview 사용 배경

고객의 니즈가 다양해지면서 백오피스 화면에서 필요한 리스트 필드가 점점 많아졌고, 그에 따라 과도한 조인이 발생하기 시작했습니다.
이를 해결하기 위해 반정규화 테이블이 필요했고, PostgreSQL의 Mview 기능을 도입하게 되었습니다.

백오피스에서 사용되는 리스트는 실시간성과 응답 속도가 중요하기 때문에, 30초마다 Mview를 자동으로 리프레시하는 워커를 만들어 운영했습니다.

느려지기 시작

이커머스 서비스 특성상 상품 데이터가 빠르게 증가하면서 Mview의 사이즈도 함께 커졌고, 조건이 포함된 쿼리 속도 또한 눈에 띄게 느려지기 시작했습니다.

처음에는 조인 성능을 개선하기 위한 용도로 도입했지만, 오히려 Mview 조회 성능이 더 낮아지며 그 사용 의미가 없어서 결국 성능 재정비를 결정하게 됩니다.

인덱스 추가

가장 먼저 Mview를 사용하는 주요 쿼리들을 분석해 공통적으로 자주 사용되는 조건을 파악했습니다.
그 조건과 원본 테이블의 인덱스를 참고하여 총 9개의 인덱스를 추가했고, ANALYZE를 통해 쿼리 시간이 실제로 줄어드는 것을 확인했습니다.

그럼에도 불구하고...

아슬아슬하게 유지되던 Mview는 결국 터졌습니다.
PostgreSQL의 MVCC 구현 방식에서는 Dead Tuple이라는 개념이 존재하는데, 이를 적절히 처리하지 못하면서 문제가 발생했습니다.

Mview는 매번 REFRESH할 때마다 기존 데이터를 삭제하고 다시 삽입하는 구조인데, 이 과정에서 Dead Tuple이 쌓이고 인덱스가 반복 생성되면서 테이블 사이즈가 기하급수적으로 커졌습니다.

  • 장애 발생 시점 기준 Dead Tuple: 약 1억 4천만 개
  • 테이블 사이즈 증가: 100GB 이상 (약 1주일 차이)

결국 이 급격한 사이즈 증가는 심각한 쿼리 성능 저하를 유발했고, 실제 장애로 이어졌습니다.

해결

뒤늦게 수동으로 VACUUM을 시도했지만 이미 커진 테이블 페이지를 감당할 수 없었고,
30초마다 이뤄지는 REFRESH 주기를 고려하면 autovacuum 최적화로도 해결할 수 없는 상황이었습니다.

최종적으로 선택한 방법은 매일 새벽 Mview를 삭제 후 새로 생성하는 것이었습니다.
이 작업은 cron 잡을 이용해 자동화했고, 이후부터는 다음과 같은 성과를 얻었습니다.

  • 테이블 사이즈: 1GB 이하로 안정화
  • 쿼리 속도: 초기 수준으로 회복

Mview 사용의 한계

이 방법은 현재 우리 서비스 백오피스에서만 사용되고 있기에, 새벽 시간에는 사용자가 거의 없어 가능한 방식입니다.
하지만 사용자 활동이 있는 서비스라면, 삭제 후 생성 과정에서 발생하는 다운타임 때문에 추천하기 어렵습니다.

또한 Mview를 반정규화 용도로 사용할 때 가장 크게 느낀 한계는 실시간성이 부족하다는 점입니다.
서비스 초반에는 30초 주기의 리프레시도 충분했지만, 데이터가 증가한 현재는 10분 가까이 걸려 실시간 반영이라 보기 어렵게 되었습니다.

그래서 지금은 CQRS 아키텍처로 전환하는 방향으로 구조 개선을 진행 중입니다.


참고
https://www.postgresql.org/docs/current/routine-vacuuming.html
https://techblog.woowahan.com/9478/
https://nrise.github.io/posts/postgresql-autovacuum/

0개의 댓글