|
| 1 | +# 5장 안정 해시 설계 |
| 2 | + |
| 3 | +## 안정 해시 |
| 4 | + |
| 5 | +- 해시 테이블 크기가 조정될 때 평균적으로 오직 k/n개의 키만 재배치하는 해시 기술. |
| 6 | + - k는 키의 개수, n은 슬롯의 개수 |
| 7 | + - 해시 테이블 크기가 조정될 때 → 서버 증설 및 서버 장애 시 |
| 8 | + - 전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분 키를 재배치함 |
| 9 | + |
| 10 | +### 해시링 |
| 11 | + |
| 12 | +- 해시 공간을 원형으로 만들어 사용 |
| 13 | +- 해시링을 사용하면 안정 해시를 구현할 수 있음 |
| 14 | + - 일부만 재배치 가능 → 서버 추가 시 해당 서버와 반 시계 방향에서 처음 만나는 서버 사이에 있는 키를 재배치함 |
| 15 | +- 서버와 키를 균등 분포 해시 함수를 사용해 해시 링에 배치 |
| 16 | +- 키의 위치에서 링을 시계 방향으로 탐색하다 만나는 최초의 서버가 키가 저장될 서버 |
| 17 | +- 서버가 추가되거나 삭제되는 상황을 감안하면 파티션의 크기를 균등하게 유지하는게 불가능 |
| 18 | + - 파티션은 인접한 서버 사이의 해시 공간 |
| 19 | +- 키의 균등 분포를 달성하기 어려움 |
| 20 | +- 가상 노드 또는 복제로 불리는 기법 활용해 해결 가능 |
| 21 | + |
| 22 | +### 가상 노드 |
| 23 | + |
| 24 | +- 실제 노드 또는 서버를 가리키는 노드 |
| 25 | +- 하나의 서버는 링 위에 여러 개의 가상 노드를 가질 수 있음 |
| 26 | + - 따라서 하나의 서버가 여러개의 파티션(해시공간)을 관리해야 함 |
| 27 | +- 가상 노드의 개수를 늘리면 키의 분포는 더 균등해짐 → 표준 편차가 작아짐(데이터가 퍼져 나갔는지 보이는 척도) |
| 28 | + - 대신 가상 노드 데이터를 저장할 공간이 더 많이 필요 |
| 29 | + |
| 30 | +# 6장 키-값 저장소 설계 |
| 31 | + |
| 32 | +### 단일 서버 |
| 33 | + |
| 34 | +- 키-값 쌍 전부를 메모리에 해시 테이블로 저장 |
| 35 | + - 모든 데이터를 메모리 안에 두는 것이 불가능할 수도 있다는 약점 |
| 36 | + - 데이터 압축 |
| 37 | + - 자주 쓰는 데이터만 두고 나머지는 디스크에 저장 |
| 38 | + |
| 39 | +## 분산 키-값 저장소 |
| 40 | + |
| 41 | +- 분산 해시 테이블 |
| 42 | + |
| 43 | +### CAP 정리 |
| 44 | + |
| 45 | +- 데이터 일관성(Consistency) |
| 46 | + - 어떤 노드에 접속하든 언제나 같은 데이터를 보게 되어야 함 |
| 47 | +- 가용성(Availability) |
| 48 | + - 일부 노드 장애 발생 시에도 응답 받을 수 있어야 함 |
| 49 | +- 파티션 감내(Partition tolerance) |
| 50 | + - 파티션 |
| 51 | + - 두 노드 사이의 통신 장애 |
| 52 | + - 파티션이 생기더라도 시스템은 계속 동작하여야 함 |
| 53 | +- CAP에선 두 가지를 충족하려면 나머지 하나는 희생되어야 함 |
| 54 | + - CA: 일관성과 가용성 지원 → 통신 장애는 피할 수 없으므로 존재하지 않는 시스템 |
| 55 | + |
| 56 | +### 문제 상황 |
| 57 | + |
| 58 | +하나의 노드가 기록 후 장애로 인해 다른 노드로 데이터를 전달하지 못했을 경우, 다른 노드는 오래된 사본을 가지게 된다. |
| 59 | + |
| 60 | +- 가용성 대신 일관성을 선택하면 데이터 불일치 문제를 피하기 위해 쓰기 연산을 중단하고 해결 될 때 까지 오류 반환 |
| 61 | +- 일관성 대신 가용성을 선택하면 낡은 데이터를 반환하더라고 읽기 연산 허용, 쓰기 연산도 허용하고 파티션 문제가 해결된 뒤에 새 데이터를 전송 |
| 62 | + |
| 63 | +### 데이터 파티션 |
| 64 | + |
| 65 | +- 데이터를 작은 파티션으로 분할한 다음 서버에 저장 |
| 66 | +- 데이터를 고르게 분산시키고 노드 추가 및 삭제 시 데이터 이동을 최소화 해야함 |
| 67 | + - 안정 해시 사용 ! |
| 68 | + |
| 69 | +### 데이터 다중화 |
| 70 | + |
| 71 | +- 데이터를 N개 서버에 비동기적으로 다중화할 필요가 있다. |
| 72 | +- 해시 링 위에 배치 후 시계 방향으로 순환하며 N개의 서버에 사본 보관 |
| 73 | + - 가상 노드를 사용하면 N개의 노드가 대응될 실제 물리 서버의 개수가 N보다 작아질 수 있음 |
| 74 | + - 하나의 서버가 여러 개의 파티션을 관리하기 때문 ! (같은 서버가 관리하는 노드를 여러 개 만날 수 있다) |
| 75 | + - 같은 물리 서버를 중복 선택하지 않도록 유의 → 같은 데이터 센터도 유의 ! |
| 76 | + |
| 77 | +### 데이터 일관성 |
| 78 | + |
| 79 | +정족수 합의 프로토콜(Quorum Consensus)을 이용해 적절히 데이터를 동기화 |
| 80 | + |
| 81 | +- N = 사본 개수 |
| 82 | +- W = 쓰기 연산에 대한 정족수. 쓰기 연산이 성공한 것으로 간주되면 적어도 W개의 서버로부터 쓰기 연산이 성공했다는 응답을 받아야 한다. |
| 83 | +- R = 읽기 연산에 대한 정족수. 읽기 연산이 성공한 것으로 간주되려면 적어도 R개의 서버로부터 응답을 받아야 한다. |
| 84 | + |
| 85 | +중재자(클라이언트와 노드 사이의 Proxy)가 필요하다. W, R, N 값을 정하는 것은 응답 지연과 데이터 일관성 사이의 타협점을 찾는 것이다. |
| 86 | + |
| 87 | +W, R = 1 인 경우 한 대의 서버로 중재자가 응답 받으면 되니까 응답 시간이 빠르지만 데이터 일관성 수준을 보장할 수 없다. 값을 키우면 응답 시간이 느려진다. |
| 88 | + |
| 89 | +- R = 1, W = N : 빠른 읽기 연산에 최적화된 시스템 |
| 90 | +- W = 1, R = N : 빠른 쓰기 연산에 최적화된 시스템 |
| 91 | +- W + R > N : 강한 일관성이 보장됨 |
| 92 | +- W + R ≤ N : 강한 일관성이 보장되지 않음 |
| 93 | + |
| 94 | +### 일관성 모델 |
| 95 | + |
| 96 | +- 강한 일관성 : 모든 읽기 연산은 가장 최근에 갱신된 결과를 반환한다. 클라이언트는 예전 데이터를 보지 못함 |
| 97 | + - 일반적으로 모든 사본에 쓰기 결과가 반영될 때까지 해당 데이터에 대한 읽기/쓰기를 금지 |
| 98 | +- 약한 일관성 : 읽기 연산은 가장 최근에 갱신된 결과를 반환하지 못할 수 있음 |
| 99 | +- 최종 일관성 : 약한 일관성의 한 형태로 갱신 결과가 ‘결국에는’ 모든 사본에 동기화 되는 모델 |
| 100 | + - 쓰기 연산이 병렬적으로 발생할 경우 일관성이 깨짐 |
| 101 | + - 클라이언트가 이 문제를 해결해야 함 |
| 102 | + |
| 103 | +### 비 일관성 해소 기법 : 데이터 버저닝 |
| 104 | + |
| 105 | +버저닝은 데이터 변경 시 해당 데이터의 새로운 버전을 만드는 것을 의미하고 각 버전의 데이터는 불변이다. |
| 106 | + |
| 107 | +버저닝의 충돌 해결 방법은 벡터 시계라는 방법이다. |
| 108 | + |
| 109 | +- 벡터 시계 = (서버, 버전) 정보를 데이터에 매단 것 |
| 110 | + - 이를 통해 충돌 판단 → 버전으로 먼저 바꾼 것인지, 후에 바꾼 것인지 판단 가능 |
| 111 | + - 충돌 감지 및 해소가 클라이언트에 들어가 클라이언트 구현이 복잡해짐 |
| 112 | + - (서버, 버전) 정보가 빨리 늘어나 임계치 도달 시 오래된 벡터 시계를 지워야 함 |
| 113 | + |
| 114 | +### 장애 감지 |
| 115 | + |
| 116 | +- 모든 노드 사이에 멀티캐스팅을 구축해 장애 감지 |
| 117 | + - 서버가 많으면 많은 통신이 일어나 비효율적 |
| 118 | +- 가십 프로토콜(gossip) 등을 사용해 분산형 장애 감지 |
| 119 | + - 각 노드는 멤버십 목록을 유지. 멤버십 목록은 각 멤버 ID와 heartbeat 카운터 |
| 120 | + - 각 노드는 주기적으로 자신의 박동 카운터를 증가 |
| 121 | + - 각 노드는 무작위로 선정된 노드들에게 주기적으로 자기의 카운터 목록을 보냄 |
| 122 | + - 목록을 받은 노드는 멤버십 목록을 최신 값으로 갱신 |
| 123 | + - 만약 어떤 멤버가 일정 시간 동안 갱신되지 않으면 장애 상태인 것으로 간주 |
| 124 | + - 이를 통해 최소한의 통신으로 장애 판단 가능 |
| 125 | + |
| 126 | +### 장애 처리 |
| 127 | + |
| 128 | +- 일시적 장애 처리 |
| 129 | + - 엄격한 정족수 사용 시 읽기 쓰기 연산 금지 |
| 130 | + - 느슨한 정족수 사용 시 가용성을 높임 |
| 131 | + - 쓰기 연산을 수행할 W개의 건강한 서버와 읽기 연산을 수행할 R개의 건강한 서버를 해시 링에서 고름 |
| 132 | + - 장애 상태인 서버로 가는 요청을 다른 서버가 잠시 맡아 처리하고 해당 서버 복구 시 일괄 반영 |
| 133 | + - 임의로 처리한 서버에는 힌트를 남겨둔다. |
| 134 | +- 영구 장애 처리 |
| 135 | + - 반 엔트로피 프로토콜 구현해 사본을 동기화 |
| 136 | + - 머클 트리(해시 트리)를 사용 |
| 137 | + - 루트 노드의 해시 값이 일치하면 두 서버의 데이터가 같다. |
| 138 | + - 따라서 이 것을 이용해 트리를 탐색하면서 다른 데이터를 갖는 버킷을 찾아 그 버킷들만 동기화한다. |
| 139 | + |
| 140 | +## 머클 트리(해시 트리) |
| 141 | + |
| 142 | +### **머클 트리의 구조** |
| 143 | + |
| 144 | +머클 트리는 **이진 트리** 형태로 구성되며, 다음과 같은 노드들로 구성됩니다: |
| 145 | + |
| 146 | +1. **리프 노드 (Leaf Node)**: |
| 147 | + - 원본 데이터의 해시 값이 저장되는 노드입니다. |
| 148 | + - 예: 데이터를 SHA-256과 같은 해시 함수로 변환한 값. |
| 149 | +2. **중간 노드 (Intermediate Node)**: |
| 150 | + - 자식 노드들의 해시 값을 결합하여 다시 해시한 값이 저장됩니다. |
| 151 | + - 예: 왼쪽 자식 해시와 오른쪽 자식 해시를 연결한 뒤 이를 해시화. |
| 152 | +3. **루트 노드 (Root Node)**: |
| 153 | + - 트리의 최상단 노드로, 전체 데이터의 해시 정보를 나타냅니다. |
| 154 | + - 이 루트 해시 값은 데이터 전체의 무결성을 검증하는 데 사용됩니다. |
| 155 | + |
| 156 | +### **머클 트리의 생성 과정** |
| 157 | + |
| 158 | +1. **데이터 준비**: |
| 159 | + - 데이터를 작은 조각(블록)으로 나눕니다. |
| 160 | +2. **리프 노드 해싱**: |
| 161 | + - 각 데이터 블록에 대해 해시 값을 계산하여 리프 노드를 만듭니다. |
| 162 | +3. **중간 노드 생성**: |
| 163 | + - 리프 노드들의 해시 값을 두 개씩 묶어 해시를 계산하여 부모 노드를 생성. |
| 164 | + - 이 과정을 반복하여 트리의 루트 노드까지 계산. |
| 165 | +4. **루트 노드 생성**: |
| 166 | + - 최종적으로 하나의 루트 해시 값이 남습니다. |
| 167 | + |
| 168 | +### **머클 트리의 특징** |
| 169 | + |
| 170 | +1. **효율적인 검증**: |
| 171 | + - 전체 데이터를 비교하지 않고, 특정 데이터 조각의 변경 여부를 확인할 수 있습니다. |
| 172 | + - 검증에는 루트 해시와 해당 데이터 조각의 해시값, 관련 노드의 해시 값만 필요합니다. |
| 173 | +2. **무결성 보장**: |
| 174 | + - 데이터가 변경되면, 해당 리프 노드와 관련된 부모 노드, 루트 노드까지 영향을 미칩니다. |
| 175 | + - 루트 해시를 통해 데이터가 변경되었는지 쉽게 확인 가능합니다. |
| 176 | +3. **저장 효율성**: |
| 177 | + - 해시 값만 저장하기 때문에 큰 데이터를 처리할 때 저장 공간이 절약됩니다. |
| 178 | +4. **분산 시스템에 적합**: |
| 179 | + - 데이터 조각을 여러 노드에 분산 저장하더라도, 루트 해시로 전체 데이터를 검증할 수 있습니다. |
| 180 | + |
| 181 | +## SSTable과 블룸 필터 |
| 182 | + |
| 183 | +### LSM 트리와 SSTable |
| 184 | + |
| 185 | +- LSM 트리는 데이터를 메모리에 유지하다가, 일정 크기 이상이 되면 데이터를 디스크로 플러시함 |
| 186 | +- 이때 디스크에 생성되는 것이 SSTable |
| 187 | +- 각 로그 구조화 저장소 세그먼트는 키-값 쌍의 연속이며 순차적으로 데이터가 생성되므로 나중의 값이 가장 최신의 값임 |
| 188 | +- 저장소 세그먼트를 키로 정렬한 형식을 **정렬된 문자열 테이블(Sorted String Table, SS테이블)**이라 부름 |
| 189 | + - 각 키는 병합된 세그먼트 파일 내에 1번만 나타나야 함 |
| 190 | + - 찾고자 하는 키가 존재할 범위를 예상하고 해당 영역에서만 값을 조회하여 찾을 수 있음. |
| 191 | + - merge sort 알고리즘 사용 → 모든 키의 인덱스를 관리할 필요 없음(부분 배열로 나누어 머지하기 때문) |
| 192 | + |
| 193 | +  |
| 194 | + |
| 195 | + - 예를 들어 handiwork 키를 찾으려면 handbag과 handsome 키의 오프셋을 알고 정렬 되어 있어 handiwork는 두 키 사이에 있음을 알 수 있음. |
| 196 | + - handbag 오프셋으로 이동해 handiwork가 나올 때 까지 스캔하면 됨 |
| 197 | + - 일부 키에 대한 오프셋을 알려주는 인덱스 테이블이 필요하지만 일부만 관리하면 됨 |
| 198 | +- 읽기 요청은 요청 범위 내 여러 키-값 쌍을 스캔해야 하므로 해당 레코드들을 블록으로 그룹화, 디스크에 쓰기 전에 압축함 |
| 199 | + - 그러면 인덱스 테이블에 각 항목은 압축된 블록의 시작을 가리키게 되고 디스크 공간 절약 및 I/O 대역폭 사용 감소라는 장점이 생김 |
| 200 | + |
| 201 | +### SS테이블 생성과 유지 |
| 202 | + |
| 203 | +- 쓰기 요청이 들어오면 인메모리 균형트리 데이터 구조에 추가한다. |
| 204 | + - 이런 인메모리 트리는 Memtable이라고도 한다. |
| 205 | +- 새로운 SS테이블 파일은 DB에서 가장 최신 세그먼트가 된다. |
| 206 | +- SS테이블을 디스크에 기록하는 동안 쓰기는 새로운 멤테이블 인스턴스에 기록한다. |
| 207 | +- 읽기 요청을 제공하려면 먼저 멤테이블에서 키를 찾는다. |
| 208 | + - 그 다음 디스크 상의 가장 최신 세그먼트 -> 두 번째 오래된 세그먼트 -> N 번째 세그먼트 순서로 찾는다. |
| 209 | +- 그리고 중간중간 백그라운드로 병합과 컴팩션 과정을 수행한다. |
| 210 | +- DB 장애 시 아직 디스크에 기록되지 않고 멤테이블에 있는 가장 최신의 쓰기 작업 데이터는 손실 |
| 211 | + - 매번 쓰기를 즉시 추가할 수 있게 분리된 로그를 디스크 상에 유지(WAL → 쓰기 요청 들어오면 WAL에 기록 후 응답) |
| 212 | + - 멤테이블 복원 시에만 필요해 순서가 정렬되지 않아도 되며 멤테이블을 SS테이블로 기록하고 나면 디스크에 저장된 데이터 삭제 |
| 213 | + - SS테이블 = 디스크 상에 정렬된 세그먼트 파일 |
| 214 | + |
| 215 | +### 성능 최적화 |
| 216 | + |
| 217 | +- LSM 트리 알고리즘은 DB에 존재하지 않는 키를 찾는 경우 느릴 수 있음 |
| 218 | + - 존재하지 않는 키를 찾는 경우 가장 오래된 세그먼트까지 거슬러 올라가야함 |
| 219 | + - 블룸 필터를 추가로 사용해 DB에 키가 존재하지 않음을 알려줘서 불필요한 디스크 읽기 줄임 |
| 220 | +- 블룸 필터는 원소가 집합에 속하는지 여부를 검사하는 확률적 자료구조 |
| 221 | + - 원소가 집합에 속한다고 판단된 경우 원소가 집합에 속하지 않는 긍정 오류 발생 가능 |
| 222 | + - 그 반대인 부정 오류는 절대 발생하지 않음 (속하지 않음 → 사실 속하는 거 였던 거임 ㅋㅋ) |
| 223 | + |
| 224 | +출처 https://www.scylladb.com/glossary/sstable/ |
| 225 | + |
| 226 | +https://goodgid.github.io/SD-Repository-and-Search-SS-table-and-LSM-tree/ |
| 227 | + |
| 228 | +https://ko.wikipedia.org/wiki/%EB%B8%94%EB%A3%B8_%ED%95%84%ED%84%B0 |
0 commit comments