백엔드/분산 시스템

[데이터 중심 애플리케이션 설계] 03장. 저장소와 검색

박지환 2023. 1. 9. 16:51
Part 1. 데이터 시스템의 기초

 

데이터베이스는 기본적으로 2가지 작업을 수행한다.

  • 데이터를 받으면 데이터를 저장
  • 데이터를 요청하면 데이터를 제공

2장에서는 애플리케이션 개발자의 관점에서 데이터베이스에 데이터를 추가하고, 질의하는 방법을 살펴보았음.

3장에서는 데이터베이스 관점에서 데이터를 저장하는 방법과 데이터를 요청 받았을 때 다시 찾을 수 있는 방법을 살펴본다.

애플리케이션 개발자는 애플리케이션에 적합한 저장소 엔진을 선택해야 하고, 특정 workload(작업 부하) 유형에서 좋은 성능을 내게끔 저장소 엔진을 조정해야 한다. 이를 위해서는 저장소 엔진의 내부 동작 방식을 대략적으로 이해하고 있어야 한다.

특히 트랜잭션 작업부하에 맞춰 최적화된 저장소 엔진과, 분석을 위해 최적화된 엔진 간에는 큰 차이가 있다.

먼저 관계형 데이터베이스와 NoSQL 데이터베이스에 사용되는 저장소 엔진을 설명한 후, log-structured(로그 구조), page-oriented(페이지 지향) 계열 저장소를 살펴본다.

데이터베이스를 강력하게 만드는 데이터 구조

가장 간단한 데이터베이스를 생각해보자

#!/bin/bash 
db_set () {
	echo "$1,$2" >> database
}

db_get () {
	grep "л$1," database | sed -e "s/A$1,//" | tail -n 1
}

이 데이터베이스는 두 개의 bash 함수로 구현된 키-값 저장소이다.

db_set(): 데이터 추가 작업은 파일의 끝에 단순히 데이터를 추가하면 되므로, 매우 간단한 작업이며 좋은 성능을 보여준다. db_set() 처럼 데이터베이스들은 내부적으로 append-only(추가 전용) 데이터 파일인 log(로그)를 사용한다.

하지만 db_get() 함수는 데이터베이스의 많은 레코드가 있으면 성능이 매우 좋지 않다. 매번 키를 찾을 때마다 전체 파일을 처음부터 끝까지 스캔해야 하며, 이 때문에 시간 복잡도가 O(n)이다 (레코드 수가 2배 늘면, 검색도 2배 오래 걸린다)

 

데이터베이스의 특정 키의 값을 효율적으로 찾기 위해 색인이라는 데이터 구조를 사용한다. 색인은 이정표 역할을 하는 부가적인 메타데이터를 유지하여 데이터의 위치를 찾는 데 도움을 준다.

색인은 추가적인 구조이기 때문에 실제 데이터의 내용에는 영향을 미치지 않지만, 질의 성능에 영향을 미친다. 단순히 쓰기 과정에서보다 오버헤드가 발생하는데, 이는 데이터를 쓸 때마다 색인도 갱신해야 하기 때문이다.

색인은 속도를 떨어트리지만, 적절한 색인은 읽기 질의 속도를 높이기 때문에 데이터베이스의 색인을 잘 선택하는 것이 저장소 시스템에 중요한 트레이드오프다. 애플리케이션의 질의 패턴에 대한 지식을 기반으로 수동으로 색인을 선택해야, 필요 이상의 오버헤드를 발생시키지 않으면서 애플리케이션에 가장 큰 이익을 안겨줄 수 있는 색인을 선택할 수 있다.

해시 색인

키-값 저장소는 보통 hash map(해시 맵), hash table(해시 테이블)로 구현한다.

앞의 예제에서 단순히 파일에 추가하는 방식으로 데이터 저장소를 구성한다고 가정했을 때, 그러면 가장 간단하게 가능한 색인 전략은 키를 데이터 파일의 바이트 오프셋에 매핑에 인메모리 해시 맵을 유지하는 전략이다.

CSV와 유사한 형식의 키-값 쌍의 로그 저장하기. 인메모리 해시 맵으로 색인했으며, 바이트 오프셋은 키의 값을 바로 찾을 수 있는 위치이다.

파일에 키-값 쌍이 추가되면 데이터의 오프셋을 반영하기 위해서 해시 맵도 갱신해야 하며, 값을 조회하려면 해시 맵을 사용해 데이터 파일에서 오프셋을 찾아 해당 위치를 구하고 값을 읽는다.

이 방식은 단순해 보이지만 실제로 많이 사용하는 접근법이다. Bitcask(비트캐스크)라는 저장소 엔진이 이 방식을 사용하고 있으며, 해시 맵을 전부 메모리에 유지하기 때문에 고성능 읽기/쓰기를 보장한다. 이러한 저장소 엔진은 각 키의 값이 자주 갱신되는 상황에 매우 적합하다. 키당 쓰기는 많지만, 메모리에 모든 키를 유지할 수 있기 때문이다.

파일에 계속 추가만 한다면 결국 디스크 공간이 부족해지는데, 이 상황은 특정 크기의 segment(세그먼트)로 로그를 나누는 방법으로 해결할 수 있다. 특정 크기에 도달하면 세그먼트 파일을 닫고 새로운 세그먼트 파일에 쓰기를 진행하는데, 이러한 방식을 사용하면 세그먼트 로그 파일들에서 중복된 키를 버리고 각 키의 최신 갱신 값만 유지하는 compaction(컴팩션)을 수행할 수 있다.

키-값 갱신 로그를 컴팩션하고(모든 고양이 동영상이 재생된 횟수를 더함) 각 키의 최신 값만을 유지

또한 컴팩션을 통해 여러 세그먼트들을 병합할 수 있다.

컴팩션과 세그먼트 병합을 동시에 수행

이러면 각 세그먼트는 키를 파일 오프셋에 매핑한 자체 인메모리 해시 테이블을 갖게 된다. 키의 값을 찾고 싶으면 최신 세그먼트에서 찾아본 후, 없으면 두 번째 세그먼트 등을 확인하면 된다.

 

