테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

복제·샤딩·합의

데이터베이스를 여러 대로 늘리는 방법이다. 복제로 버티고, 샤딩으로 나누고, Raft 같은 합의로 한 목소리를 낸다.

목차 (7)
  1. 리더-팔로워 복제
  2. 다중 리더와 리더 없는 복제
  3. 샤딩
  4. 일관된 해싱과 재분배
  5. 합의와 Raft
  6. CAP과 PACELC
  7. 분산 트랜잭션과 2단계 커밋

리더-팔로워 복제

같은 데이터를 여러 서버에 두는 복제(replication)는 세 가지를 위해서다. 한 대가 죽어도 버티고(가용성), 읽기를 나눠 받고(확장), 사용자 가까이에 둔다(지연). 가장 흔한 모양은 쓰기를 한 대(리더, primary)만 받고 나머지(팔로워, replica)는 리더가 보내는 변경 기록을 순서대로 적용하는 방식이다. 변경 기록으로는 대개 저장 엔진의 WAL이나 논리적인 행 변경 기록을 쓴다(저장 엔진 노트).

클라이언트가 리더에 쓰면 리더는 동기 팔로워의 확인을 받고 나서 성공을 답한다. 비동기 팔로워에는 변경이 늦게 도착해, 그 전에 그 팔로워에서 읽은 다른 클라이언트는 옛 값을 본다클라이언트리더팔로워 1(동기)팔로워 2(비동기)시간 →x=2 쓰기확인성공 응답이제야 x=2다른 클라이언트가 읽기: x=1(옛 값)복제 지연
동기 복제와 비동기 복제. 동기 팔로워의 확인을 기다리면 응답이 늦어지는 대신 리더가 죽어도 그 쓰기가 남고, 비동기 팔로워는 늦게 따라와 그 사이 옛 값을 보여 준다.
  • 동기와 비동기: 모든 팔로워를 동기로 두면 한 대만 느려도 쓰기가 멈춘다. 그래서 보통 하나만 동기(반동기)로 두거나 전부 비동기로 둔다. 비동기에서 리더가 죽으면 팔로워에 아직 안 간 커밋이 사라질 수 있다.
  • 복제 지연(replication lag): 비동기 팔로워에서 읽으면 방금 쓴 글이 안 보이거나(자기 쓰기 읽기 위반), 새로 고칠 때마다 다른 팔로워에 붙어 시간이 거꾸로 가는 것처럼 보일 수 있다. 자기가 쓴 데이터는 리더에서 읽거나, 한 사용자를 한 팔로워에 붙여 둔다.
  • 장애 조치(failover): 리더가 죽으면 가장 앞선 팔로워를 새 리더로 올리고 나머지가 따르게 한다. 어려운 것은 "죽었다"는 판단이다. 잠깐 느렸을 뿐인 옛 리더가 돌아와 자기가 리더라고 믿으면 두 리더가 각자 쓰기를 받는 분할 뇌(split brain)가 된다. 이를 막으려고 다음 절의 합의를 쓴다.

PostgreSQL 문서의 Comparison of Different Solutions가 공유 디스크, WAL 전송, 논리 복제 등 여러 방식의 장단점을 표로 비교한다.

다중 리더와 리더 없는 복제

여러 데이터센터에서 저마다 쓰기를 받으려면 리더를 여럿 둔다(다중 리더). 지역마다 빠르게 쓰지만 같은 행을 두 곳에서 고치면 충돌이 나고, 마지막 쓰기 승리(last write wins)처럼 한쪽을 버리거나 응용 프로그램이 합쳐야 한다.

아예 리더 없이 아무 복제본에나 쓰는 방식도 있다(리더 없는 복제). Amazon의 2007년 논문 「Dynamo: Amazon's Highly Available Key-value Store」가 알린 설계로, Cassandra 같은 데이터베이스가 따른다. 복제본 N개 중 W개가 쓰기를 확인하면 성공, 읽을 때는 R개에 물어 가장 새 버전을 고른다. R + W > N이면 읽는 집합과 쓴 집합이 반드시 한 곳 이상 겹치므로 최신 쓰기를 만난다.

