CS 지식/데이터베이스

깔끔쟁이 MySQL (InnoDB) vs 게으름뱅이 PostgreSQL

마늘냄새폴폴 2025. 5. 4. 19:58

데이터베이스에서 가장 중요한 작업은 뭘까요? Create? Read? 어느것 하나 중요하지 않은게 없죠. 

 

그럼 데이터베이스에서 가장 리소스가 많이 드는 작업은 뭘까요? HDD에선 헤드를 여러번 움직여야하고 SSD에선 erase 후에 write 를 해야하는 CUD가 아닐까 싶습니다. 

 

이번 포스팅에선 데이터베이스에서 리소스가 가장 많이 드는 작업인 CUD를 데이터베이스들이 어떻게 최적화하는지에 대해서 공부해본 내용을 정리해볼까합니다. 

 

Dirty Page

데이터베이스는 삽입, 변경, 삭제 쿼리 (이하 쓰기 쿼리) 가 들어오면 곧장 이를 물리적인 저장소에 저장하는 것이 아니라 Page라고 부르는 메모리에 저장해뒀다가 한번에 flush라는 명령어를 통해 밀어넣습니다. 

 

이렇게 쿼리가 실행되고 메모리에 올라와있지만 아직 물리 저장소엔 적용되지 않은 Page를 Dirty Page라고 부릅니다. 이 Dirty Page를 한번에 flush를 하는 이유는 리소스가 많이 드는 쓰기 작업을 한번에 처리함으로써 쓰기 작업을 최적화해야하기 때문입니다. 

 

이번에 포스팅할 주된 내용이 바로 이 flush에 대한 내용입니다. Dirty Page, 말 그대로 더러워진 페이지를 얼마나 깔끔하게 청소하는지에 대한 내용이거든요. 다다음 섹션에서 MySQL, MariaDB 진영인 InnoDB 스토리지 엔진과 PostgreSQL이 어떻게 flush를 하는지에 대해서 자세히 다뤄보도록 하겠습니다. 

 

Checkpoint

RDBMS에는 서로 다른 트랜잭션이 충돌했을 때 먼저 도착한 쪽은 정상적으로 진행시키고 충돌된 트랜잭션은 롤백을 시켰다가 진행시킨 트랜잭션이 성공하면 그 때 재실행을 위한 로직이 포함되어있습니다. 

 

그런데! 어디까지 롤백하고 어디서부터 재실행할건지에 대한 정보가 있어야 재실행을 하던지 말던지 하겠죠? 그 정보가 바로 Checkpoint입니다. 

 

여기서 중요하게 등장하는 개념이 바로 LSN인데요. LSN은 쉽게 얘기해서 트랜잭션의 실행 순번이고 트랜잭션이 커밋될 때마다 하나씩 증가합니다. 

 

이 Checkpoint라는 개념은 추후 소개할 내용에서 이해하는데 중요한 개념이니 앞으로 나올 내용에서 계속 상기해야할 내용입니다. 

 

깔끔쟁이 MySQL (InnoDB)

InnoDB가 왜 깔끔쟁이냐하면 flush 속도를 동적으로 계산해서 Dirty Page가 쌓이면 flush 속도를 빠르게 조정하고 덜 쌓이면 flush 속도를 느리게 조정하고 이런식으로 Dirty Page가 쌓여있지 않게 지속적으로 청소를 해주기 때문입니다. 

 

그럼 InnoDB가 어떤 기준으로 flush 속도를 조정하는지에 대해서 봐야겠죠

 

Redo Log

결론을 먼저 말하자면 Redo Log에서 측정한 Checkpoint Age라는 것을 기준으로 flush의 속도를 조절합니다. 물론 Checkpoint Age값만 가지고 속도를 조절하는건 아니지만 이 값이 중요하기 때문에 다른 값들은 넘어가도록 하겠습니다. 

 

Checkpoint Age란 Redo Log에서 현재 LSN과 물리 저장소에 flush된 LSN의 차이를 비교한 것 입니다. Redo Log는 순환형 버퍼이고 현재 LSN을 Head, 물리 저장소에 flush된 LSN은 Tail이라고 부릅니다. 

 

이를 그림으로 나타내면 다음과 같습니다. 

 

 

이 Head와 Tail의 간격을 Checkpoint Age라고 부릅니다. Checkpoint Age의 크기가 전체 중 몇 퍼센트냐에 따라 flush 속도를 동적으로 조절하게 됩니다. 

 

여기서 의문이 들더군요. 

 

Tail을 앞으로 땡기는 트리거가 뭘까?

 

우선 Tail을 앞으로 땡기는 트리거는 Redo 로그를 물리 저장소에 적었다는 신호인 log_io_complete를 보고 Tail을 앞으로 땡기게 됩니다. 연속적으로 드는 의문은 왜 flush할 때가 아니라 I/O가 끝났을 때를 기준으로 Tail을 땡길까? 였습니다. 

 

I/O가 끝났을 때 Tail을 땡기는 이유는 아무리 Dirty Page를 flush해서 물리 저장소에 저장했다고 하더라도 Redo Log가 정상적으로 저장되지 않으면 추후에 장애로 회복해야할 때 혹은 트랜잭션 충돌로 롤백하고 회복해야할 때 Redo Log를 기준으로 진행하기 때문입니다. 

 

flush가 아무리 되었다고 하더라도 Redo Log가 저장되지 않으면 아무 의미 없다는 것입니다. Redo Log의 의미는 "이 시점까지는 복구할 수 있다" 라는 의미이므로 Redo Log를 기준으로 Tail을 앞으로 땡기는 것이죠. 

 

 