이러한 방식을 실제로 구현하기 위해서는 세부적으로 많은 사항을 고려해야 한다.

  • 파일 형식: 바이트 단위의 문자열 길이를 부호화한 후 바이너리 형식을 사용하는 것이 빠르고 간단하다
  • 레코드 삭제: 키와 관견된 값을 삭제하려면 tombstone(툼스톤)을 추가하여 세그먼트가 병합될 때 병합 과정에서 삭제된 키의 이전 값을 무시되도록 해야한다.
  • 고장 복수: 데이터베이스가 재시작되면 인메모리 해시 맵을 다시 메모리에 올려야 한다. 세그먼트 파일이 크면 오래 걸리기 때문에, 비트캐스크는 스냅숏을 사용해 복구 속도를 높힌다.
  • 부분적으로 레코드 쓰기: 데이터베이스가 로그를 추가하던 중 죽을 수 있다. 비트캐스크 파일은 체크섬을 포함하고 있어 로그의 손상된 부분을 탐지해 무시할 수 있다.
  • 동시성 제어: 로그 쓰기의 일반적인 동시성 제어는 하나의 쓰기 스레드만 사용하는 것이다. 세그먼트는 추가 전용이거나 immutable(불변)이므로 다중 스레드로 동시 읽기가 가능하다.

추가 전용 설계가 좋은 설계인 이유는 다음과 같다.

  • 추가와 세그먼트 병합은 순차적인 쓰기 작업이기 때문에 보통 무작위 쓰기보다 훨씬 빠르다.
  • 세그머트 파일이 추가 전용이나 불변이면 동시성과 고장 복구가 훨씬 더 간단하다. 이전 값과 새로운 값 부분을 포함한 파일을 나누어 함께 남아있기 떄문이다.
  • 오래된 세그먼트 병합은 시간이 지남에 따라 조각화되는 데이터 파일 문제를 피할 수 있다.

해시 테이블 색인의 제한 사항은 다음과 같다.

  • 해시 테이블은 메모리에 저장해야 하므로 키가 너무 많으면 문제가 된다. 디스크에 해시 맵을 유지할 수는 있지만 무작위 접근 I/O가 많이 필요하고 디스크의 확장 비용이 비싸고 충돌 해소를 위한 성가신 로직이 필요하기 때문이다.
  • 해시 테이블은 range query(범위 질의)에 효율적이지 않다. 해시 맵은 범위 안의 모든 개별 키를 일일히 조회해야 하기 때문이다.

SS테이블과 LSM 트리

세그먼트 파일 형식에서 일련의 키-값 쌍을 키로 정렬한다면, 이는 Sorted String Table(정렬된 문자열 테이블, SS테이블)이라고 부르는 형식이 된다. 각 키는 병합된 세그먼트 파일 내에 한 번만 나타나야 한다(컴팩션이 이를 보장한다).

SS 테이블은 해시 색인을 가진 로그 세그먼트보다 몇 가지 큰 장점이 있다.

  • 세그먼트 병합은 파일이 사용 가능한 메모리보다 크더라도 간단하고 효율적이다. 이 접근법은 mergesort(병합정렬) 알고리즘에서 사용하는 방식과 유사하다.
  • 파일에서 특정 키를 찾기 위해 더는 메모리에 모든 키의 색인을 유지할 필요가 없다.
  • 읽기 요청은 요청 범위 내에서 여러 키-값 쌍을 스캔해야 하기 때문에 해당 레코드들을 블록으로 그룹화하고 디스크에 쓰기 전에 압축한다(위 그림의 음영 영역). 그러면 희소 인메모리 색인의 각 항목은 압축된 블록의 시작을 가리키게 된다. 이를 통해 디스크 공간을 절약하고, I/O 대역폭 사용도 줄일 수 있다.

SS테이블 생성과 유지

데이터를 키로 정렬하기 위해서는 디스크보다 메모리에 유지하는 편이 훨씬 쉽다. Red-Black tree나 AVL 트리와 같은 데이터 구조를 활용하여 임의 순서로 키를 삽입하고 정렬된 순서로 해당 키를 다시 읽을 수 있다.

따라서 저장소 엔진을 다음과 같이 만들 수 있다.

  • 쓰기가 들어오면 인메모리 balanced tree(균형 트리)에 추가한다. 이 인메모리 트리는 memtable(멤테이블)이라고도 한다.
  • 멤테이블의 크기가 임곗값보다 커지면 SS테이블 파일로 디스크에 기록한다. SS테이블 파일은 데이터베이스의 가장 최신 세그먼트가 된다.
  • 읽기 요청을 제공하려면 멤테이블에서 키를 찾고, 그 다음 디스크의 최신 세그먼트 순서로 찾는다.
  • 세그먼트 파일을 병합하고 컴팩션한다. 이 과정은 백그라운드에서 수행된다.

만약 데이터베이스가 고장나면 아직 디스크로 기록되지 않은 최신 쓰기가 손실되므로, 이런 문제를 피하기 위해서는 매번 쓰기를 즉시 추가할 수 있는 분리된 로그를 디스크 상에 유지해야 한다.

SS테이블에서 LSM 트리 만들기

위에 기술된 알고리즘은 LevelDB, RocksDB, Bigtable, Cassandra, HBase의 키-값 저장소 엔진 라이브러리에서 사용한다.

위의 색인 구조는 Log-Structured Merge-Tree, LSM(로그 구조화 병합 트리)란 이름으로 발표되었다. 정렬된 파일 병합과 컴팩션 원리를 기반으로 하는 저장소 엔진을 LSM 저장소 엔진이라 부른다.

Lucene(루씬)은 ElasticSearch나 Solar에서 사용하는 전문 검색 엔진이며, term dictionary(용어 사전)을 저장하기 위해 유사한 방법을 사용한다. 검색 질의로 단어가 들어오면 단어가 언급된 모든 문서를 찾는다. 이 접근법은 키를 단어로, 값은 단어를 포함한 모든 문서의 ID 목록으로 하는 키-값 구조로 구현한다. 루씬에서 용어와 포스핑 목록의 매핑은 SS테이블 같은 정렬 파일에 유지하고 필요에 따라 백그라운드에서 병합한다.

성능 최적화

세부 사항이 많을수록 저장소 엔진을 잘 동작하게 만든다.

예를 들어 LSM 트리 알고리즘은 데이터베이스에 존재하지 않는 키를 찾는 경우 느릴 수 있다(없는 키를 찾기 위해 세그먼트 끝까지 찾기 때문).

이런 종류의 접근을 최적화하기 위해 저장소 엔진은 보통 Bloom Filter(블룸 필터, 집합 내용을 approximating(근사하는) 메모리 효율적 데이터 구조)를 추가적으로 사용한다. 블룸 필터는 키가 데이터베이스에 존재하지 않음을 알려주므로 존재하지 않는 키를 위한 불필요한 디스크 읽기를 많이 절약할 수 있다.

또한 SS테이블을 압축하고 병합하는 순서와 시기를 결정하는 다양한 전략이 있다. 일반적으로 size-tiered(크기 계층, 새롭고 작은 SS테이블을 오래되고 큰 SS테이블에 병합)과 leveled compaction(레벨 컴팩션, 키 범위를 더 작은 테이블로 나누고 오래된 데이터를 개별 레벨로 이동시킴)이 있다.

