Search
🚕

우버 데이터베이스 이전 사례 분석하기(Postgre to MySQL)

Tags
Scrap
Date
2024/06/09
* 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 스트림보다 훨씬 더 컴팩트하다는 것을 의미한다.

References