사전 프라이머리 샤드
개념

프라이머리 샤드

gabury1고친 사람 github-actions[bot]

프라이머리 샤드는 나눠 둔 데이터 조각마다 하나씩 있는 원본입니다. 그 조각에 들어오는 쓰기를 맨 먼저 받아 적습니다. 복사본들은 원본이 적은 것을 뒤따라 적습니다. 덕분에 같은 조각의 복사본들이 서로 다른 내용을 갖지 않습니다.

쉽고 빠른 이해

프라이머리 샤드는 데이터 조각 하나의 쓰기 창구 노릇을 합니다. 주문 데이터를 조각 셋으로 나눴다면 원본도 셋입니다. 주문 하나를 새로 넣는 요청은 그 주문이 속한 조각의 원본으로 갑니다.

창구가 하나가 아니면 같은 주문을 고치는 두 요청이 복사본마다 다른 순서로 적힐 수 있습니다. 그러면 어느 복사본을 읽느냐에 따라 답이 달라집니다.

이렇게 돕니다:

  1. 쓰기 요청은 그 조각의 원본이 먼저 받아 적습니다
  2. 원본이 같은 쓰기를 복사본들에 보냅니다
  3. 원본이 든 서버가 죽으면 복사본 하나가 새 원본이 됩니다

대가는 원본 수를 처음에 정해야 한다는 것입니다. 나중에 바꾸려면 데이터를 새로 나눠 옮겨야 합니다.

원본을 여럿 두는 것은 데이터나 쓰기가 서버 한 대를 넘을 때입니다. 작은 데이터라면 원본 하나로 충분합니다.

상세

은행이 고객 장부를 여러 권으로 나눠 둔다고 해 봅시다. 장부 한 권마다 원본 하나와 복사본 하나가 서로 다른 금고에 있습니다. 입금과 출금은 언제나 원본에 먼저 적습니다. 복사본은 원본에 적힌 줄을 차례대로 베껴 적을 뿐입니다.

앞 비유의 장부 한 권이 데이터 조각 하나입니다. 원본 장부가 프라이머리 샤드이고, 금고는 그 조각을 든 서버입니다.

프라이머리 샤드는 데이터 조각 하나에 하나씩 정해 두는 원본입니다. 그 조각을 고치는 요청은 모두 원본이 먼저 받습니다. 원본을 하나로 정해 두면 쓰기의 순서도 하나로 정해집니다.

이 이름은 주로 Elasticsearch와 OpenSearch 같은 [[검색 엔진]]에서 씁니다. 그래서 아래 예시도 검색 엔진에서 쓰는 말로 적습니다.

샤드와 원본 조각

데이터가 서버 한 대에 다 안 들어가면 여러 서버에 나눠 담습니다. 이렇게 나눈 단위를 이 글에서는 조각이라고 부릅니다. 주문 데이터를 셋으로 나누면 조각이 셋입니다.

검색 엔진은 데이터 한 건을 문서라고 합니다. JSON(JavaScript Object Notation) 형식으로 적은 레코드 한 건이라 테이블의 행에 해당합니다. 주문 하나가 문서 하나입니다.

문서를 담는 묶음은 인덱스입니다. 관계형 [[데이터베이스]]의 테이블에 가까운 단위입니다. 인덱스 하나가 조각 여럿으로 나뉩니다.

조각을 한 벌만 두면 그 조각을 든 서버가 죽을 때 데이터도 함께 사라집니다. 그래서 같은 조각을 다른 서버에 한 벌 더 둡니다. 같은 데이터를 여러 곳에 베껴 두는 이 일이 복제입니다.

조각마다 이렇게 여러 벌을 둡니다. 그 한 벌 한 벌이 샤드입니다. 조각 셋을 두 벌씩 두면 샤드는 여섯입니다.

샤드를 들고 있는 서버 하나는 노드입니다. 노드 한 대가 샤드 여럿을 함께 들기도 합니다.

