* 2016년 글임을 감안하기
* 영어로 된 글을 이해해서 MySQL과 관계형 데이터베이스의 작동 방식을 실제 사례와 이해하기 위해 재정리하는 글
* 유데미 데이터베이스 강의 듣다가 데이터베이스 엔진 챕터에서 예제로 나와서 찾아보았음₩
운영 시 PG의 단점
•
쓰기에 비효율적인 아키텍처
•
비효율적인 데이터 복제
•
테이블 손상 문제
•
열악한 복제본 MVCC(다중 버전 동시성 제어) 지원
•
최신 릴리스 버전으로 업그레이드하기 어려움
PG 아키텍처의 핵심 특징
관계형 데이터베이스가 수행해야 하는 작업은 다음과 같다.
•
CRUD 기능 제공
•
스키마 변경 기능 제공
•
다양한 클라이언트에서 작업하는 데이터에 대해 트랜잭션 보기를 갖도록 MVCC 매커니즘 구현 및 제공
위의 요구사항을 만족할 수 있도록 관계형 데이터베이스가 디스크의 데이터를 표현하는 방법을 디자인해야 한다. 따라서 Postgres가 지원하는 핵심 디자인 관점을 정리해보면
1.
튜플이라고도 불리는, 변경 불가능한 행 데이터(row data)
•
ctid는 개념적으로 튜플의 디스크 상의 위치를 나타내고, 튜플은 ctid에 의해 고유하게 식별된다.
•
여러 ctid == 단일 행
•
테이블 자체에 인덱스 필드를 ctid에 매핑하는 데이터 구조(B-트리)로 구성된 인덱스가 있다.
CREATE TABLE users (
id SERIAL,
first TEXT,
last TEXT,
birth_year INTEGER,
PRIMARY KEY (id)
);
CREATE INDEX ix_users_first_last ON users (first, last);
CREATE INDEX ix_users_birth_year ON users (birth_year);
---
ctid id first last birth_year
A 1 Blaise Pascal 1623
B 2 Gottfried Leibniz 1646
C 3 Emmy Noether 1882
D 4 Muhammad al-Khwārizmī 780
E 5 Alan Turing 1912
F 6 Srinivasa Ramanujan 1887
G 7 Ada Lovelace 1815
H 8 Henri Poincaré 1854
---
id ctid
1 A
2 B
3 C
4 D
5 E
6 F
7 G
8 H
SQL
복사
•
결론적으로 중요한 점은: 내부적으로 pg는 row 버전을 보유하는 다른 필드를 사용해서 어떤 튜플이 가장 최근인지 확인하고, 추가된 필드를 통해 데이터베이스는 최신 행 버전을 볼 수 없는 트랜잭션에 제공할 행 튜플을 제공한다.
◦
트랜잭션이 최신 버전이 아닌 행 튜플에 접근했을 때 어떤 버전의 튜플을 제공할지 결정한다는 뜻인 듯.
•
pg를 사용하면 기본 인덱스와 보조 인덱스는 모두 디스크 상의 튜플 오프셋을 직접 가리키고, 인덱스를 어떻게 정의하고 사용하든 튜플 위치가 변경되면 모든 인덱스를 업데이트 해야 한다.
⇒ 처음에 언급한 단점 중 쓰기에 비효율적인 아키텍처 에 대한 설명.
⇒ 이 문제를 쓰기 증폭(Write Amplification) 문제라고 한다
•
왜 문제가 될까? 아주 작은 데이터 하나를 쓸 때도 다음 4가지 단계 + WAL 반영이라는 아주 큰 총 쓰기 수를 가진다.
// 바로 위 예제에 대해서.
1. 테이블스페이스에 새 행 튜플을 쓴다.
2. 새 튜플에 대한 레코드를 추가하기 위해 기본 키 인덱스를 업데이트한다.
3. 보조 키 인덱스를 업데이트해서 새 튜플에 레코드를 추가한다.
4. 새 튜플에 대한 레코드를 추가하기 위해 다른 보조키 인덱스를 또 업데이트한다.
SQL
복사
•
2, 3단계에 대해서; 해당 부분이 변경되지 않았더라도 새 행 튜플을 생성해서 업데이트 되어야 함.
•
12개의 인덱스가 정의된 테이블에서: 단일 인덱스로만 처리되는 필드의 업데이트에 새 행의 ctid를 반영하기 위해 12개 인덱스 모두에 프로파게이트(전파)되어야 함.
⇒ 매우 비효율적임
2.
복제
•
테이블에 새 행을 삽입할 때 스트리밍 복제가 활성화된 경우 pg는 이를 복제해야 하고, 충돌 복구를 위해 db는 WAL(Write Ahead Log)를 유지하고 2단계 커밋을 구현한다.
◦
WAL은 ACID에서 Atomicity와 Durability를 허용하므로 스트리밍 복제가 비활성화된 경우에도 db는 wal을 유지해야 함.
•
pg 데몬이 처음 실행될 때, 프로세스는 원장 데이터(ledger, 마스터 테이블을 의미하는 듯)를 디스크의 실제 데이터와 비교하고, 원장 데이터에 디스크에 미반영된 데이터가 포함된 경우 WAL에 표시된 데이터를 반영하도록 모든 튜플/인덱스 데이터를 수정함. 이후 WAL에 나타나지만 부분적으로 적용된 트랜잭션에서 나온 모든 데이터를 롤백함(==트랜잭션이 커밋되지 않았음)
•
pg는 마스터db의 WAL을 복제본으로 전송해서 스트리밍 복제를 구현함.
== 각 복제본 db는 마치 충돌 복구 중인 것처럼 작동하고 충돌 후 시작되는 것처럼 지속적으로 WAL 업데이트를 적용함.
•
스트리밍 복제는 WAL을 적용하는 동안 읽기 쿼리를 제공하고, 충돌 복구 모드의 경우 스트리밍 WAL을 완료할 때까지의 쿼리 제공을 거부한다는 차이 이외에는 유사한 작동.
⇒ 복제는 디스크상의 변경 수준에서 발생하기 때문에 위에 언급한 쓰기 증폭 문제도 자연스럽게 복제 계층이 적용된다.
⇒ 쓰기 증폭 문제 == 복제 증폭 문제
⇒ 단일 데이터 센터 안이라면 그럭저럭 괜찮겠지만 데이터 센터 간에 복제가 이루어져야 한다면? Cascading Replication은 데이터센터 간 대역폭 요구 사항을 마스터와 단일 복제본 사이에 필요한 양으로 제한한다.
⇒ 복제 프로토콜의 장황함으로 인해 많은 인덱스를 사용하는 db에 여전히 압도적인 양의 데이터가 발생할 수 있다.
⇒ 매우 높은 대역폭의 처리량이 필요한데 돈이 당연히 많이 들었을 듯.
3.
데이터 손상: 이외에도 DB 용량을 늘리려고 마스터 데이터베이스를 조정하다가 pg 9.2 버전에서 WAL 레코드가 잘못 적용되는 에러가 발생했다. 해당 버전에서만 발생했다고는 하는데 pg에 대한 신뢰가 떨어졌다.
4.
MVCC 복제본
•
pg에는 실제 복제본을 제공하는 mvcc가 없다.
•
pg는 mvcc에 대해 이전 행 버전의 복사본을 유지해야 한다.
•
트랜잭션이 설정 시간 동안 WAL 어플리케이션을 차단하면 해당 트랜잭션을 종료시켜버린다.
•
이런 설계는 레플리카가 마스터보다 몇 초 정도 지연될 수 있음을 의미하고 트랜잭션 종료를 초래하는 코드를 작성하기 쉽다.
•
ORM 쓸 때도 개발자는 위의 문제를 파악하기가 어렵다.
MySQL 아키텍처의 핵심 특징
1.
InnoDB On-Disk Representation
•
가장 중요한 아키텍처 차이점은 Postgres가 인덱스 레코드를 디스크 위치에 직접 매핑하는 반면 InnoDB는 보조 구조를 유지한다는 것
•
InnoDB 보조 인덱스 레코드는 디스크상의 행 위치에 대한 포인터를 유지하는 대신, 기본 키값에 대한 포인터도 유지함.
MySQL의 보조 인덱스 ⇒ 인덱스 키 ⇒ 기본 키
•
InnoDB가 Postgres에 비해 보조 키 조회를 수행할 때 불리할 수 있다. pg는 하나의 인덱스만 검색해도 되는데, InnoDB에서는 두 개의 인덱스를 검색해야 하기 때문.
•
하지만 데이터가 정규화되므로 행 업데이트는 실제로 변경되는 인덱스 레도크만 업데이트하면 됨. 일반적으로 InnoDB는 행 업데이트를 제자리에서 수행하기까지 함. 트랜잭션이 행을 참조해야 하면 이전 행을 롤백 세그먼트라는 특수 영역에 복사함.
•
쉽게 정리하면 행 업데이트 시 수행하는 내부 작업이 압도적으로 적고, 한 필드를 업데이트했다고 모든 인덱스를 수정해야 하는 작업이 발생하지 않는다는 것.
2.
복제
•
MySQL은 다양한 복제 모드를 제공함. 대표적으로
◦
명령문 기반 복제: 논리적 SQL 문을 복제
▪
컴팩트하지만 소량의 데이터를 업데이트하기 위해 비용이 많이 드는 명령문을 적용하려면 복제본 필요
◦
행 기반 복제: 변경된 행 레코드를 복제
◦
혼합 복제: 말그대로 혼합
•
MySQL에서는 기본 인덱스에만 행의 디스크 상 오프셋에 대한 포인터가 있음. 대조적으로 Postgres를 사용하면 디스크에 발생한 모든 물리적 변경 사항이 WAL 스트림에 포함되어야 함. Postgres는 새 튜플을 삽입하고 해당 튜플을 가리키도록 모든 인덱스를 업데이트해야 한다는 의미. WAL 스트림에는 많은 변경 사항이 적용된다.
•
MySQL 복제 바이너리 로그가 PostgreSQL WAL 스트림보다 훨씬 더 컴팩트하다는 것을 의미한다.