LSM의 기본 개념인 백그라운드에서 연쇄적으로 SS테이블을 지속적으로 병합하는 개념은 간단하고 효과적이다. 이 개념은 데이터셋이 메모리보다 훨씬 더 크더라도 효과적이며, 데이터가 정렬되어 있으므로 범위 질의를 효율적으로 실행할 수 있다. 그리고 디스크 쓰기 또한 순차적이기 때문에 매우 높은 쓰기 처리량을 보장할 수 있다.

B트리

가장 일반적인 색인 유형은 B-tree(B 트리)이다. 1970년에 등장했으며 대부분의 관계형 데이터베이스와 비관계형 데이터베이스에서 사용한다.

B 트리가 LSM와 비슷한 점은 SS테이블과 같이 키로 정렬된 키-값 쌍을 유지하기 때문에 키-값 검색과 범위 질의에 효율적이라는 점 밖에 없다. B 트리는 설계 철학이 매우 다르다.

LSM은 데이터베이스를 가변 크기를 가진 세그먼트로 나눈데 반해, B 트리는 4KB+ 크기의 고정 크기 블록이나 페이지로 나누고 한 번에 하나의 페이지에 대해 읽기/쓰기를 한다. 이러한 설계는 근본적으로 하드웨어와 더 밀접하다.

B 트리 색인을 이용한 키 검색

각 페이지는 주소나 위치를 이용해 식별할 수 있다(메모리의 포인터와 같이, 디스크의 포인터라는 느낌). 이 페이지 참조 구조는 위의 그림과 같은 페이지 트리를 구성하는데 사용한다.

한 페이지는 B 트리의 root(루트)로 지정되고, 키를 찾기 위해서는 루트에서 시작한다. 페이지는 여러 키와 하위 페이지의 참조를 포함한다. 각 하위 페이지는 키가 계속 이어지는 범위를 담당하고 참조 사이의 키를 해당 범위 경계가 어디인지 나타낸다. 그리고 최종적으로 leaf page(리프 페이지)에 도달하여 개별 키를 찾는다.

B 트리의 한 페이지에서 하위 페이지를 참조하는 수를 branching factor(분기 계수)라 부르고, 위의 그림에서는 6이다. 실제 분기 계수는 페이지 참조와 범위 경계를 저장할 공간에 의존적인데, 보통 수백개에 달한다.