노드 여러 대를 한데 묶어 하나처럼 굴리는 무리가 클러스터입니다. 앱 쪽에서 보면 노드가 몇 대든 저장소 하나처럼 보입니다.

한 조각의 샤드 가운데 쓰기를 먼저 받는 하나가 프라이머리 샤드입니다. 나머지 샤드는 레플리카 샤드라고 부릅니다. 이 글에서 원본이라고 하면 프라이머리 샤드를, 복사본이라고 하면 레플리카 샤드를 가리킵니다.

원본과 복사본은 서로 다른 노드에 둡니다. 한 노드에 같이 두면 그 노드가 죽을 때 둘 다 잃기 때문입니다. 아래 그림은 조각 셋을 원본 하나와 복사본 하나씩 두고 노드 세 대에 흩어 놓은 모습입니다.

flowchart TD
    subgraph 클러스터
        subgraph 노드1
            P0["원본 · 조각0"]
            R1["복사본 · 조각1"]
        end
        subgraph 노드2
            P1["원본 · 조각1"]
            R2["복사본 · 조각2"]
        end
        subgraph 노드3
            P2["원본 · 조각2"]
            R0["복사본 · 조각0"]
        end
    end
    P0 -.-> R0
    P1 -.-> R1
    P2 -.-> R2

점선은 같은 조각의 원본과 복사본을 잇습니다. 짝을 이루는 둘은 언제나 다른 노드에 있습니다. 그래서 노드1이 죽어도 조각0 은 노드3의 복사본에 남습니다.

쓰기를 원본 하나가 먼저 받는 까닭

원본과 복사본이 쓰기를 따로 받으면 어떻게 될까요. 같은 문서의 값을 5 로 바꾸는 요청과 7 로 바꾸는 요청이 거의 같이 들어옵니다. 원본에는 5 다음 7 이 적힙니다. 복사본에는 7 다음 5 가 적힐 수 있습니다.

sequenceDiagram
    participant A as 요청A (값 5)
    participant B as 요청B (값 7)
    participant 원본
    participant 복사본
    A->>원본: 5 로 바꾼다
    B->>원본: 7 로 바꾼다
    B->>복사본: 7 로 바꾼다
    A->>복사본: 5 로 바꾼다
    Note over 원본: 마지막 값 7
    Note over 복사본: 마지막 값 5

두 요청이 도착한 순서가 원본과 복사본에서 서로 다릅니다. 그래서 원본은 7 을, 복사본은 5 를 마지막 값으로 갖게 됩니다.

쓰기를 원본 한 곳에만 받게 하면 이런 엇갈림이 생기지 않습니다. 원본이 적은 순서가 곧 그 조각의 쓰기 순서가 됩니다. 복사본은 그 순서를 따라 적기만 합니다. 이렇게 한 벌이 쓰기를 받고 나머지가 따라 적는 방식이 주-백업 복제(primary-backup replication)입니다.

쓰기 한 번이 지나가는 순서를 그림으로 보면 이렇습니다. 요청을 처음 받은 노드는 문서가 어느 조각에 속하는지 계산해서 그 조각의 원본으로 넘깁니다. 이 계산은 아래 「원본 수를 처음에 정하는 까닭」에서 봅니다.

sequenceDiagram
    participant 앱
    participant 받은 노드
    participant 원본 as 프라이머리 샤드
    participant 복사본 as 레플리카 샤드
    앱->>받은 노드: 문서를 넣는다
    받은 노드->>원본: 이 조각의 원본으로 넘긴다
    원본->>원본: 먼저 적는다
    원본->>복사본: 같은 쓰기를 보낸다
    복사본-->>원본: 적었다
    원본-->>받은 노드: 성공
    받은 노드-->>앱: 성공

그림에서 볼 것은 성공 응답이 복사본을 거친 뒤에 나간다는 점입니다. 앱이 성공을 받았을 때 그 쓰기는 원본과 복사본에 함께 적혀 있습니다. 그래서 바로 뒤에 원본을 잃어도 그 쓰기는 복사본에 남습니다.

