CS 지식/데이터베이스

PostgreSQL의 1순위 장애 이유 Auto Vacuum Bloating

마늘냄새폴폴 2025. 4. 29. 22:09

PostgreSQL은 이제 명실상부 RDBMS의 대표주자로서 자리를 잡았습니다. 다양한 인덱스 설정으로 RDBMS 중에서도 읽기 성능은 상위권에 있으면서, 개발자가 원한다면 CP 데이터베이스 AP 데이터베이스로 스왑이 가능하고, 오픈소스라는 특징 때문에 엄청나게 방대한 커뮤니티를 가지고 있다는 것이 바로 그 이유이죠. 

 

이런 팔방미인 PostgreSQL에게 약점이 있으니 바로 Vacuum입니다. PostgreSQL은 튜닝할 수 있는 설정이 굉장히 많아서 잘만 사용하면 뛰어난 성능의 데이터베이스로 만들 수 있지만 자칫 잘못 사용하면 겉잡을 수 없이 커지는 장애가 발생하게 되는데요. 

 

오늘 포스팅에서는 PostgreSQL의 1순위 장애 이유인 Auto Vacuum Bloating에 대해서 공부해본 내용을 정리해보도록 하겠습니다. 

 

Vacuum

Auto Vacuum Bloating에 들어가기 전에 Vacuum에 대해서 먼저 짚고 넘어가겠습니다. 

 

Vacuum은 PostgreSQL이 가지는 독특한 특징 중 하나인데요. Oracle과 MySQL이 Read Committed 격리 수준에서 발생하는 Non Repeatable Read 부정합 문제를 해결하기 위해 Undo 테이블을 도입한 것과 달리 PostgreSQL은 튜플을 가장 오래 잡고있는 트랜잭션 ID를 나타내는 Xmax와 가장 최근에 잡고있던 트랜잭션 ID인 Xmin을 이용해서 MVCC를 구현했습니다. 

 

이 과정에서 트랜잭션 ID가 Xmax, Xmin에 기록되는데 트랜잭션은 시작과 끝이 있기에 끝나면 Xmax, Xmin에 적혀있는 데이터가 의미없는 쓰레기 데이터가 되고 이 쓰레기 데이터를 청소해주기 위해 Vacuum이 동작하게됩니다. 

 

PostgreSQL은 Oracle이나 MySQL과 같이 Undo 테이블로 MVCC를 관리하는게 마음에 안들었나봅니다. Undo 테이블로 MVCC를 관리하겠다는 말은 데이터 정합성을 위해 락킹 매커니즘이 실행될 때 테이블 락이 걸려 전체적인 성능이 떨어지게 됩니다. 그래서 PostgreSQL은 테이블 락이 아닌 Vacuum을 이용한 행간 락이기 때문에 성능상 큰 이점을 가져올 수 있었습니다. 

 

하지만 사이드 이펙트가 없는 완벽한 기술은 없다는 것을 또 한번 증명해버립니다. PostgreSQL이 Vacuum을 이용해 성능을 잡았지만 아이러니하게도 이 Vacuum을 제대로 다루지 못하면 성능이 떨어지게 됩니다. 

 

이게 바로 Auto Vacuum Bloating을 유발하는 문제이고 다음 챕터에서 본격적으로 서술해보겠습니다. 

 

Auto Vacuum Bloating

Auto Vacuum이란 Vacuum이 특정 주기를 가지고 트랜잭션 ID들을 청소하는 것을 말합니다. 쉽게 생각해서 다른 고수준 언어들의 GC (Garbage Collector) 와 비슷하다고 생각하시면 됩니다. 

 

Auto Vacuum Bloating을 CRUD에서 CUD의 관점에서와 R의 관점, 이렇게 두개의 관점으로 바라보고 정리해보겠습니다. 

 

Exclusive Lock, Shared Lock

트랜잭션, 특히 RDBMS의 트랜잭션은 ACID로 강한 일관성을 보장하는데요. 여기서 C에 해당하는 Consistency를 보장하기 위해 다양한 락킹 매커니즘을 사용하게 됩니다. 

 

트랜잭션 A가 쓰고 있는 데이터를 트랜잭션 B가 마음대로 접근해서 값을 변경하면 안되니까요. RDBMS에는 전통적으로 Exclusive Lock과 Shared Lock으로 행간 락을 걸어서 데이터의 일관성을 지켜냅니다. 

 

