[데이터 중심 애플리케이션 설계] 03. 저장소와 검색
[데이터 중심 애플리케이션 설계] 03. 저장소와 검색
데이터 중심 애플리케이션 설계 시리즈의 글입니다.
데이터베이스가 데이터를 저장하는 방버과 요청했을때 다시 찾을 수 있는 방법을 자세히 알아보자
Data Structures That Power Your Database
- 가장 간단한 구조를 생각해보면?
db_set key value: db저장db_get key: db로드- 이와같이 많은 db들은 내부적으로 log(append-only data file)를 사용
db_get은 O(n)의 시간복잡도를 가짐. → 비효율- 특정 키의 값을 효율적으로 찾기 위해 다른 데이터 구조 필요 (Index)
- Index (색인)
- 어떤 부가적인 메타데이터를 유지
- 다양한 방법으로 검색한다면 다양한 index필요
- 데이터를 쓸때마다 index도 갱신해야하므로 쓰기과정에서 overhead발생
- read/write속도간의 trade-off
Hash Indexes
- key-value store
- dictionary type과 유사
- hash map(hash table) tkdyd
- in-memory data structure 를 사용한다면?
- key를 데이터파일의 file byte offset에 매핑
- 값 조회시 해당 오프셋을 찾아 값을 읽음
- 키의 값이 자주 갱신되는 상황에 적합.
- append only라 디스크 공간이 부족해진다면?
- 특정 크기의 segment로 로그를 나누는게 좋은 해결책.
- segment file들을 compaction(로그에서 중복된 키를 버리고 각 키의 최신 갱신값만 유지)해서 사용
- compaction 수행시에는 background thread에서 수행
- 동시에 여러 segment를 병합. → 병합된 결과는 새로운 파일로 만듦
- 각 segment 는 자체 in-memory hash table을 갖게됨
- 실제 구현에서 중요한 문제
- file형식 : binary format추천
- 레코드 삭제 : 특수한 삭제 레코드(tombstone)을 추가.
- crash recovery: db재시작시 in-memory hashmap은 날아감.. 파일 복원이 가능하나 오래걸림. snapshot드을 사용해 복구 속도를 높힐수잇음
- paritally written records : 레코드 추가 도중 죽는다면? → checksum등을 사용해 손상부분 탐지
- concurrency control (동시성제어) : 쓰기 스레드만 쓴다거나..
- append-only 로그가 좋은 설계인 이유
- appending / segment merging은 sequential한 작업이라, random writes보다 빠름.→ 특히 magnetic spinning-disk hard drive에서 유리.
- concurrency, crash recovery가 더 쉬워짐
- segment merging은 데이터파일이 조각화되는 경향이 없음.
- hash table index의 한계
- key가 너무많으면 문제가됨.(해시충돌)
- 해시테이블은 범위질의에 효율적이지않음.
SSTables, LSM-Trees
SS Table : key로 정렬된 형식 = Sorted String Table
- 장점
- segment merging시 mergesort방식으로 햇을때 메모리보다 더 큰 파일을 사용 가능.
- 다중 segment가 동일한 key를 포함한 경우 가장 최근의 값만 유지하고 오래된 값은 버림.
- 특정 key를 찾을 때 메모리에 모든 키의 index를 유지할 필요 없음
- sparse in-memory index는 압축된 블록의 시작을 가리킴. → 디스크 공간 + I/O 대역폭 사용 절약
- 생성과 유지
- 정렬된 구조는 디스크보다 메모리에 유지하는게 쉬움.
- red-black tree나 AVL tree같은 데이터 구조를 사용하면 임의 순서로 키를 삽입하고 정렬된 순서로 해당 키를 다시 읽을 수 있음.
- 단 db가 고장나면 디스크에 기록되지 않은 메모리의 테이블(memtable)은 손실될수잇음. → 분리된 log를 디스크상에 유지해야. 나중에 복원할때 사용할수있음.
- LSM Tree 만들기
- levelDB, RocksDB, key-value store engine library, HBase에 사용되는 알고리즘.
- Log-Structured Merge-Tree(LSM Tree: 로그 구조화와 병합트리) : log 구조화와 파일시스템의 초기 작업의 기반이 됨.
- LSM store engine: 정렬된 파일 병합과 compaction 원리를 기반으로 하는 저장소 엔진
- Lucene : elastic search, Solr에서 full-text search에 사용되는 indexing engine.
- 검색 질의로 단어가 들어오면 단어가 언급된 모든 문서를 찾음
- key를 word(term)으로, value를 word를 포함하는 문서의 id리스트로 하는 key-value 구조로 구현.
- ⇒ 역색인 (inverted index)
- 성능 최적화
- db에 존재하지 않는 key를 찾는경우 성능 저하됨(풀스캔해서 없는걸 확인해야하니..)
- 이를 방지하기 위해 보통 Bloom filter를 사용.
bloom filter (블룸필터) : 집합 내용을 근사한 메모리 효율적 데이터 구조
이해가 쉽게 예를 들어보자면,
인덱스: 0 1 2 3 4 5 6 7
값 : 0 0 0 0 0 0 0 0
- 해시 함수 2개 (예시)
- h₁(x) = (단어 길이) mod 8
- h₂(x) = (단어 첫 글자의 알파벳 순서) mod 8
- “apple” 추가하기
- 단어 길이 5 → h₁(apple)=5 mod 8 = 5
- 첫 글자 A → 순서 1 → h₂(apple)=1 mod 8 = 1
- 비트 배열의 인덱스 5와 1을 1로 표시
인덱스: 0 1 2 3 4 5 6 7
값 : 0 1 0 0 0 1 0 0
- “apple” 조회하기
- h₁(apple)=5, h₂(apple)=1 → 배열[5]=1, 배열[1]=1
- 둘 다 1이니 “있을지도(maybe)”
- (실제로 넣었으니 “있음”)
- 삽입: 해시 함수로 뽑은 칸들을 1로 체크
- 조회: 체크된 칸 중 하나라도 0이면 “없음”, 모두 1이면 “있을지도”
- 메모리는 비트 배열만 쓰고, 어떤 원소가 들어왔는지 따로 저장하지 않음
- 그래서 거짓 음성(false negative)은 절대 없지만, 해시 충돌로 거짓 양성(false positive)은 가끔 발생
- key가 db에 존재하지 않음을 알려주므로 불필요한 디스크 읽기를 절약할수잇음
- SS table을 압축, 병합하는 순서와 시기를 결정하는 다양한 전략 사용
- size-tiered : 상대적으로더 새롭고 작은 ss테이블을 상대적으로 오래되고 큰 ss테이블에 연이어 병합
- leveled compaction : 키 범위를 더 작은 ss table로 나누고 오래된 데이터는 개별 level로 이동해 컴팩션을 점진적으로 진행 → 디스크 공간 절약
- 기본적으로 LSM Tree = background에서 연쇄적으로 ss table을 지속적으로 병합
- 범위 처리가 유리해짐
- 매우 높은 쓰기 처리량 보장.
B-Trees
- 가장 널리 사용되는 index 구조
- SS-Table과 같이 정렬된 key-value 쌍을 유지. → 범위 질의에 효율적
- 4KB(혹은 그 이상)의 고정 크기 block이나 page로 나누고 한번에 하나의 page에 읽기 / 쓰기를 함.
- ⇒ HW와 밀접한 관련의 설계
- 한 page가 다른 page를 참조 (포인터, 대신 디스크에 저장됨)
- key를 찾을때 root에서 시작되어, 최종 개별 키 (leaf page)를 포함하는 페이지에 도달.
- branching factor(분기계수) : 한 페이지에서 하위 페이지를 참조하는 수. 보통 수백개에 달함
- 값 갱신시 key를 포함하는 leaf page검색 후, 값을 갱신하고 다음 페이지를 디스크에 다시 기록.
- 키 추가시 새 키를 포함하는 넘위의 페이지를 찾아 해당 페이지에 키와 값을 추가
- 새 키를 수용할 페이지에 여유공간이 없으면 페이지 하나를 반으로 나눈 뒤 상위 페이지가 새로운 키 범위의 하위 부분을 알 수 있도록 갱신
- 이 알고리즘은 트리가 계속 균형을 유지하는것을 보장.
- n개 key가진 b-Tree는 O(logn)의 깊이 보장. 보통 3~4단계면 충분.
- 신뢰할 수 있는 B-tree 만들기
- B-tree의 기본동작 : 새로운 데이터를 디스크상의페이지에 덮어씀(overwrite)
- 일부 동작은 여러 페이지 덮어쓰기를 필요로함 (ex. 삽입시 페이지가 분할되는 경우)
- 일부 페이지만 쓰읽게된다면? → orphan page(고아페이지)가 생길수 있어서 위험
- 복구가능성을 위해, WAL(write-ahead log, 쓰기 전 로그 =redo log) 를 기록. 변경된 내용을 적용 하기 전, 모든 B-tree의 변경 사항을 기록하는 append-only file.
- 다중 스레드가 B-tree에 접근 할 일이 있다면 신중하게 동시성 제어를 해야함. 보통은 latch(가벼운 lock)로 트리의 데이터 구조를 보호.
- log구조화 방식은 간단. background에서 모든 병합을 수행하고 때때로 new segment를 old segment로 바꿈.
- B-tree optimization(최적화)
- copy-on-wirte scheme(COW) 방식 : 원본 데이터를 공유하다가 쓰기 시점에 추가 메모리를 사용하여 복사 한 뒤, 복사본을 수정하는 방식. 트리 상위 페이지는 이 위치를 가르키게됨
- 저장시 축약한 key를 저장해 공간을 절약
- leaf페이지를 디스크상에 연속된 순서로 나타나게끔 트리를 배치 (단, 트리가 커지면 순서 유지가 어려움)
- 트리에 포인터를 추가 (ex. leaf가 양쪽 형제에 대한 참조를 가짐)
- fractal tree
B-Tree vs LSM-Tree
- LSM tree는 쓰기에서 더 빠름
- B-Tree는 읽기에서 더 빠름
- LSM Tree는 압축률이 더 좋음
- B-Tree는 각 key가 index의 한곳에만 존재
- LSM Tree는 다른 세그먼트에 key의 다중 복사본이 존재할수있음
- 강력한 transaction semantic을 제공시에는 B-tree가 더 매력적
- B-tree는 기존 DB 아키텍처에 아주 깊게 뿌리내림
- 그러나 쓰기위주 새로운 db저장소에서는 Log structured index가 점점 인기를 얻고있음
Other Indexing Structured
- Primary Key index : pk로 한 row를 고유하게 식별
- Secondary index : 효율적으로 join수행시 결정적인 역할
- index안에 value저장
- clustered index : index에 row를 바로 저장
- non clustered index : index에 row의 reference를 저장. (실제 row가 저장된 heap file을 참조)
- covering index (=index with included column) : index안에 column 일부를 저장
- clustered index와 covering index는 읽기 성능은 높이지만 쓰기 오버헤드를 높힐수있음
- Multi-column index
- concatenated index (결합색인) : 하나의 key에 여러 field 를 결합 ex. (성, 이름) - 전화번호
- multi-dimensional index (다차원 색인)
- 한번에 여러 컬럼에 질의하는 더 일반적인 방법. 지리공간 데이터에 중요하게 사용.
- ex. 특정 좌표의 반경 50m 이내 음식점 찾기
- B-Tree나 LSM-Tree는 이러한 질의에 효율적으로 검색 불가능
- R-Tree같은 전문 공간 색인을 사용. (ex. PostGIS)
- Full-text search (전문검색) 과 fuzzy index(퍼지 색인)
- fuzzy query(애매모호한 질의)에는 다른 기술이 필요 ex. 철자가 틀린 단어로 유사한 키 검색
- 동의어로 질의를 확장
- 인접해 나타난 단어를 검색
- 언어학적 테스트 분석해 사용
- editdistance 를 사용
- levenshtein automaton : trie 와 유사. 특정 edit distance이내 단어 검색.
- 모든것을 memory에 보관
- disk = 지속성이 있고 가격이 더 저렴
- 그러나 ram 가격이 싸짐. → in-memory database 의 개발
- memcache : 재시작시 데이터 손실. 캐시용도로만 사용
- 지속성있는 in-memory DB도 개발됨
- 특수하드웨어(배터리 지속ram 등) 사용
- 변경사항 로그 기록
- 디스크에 주기적 snapshot 기록
- disk 기반 index로 구현하기 어려운 data model 제공
- ex. redis - priority queue, set
- anti-caching
- 최근에 사용하지 않은 데이터를 메모리에서 디스크로 보내고 나중에 다시 접글할때 메모리에 적재하는 방식.
- 가용 메모리보다 더 큰 데이터셋을 지원하게끔 확장 가능
- 비휘발성메모리(NVM: non-volatile memory) 기술이 확장될수록 기존 db들의 설계가 변경될것!!
Transaction Processing or Analytics?
- transaction : 꼭 ACID 속성을 가질 필요는 없음. low latency read/write를 가능하게 하는 처리를 뜻함.
- OLTP (online transaction processing) : 트랜잭션 처리 시스템. index를 이용해 레코드를 찾고 사용자의 입력을 토대로 삽입/갱신하는 패턴.
- data analytic에도 점점 많이 사용되고있음 = business intelligence
- OLAP (online anlaytic processing) : 분석 시스템. 온라인 분석 처리.
| OLTP | OLAP | |
| 주요 읽기 패턴 | query당 적은 수의 record를 key 기준으로 가져옴. | 많은 record에 대한 집계 (aggregate) |
| 주요 쓰기 패턴 | random-access, low-latency write from user input | bulk import (ETL), or event stream |
| 주요 사용처 | end user/consumer via web application | internal analyst, for decision support |
| 데이터 표현 | latest state of data | history of events |
| 데이터셋 크기 | GB or TB | TB or PB |
Data Warehousing
- 분석가들이 OLTP작업에 영향을 주지 않고 마음껏 질의할 수 있는 개별 데이터베이스
- 다양한 OLTP시스템에 있는 데이터의 읽기 전용 복사본
- ETL(extract-transform-load) : data warehouse로 데이터를 가져오는 과정
- 분석 접근 패턴에 맞게 최적화할수있음
OLTP Database vs Data warehouse
- sql 은 둘다 지원
- OLTP는 transaction processing 지원
- data warehouse는 analytic workload지원
Stars and snowflakes : Schemas for Analytics
- OLTP에서는 필요에따라 다양한 데이터모델을 사용
- OLAP에서는 star schema(dimensional modeling) 라는 정형화된 방식을 사용
- fact table : 특정시각에 발생한 event. 다른 컬럼은 dimension table의 foreign key reference.
- dimension은 who, when, where, what , how, why를 나타냄
- fact table이 가운데 있고 dimension table이 이를 둘러싸고있는 모양 = star schema
- snowflake schema : star schema의 변형. 차원이 하위 차원으로 더 세분화됨.
Column-Oriented Storage
- 각 컬럼을 모두 한곳에 저장하는 방식
- 질의에 사용되는 컬럼만 읽고 구문을 분석
- 각 컬럼 파일에 포함된 모든 row가 같은 순서인점에 의존해, 특정 row를 만드려면 칼럼별 특정 index의 값을 가져와 합치면 한 row가 완성된다
Column compression
- 데이터를 압축하면 디스크 처리량을 더 줄일 수 있음
- column-oriented storage는 압축에 적합함. 동일 컬럼에서는 많은 값이 반복해서 나타나기때문.
- ex. bitmap encoding
메모리 대역폭과 벡터화 처리
- 디스크로부터 메모리로 데이터를 가져오는 대역폭이 큰 병목
- column-oriented storage는 CPU cycle을 효율적으로 사용하기에 적합
- ex. 압축된 컬럼데이터를 cpu cache에 딱 맞는 사이즈로 가져온 뒤, 이 작업을 tight loop에서 반복
- bit연산자 (AND, OR)는 압축된 컬럼을 바로 연산할수있음. → vectorized processing
Sort order in column storage
- 컬럼저장소에서는 순서가 그닥 중요하지않음
- 보통 삽입된 순서로 저장하는 방식이 가장 easy
- 그러나 order를 도입해 색인 메커니즘으로 사용할수는있음
- 첫번째 칼럼에서 같은 값을 가진 row들의 정렬 순서를 두번째 컬럼에서 정함
- ex. 같은날짜에 판매한 제품을 그룹화
- 정렬은 컬럼압축에도 도움이 됨. 정렬 후에 같은값이 연속될확률이 높기때문.
**다양한 순서 정렬 **
- 다양한 질의는 서로 다른 정렬순서의 도움을 받음
- 데이터를 다양한 방식으로 정렬하여 따로 저장
- 복제데이터를 서로 다른 방식으로 정렬해 저장. → 질의 처리시 질의 패턴에 가장 적합한 버전을 사용
Writing to column storage
- data warehouse의 대부분의 작업은 읽기전용 질의
- 압축, 정렬 모두 읽기최적화 기여. 쓰기는 어렵게 만듬.
- update-in-place는 압축컬럼에서 불가능. (하나의 작업이 모든 row에 영향을 미침)
- 그렇다면? 메모리저장소에 쓰기작업을 따로 저장한 뒤, 충분한 쓰기가 모이면 칼럼 파일에 병합하고 대량으로 새로운 파일에 기록.
Aggregation
- 보통 데이터 분석 질의에는 aggregation function이 많이 사용됨 ( count, sum, avg..)
- 자주 사용하는 count나 sum 을 캐시한다면? ⇒ materialized view (구체화 뷰)
- 원본데이터 갱신시 materialized view도 갱신됨
- data cube (OLAP cube) : materialized view의 특별 사례.
- ex. 한 축은 date, 한 축은 product인 2차원테이블
- 날짜+제품을 결합한 aggregation을 얻을 수 있음
- 날짜별 제품 판매량, 제품별 특정 날짜 판매량 등..
- 예시외에 일바적으로는 2차원 이상의 aggregation테이블을 생성.
- 미리 계산된 값을 사용하여 aggregation 조회가 매우 빠름
- 단점은 원시데이터에 질의하는것과 동일한 유연성이 없음.