읽기는 복사본도 받는다

읽기는 순서를 다툴 일이 없습니다. 값을 바꾸지 않기 때문입니다. 그래서 검색이나 조회 요청은 원본과 복사본 가운데 어느 쪽이 받아도 됩니다.

복사본을 늘리면 읽기를 나눠 받을 노드가 늘어납니다. 쓰기는 사정이 다릅니다. 쓰기는 원본이 먼저 받습니다. 그리고 복사본마다 한 번씩 더 적어야 합니다. 복사본이 늘수록 쓰기 한 번에 드는 일도 늘어납니다.

원본을 잃었을 때

원본을 든 노드가 죽으면 그 조각은 쓰기를 받을 창구를 잃습니다. 이때 클러스터는 그 조각의 복사본 가운데 하나를 새 원본으로 올립니다. 이 일을 승격이라고 부릅니다.

승격이 끝나면 쓰기는 새 원본으로 갑니다. 복사본 하나가 원본이 됐으므로 복사본 수는 하나 줄었습니다. 클러스터는 남은 노드에 복사본을 새로 만들어 수를 다시 채웁니다. 아래 그림은 조각0 하나만 놓고 이 과정을 세 단계로 본 것입니다.

flowchart TD
    subgraph S1["① 노드1이 죽기 전"]
        A1["노드1 · 원본"]
        A2["노드2 · 복사본"]
    end
    subgraph S2["② 노드1이 죽은 뒤"]
        B2["노드2 · 원본으로 승격"]
    end
    subgraph S3["③ 다시 채운 뒤"]
        C2["노드2 · 원본"]
        C3["노드3 · 새 복사본"]
    end
    S1 --> S2 --> S3

복사본이 하나도 없던 조각은 올릴 것이 없습니다. 그 조각은 노드가 돌아올 때까지 쓰지도 읽지도 못합니다. 원본만 두고 복사본을 안 두는 것은 이 위험을 받아들이는 선택입니다.

원본 수를 처음에 정하는 까닭

문서가 어느 조각으로 갈지는 계산으로 정합니다. 흔한 방법은 문서 ID 를 해시 함수에 넣어 숫자를 얻는 것입니다. 그 숫자를 원본 수로 나눈 나머지가 조각 번호가 됩니다. 해시 함수는 같은 입력에 언제나 같은 숫자를 내는 함수입니다.

이 계산 덕분에 문서를 찾을 때 모든 조각을 뒤질 필요가 없습니다. 계산 경로를 그리면 이렇습니다.

flowchart TD
    ID["문서 ID"] --> H["해시 함수"]
    H --> N["숫자"]
    N --> M["원본 수로 나눈 나머지"]
    K["원본 수"] --> M
    M --> S["조각 번호"]

이 계산에 옆에서 들어가는 값은 원본 수 하나입니다. 원본 수를 바꾸면 같은 문서의 나머지가 달라집니다. 아래 표는 해시 값이 7 부터 12 인 문서 여섯 개를 원본 셋과 넷에 나눠 본 것입니다.

해시 값 원본 3개일 때 조각 원본 4개일 때 조각 옮겨야 하나
7 1 3 ✓
8 2 0 ✓
9 0 1 ✓
10 1 2 ✓
11 2 3 ✓
12 0 0 ✗

원본을 하나 늘렸을 뿐입니다. 그런데 여섯 개 가운데 다섯 개가 다른 조각을 가리킵니다. 원본 수만 바꾸고 데이터를 그대로 두면 문서를 엉뚱한 조각에서 찾게 됩니다.

그래서 원본 수는 인덱스를 만들 때 정합니다. 바꾸려면 데이터를 새로 나눠 옮깁니다.

복사본은 원본을 베낀 것이라 조각 번호 계산에 들어가지 않습니다. 그래서 복사본 수는 운영 중에도 늘리고 줄일 수 있습니다.

원본 수를 고르는 기준

