들어가며
OpenSearch를 처음 열어보면 용어가 쏟아집니다.
클러스터 > 노드 > 인덱스 > 샤드 > 세그먼트
다섯 겹입니다. 그냥 데이터를 넣고 검색하고 싶을 뿐인데 왜 이렇게 복잡할까요?
답부터 말하면, 각 계층이 서로 다른 문제를 풀기 위해 생겼기 때문입니다. 누가 멋있어 보이려고 층을 쌓은 게 아니라, 문제가 하나씩 생길 때마다 층이 하나씩 늘어난 겁니다. 그래서 “이 층은 무슨 문제를 푸는가”만 알면 다섯 겹이 한 줄로 꿰어집니다.
이번 글의 범위
- 다루는 것: 다섯 계층이 각각 푸는 문제, 샤드와 세그먼트의 관계
- 다루지 않는 것: 검색이 빠른 이유(2편), 색인 과정(3편), 복제 상세(6편)
도서관으로 먼저 감 잡기
추상적인 용어를 늘어놓기 전에, 도서관 하나를 떠올려 봅시다.
| OpenSearch | 도서관 | 특징 |
|---|---|---|
| 클러스터 | 도서관 전체 | 건물 여러 채를 묶어서 하나의 도서관으로 운영 |
| 노드 | 건물 한 채 | 실제로 책을 보관하고 사서가 일하는 곳 |
| 인덱스 | 주제 구역 | “소설”, “과학” 처럼 종류별로 나눈 구역 |
| 샤드 | 책장 하나 | 구역이 너무 커서 책장 여러 개로 나눠 담음 |
| 세그먼트 | 인쇄된 책 한 권 | 한번 인쇄하면 내용을 고칠 수 없음 |
마지막 줄이 이 시리즈 전체에서 가장 중요합니다.
세그먼트는 “인쇄된 책”입니다. 이미 찍어낸 책의 3페이지만 살짝 고칠 수는 없죠. 고치려면 새 판을 인쇄해야 합니다. 세그먼트도 똑같습니다. 한번 만들어지면 절대 수정되지 않아요.
이 한 가지 성질에서 앞으로 다룰 거의 모든 것이 파생됩니다. 색인 중에도 검색이 안 막히는 이유(4편)도, 방금 넣은 문서가 바로 안 보이는 이유(5편)도 전부 여기서 나옵니다.
각 계층이 푸는 문제
이제 비유를 걷어내고, 각 층이 어떤 문제 때문에 생겼는지 봅시다.
정리하면 이렇습니다.
| 계층 | 이게 없으면 생기는 문제 |
|---|---|
| 클러스터 | 장비 여러 대를 각각 관리해야 함. 어디에 뭐가 있는지 사람이 추적 |
| 노드 | — (실제로 일하는 주체. 클러스터의 구성원) |
| 인덱스 | 상품과 로그가 한 덩어리에 섞임. 설정도 따로 못 줌 |
| 샤드 | 데이터가 장비 한 대 용량을 넘으면 더 못 넣음 |
| replica | 장비 한 대 죽으면 그 데이터는 영영 못 읽음 |
| 세그먼트 | 검색할 때마다 전체 문서를 처음부터 다 뒤져야 함 |
가장 흔한 오해: 샤드는 노드에 “걸쳐” 있지 않다
여기서 많이들 헷갈립니다.
“샤드는 여러 노드에 분산된다”
이 문장은 절반만 맞습니다. 정확히는 이렇습니다.
- 인덱스가 여러 샤드로 쪼개진다 (분할)
- 그 샤드들이 여러 노드에 흩어진다 (배치)
- 샤드 하나는 언제나 노드 하나 안에 통째로 들어있다
샤드 하나가 반으로 갈려서 노드 A와 B에 절반씩 얹히는 일은 없습니다.
primary 3개 / replica 1개인 인덱스는 위처럼 총 6조각으로 배치됩니다. 초록이 primary(원본), 파랑이 replica(복사본)예요.
primary와 replica는 같은 노드에 놓이지 않습니다.
당연한 이야기입니다. 같은 노드에 원본과 복사본을 함께 두면, 그 노드가 죽는 순간 둘 다 사라지니까요. 위 그림에서도 P0은 Node A에, 그 복사본 R0은 Node B에 있습니다.
왜 샤드는 쪼갤 수 없을까
샤드 하나가 곧 Lucene 인덱스 하나이기 때문입니다.
Lucene(루씬)이란?
자바로 만든 검색 라이브러리입니다. 실제로 문서를 저장하고 검색하는 일은 전부 Lucene이 합니다. OpenSearch는 그 위에 “여러 대로 나눠서 돌리고, 복사본을 관리하고, HTTP API로 노출하는” 껍데기를 씌운 것에 가깝습니다.
Lucene 인덱스는 자기완결적입니다. 검색에 필요한 모든 것이 그 안에 들어있어요.
[P0] 샤드 하나의 실체 (= Lucene 인덱스 하나)
├── _0.cfs ← 세그먼트 (인쇄된 책 1)
├── _1.cfs ← 세그먼트 (인쇄된 책 2)
├── _2.cfs ← 세그먼트 (인쇄된 책 3)
├── _2_1.liv ← 삭제 표시 (4편에서 다룸)
└── segments_5 ← 목차 (어떤 책들이 유효한지)이 묶음의 절반만 떼어내면 검색이 되지 않습니다. 그래서 물리적으로 쪼갤 수가 없어요.
여기서 중요한 결론이 하나 나옵니다.
검색 요청이 오면, 각 샤드는 자기 안에서만 답을 찾고, 그 부분 답들을 모아서 최종 결과를 만든다.
샤드끼리 서로를 참조하지 않습니다. 각자 독립적으로 자기 몫을 검색하고, 코디네이터 노드가 결과를 합칩니다. 이 구조 덕분에 노드를 늘리면 검색이 그만큼 빨라집니다.
직접 확인해보기
_cat API로 실제 구조를 눈으로 볼 수 있습니다.
# 클러스터에 노드가 몇 대 있나
GET _cat/nodes?v
# 인덱스 목록과 각 인덱스의 샤드/문서 수
GET _cat/indices?v
# 샤드가 어느 노드에 어떻게 배치됐나 ← 가장 유용
GET _cat/shards?v_cat/shards 결과는 이렇게 나옵니다.
index shard prirep state docs store node
products 0 p STARTED 1200 4.2mb node-a
products 0 r STARTED 1200 4.2mb node-b
products 1 p STARTED 1189 4.1mb node-b
products 1 r STARTED 1189 4.1mb node-cprirep 컬럼의 p가 primary, r이 replica입니다. 0번 샤드의 원본은 node-a에, 복사본은 node-b에 있는 게 보이죠. 앞서 말한 “원본과 복사본은 다른 노드에” 규칙이 실제로 지켜지고 있습니다.
세그먼트까지 보고 싶다면 이렇게 합니다.
GET _cat/segments/products?v여기서 세그먼트가 여러 개인 게 보일 텐데, 왜 하나가 아니라 여러 개인지가 3편의 주제입니다.
정리
- 다섯 계층은 각각 다른 문제를 푼다. 샤드는 용량 문제, replica는 장애 문제, 세그먼트는 검색 속도 문제.
- 샤드는 노드에 걸쳐 있지 않다. 샤드 하나 = Lucene 인덱스 하나 = 노드 하나 안에 통째로.
- 세그먼트는 인쇄된 책이다. 한번 만들면 고칠 수 없다. 이 성질이 앞으로 나올 거의 모든 것의 원인이다.
다음 글에서는 세그먼트 안에 정확히 뭐가 들어있길래 검색이 그렇게 빠른지 알아봅니다.