현재 회사에서는 MariaDB, MySQL, PostgreSQL을 모두 쓰고 있습니다. 인프라 엔지니어로서 MariaDB, MySQL, PostgreSQL의 내부 아키텍처를 배우기 위해서 작성되었습니다.
개요
본 문서는 Understanding MariaDB Architecture를 읽고 이해한 내용을 정리하는 문서입니다.
Storage Engine이란?
MariaDB는 Storage Engine*에 테이블 생성, 데이터 조회, 인덱스나 캐싱을 위임합니다. - [Ref]
Storage Engine은 DB가 읽기, 쓰기 작업을 하는 방식을 결정합니다.
MariaDB는 버전 별로 다양한 Storage Engine을 지원하지만, 본 문서에서는 InnoDB를 기준으로 MariaDB를 설명합니다.
이후 나오는 모든 내용은 MariaDB + InnoDB의 내용이며, 편의를 위해 생략합니다.
Primary Key & Index
모든 테이블은 Primary Key에 의해 정렬되며 분산 저장됩니다.
이러한 Primary Key와 관련된 몇가지 규칙이 적용됩니다.- [Ref]
개발자가 Primary Key를 명시적으로 선언할 수 있습니다.
선언하지 않으면 열이 NOT NULL인 첫 번째 Unique Index가 PK로 할당됩니다.
1,2 가 없는 경우 6 Byte의 숫자가 Cluster Index로서 PK로 자동 할당됩니다.
Cluster Index란 말의 의미는 무엇일까?
모든 레코드들은 Primary Key가 유사한 레코드들끼리 묶여서 저장됩니다.
따라서 쓰기 성능은 저하되지만 Primary Key 기반의 읽기 성능은 개선됩니다.
Index로 조회를 할 경우에도 Index, Primary Key 순으로 타고들어가며 읽기 성능이 개선됩니다. - [Ref]
Page(Block)이란?
데이터의 저장 및 조회를 할 때에는 Page를 기준으로 합니다.
Page는 Disk*와 Buffer Pool* 사이에 데이터를 읽고 쓰는 최소 단위입니다.
즉 이를 정리하면 다음과 같습니다.
일반적으로 1개의 레코드를 조회해도 1개의 Page를 조회하게 됩니다.
많은 레코드를 조회하다 보면 분할된 N개의 Page로 조회될 수 있습니다.
Disk와 Buffer Pool은 뭐가 다를까?
Disk I/O는 데이터베이스의 주요 병목 지점으로 작동합니다.
따라서 직접적인 Disk I/O가 일어나지 않도록 Buffer Pool(memory)를 활용합니다.
자세한 내용은 Buffer Pool이란?에서 다루겠습니다.
PK, Index의 크기와 Page 성능 비교
Page는 기본적으로 Primary Key, Index 정보를 포함합니다.
즉 Primary Key, Index의 크기에 따라서 단일 Page의 레코드 수량이 정해집니다.
아래와 같은 SQL Query가 하나의 Page를 생성한다고 생각하시면 좋습니다.
SELECT * FROM blog_posts LIMIT 5 OFFSET 0;
이때 Primary Key, Index의 크기가 증가하면 단일 조회에 많은 Page가 필요합니다.
즉 많은 수의 Page를 Disk와 Buffer Pool이 주고받으면서 성능 저하가 발생합니다.
Primary Key | Cnt | Index | Cnt | 1K Records | 100K Records | 10M Records |
---|---|---|---|---|---|---|
10 Byte | 1 | 10 Byte | 1 | 20 KB1) | 2 MB 3) | 200 MB 5) |
50 Byte | 1 | 60 Byte | 1 | 100 KB2) | 10 MB 4) | 1 GB 6) |
겉보기에 10, 50 Byte의 차이는 크지 않게 느껴질 수 있습니다만,
레코드의 수가 늘어날수록 기하급수적으로 많은 데이터를 주고받게 됩니다.
1) (10 Byte + 10 Byte) * 1,000 Record = 20,000 Byte ≒ 20KB
2) (50 Byte + 50 Byte) * 1,000 Record = 100,000 Byte ≒ 100KB
3) (10 Byte + 10 Byte) * 100,000 Records = 2,000,000 Byte ≒ 2MB
4) (50 Byte + 50 Byte)* 100,000 Records = 10,000,000 Byte ≒ 10MB
5) (10 Byte + 10 Byte) * 10,000,000 Records = 200,000,000 Byte ≒ 200MB
6) (50 Byte + 50 Byte)* 10,000,000 Records = 1,000,000,000 Byte ≒ 1GB
이러한 데이터는 모두 Buffer Pool에 저장이 되므로,
이후 Buffer Pool의 공간이 부족해서 BufferChaceHitRatio가 낮아질 수 있습니다.
Tablespace란 무엇일까?
DB에는 다양한 데이터가 있으며 DML, DDL 등으로 이를 제어할 수 있습니다.
데이터 공간을 Tablespace라 부르며 유형 및 특징에 따라 3가지로 분류합니다. - [Ref]
System Tablespace : .ibdata
File-Per-Table Tablespace : .ibd
Temporary Tablespace : .ibdtmp
세부 제약사항은 각 유형 및 버전 별로 다를 수 있겠지만
대부분의 Tablespace는 자동으로 줄어들지 않으며 별도의 조치사항을 취해야 합니다.
또한 SQL CTE Syntax로 만드는 임시 테이블은 Temporary Tablespace를 생성하므로
SQL Subquery를 통해서 조회하는 것과 그 성능면에서 차이가 있을 수 있습니다.
SQL 실행 계획을 통해서 반드시 그 차이를 비교하고 실행하는 것이 좋습니다.
SQL CTE Syntax
WITH user_lsit AS (
SELECT * FROM user LIMIT 5 OFFSET 0
)
EXPLAIN
SELECT *
FROM user_list AS user
LEFT OUTER JOIN user_details AS detail
ON user.id = detail.id
SQL Subquery
EXPLAIN
SELECT *
FROM (
SELECT * FROM user LIMIT 5 OFFSET 0
) AS user
LEFT OUTER JOIN user_details AS detail
ON user.id = detail.id
Transaction Logs에 대하여
데이터 무결성 및 일관성을 유지하기 위해서 Transaction을 사용합니다.
Transaction은 일반적으로 다음과 같은 특징을 가집니다.
일련의 작업이 모두 완료될 때까지는 해당 레코드(혹은 인덱스)에 Lock이 걸립니다.
작업이 모두 완료되면 해당 값이 일시에 바뀌며 이를 Commit이라 합니다.
작업 하나라도 실패하면 모든 값이 원래대로 돌아가며 이를 ROLLBACK이라 합니다.
Transaction의 특징을 달성하기 위해서 Undo Log, Redo Log를 씁니다, - [Ref]
시작할때 Undo, Redo Log를 기록하고 메모리 및 임시 디스크에 데이터를 버퍼합니다.
아래 상황에서 두 Log를 활용해서 Rollback 및 Background Flushing을 합니다.
Undo Log : .ibdata1 등의 System Tablespace에 저장되며 Transaction이 데이터를 쓸 때 Table Index & Data(Buffer Pool, Physical File)에 데이터를 삽입합니다. - [Ref]
Redo Log : .ib_logfile0 등으로 저장되며 Crash Recovery 및 Tablespace에 대한 Transaction Background Flushing 등에서 사용됩니다. - [Ref]
Phyiscal File이란 무엇일까?
앞서 데이터 유형에 따라서 Tablespace에 데이터를 저장함을 배웠습니다.
Phyiscal File은 위에서 언급한 .idbdata, .idb, .idbtmp 등의 파일을 의미합니다.
Buffer Pool이란?
Buffer Pool은 데이터와 인덱스가 거쳐가는 임시 창고같은 개념입니다.
Disk I/O를 최대한 줄여서 DB 병목 현상을 줄이고자 설계된 개념입니다. - [Ref]
실무에서도 Amazon RDS를 쓰고 있다면 파라미터 그룹*에서 이를 찾을 수 있습니다.
아래 그림의 innodb_buffer_pool_size라는 값이 Buffer Pool 용량입니다.
Amazon RDS은 1 GB 용량을 선택하면 실제로는 943.5 MB를 전달받습니다. - [Ref]
이 때의 innodb_buffer_pool_size는 아래와 같은 공식으로 계산할 수 있다고 합니다.
{DBInstanceClassMemory*3/4}
DBInstanceClassMemory는 943.5 GB에서 다양한 수치를 제외해야 합니다.
따라서 즉각적인 계산이 어렵기에 다음의 SQL문이 더 정확합니다.
SELECT @@innodb_buffer_pool_size
Buffer Pool을 사용하여 변형된 LRU 알고리즘*을 사용하여 성능을 끌어올립니다.
변형된 LRU 알고리즘이란 무엇일까? -[Ref]
LRU(Least Recently Used) 알고리즘은 가장 오래 참조되지 않은 값을 제거합니다.
일반적으로 메모리의 Head 부분에 데이터를 넣다보면 Tail이 가장 오래된 것일 겁니다.
하지만 변형된 LRU에서는 Midpoint 부분을 37%부분으로 정하고 여기를 기준으로 합니다.
MariaDB 10.5.14 이전 버전에서는 Change Buffer라는 개념도 있었습니다.
Page는 디스크에서 수정되지 않고 Buffer Pool에서 수정되며 모든 작업이완료되면 Buffering 작업을 통해서 디스크에 반영됩니다. 이 작업은 INSERT, UPDATE 그리고 DELETE 쿼리가 영향을 받게 됩니다.
현재 Change Buffer는 사용되지 않는가?
MariaDB 10.5.15, 10.6.7, 10.7.3, 10.8.2에서 비활성화 되었습니다.
MraiaDB 10.9.0 부터는 InnoDB에서 사용되지 않고 있습니다.
MariaDB 11.0.0 부터는 InnoDB에서 해당 기능이 삭제되었습니다.
추가로 MariaDB 서버 모니터링 및 성능 최적화: InnoDB Buffer Pool 1부 내용을 확인하는 것도 좋을 것 같습니다.
Background Thread란 무엇인가?
Background Thread는 Buffer Pool과 디스크 사이에서 작동하며
Page를 비롯한 다양한 처리를 하기 위해서 존재하는 스레드입니다.
가장 대표적으로 Change Buffer에서 Tablespace로 Dirty Page를 Flushing하는 작업 등이 포함됩니다. - [Ref]
아래의 쿼리 중 하나를 실행하면 Background Thread를 볼 수 있습니다.
Amazon RDS for MariaDB 10.6에서 아래처럼 빈값으로 보입니다.
하지만 더블클릭해서 열어보면 Status의 값이 보입니다. - [Ref]
Dirty Page란 무엇인가?
Dirty Page는 Page의 데이터가 Buffer Pool에만 반영된 상태입니다.
이 데이터는 주기적으로 Page Flushging을 통해 디스크에 반영됩니다. - [REF]
주기적으로 Page Flushing이 나는 부분을 Checkpoint라고 부릅니다.
이렇게 Page Flushing이 나는 부분을 Checkpoint라고 부릅니다.
이 작업은 주로 아래의 3가지 System Variables에 의해 제어됩니다. - [Ref]
SELECT @@innodb_max_dirty_pages_pct, @@innodb_io_capacity, @@innodb_log_file_size
Amazon RDS for MariaDB 10.6의 기본값은 다음과 같습니다.
innodb_max_dirty_pages_pct 값이 높아지면 Buffer Pool의 공간 낭비는 줄어들지만 Page Level이 계속 증가하여 Page 내의 공간이 낭비됩니다.
innodb_max_dirty_pages_pct 값이 낮으면 Background Thread의 Master Thread가 Page Flush하는 속도가 빨라지지만 그만큼 Buffer Pool의 효율성이 떨어지고 Disk I/O가 많아집니다.
그냥 주관적인 가정
Amazon RDS for MariaDB 10.6에서 Read Replica를 설정하고
쓰기 인스턴스에서 innodb_max_diry_pages_pct 값을 줄여서 Page Flush 빈도 및 속도를 빠르게 하면 Read Replica에 데이터 동기화도 빈번하고 빠르게 일어나지 않을까?
Disk I/O로 인한 병목은 쓰기 인스턴스에만 발생을 하고
Read Replica의 병목은 발생하지 않으니 문제가 될 것 같지않다.
다만 어디까지나 가정이기 때문에 고민해볼 여지는 있을 것 같다.
Checksum & Doublewrite Buffer이란?
모든 Page에는 Checksum이 존재합니다.
Page Flushing을 통해서 디스크에 Page가 쓰고 나면 Checksum을 확인하는 절차가 있습니다. 이 절차는 innodb_checksum_algorithm에 의해서 진행됩니다. - [Ref]
SELECT @@innodb_checksum_algorithm
Page Flushing이 중간에 끊기면 Checksum이 일치하지 않고 이 경우 Transaction Logs를 활용하여 데이터를 복구합니다.
따라서 InnoDB는 필수 Page를 디스크에 2번 기록합니다.
새 Page의 복사본을 먼저 기록하고 이전 Page를 덮어씌웁니다.
이러한 복사본은 Doublewrite Buffer라는 파일에 기록됩니다.
기본적으로 활성화 되어 있으며 이는 innodb_doublewrite를 통해 확인 가능합니다.
SELECT @@innodb_doublewrite
또한 경우에 따라서 Doublewrite Buffer 파일 경로를 오버라이드 할 수 있습니다. 이 경우 innodb_doublewrite_file을 설정하면 됩니다.
이 값을 설정하지 않으면 조회도 되지 않습니다.
SELECT @@innodb_doublewrite_file
결론
MariaDB의 Storage Engine인 InnoDB에 대해서 배웠습니다.
InnoDB 모든 데이터 제어에 Buffer Pool을 활용합니다.
조회 단계에서는 Buffer Pool의 변형된 LRU 알고리즘을 주로 적용을 받고
삽입, 수정, 삭제 단계에서는 Buffer Pool에 Dirty Page를 기록해두고 일정 기준을 넘어가면 Page Flushing이 일어납니다.
MAriaDB에서 주의깊게 확인할 사항은 다음과 같습니다.
Buffer Pool의 용량이 충분히 남아있는지
Disk I/O가 과도하게 발생하고 있는지 (Page Flushing 확인)