B 트리에[ 존재하는 키의 값을 갱신하려면 키를 포함하고 있는 리프 페이지를 검색하고, 페이지의 값을 바꾼 다음 페이지를 디스크에 다시 기록한다.

페이지 분리로 커진 B 트리

새로운 키를 추가하려면 새로운 키를 포함하는 범위의 페이지를 찾아 해당 페이지에 키와 값을 추가하는데, 만약 여유 공간이 없다면 위의 그림처럼 페이지 하나를 반쯤 채워진 페이지 둘로 나누고 상위 페이지가 새로운 키 범위의 하위 부분들을 알 수 있게 갱신한다.

B 트리 알고리즘은 균형을 유지하는 balanced tree이기 때문에, n개의 키를 가진 B 트리의 깊이는 항상 O(logn)인다. 대부분의 데이터베이스는 B 트리의 깊이가 3이나 4단계 정도면 충분하므로 검색하는 페이지를 찾기 위해 많은 페이지 참조를 따라가지 않아도 된다(분기 계수 500의 4KB 페이지의 4 단계 트리는 256TB까지 저장할 수 있다).

신뢰할 수 있는 B 트리 만들기

B 트리의 기본적인 쓰기 동작은 새로운 데이터를 디스크 페이지에 덮어씌운다. 이 동작은 페이지의 위치는 변경하지 않는다고 가정하며, 따라서 가리키는 모든 참조는 온전하게 남는다. 이는 LSM와는 대조적이다. 또한 디스크의 페이지를 덮어쓰는 일은 실제 하드웨어 동작이라고 생각할 수 있다.

일부 동작은 다양한 페이지의 덮어쓰기를 필요로 하는데(삽입을 위한 페이지 분할 등), 이 과정에서 데이터베이스가 고장 난다면 orphan page(고아 페이지)가 발생하고 색인이 훼손되기 때문에 매우 위험하다. 이를 해결하기 위해 일반적으로 디스크 상에 write-ahead log, WAL(쓰기 전 로그)라는 B 트리의 모든 변경 사항을 기록하는 추가 전용 데이터 구조를 추가한다. 이 로그는 데이터베이스가 복구될 때 일관성 있는 상태로 B 트리를 다시 복구하는데 사용된다.

다중 스레드가 동시에 B 트리에 접근한다면 주의 깊게 동시성 제어를 해야 한다. 이는 보통 latch(래치, 가벼운 잠금)으로 트리의 데이터 구조를 보호한다. 이 상황에서 LSM은 백그라운드에서 원자적으로 새로운 세그먼트를 이전 세그먼트로 바꾸면 되기 때문에 훨씬 간단하다.

B 트리 최적화

최적화 기법 몇 가지를 언급하자면 다음과 같다.

  • 페이지 덮어 쓰기와 WAL 유지 대신, 일부 데이터베이스는 copy-on-write scheme(쓰기 시 복사 방식)을 사용한다. 이는 변경된 페이지를 다른 위치에 기록하고 트리에 상위 페이지의 새로운 버전을 ㅁ나들어 새로운 위치를 가리키게 하는 것이다. 이 접근 방식은 동시성 제어에도 유용하다.
  • 페이지 키를 축약해 쓰면 공간을 절약할 수 있다. 키는 키 범위 사이의 경계 역할만 해주면 되기 때문이다. 페이지 하나에 키를 더 많이 채우면 트리는 더 높은 분기 계수를 얻고, 그러면 트리 깊이 수준을 낮출 수 있다.
  • 일반적으로 페이지는 디스크 상의 어디에나 위치할 수 있지만, 키 범위를 스캔해야 할 경웬느 모든 페이지에 대해 디스크 탐색을 수행해야 하기 떄문에 페이지 단위 배치는 비효율적이다. 따라서 많은 B 트리 구현에서 리프 페이지를 디스크 상에 연속된 순서로 배치하려고 노력한다. 하지만 트리가 커지면 순서를 유지하기 어렵다. 이와 반대로 LSM은 병합하는 과정에서 세그먼트를 한 번에 다시 쓰기 때문에 디스크에서 연속된 키를 서로 가깝게 유지하기가 더 쉽다.
  • 트리에 포인터를 추가한다. 예를 들어 리프 페이지가 양쪽 형제 페이지에 대한 참조를 가지면 상위 페이지로 다시 이동하지 않아도 순서대로 키를 스캔할 수 있다.
  • fractal tree(프랙탈 트리, B+ 트리)라는 B 트리의 변형은 디스크 찾기를 줄이기 위해 LSM의 개념을 일부 빌렸다.

B 트리와 LSM 트리 비교

경험적으로 LSM 트리는 보통 쓰기에서 더 빠른 반면 B 트리는 읽기에서 더 빠르다. LSM에서는 여러 데이터 구조와 SS테이블을 확인해야 하기 때문에 읽기가 더 느리다.

하지만 벤치마크는 보통 작업부하의 세부 사항에 민감하기 떄문에, 비교가 유효하려면 실제 필요한 작업부하로 시스템을 테스트해야 한다.

LSM 트리의 장점

B 트리 색인은 모든 데이터 조작은 최소한 두 번(쓰기 전 로그 + 트리 페이지) 기록해야 한다. 페이지의 일부만 바뀌어도 전체 페이지를 기록해야 하는 오버헤드도 있다.

LSM 또한 SS테이블의 반복된 컴팩션과 병합으로 인해 여러 번 데이터를 다시 쓴다. 데이터베잇스에 쓰기 한 번이 데이터베이스 수명 동안 디스크에 여러 번의 쓰기를 야기하는 이런 효과를 write amplification(쓰기 증폭)이라고 한다. SSD의 경우 블록 덮어쓰기 횟수가 제한되기 떄문에 SSD의 경우 쓰기 증폭은 특별한 관심사다.

쓰기가 많은 애플리케이션에서 성능 병목은 데이터베이스가 디스크에 쓰는 속도일 수 있다. 이 경우 쓰기 증폭이 바로 성능 비용이며, 저장소 엔진이 디스크에 기록할수록 디스크 대역폭 내 처리할 수 있는 초당 쓰기는 점점 줄어든다.

LSM 트리는 보통 B 트리보다 쓰기 처리량을 높기 유지할 수 있다. LSM 트리가 상대적으로 쓰기 증폭이 더 낮고 순차적으로 컴팩션된 SS테이블 파일을 쓰기 떄문이다. 이는 자기 하드드라이브에서 특히 중요한데, 이는 자기 하드드라이브가 순차 쓰기가 임의 쓰기보다 훨씬 더 빠르기 때문이다.

LSM 트리는 압축률이 더 좋다. 그래서 보통 B 트리보다 디스크에 더 적은 파일을 생성한다. B 트리 저장소 엔진은 파편화로 인해 사용하지 않는 디스크 공간 일부가 남는데 반해, LSM 트리는 주기적으로 파편화를 없애기 위해 SS테이블을 다시 기록하기 떄문에 저장소 오버헤드가 더 낮다.

대다수의 SSD 펌웨어는 순차 쓰기로 전환 시 내부적으로 LSM을 사용하므로 저장소 엔진의 쓰기 패턴이 SSD에 미치는 영향이 분명하지 않지만, LSM의 낮은 쓰기 증폭과 파편화 감소는 SSD의 경우 훨씬 유리하다. 밀집된 데이터 표현은 가능한 I/O 대역폭 내에서 더 많은 읽기와 쓰기 요청이 가능하다.

LSM 트리의 단점

LSM 저장소의 단점은 컴팩션 과정이 진행 중인 읽기/쓰기 성능에 영향을 준다는 점이다. 저장소 엔진이 컴팩션을 점진적으로 동시성 영향 없이 수행하려 하기 떄문에, 디스크에서의 비싼 컴팩션 연산이 끝날 때까지 요청을 대기해야 하는 상황이 발생할 수 있다. LSM 저장소 엔진의 상위 백분위 질의의 응답 시간은 꽤 길다. 반면 B 트리의 성능은 LSM 저장소 엔진 보다 예측하기 쉽다.

또다른 문제는 컴팩션의 높은 쓰기 처리량이다. 디스크의 쓰기 대역폭이 유연한데 반해 컴팩션은 더 데이터베이스가 점점 커질수록 더 많은 대역폭이 필요해 한다.

또한 쓰기 처리량이 높음에도 컴팩션 설정을 주의 깊게 싸지 않으면 컴팩션이 유입 쓰기 속도를 따라갈 수 없어 병합되지 않은 세그먼트 수가 증가한다. 이는 읽기 또한 느려지게 만든다. 보통 SS테이블 기반 저장소 엔진은 컴팩션이 유입 속도를 따라가지 못해도 유입 쓰기의 속도를 조절하지 않으므로 이런 상황을 감지하지 위한 명시적 모니터링이 필요하다.

LSM 저장소 엔진은 다른 세그먼트에 같은 키의 복사본이 존재할 수 있는 반면, B 트리는 키가 색인의 한 곳에만 정확하게 존재한다. 따라서 강력한 트랜잭션 시맨틱을 제공하는 제공하는 데이터베이스에는 B 트리가 훨씬 매력적이다.

B 트리는 데이터베이스 아키텍처에 깊이 뿌리내렸고 많으 작업부하에 대해 지속적으로 좋은 성능을 제공하므로 계속 사용될 것이다. 새로운 데이터 저장소에서는 LSM이 인기를 얻고 있다. 사용 사례에 맞는 것을 찾기 위해서는 테스트를 통해 경험적으로 결정하는 방법도 나쁘지 않다.

기타 색인 구조

키-값 색인의 대표적인 예는 관계형 모델의 primary key(기본 키) 색인이다. 기본키로 관계형 테이블에서 하나의 로우를, 문서 데이터베이스에서 하나의 문서를, 그래프 데이터베이스에서 하나의 정점을 고유하게 식별할 수 있다.

또한 secondary index(보조 색인)을 사용하는 방식도 매우 일반적이다. 관계형 데이터베이스에서는 CREATE INDEX 명령으로 보조 색인을 만들 수 있으며, 보통 효율적으로 조인을 수행하는 데 결정적인 역할을 한다.

보조 색인은 기본키 색인과는 다르게 키가 고유하지 않으므로, 로우 식별자 목록을 만들거나 로우 식별자를 추가해 각 키를 고유하게 만드는 방법이 있다.

색인 안에 값 저장하기

색인에서 값은 질의의 실제 로우이거나, 참조이다. 참조의 경우 로우가 저장된 곳을 heap file(힙 파일)이라 하고 특정 순서 없이 데이터를 저장한다. 힙 파일 접근은 여러 보조 색인이 존재할 때 데이터 중복을 피할 수 있다. 각 색인은 힙 파일에서 위치만 참조하고 실제 데이터는 일정한 곳에 유지한다.

힙 파일 접근 방식은 키를 변경하지 않고 값을 갱신할 때 효율적이다. 새로운 값이 이전 값보다 공간을 많이 필요로 하지 않는다면 덮어씌울 수 있고, 많은 공간을 필요로 한다면 새롱누 곳으로 위치를 이동시킨 후 모든 색인이 새로운 힙 위치를 가리키게끔 갱신 혹은 이전 힙 위치에 전방향 포인터를 남겨야 한다.

색인에서 힙 파일로 다시 이동하는 일은 읽기 성능에 불이익이 너무 많기 때문에 어떤 상황에서는 색인 안에 바로 색인된 로우를 저장하는 편이 바람직하다. 이를 clustered index(클러스터드 색인)이라고 한다. MySQL의 InnoDB 저장소 엔진의 테이블 기본키가 클러스터드 색인이다.

클러스터등 색인과 비클러스터드 색인의 절충안을 covering index(커버링 색인), index with included column(포괄열이 있는 색인)이라 하는데, 이는 색인 안에 칼럼 일부를 저장하여 일부 질의에 응답이 가능하게 만든다.

모든 종류의 데이터 복제와 마찬가지로 클러스터드 색인과 커버링 색인은 읽기 성능을 높일 수 있지만 추가적인 저장소가 필요하고 쓰기 과정에 오버헤드가 발생하며, 애플리케이션 단에서 복제로 인한 불일치를 파악 할 수 없기 떄문에 데이터베이스는 트랜잭션 보장을 강화하기 위해 별도의 노력이 필요하다.

다중 칼럼 색인

색인은 하나의 키만 값에 대응하기 때문에 테이블의 multi-column(다중 칼럼) 또는 문서의 다중 필드에 동시에 질의하는 경우 충분하지 않다.

다중 칼럼 색인의 가장 일반적인 유형은 concatenated index(결합 색인)이다. 결합 색인은 하나의 키에 여러 필드를 단순히 추가하는 방식이다. (전화번호부의 (성, 이름) 키를 통한 전화번호 값 찾기)

다차원 색인은 다중 칼럼 질의의 더 일반적인 방법이다. 특히 지리 공간 데이터에서 중요하게 사용된다. 예를 들어 위도, 경도를 포함한 이차원 질의를 할 경우 다음과 같은 질의를 해야 한다.

SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
													AND longitude > -0.1162 AND longitude < -0.1004;

B 트리나 LSM 트리는 이런 유형의 질의에 효율적으로 응답할 수 없다.

한 가지 방법은 이차원 위치를 space-filling curve(공간 채움 곡선)을 이용해 단일 숫자로 변환한 다음 일반 B 트리 색인을 이용하는 방법이 있으며, 또 다른 일반적인 방법은 R 트리처럼 specialized spatial index(전문 공간 색인)을 사용하는 것이다.

다차원 색인은 지리학적인 위치에만 사용되지 않는다. RGB 색상을 위해 3차원 색인을 사용한다던가 관측을 위한 날짜, 기온의 2차원 색인을 사용할 수도 있다. 이러한 색인들은 동시에 범위를 줄일 수 있어 효율적이다.

전문 검색과 퍼지 색인

철자가 틀린 단어와 같은 유사한 키에 대한 검색, 즉 fuzzy(애매모호)한 질의에는 위의 색인 기법과 다른 기법이 필요하다.

예를 들어 전문 검색 엔진은 특정 단어를 검색할 때 동의어나 인접한 단어, 언어학적 분석으로 질의를 확장한다. 루씬의 경우 오타에 대처하기 위해 edit distance(편집 거리, 오타의 양) 내 단어를 검색할 수 있다.

루씬은 용어 사전을 위해 SS테이블 같은 구조를 사용하고, 이 구조를 위해 인메모리 색인을 사용하는데 이 색인은 여러 키 내 문자에 대한 finite state automation(유한 상태 오토마톤)으로 trie(트라이)와 유사하다. 또한 이 오토마톤은 levenshtein automation(레벤슈타인 오토마톤, 특정 편집 거리에서 효율적인 단어 검색 제공)으로 변환할 수 있다.

모든 것을 메모리에 보관

지금까지의 모든 데이터 구조는 모두 디스크 한계에 대한 해결책이였다. 디스크의 읽기/쓰기 성능을 위해서는 주의해서 데이터를 디스크에 넣어야 한다.

이러한 단점에도 디스크를 사용하는 이유는 지속성(비휘발성), 그리고 가격 때문이다.

최근에는 램이 점점 저렴해졌고, 데이터셋 대부분이 그다지 크지 않기 떄문에 메모리에 전체를 보관하는 인메모리 데이터베이스가 개발됐다.

보통 인메모리 데이터베이스는 지속성을 목표로 하므로, 배터리 전원 공급 RAM 같은 특수 하드웨어를 사용하거나 디스크에 로그/스냅숏을 남겨 상태를 복제하는 방식을 사용한다.

VoltDB, MemSQL, Oracle TimesTen은 관계형 모델의 인메모리 데이터베이스이고, RAMCloud는 지속성 있는 오픈소스 인메모리 키-값 저장소이며, Redis와 Couchbase는 비동기로 디스크에 기록하기 때문에 약한 지속성을 제공한다.

인메모리 데이터베이스의 성능 장점은 디스크에서 읽지 않아도 된다는 것 외에도, 디스크에 기록하지 위한 형태로 부호화하는 오버헤드를 피할 수 있어 빠르다는 장점을 가지고 있다.

또한 인메모리 데이터베이스는 디스크 기반 색인으로 구현하기 어려운 데이터 모델을 제공한다. 예를 들어 레디스는 우선순위 큐와 set과 같은 다양한 데이터 구조를 인터페이스로 제공한다. 이러한 것들은 메모리에 모든 데이터를 유지하기 떄문에 구현이 간단하다.

또한 최근에는 인메모리 데이터베이스 아키텍처는 anti-caching(안티 캐싱, 메모리가 충분하지 않을 때 가장 최근에 사용하지 않은 데이터를 디스크로 내보내고 나중에 필요하는 적재하는 스왑 방식)을 통해 디스크 중심 아키텍처보다 더 큰 데이터셋을 지원하게끔 확장할 수 있다. 안티 캐싱을 통한 데이터베이스는 전체 메모리 페이지보다 개별 레코드 단위로 작업할 수 있기 때문에 OS 가상 메모리와 스왑보다 더 효율적으로 메모리를 관리할 수 있다.

나아가 non-volatile memory(비휘발성 메모리) 기술이 발전면 저장소 엔진 설계의 변경이 필요할 것이다.

트랜잭션 처리나 분석?

초창기 비즈니스 데이터 처리는 데이터베이스 쓰기가 판매/발주/지불 등의 commercial transaction(커머셜 트랜잭션, 상거래)에 해당했다. 지금은 금전 거래가 아닌 영역으로 데이터베이스가 확장되었지만 트랜잭션이라는 용어는 변하지 않고 논리 단위 형태로서 읽기와 쓰기 그룹을 나타내고 있다.

데이터베이스가 다양한 종류의 데이터를 사용하기 시작했지만 기본적인 접근 패턴은 비즈니스 트랜잭션 처리와 유사하다. 레코드가 사용자 입력을 기반으로 삽입/갱신된다. 이러한 대화식 애플리케이션의 접근 패턴을 online transaction processing, OLTP(온라인 트랜잭션 처리)라고 한다.

그러나 데이터에비스를 data analytic(데이터 분석)에도 많이 사용하기 시작했다. 분석 질의는 많은 수의 레코드를 스캔해 집계/통계를 계산한다. 이런 질의는 경영 의사결정을 위한 보고서인 business intelligence(비즈니스 인텔리전스)가 된다. 이러한 사용 패턴을 online analytric processing, OLAP(온라인 분석 처리)라고 한다.

아래는 OLTP와 OLAP 차이점을 나타낸 표이다.

특성
트랜잭션 처리 시스템 (OLTP)
분석 시스템 (OLAP)
주요 읽기 패턴
질의당 적은 수의 레코드, 키 기준으로 가져온
많은 레코드에 대한 집계
주요 쓰기 패턴
임의 접근, 사용자 입력을 낮은 지연 시간으로 기록
대규모 불러오기 (Bulk import ,ETL) 또는 이벤트 스트림
주요 사용처
웹 애플리케이션을 통한 최종 사용자/소비자
의사결정 지원을 위한 내부 분석가
데이터 표현
데이터의 최신 상태(현재 시점)
시간이 지나며 일어난 이벤트 이력
데이터셋 크기
기가바이트에서 테라바이트
테라바이트에서 페타바이트

처음에는 OLTP와 OLAP를 위해 동일한 데이터베이스를 사용하였지만, 1980년대 후반 회사들은 이후 개별 데이터베이스에서 분석을 수행하는 경향을 보였다. 이 개별 데이터베이스를 data warehouse(데이터 웨어하우스)라고 불렀다.

데이터 웨어하우징

대개 기업 수십 가지의 트랜잭션 처리 시스템을 갖추고 있다. 이런 시스템들은 복잡해서 보통 서로 독자적으로 운영된다.

OLTP 시스템은 사업 운영에 대단히 중요하기 때문에 일반적으로 높은 가용성과 낮은 지연 시간의 트랜잭션 처리를 기대한다. 따라서 OLTP 데이터베이스에 질의 비용이 비싼 ad-hoc analytic query(즉석 분석 질의)를 실행하는 것을 꺼려한다. 이러한 질의는 데이터셋의 많은 부분을 스캔해 이와 동시에 실행되는 트랜잭션의 성능을 저하시킬 가능성이 있다.

반대로 데이터 웨어하우스는 다양한 OLTP 시스템에 있는 데이터의 읽기 복사본으로서, 분석가들이 OLTP 작업에 영향을 주지 않고 마음껏 질의할 수 있는 개별 데이터베이스다.

데이터 웨어하우스에 대한 ETL의 간략한 개요

데이터는 OLTP 데이터베이스에서 주기적인 데이터 덤프나 지속적인 갱선 스트림을 사용해 추출(extract)하고 분석 친화적인 스키마로 변환(transform)하고 깨끗하게 정리한 다음 데이터 웨어하우스에 적재(load)한다. 이 과정을 ETL(Extract-Transform-Load)라고 한다.

거의 모든 대기업은 데이터 웨어하우스가 있지만 소규모 기업은 그렇지 않다. 소규모 기업은 비교적 적은 양의 데이터를 가지고 있기 때문이다. 소규모 기업에서 간단한 일(작은 데이터 분석)이 대기업에서는 (많은 데이터 분석을 위해)많은 노력을 필요로 한다.

개별 데이터 웨어하우스를 사용하는 큰 장점은 분석 접근 패턴에 맞게 최적화할 수 있다는 점이다.

OLTP 데이터베이스와 데이터 웨어하우스의 차이점

SQL은 일반적으로 분석 질의에 적합하기 떄문에 데이터 웨어하우스의 데이터 모델은 가장 일반적인 관계형 모델을 사용한다. SQL 질의를 생성하고 결과를 시각화하고 분석가가 drill-down(드릴 다운), slicing(슬라이싱), dicing(다이싱) 같은 작업을 통해 데이터를 탐색할 수 있게 해주는 여러 그래픽 데이터 분석 도구가 있다.

데이터 웨어하우스와 관계형 OLTP 데이터베이스는 둘 다 SQL 질의 인터페이스를 지원하기 떄문에 비슷해 보이지만, 각각은 매우 다른 질의 패턴에 맞게 최적화됐기 떄문에 시스템의 내부가 완전히 다르다.

따라서 다수의 데이터베이스 벤더는 트랝잭션 처리와 작업부하 양쪽 모두 지원하기보다는 둘 중 하나를 지원하는 데 중점을 둔다.

Teradata, Vertica, SAP HANA, ParAccel 같은 데이터 웨어하우스가 있으며, Apache Hive, Spark SQL, Cloudera Impala, Facebook Presto, Apache Tajo, Apache Drill 등의 SQL-on-Hadoop 오픈소스 프로젝트가 생겨났다.

분석용 스키마: 별 모양 스키마와 눈꽃송이 모양 스키마

OLTP의 트랜잭션 처리 영역에서는 애플리케이션의 필요에 따라 데이터 모델이 광범위하고 다양하게 사용된다. 반면 분석에서는 데이터 모델은 star schema(별 모양 스키마, dimensional modeling라고도 부름)로 알려진 정형화된 방식을 사용한다.

데이터 웨어하우스에서 사용하는 별 모양 스키마 예제

위의 스키마는 식료품 소매업에서 볼 수 있는 데이터 웨어하우스를 나타낸다. 스키마 중심에 fact table(사실 테이블)이 있다. 이 테이블의 로우는 특정 시각에 발생한 이벤트에 해당한다(고객의 제품 구매, fact table은 분석의 유연성을 극대화하기 위해 보통 개별 이벤트를 담는다).

fact table의 다른 칼럼은 dimension table(차원 테이블)이라 부르는 다른 테이블을 가리키는 외래 키 참조로써, fact table의 각 로우는 이벤트를 나타내고 차원은 이벤트의 속성인 who(누가), when(언제), where(어디서), what(무엇을), how(어떻게), why(왜)를 나타낸다.

고객이 여러 다양한 제품을 동시에 구매하면 fact table에는 개별 로우로 표시되며, 날짜와 시간도 추가적인 정보를 얻기 위해 차원 테이블을 사용해 표현한다.

“별 모양 스키마”란 이름은 테이블 관계가 시각화될 때 fact table이 가운데 있고 차원 테이블로 둘러싸고 있다는 사실에서 비롯됐다.

이 템플릿에서 차원을 하위 차원으로 더 세분화(정규화)한 변형을 snowflake schema(눈꽃송이 모양 스키마)라고 하며 세분화된 정보를 왜래 키로 참조할 수 있게 한다.

일반적인 데이터 웨어하우스에서 테이블의 폭은 매우 넓다(수백개 이상). 차원 테이블 또한 모든 메타데이터를 포함하므로 폭이 매우 넓다.

칼럼 지향 저장소

fact table에는 엄청난 개수의 로우와 페타바이트 데이터가 있으며, 칼럼이 보통 100개 이상이지만, 일반적인 데이터 웨어하우스 질의는 한 번에 4~5개 칼럼에만 접근한다.

아래와 같은 질의를 효율적으로 실행하기 위해서는 어떻게 해야 할까?

SELECT 
  dim_date.weekday, 
  dim_product.category, 
  SUM(fact_sales.quantity) AS quantity_sold 
FROM 
  fact_sales 
  JOIN dim_date ON fact_sales.date_key = dim_date.date_key 
  JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk 
WHERE 
  dim_date.year = 2013 
  AND dim_product.category IN ('Fresh fruit', 'Candy') 
GROUP BY 
  dim_date.weekday, 
  dim_product.category;

OLTP 데이터베이스에서는 저장소는 로우 지향적으로 데이터가 배치된다. 즉 테이블의 한 로우의 모든 값은 인접하게 저장된다. 하지만 로우 지향적인 데이터베이스가 위의 질의를 처리하기 위해서는 모든 로우를 메모리로 적재한 다음 구문을 해석해 필요하지 않은 로우를 필터링해야 한다.

칼럼 지향 저장소는 각 칼럼 별로 모든 값을 함께 저장하는 방식이다. 따라서 질의에 사용되는 칼럼만 읽고 구문 분석하면 된다.

관계형 데이터를 로우 단위가 아닌 칼럼 단위로 저장

칼럼 압축

데이터를 압축하면 디스크 처리량을 더 줄일 수 있다. 칼럼 지향 저장소는 대개 압축에 적합한데, 칼럼의 데이터에 따라 다양한 압축 기법을 사용할 수 있다. 그 중 한 가지 기법은 데이터 웨어하우스에서 특히 효과적인 bitmap encoding(비트맥 부호화)이다.

압축된 단일 칼럼의 비트맵 색인 저장소

보통 칼럼에서 고유 값의 수는 로우 수에 비해 적다(소매업의 경우 수십억 개의 판매 거래에서 고유 제품은 단지 100,000개만 있음). n개의 고유 값을 가진 칼럼을 가져와 n의 개별 비트맵으로 변환할 수 있다. 고유 값 하나가 하나의 비트맵이기 때문에, 각 로우는 비트맵 배열에서 해당 고유값을 가지면 1, 가지고 있지 않으면 0으로 표시할 수 있다.

여기서 n이 클 경우 비트맵은 0이 더 많기 때문에(sparse, 희소한 상황이라고 함), 비트맵을 추가적으로 run-length encoding하여 칼럼의 부호화를 현저히 줄일 수 있다.

이런 비트맵 색인 데이터 웨어하우스에서 일반적으로 사용하는 질의 종류에 매우 적합하다.

WHERE product_sk IN (30, 68, 69) : 고유 값의 대한 비트맵 3개를 적재하고 비트맵들을 비트 OR를 개산한다. 이 계산은 매우 효율적으로 수행할 수 있다.

WHERE product_sk = 31 AND store_sk = 3 : 고유 값의 비트맵을 적재하고 비트맵의 비트 AND를 계산한다. 이 계산은 각 칼럼에 동일한 순서로 로우가 포함되어 있기 때문에 가능하다.

메모리 대역폭과 벡터화 처리

데이터 웨어하우스 질의는 디스크로부터 메모리로 데이터를 가져오는 대역폭이 큰 병목이지만, 메민 메모리에서 CPU 캐시로 가는 대역폭을 최적화하고, CPU 명령 처리 파이프라인에서의 branch mispredicition과 bubble을 회피하며 single-instruction-multi-data, SIMB 명령을 사용하게끔 최적화해야 한다.

칼럼 저장소 배치는 데이터 양을 줄이는 장점 외에도 CPU 주기를 효율적으로 사용하기에도 적합하다. 질의 엔진이 압축된 질의 데이터를 CPU의 L1 캐시에 넣고 작업을 tight loop에서 반복한다. 칼럼 압축을 사용하면 L1 캐시에 칼럼의 더 많은 로우를 저장할 수 있고, 비트 AND/OR 등의 연산으로 데이터 덩어리를 바로 연산할 수 있다. 이런 기법을 vectorized processing(벡터화 처리)라고 한다.

칼럼 저장소의 순서 정렬

칼럼 저장소에서도 SS테이블처럼 순서를 도입해 색인 메커니즘으로 사용할 수 있다. 칼럼별로 저장됐을지라도 데이터는 한 번에 전체 로우를 정렬해야 한다. 이 때 데이터베이스 관리자가 공통 질의에 대한 insight를 바탕으로 정렬해야 하는 칼럼을 선택할 수 있다. 이후 질의 최적화기가 훨씬 빠른 속도로 로우를 스캔할 수 있을 것이다.

첫 번쨰 정렬 이후에 해당 칼럼에서 같은 값을 가지는 로우들의 정렬 순서를 두 번쨰 칼럼에서 정할 수 있는 추가적인 조치가 가능하다.

정렬된 순서의 또 다른 장점으로는 칼럼 압축이 있다. 정렬된 칼럼들은 같이 값이 연속되서 길게 반복되는데, 이를 간단한 run-length encoding으로도 수십억 개의 로우를 수 킬로바이트으로 압축할 수 있다. 이러한 압축 효과는 첫 번째 정렬 키에서 가장 강력하고, 이후 효과가 떨어진다.

다양한 순서 정렬

데이터베이스에서 데이터를 잃지 않으려면 데이터를 여러 장비에 복제해 두는 작업이 필요한데, 이 때 복제 데이터를 서로 다른 방식으로 정렬해서 저장하면 질의를 처리할 때 질의 패턴에 갖아 적합한 버전을 찾을 수 있다.

칼럼 지향 저장소에 쓰기

데이터 웨어하우스의 칼럼 지향 저장소, 압축, 정렬은 모두 읽기 질의를 더 빠르게 하지만 쓰기를 어렵게 한다는 단점이 있다.

이러한 단점은 LSM 트리라는 해결책으로 해결 가능하다. 모든 쓰기는 먼저 인메모리 저장소로 이동해 정렬된 구조에 추가하고 디스크에 쓸 준비를 한 후, 충분히 쓰기를 모으면 디스크의 칼럼 파일에 병합하고 대량으로 새로운 파일에 기록한다. 이 때 질의는 디스크의 칼럼 데이터와 메모리의 최근 쓰기를 모두 조사해 두 가지를 결합해야 한다.

집계: 데이터 큐브와 구체화 뷰

데이터 웨어하우스의 다른 측면으로는 materialized aggregate(구체화 집계)가 있다. 데이터 하우스 질의는 보통 SQL의 COUNT, SUM, AVG, MIN, MAX 같은 집계 함수들을 포함하는데, 동일한 집계를 많은 다양한 질의에서 사용한다면 매번 원시 데이터를 처리하는 것은 낭비다. 이러한 집계 함수 결과를 캐시하는 것이 효율적이다.

이런 캐시를 만드는 한 가지 방법은 materialized view(구체화 뷰)이며 관계형 데이터 모델에서는 대개 이런 캐시를 view로 정의한다. 데이터가 변경되면 구체화 뷰도 갱신해야 하고, 이러한 갱신의 쓰기 비용은 비싸기 때문에 OLTP 데이터베이스는 구체화 뷰를 자주 사용하지 않는다. 하지만 데이터 웨어하우스는 읽기 비중이 크기 때문에 구체화 뷰를 사용하는 전략은 합리적이다.

data cube(데이터 큐브) 또는 OLAP 큐브라고 알려진 구체화 뷰는 일반화된 구체화 뷰의 특별 사례이다.

합으로 데이터를 집계한 2차원 데이터 큐브

예를 들어 위의 2차원 테이블의 각 셀은 두 축(외래 키, 위에서는 date와 product)를 결합한 모든 fact의 속성의 집계 값(위에서는 sum)이다. 일반적으로 fact는 대개 2차원 이상이지만 원리는 동일하다.

구체화 데이터 큐브의 장점은 특정 질의를 효과적으로 미리 계산했기 때문에 해당 질의를 수행할 때 매우 빠르다. 백만개의 로우를 스캔할 필요 없이 적절한 차원을 따라 합계를 살펴보면 된다.

데이터 큐브의 단점은 원시 데이터에 질의하는 것과 동일한 유연성이 없다는 점이다. 해당 차원이 계산되지 않았다면 사용할 수 없다. 따라서 대부분의 데이터 웨어하우스는 가능한 많은 원시 데이터를 유지하고, 데이터 큐브와 같은 집계 값은 특정 질의에 대한 성능 향상에만 사용한다.

정리

이번 장에서는 데이터베이스가 어떻게 저장과 검색(질의)을 다루는지 근본적인 내용을 알아보았다.

 

고수준에서 저장소 엔진은 트랜잭션 처리 최적화(OLTP)와 분석 최적화(OLAP)라는 큰 두 가지 범주로 나뉜다. OLTP와 OLAP의 사용 사례에서 보면 접근 패턴 간 큰 차이가 있다.

  • OLTP 시스템은 보통 사용자 대면이기 떄문에 대량의 요청을 받을 수 있다. 부하를 처리하기 위해 보통 애플리케이션이 각 질의마다 작은 수의 레코드만 다룬다. 애플리케이션은 키의 일부만 사용하는 레코드를 요청하고 저장소 엔진은 요청한 키의 데이터를 찾기 위해 색인을 사용한다. 이 경우는 대개 디스크 탐색이 병목이다.
  • OLAP 시스템(데이터 웨어하우스와 같은 분석 시스템)은 최종 사용자가 아닌 비즈니스 분석가 주로 사용하기 떄문에 덜 알려져 있다. OLTP 시스템보다 훨씬 더 적은 수의 질의를 다루지만 각 질의는 대개 매우 다루기 어렵고 짧은 시간에 수백만 개의 레코드를 스캔해야 한다. 이 경우는 일반적으로 디스크 대역폭(디스크 탐색이 아닌)이 병목이다. 칼럼 지향 저장소는 이런 종류의 작업부하를 저리할 때 사용 가능한 날로 인기가 높아지고 있는 솔루션이다.

OLTP 측면에서 두 가지 주요한 관점을 살펴봤다.

  • 로그 구조화 관점에서 파일에 추가와 오래된 파일의 삭제만 허용하고 한 번 쓰여진 파일은 절대 갱신하지 않는다. 로그 구조화 저장소 엔진의 핵심 아이디어는 임의 접근 쓰기를 체계적으로 디스크에 순차 쓰기로 바꾼 것이다. 하드 드라이브와 ssd의 성능 특성에 맞춰 쓰기 처리량을 높이는 것이 가능하다. Bitcesk, SS Table, LevelDB, Cassandra, HBase, Lucene 등이 이 그룹에 속한다.
  • 제자리 갱신 관점에서 넢어쓰기 할 수 있는 고정 크기 페이지의 셋으로 디스크를 다룬다. 이 관점에서 가장 큰 예가 B 트리다. B 트리는 모든 관계형 데이터베이스와 많은 비정형 데이터베이스에서도 사용한다.

OLTP에서 복잡한 색인 구조와 모든 데이터를 메모리에 유지하기 위해 최적화된 데이터베이스에 대해 간략히 살펴봤다.

OLAP에서 일반적인 데이터 웨어하우스의 고수준 아키텍처를 살펴보기 위해 저장소 엔진의 내부를 알아봤다

  • 분석 작업 부하가 OLTP와 많이 다른 이유는 질의가 많은 수의 로우를 순차적으로 스캔해야 하기 때문이다. 이 때는 색인을 사용하는 방법이 적절하지 않다. 대신 질의가 디스크에서 읽는 데이터의 양을 최소화하기 위해 데이터를 매우 작게 부호화하는 일이 중요해졌다. 칼럼 지향 저장소가 이 목표를 달성하는데 도움이 된다.

 

애플리케이션 개발자가 저장소 엔진의 내부에 대한 지식이 있다면 특정 애플리케이션에 어떤 도구가 가장 적합한지 판단하기에 유리하다. 데이터베이스 파라미터 조정이 필요하다면 이런 이해는 크거나 작은 값이 가지는 효과가 무엇인지 상상할 수 있게 해준다.

 

Reference: http://www.yes24.com/Product/Goods/59566585

 

데이터 중심 애플리케이션 설계 - YES24

데이터는 오늘날 시스템을 설계할 때 마주치는 많은 도전 과제 중에서도 가장 중심에 있다. 확장성, 일관성, 신뢰성, 효율성, 유지보수성과 같은 해결하기 어려운 문제를 파악해야 할 뿐 아니라

www.yes24.com