이렇게 flush를 동적으로 조절하면 어떤 이득이 있길래 이런 정책을 채택한 것일까요?

 

Checkpoint Storm

Dirty Page를 제때제때 치워주지 않으면 flush를 한번 할 때 엄청나게 많은 양을 물리 저장소에 써야하고 리소스가 많이 드는 쓰기 작업은 곧 I/O 오버헤드를 일으키고 이것이 CPU 오버헤드를 야기하면서 서버 자체의 성능을 떨어뜨리게 됩니다. 

 

깔끔쟁이인 InnoDB는 지속적으로 Checkpoint Age를 계산하면서 flush 속도를 동적으로 조절해 Dirty Page를 제때제때 물리 저장소에 밀어 넣어 Checkpoint Storm이 발생하지 않게 노력하는 것이죠. 

 

그럼 PostgreSQL은 어떨까요? 

 

게으름뱅이 PostgreSQL

PostgreSQL은 InnoDB처럼 동적으로 flush속도를 조절하는 기능은 존재하지 않습니다. 대신 PostgreSQL은 Checkpoint Timeout과 WAL의 크기에 따라 flush를 호출하는 식으로 Dirty Page를 청소합니다. 

 

즉, 이런 것이죠. Checkpoint Timeout이 5분이면 5분동안 Dirty Page를 쌓아두고 있다가 5분이 지나면 자동으로 flush를 진행시키는 것입니다. 혹은, WAL의 크기가 특정 크기를 넘어가면 flush를 하는 것이죠. 

 

그럼 갑자기 쓰기 작업이 많이 요청되었고 5분동안 Dirty Page가 엄청나게 많이 쌓이면 어떡하죠? 

 

뭐.. 그럼 Checkpoint Storm이 생기는죠..

 

하지만 PostgreSQL도 이런 문제에 대해 인지하고 Checkpoint Storm을 대비하기위해 completion target이라는 개념을 도입합니다. 

 

completion target이라는 개념은 특정 시간동안 flush를 일정하게 나눠서 처리하겠다는 의미로 예를 들어 Checkpoint Timeout이 5분이고 completion target의 0.5라면 (기본값) 2분30초동안 flush를 일정하게 나눠서 처리하겠다는 의미입니다. 

 

이에 대한 공식은 다음과 같습니다. 

 

Dirty Page의 수 / Checkpoint Timeout * Completion Target

 

예를 들어 Checkpoint Timeout이 5분이고 completion target이 0.5, Dirty Page의 수가 1만개라면 

 

2분30초동안 초당 67개정도 되는 Dirty Page를 처리하는 것입니다. 이렇게 I/O를 분산해서 처리하면 Checkpoint Storm이 발생하지 않게 되는 것이죠. 

 

그럼 여기서 의문이 들더군요. 그럼 2분30초동안 1만개를 처리하는데 나머지 2분30초동안 또 몇천개가 쌓여있으면 그 몇천개를 한번에 flush해야하는건 똑같지 않나? 라는 의문이요. 

 

그래서 completion target을 0.8이나 0.9까지 올리는 것이 일반적이라고 합니다. Checkpoint Timeout이 5분인 경우 0.9라면 4분30초동안 나눠서 처리하고 30초동안 쌓이는 것은 한번에 flush 하는 것이죠. 

 

하지만! 또 다시 의문이 들었습니다. 

 

completion target으로인해 계산한 수치는 맨처음인데 만약 그 이후에 그 수치를 상회하는 쓰기 작업이 들어오게 되면 completion target이 0.9인 경우 마지막 30초에 한번에 flush해야하면 Checkpoint Storm이 발생할 수 있는 것이 아닌가 하는 의문이요. 

 

이를 통해 저는 "아, Checkpoint Storm이 사실 실환경에선 큰 문제는 아니구나" 라는 결론에 도달했습니다. 

 

왜냐하면 진짜 이 completion target으로도 해결할 수 없다면 PostgreSQL진영에서 InnoDB처럼 adaptive flush를 적용했거나 PostgreSQL을 사용하는 많은 개발자들이 InnoDB로 넘어갔을 것이기 때문이죠. 

 

실제로 PostgreSQL에서 장애를 유발하는 것들 중엔 Checkpoint Storm이 최상위에 위치해있지는 않습니다. 

 

대부분의 것들은 completion target으로 커버가 가능하고 이를 상회하는 쓰기 작업은 실환경에서 성능에 큰 영향을 미치지 않는다는 것이죠. 

 

마치며

이번 포스팅에선 깔끔쟁이 MySQL과 게으름뱅이 PostgreSQL이라는 주제로 Checkpoint Storm에 대한 내용을 공부해보고 정리해봤습니다. 사실 PostgreSQL이 게으름뱅이 정도는 아닌 것 같긴 한데 워낙 깔끔쟁이인 InnoDB 때문에 제목을 한번 자극적으로 지어봤습니다. 

 

데이터베이스 관련 이론 공부를 할 때면 너무 깊이있게 들어가지 않으려고 노력하는 편인데 요즘 백엔드 개발자와 DBA사이에 있는 무언가를 공부하는 느낌이라 조금 주의하고 있습니다. 

 

제 목표에 DBA는 없기 때문에 적당히 개요부분만 알고 넘어가는 방향으로 공부하고있습니다. 사실 관련 문서를 찾아보면서 어떤 속성값을 어떻게 조절하면 어떤 효과가 있다와 같은 것들이 나오는데 너무 지엽적이라서 조금 당황스러웠습니다. 

 

그래도 데이터베이스 관련된 공부는 언제나 재밌네요. 다음 포스팅에서도 재밌게 공부하고 정리해보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!