원본이 너무 적으면 조각 하나가 커집니다. 큰 조각은 노드 한 대에 몰립니다. 노드를 늘려도 그 조각은 나눠 들 수 없습니다. 원본 수가 곧 쓰기를 나눠 받을 수 있는 노드 수의 상한이기 때문입니다.

원본이 너무 많으면 조각마다 드는 관리 비용이 쌓입니다. 클러스터는 조각마다 어느 노드에 있는지, 살아 있는지를 따로 챙깁니다. 검색 한 번도 조각마다 따로 돌린 뒤 결과를 합칩니다. 작은 조각이 많을수록 이 합치는 일이 커집니다.

데이터가 작고 앞으로도 크게 안 늘 인덱스라면 원본 하나로 충분합니다. 나눌 까닭이 없는 데이터를 나누면 관리 비용만 듭니다. 원본을 여럿 두는 것은 데이터나 쓰기가 노드 한 대를 넘을 때입니다.

검색 엔진에서 정하는 모습

Elasticsearch 와 OpenSearch 는 인덱스를 만들 때 설정으로 원본 수와 복사본 수를 정합니다.

아래는 주문 인덱스를 원본 셋, 조각마다 복사본 하나로 만드는 요청입니다. 오른쪽 주석은 이 설정으로 생기는 샤드 수입니다.

JSON
PUT /orders
{
  "settings": {
    "number_of_shards": 3,   // 원본 3개
    "number_of_replicas": 1  // 복사본 3개
  }
}

number_of_shards 가 원본 수입니다. number_of_replicas 는 원본 하나마다 둘 복사본 수입니다. 이 인덱스는 원본 셋과 복사본 셋, 모두 여섯 개의 샤드를 갖습니다. 「샤드와 원본 조각」의 그림이 바로 이 배치입니다.

원본 수는 만든 뒤로 못 바꿉니다. 복사본 수는 나중에 바꿀 수 있습니다. 「원본 수를 처음에 정하는 까닭」에서 본 조각 번호 계산 때문입니다.

MongoDB 의 프라이머리 샤드

MongoDB 에도 프라이머리 샤드라는 말이 있습니다. 뜻은 다릅니다. 여기서 샤드는 조각의 한 벌이 아니라 데이터를 나눠 드는 서버 무리 하나입니다.

MongoDB 에서 문서를 담는 묶음은 컬렉션입니다. 관계형 데이터베이스의 테이블에 가깝습니다.

MongoDB 의 프라이머리 샤드는 데이터베이스마다 하나씩 정해지는 기본 샤드입니다. 조각으로 나누지 않은 컬렉션은 이 샤드에 만들어집니다.

이 프라이머리는 쓰기 순서를 정하는 원본과 관계가 없습니다. 검색 엔진의 뜻으로 읽으면 엇갈립니다. MongoDB 에서 이 말은 데이터베이스의 기본 샤드를 뜻합니다.

관련 항목

프라이머리 샤드와 짝을 이루는 조각

레플리카 샤드 · 샤드 · 샤딩 · 수평 분할 · 파티셔닝 · 세그먼트

프라이머리 샤드를 담고 굴리는 단위

인덱스 · 노드 · 클러스터 · 클러스터 매니저 노드 · 코디네이팅 노드 · 샤드 할당

프라이머리 샤드가 쓰기를 퍼뜨리는 복제 방식

복제 · 주-백업 복제 · 리더 기반 복제 · 동기 복제 · 비동기 복제 · 복제 지연

프라이머리 샤드를 잃을 때 이어지는 복구 단계

페일오버 · 승격 · 미할당 샤드 · 클러스터 상태 · 고가용성 · 스플릿 브레인

문서를 프라이머리 샤드에 나누는 계산

라우팅 · 해시 함수 · 해시 기반 샤딩 · 일관성 해싱 · 리샤딩 · 리인덱싱

프라이머리 샤드를 쓰는 제품

Elasticsearch · OpenSearch · Lucene · MongoDB · 레플리카 셋 · 컬렉션

다른 이름: primary shard · 주 샤드 · 원본 샤드