N=3, W=2, R=2: 쓰기는 복제본 1·2에, 읽기는 복제본 2·3에 가서 복제본 2에서 겹친다. 표는 R + W가 N보다 클 때만 늘 겹친다는 것을 보여 준다쓰기 W=2읽기 R=2복제본 1v2복제본 2v2복제본 3v1읽은 두 곳 중 버전이 높은 v2를 고른다. 복제본 3은 나중에 고쳐 둔다(읽기 복구).NRWR + W > N늘 겹치나 · 버티는 장애(읽기/쓰기)3224 > 3겹친다 · 1대 / 1대3112 ≤ 3안 겹칠 수 있다 · 2대 / 2대3134 > 3겹친다 · 2대 / 0대5336 > 5겹친다 · 2대 / 2대5235 ≤ 5안 겹칠 수 있다 · 3대 / 2대
정족수 읽기와 쓰기. N=3에서 W=2, R=2면 어떤 두 곳을 골라도 겹친다. 표는 각 조합의 모든 경우를 직접 확인한 결과로, 겹침과 장애 허용이 맞바뀐다.

R + W > N은 최신 쓰기를 "만날" 수 있다는 보장일 뿐, 선형 일관성(모든 연산이 한 줄로 늘어선 것처럼 보이는 성질)까지 주지는 않는다. 쓰기가 일부 복제본에만 닿은 채 실패하거나, 동시에 쓴 두 값의 순서를 시계로 정하거나, 죽은 복제본 대신 임시로 받은 노드(sloppy quorum)가 끼면 옛 값이 보일 수 있다. 어긋난 복제본은 읽을 때 고치거나(읽기 복구) 배경에서 비교해 맞춘다(anti-entropy).

샤딩

복제는 같은 데이터를 늘릴 뿐이라 데이터 자체가 한 대에 안 들어가거나 쓰기가 한 대로 감당이 안 되면 소용없다. 그때는 데이터를 나눠 서버마다 일부만 맡긴다. 이것이 샤딩(sharding, partitioning)이다. 보통 샤드마다 다시 복제를 둔다.

시간 순 주문 300건의 배치: 범위 샤딩은 0·0·300건으로 마지막 샤드에 몰리고, 해시 샤딩은 89·104·107건으로 퍼진다범위 샤딩: 주문 시각 구간으로 나눈다0건샤드 1 · ~ 8월0건샤드 2 · 9월300건샤드 3 · 10월 ~새 주문이 모두 마지막 샤드로뜨거운 곳(hotspot)이 된다해시 샤딩: hash(주문 키) mod 389건샤드 1 · 나머지 0104건샤드 2 · 나머지 1107건샤드 3 · 나머지 2고르게 퍼진다. 대신"10월 주문 전부"는세 샤드에 모두 묻는다
같은 300건을 범위로 나눌 때와 해시로 나눌 때. 시간처럼 계속 커지는 키를 범위로 나누면 쓰기가 끝 샤드 하나에 몰린다.
  • 범위 샤딩: 키 구간마다 샤드를 정한다. 범위 질의가 한두 샤드에서 끝나지만, 시간이나 증가하는 번호를 키로 쓰면 새 쓰기가 마지막 샤드에 몰린다. 구간이 커지면 둘로 갈라 옮긴다.
  • 해시 샤딩: 키의 해시로 샤드를 정한다. 고르게 퍼지지만 범위 질의는 모든 샤드에 물어야 한다. 그래서 (사용자 id, 시각)처럼 앞 열은 해시로 나누고 같은 샤드 안에서는 뒤 열로 정렬하는 복합 키가 흔하다.
  • 뜨거운 키: 해시로도 못 푸는 쏠림이 있다. 유명인 계정 하나에 쓰기가 몰리면 그 키가 든 샤드는 늘 뜨겁다. 키 뒤에 임의의 숫자를 붙여 여러 샤드로 쪼개고 읽을 때 모으는 식으로 응용 쪽에서 푼다.
  • 보조 인덱스: 샤드 키가 아닌 열로 찾으려면 샤드마다 자기 데이터의 인덱스를 두고 모두에 묻거나(지역 인덱스), 인덱스 자체를 따로 나눠 둔다(전역 인덱스). 전역 인덱스는 쓰기 하나가 여러 샤드를 고치는 일이 된다.