Exclusive Lock은 주로 쓰기 작업에서 어떤 데이터에 '독점적인' 락을 부여해서 해당 데이터를 쓸 수 없게 하는 Lock이고 Shared Lock은 주로 읽기 작업에서 작동하는 Lock입니다. 

 

데이터를 쓸 때는 락을 부여한다는게 납득이 가지만 읽을 때도 굳이 락을 획득해야하는 이유가 뭘까요? 그건 단순히 Shared Lock은 '읽을 수 없다!'라는 느낌보단 '내가 읽고 있을 때 아무도 건드리지마!'에 가깝습니다. 

 

왜냐하면 읽고 있을 때 누가 데이터를 변경해버리면 내가 읽는 작업 뒤에 있을 데이터 변경에 대해서 일관성이 깨질 수도 있으니까요. 

 

이제 Exclusive Lock은 X Lock, Shared Lock은 S Lock라는 약칭으로 부르도록 하겠습니다. 

 

Create, Update, Delete

쓰기 작업을 할 때는 X Lock을 획득해야하고 이 과정에서 트랜잭션은 이 락이 해제될 때까지 작업을 기다려야합니다. 이 작업이 늘어지게 되면 Vacuum이 제때 쓰레기 데이터들을 지워주지 못해 데이터가 쌓이고 쌓여 결국 폭발하게 되는 것입니다. 

 

보통 배치 작업을 진행할 때 CUD 쿼리가 많이 발생하게 되어 이런 상황이 발생하곤합니다. 일반적인 상황에선 읽기 쿼리가 90퍼센트 가깝기 때문에 자주 발생하는 문제는 아니지만 하루에 한번 혹은 한달에 한번 발생하는 배치 쿼리에서 문제가 발생할 여지가 있습니다. 

 

이를 해결하기 위해서 배치 작업을 돌릴 땐 작은 청크 단위로 꼬박꼬박 커밋을 해주면서 Long Term 트랜잭션이 발생하지 않도록 관리해주는 것이 핵심입니다. 

 

Read

Read에선 중첩 트랜잭션과 MultiXAct를 기준으로 Auto Vacuum Bloating을 설명할 수 있습니다. 이에 대한 자세한 내용은 아래의 링크에 정리되어있으니 참고해주세요!

 

https://coding-review.tistory.com/519

 

@Transactional과 PostgreSQL은 어울리지 않는다.

제목이 꽤나 자극적이지만 아무리 생각해도 이거만큼 좋은 제목은 떠오르지 않았습니다. 제가 오늘 포스팅을 하게된 계기가 되는 포스팅도 저자분이 "@Transactional의 해로움" 이라고 할 정도이니

coding-review.tistory.com

 

간단하게 다시 정리하자면 MultiXAct는 멀티 트랜잭션 상황에서 트랜잭션 ID를 관리하기 위한 자료구조로써 S Lock을 관리할 때 어떤 트랜잭션이 어떤 행에 묶여있는지를 나타냅니다. 

 

보통 Map<String, List<String>> 의 형태를 띄고 있으며 앞에 String에는 행에 대한 정보가 들어가있고 List에는 트랜잭션에 대한 정보가 있어서 어떤 행에 어떤 트랜잭션이 묶여있는지 직관적으로 알 수 있습니다. 

 

하지만 이런 S Lock이 늘어나게 되면 MultiXAct가 늘어나게 되는데 이 때 상황을 더 악화시키는 것이 바로 중첩 트랜잭션입니다. 

 

트랜잭션이라는 작업 자체만으로도 Vacuum 입장에선 난감해집니다. 왜냐하면 트랜잭션이 걸리게 되면 Vacuum은 어떤 데이터는 지우면 안되고, 어떤 데이터는 롤백할 대상이고, 어떤 데이터는 그대로 가게 두어야하기 떄문에 굉장히 복잡해지는 것이죠. 

 

이 상황에서 트랜잭션 안에서 또 트랜잭션을 열어버리면 Vacuum은 더 혼란에 빠지게 되고 MultiXAct가 쌓이게됩니다. 이런 과정에서 Auto Vacuum이 정상적으로 동작하지 않게 되고 데이터가 쌓이면 I/O 오버헤드가 발생하고 성능이 떨어지게 됩니다. 

 

하지만 PostgreSQL 진영에서도 이러한 문제를 파악하고 MultiXAct Wraparound 라는 개념으로 이를 해결하고자 노력하고 있는데요. 

 