일관된 해싱과 재분배

해시 샤딩을 hash(키) mod N으로 하면 서버 수 N이 바뀔 때 거의 모든 키의 자리가 바뀐다. 1997년 Karger 등의 「Consistent Hashing and Random Trees」가 내놓은 일관된 해싱은 키와 노드를 같은 해시 공간(링)에 올리고, 키를 시계 방향으로 처음 만나는 노드에 맡긴다. 노드를 더하면 그 노드 바로 앞 구간만 옮겨진다.

노드 넷이 놓인 해시 링에 노드 5를 더하면 바로 앞 구간의 열쇠만 옮겨진다. 노드 4대에서 5대로 늘릴 때 옮겨지는 열쇠는 나머지 방식 79.6%, 링(점 1개) 47.8%, 링(점 100개) 17.6%해시 링(0 ~ 2³²)N3N5N1N2N4열쇠는 시계 방향다음 노드가 맡는다굵은 호: N5가 N1에게서 넘겨받는 구간4대 → 5대: 옮겨지는 열쇠열쇠 10,000개, FNV-1a 해시로 계산hash mod N79.6%링, 노드당 점 1개47.8%링, 노드당 점 100개17.6%이상적인 값 1/520.0%
열쇠 1만 개를 노드 4대에서 5대로 옮길 때. mod 방식은 80%가 자리를 바꾸고, 노드마다 링에 점을 100개씩 찍은 일관된 해싱은 이상적인 20%에 가까운 18%만 새 노드로 옮긴다.

노드마다 점이 하나뿐이면 구간 길이가 들쭉날쭉해 어떤 노드는 두 배 넘게 맡는다. 그래서 노드 하나를 링 위 여러 점(가상 노드)으로 찍어 구간을 고르게 한다. 다른 해법은 처음부터 샤드를 노드 수보다 훨씬 많이(예: 노드 10대에 샤드 1,000개) 만들어 두고, 노드가 늘면 샤드를 통째로 옮기는 것이다. 키→샤드 대응은 그대로라 옮기는 단위가 단순하다. 어느 쪽이든 재분배 중에는 옮기는 데이터만큼 네트워크와 디스크를 쓰므로 자동으로 하기보다 사람이 확인하며 진행하는 경우가 많다.

합의와 Raft

장애 조치에서 "누가 리더인가"를 여러 서버가 하나로 합의해야 분할 뇌가 생기지 않는다. 이런 문제를 합의(consensus)라 한다. 오랫동안 Paxos가 표준 해법이었지만 이해하고 구현하기 어렵기로 유명했다. 2014년 Ongaro와 Ousterhout의 「In Search of an Understandable Consensus Algorithm」이 이해하기 쉬움을 목표로 설계한 것이 Raft다. etcd, CockroachDB, TiKV 같은 시스템이 쓴다.

Raft 단계 그림: 서버 다섯 대의 역할, 임기, 로그가 리더 장애, 선거, 복제, 커밋, 옛 리더 복귀를 거치며 바뀐다S1리더임기 1x←1S2팔로워임기 1x←1S3팔로워임기 1x←1S4팔로워임기 1x←1S5팔로워임기 1x←1S1꺼짐임기 1x←1S2팔로워임기 1x←1S3팔로워임기 1x←1S4팔로워임기 1x←1S5팔로워임기 1x←1S1꺼짐임기 1x←1S2팔로워임기 1x←1S3후보임기 2x←1S4팔로워임기 1x←1S5팔로워임기 1x←1S1꺼짐임기 1x←1S2팔로워임기 2x←1S3리더임기 2x←1S4팔로워임기 2x←1S5팔로워임기 1x←1S1꺼짐임기 1x←1S2팔로워임기 2x←1S3리더임기 2x←1y←2S4팔로워임기 2x←1S5팔로워임기 2x←1S1꺼짐임기 1x←1S2팔로워임기 2x←1y←2S3리더임기 2x←1y←2S4팔로워임기 2x←1y←2S5팔로워임기 2x←1S1꺼짐임기 1x←1S2팔로워임기 2x←1y←2S3리더임기 2x←1y←2S4팔로워임기 2x←1y←2S5팔로워임기 2x←1S1꺼짐임기 1x←1S2팔로워임기 2x←1y←2S3리더임기 2x←1y←2S4팔로워임기 2x←1y←2S5팔로워임기 2x←1y←2S1팔로워임기 2x←1y←2S2팔로워임기 2x←1y←2S3리더임기 2x←1y←2S4팔로워임기 2x←1y←2S5팔로워임기 2x←1y←2커밋된 항목아직 커밋 전표 요청항목 전송표·확인
1 / 9

임기 1. S1이 리더이고 모두 x←1을 커밋했다. 리더는 주기적으로 하트비트(빈 AppendEntries)를 보낸다.

Raft의 리더 선출과 로그 복제. 임기(term)마다 리더는 많아야 하나이고, 항목은 과반에 복제된 뒤에야 커밋된다. 과반끼리는 반드시 한 대 이상 겹치므로, 커밋된 항목은 다음 리더의 로그에도 늘 있다.
  1. 임기(term): 시간을 번호 붙은 임기로 나눈다. 임기마다 선거가 한 번 있고 리더는 많아야 하나다. 더 높은 임기를 본 서버는 곧바로 팔로워로 물러난다.
  2. 선거: 팔로워는 하트비트가 끊기면 무작위로 정한 시간 뒤 임기를 올려 후보가 되고 표를 청한다. 서버는 임기마다 한 표만 주고, 후보의 로그가 자기만큼 최신일 때만 준다. 과반을 얻으면 리더다. 시간이 무작위라 동시에 후보가 되는 일이 드물고, 표가 갈리면 다음 임기에 다시 한다.
  3. 로그 복제: 클라이언트 명령은 리더의 로그 끝에 붙고 AppendEntries로 팔로워에 간다. 과반이 받으면 커밋되고, 그때 상태 기계에 적용하고 답한다.
  4. 안전성: 커밋은 과반에 있고, 새 리더는 과반의 표를 얻어야 하며, 표는 로그가 뒤지지 않은 후보에게만 간다. 두 과반은 반드시 겹치므로 새 리더는 모든 커밋된 항목을 가지고 있다.

Raft 누리집 raft.github.io에서 서버를 직접 끄고 켜며 선거와 복제를 보는 시각화를 돌려 볼 수 있다. 다섯 대 클러스터는 두 대까지 죽어도 버티고, 쓰기는 과반의 확인을 기다리므로 가장 느린 두 대를 뺀 나머지 중 가장 느린 서버만큼 걸린다.

CAP과 PACELC

CAP 정리는 Eric Brewer가 2000년에 추측으로 내놓고 2002년 Gilbert와 Lynch가 「Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services」에서 증명했다. 정확히 말하면 이렇다. 네트워크가 갈라져 메시지가 사라질 수 있는 시스템에서, 모든 읽기가 가장 최근 쓰기를 보는 선형 일관성(C)과, 살아 있는 모든 노드가 모든 요청에 결국 답하는 가용성(A)을 분할이 일어난 동안 함께 가질 수는 없다.