PostgreSQL은 MultiXAct를 unsigned integer로 2의 32승 빼기 1만큼 즉, 43억 비트 (= 530MB) 의 데이터를 저장할 수 있습니다. 개발자가 autovacuum_multixact_freeze_max_age 라는 설정을 조절하면 Auto Vacuum에서 강제 Vacuum으로 넘어가게 만들 수 있는 것인데요. 

 

예를 들어서 50MB로 설정했다면 MultiXAct가 50MB를 넘어가면 바로 Vacuum을 진행하는 것이죠. 

 

만약 Auto Vacuum으로 해결하지 못한다면 강제로 읽기 전용 모드로 전환하거나 심한 경우 PostgreSQL의 프로세스를 종료시켜 데이터 일관성을 지킵니다. 

 

즉, PostgreSQL은 장애상황에서 일관성을 반드시 지키려고 하는 쪽이고 CAP 정리에 따르면 CP 데이터베이스가 되는 것입니다. 

 

보통의 RDBMS는 강한 일관성이 장점이기에 기본적으로 CP 데이터베이스의 형태를 띄고 있죠. 

 

해결책

Auto Vacuum Bloating이 뭔지는 대강 알게 되었고 이제 이를 해결해야겠죠? 크게 두 가지 방향성이 있을 것 같은데요. 한번 살펴보죠!

 

Auto Vacuum 민감도를 높이기

기본적으로 PostgreSQL은 Auto Vacuum에 대해 굉장히 보수적입니다. 왜냐하면 앞서 설명드렸다시피 PostgreSQL은 성능에 진심이기에 부하를 최소화하기 위해 보수적으로 동작하는 것이죠. 

 

실서비스에선 PostgreSQL의 튜닝이 자유롭다는 장점을 십분 활용하여 Auto Vacuum Bloating을 방지해야합니다. 

 

파라미터 의미 GPT 추천
autovacuum_vacuum_threshold 몇 건 이상 쓰레기 데이터가 생겨야 Vacuum을 작동시킬지 디폴트는 50이고 이를 낮춰서 튜닝한다
autovacuum_vacuum_scale_factor 테이블 크기에 대한 비율로서 테이블 크기가 크면 더 자주 작동하게 할 수 있습니다.  디폴트는 0.2이고 0.05~0.02로 낮춘다
autovacuum_vacuum_cost_limit Auto Vacuum 시 한 번에 허용하는 I/O  디폴트는 200이고 1000이상으로 높인다
autovacuum_vacuum_cost_delay Auto Vacuum 작업 후 쉬는 시간 (ms) 디폴트는 20ms이고 0으로 줄이거나 5ms로 줄인다.

 

이렇게 설정을 해주면 Auto Vacuum Bloating을 미연에 방지할 수 있게 됩니다. 

 

중첩 트랜잭션 줄이기

프로그래밍이 고수준으로 가면 갈수록 성능이 나빠진다는 사실은 누구나 알고 있는 사실입니다. 사실 중첩 트랜잭션은 요즘 프레임워크를 사용하다보면 숨쉬듯이 자주 나오게 되는 것 중에 하나입니다. 

 

앞서 언급했듯이 트랜잭션을 열고 그 안에서 트랜잭션을 또 진행하게 되면 Auto Vacuum Bloating이 자주 발생하게 되어 중첩 트랜잭션을 줄이는 방향으로 프로그래밍 해야합니다. 

 

사실 중첩 트랜잭션을 사용하는게 코딩 자체는 굉장히 편해집니다. 사실 별거 생각할거 없이 논리적으로만 작성하면 되기에 크게 어렵지 않거든요. 

 

하지만 이렇게 중첩 트랜잭션을 많이 열어버리면 Auto Vacuum Bloating을 많이 유발할 수 있어 너무 고수준으로 프로그래밍하지 않도록 주의해야할 것 같습니다. 

 

마치며

PostgreSQL은 잘 사용하면 정말 멋진 무기가 되지만 자칫 잘못 사용하게 된다면 큰 장애로 이어지는 양날의 검인 것 같습니다. 하지만 그만큼 개발자가 자체적으로 튜닝할 수 있는 수 많은 여건들 덕분에 지금의 PostgreSQL이 큰 사랑을 받고 있는 것이겠지요.

 

이번 포스팅에선 PostgreSQL의 장애 요인 1순위 Auto Vacuum Bloating에 대해서 공부해보고 정리해봤습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!