N1·N2와 N3 사이 네트워크가 끊겼을 때 N3에 들어온 쓰기를 거절하면 일관성을, 받으면 가용성을 지킨다. 아래는 평소에 동기 복제로 일관성을 얻으면 지연이 늘고 비동기 복제로 지연을 줄이면 옛 값을 읽을 수 있다는 PACELC의 나머지 절반네트워크 분할(P)이 생겼다다수 쪽(2대)소수 쪽(1대)N1N2N3끊김쓰기 x=2 요청 ↓C를 고르면(CP)N3은 과반이 아니라 쓰기를 거절한다.값은 한 가지로 남지만 N3 쪽은 멈춘다.A를 고르면(AP)N3도 받는다. 양쪽 x가 1과 2로 갈린다.다시 이어지면 충돌을 합쳐야 한다.분할이 없을 때(E): 지연(L) ↔ 일관성(C)동기 복제: 모든 복제본 확인을 기다려 일관되지만 응답이 느려진다.비동기 복제: 바로 답해 빠르지만 팔로워에서 옛 값을 읽을 수 있다.
분할이 일어났을 때의 선택과, 분할이 없을 때의 맞바꿈. CAP은 위 그림의 상황만 말하고, PACELC는 아래 절반을 더한다.
  • 흔한 오해: "셋 중 둘을 고른다"가 아니다. 네트워크 분할은 고르는 것이 아니라 일어나는 일이라, 실제 선택은 분할이 났을 때 C와 A 중 무엇을 포기하느냐다. Brewer 자신도 2012년 글 「CAP Twelve Years Later」에서 이 단순화가 오해를 낳았다고 정리했다.
  • C와 A의 뜻: CAP의 C는 선형 일관성이라는 좁은 뜻이라 ACID의 C(제약 조건 지키기)와 다르고, A는 "모든 살아 있는 노드가 답한다"는 강한 뜻이라 흔히 말하는 99.99% 가용성과 다르다. Raft 클러스터는 소수 쪽이 쓰기를 거절하므로 CAP의 A는 아니지만, 실무에서는 충분히 가용성이 높다.
  • PACELC: Daniel Abadi는 「Consistency Tradeoffs in Modern Distributed Database System Design」(2012)에서 분할이 없는 평소가 더 중요하다고 짚었다. 분할(P)이면 A와 C 사이, 그렇지 않으면(E) 지연(L)과 C 사이에서 고른다. 동기 복제는 PC/EC, Dynamo식 저장소는 기본 설정이 PA/EL에 가깝다.

분산 트랜잭션과 2단계 커밋

샤드 두 곳의 행을 한 트랜잭션으로 고치려면 두 샤드가 함께 커밋하거나 함께 취소해야 한다. 고전적인 방법이 2단계 커밋(two-phase commit, 2PC)이다. 조정자가 모든 참여자에게 "커밋할 수 있나"를 묻고(준비), 참여자는 변경을 디스크에 기록한 뒤 "예"라고 약속한다. 모두 "예"면 조정자는 커밋 결정을 기록하고 알린다. 하나라도 "아니오"면 모두 취소한다.

2단계 커밋의 메시지: 준비 요청, 두 참여자의 예 응답, 조정자의 커밋 기록, 커밋 알림. 준비 뒤 결정을 받기 전까지 참여자는 불확실 상태로 잠금을 쥐고 기다린다조정자참여자 A참여자 B1단계: 준비2단계: 결정준비?기록기록예커밋 기록커밋!반영·잠금 해제반영·잠금 해제불확실 구간: 잠금을 쥐고 결정을 기다린다조정자가 커밋 기록 뒤 알리기 전에 죽으면, 참여자는 혼자 커밋도 취소도 못 하고 기다린다.그래서 조정자의 결정을 Raft 같은 합의로 복제해 두는 설계(Spanner 등)가 쓰인다.
2단계 커밋. 준비에 '예'라고 답한 참여자는 그 뒤로 혼자 결정할 권리를 잃는다. 조정자가 사라지면 결정을 들을 때까지 잠금을 쥔 채 막힌다.

2PC의 약점은 막힘(blocking)과 비용이다. 왕복이 두 번이고 준비 기록마다 디스크 쓰기가 필요하며, 가장 느린 참여자만큼 기다린다. 그래서 요즘의 분산 SQL 데이터베이스는 참여자 각각을 Raft 그룹으로 만들고 조정자의 결정도 합의로 복제해, 한 대가 죽어도 결정이 사라지지 않게 한다. PostgreSQL도 외부 조정자가 쓰도록 PREPARE TRANSACTION을 제공하지만, 준비된 채 남은 트랜잭션이 잠금과 VACUUM을 막으므로 조정자 없이 쓰지 말라고 경고한다. 여러 서비스에 걸친 작업은 2PC 대신 단계마다 커밋하고 실패하면 보상 작업으로 되돌리는 사가(saga) 패턴을 쓰기도 한다. 이 노트의 주제 전반은 Martin Kleppmann의 『Designing Data-Intensive Applications』가 한 권으로 깊게 다룬다.