<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Index on Mario Blog</title><link>https://codemario318.github.io/tags/index/</link><description>Recent content in Index on Mario Blog</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Tue, 04 Mar 2025 13:51:49 +0900</lastBuildDate><atom:link href="https://codemario318.github.io/tags/index/index.xml" rel="self" type="application/rss+xml"/><item><title>3.1. 저장소와 검색: 데이터베이스를 강력하게 만드는 데이터 구조</title><link>https://codemario318.github.io/post/designing-data-intensive-application/3/1/</link><pubDate>Tue, 04 Mar 2025 13:51:49 +0900</pubDate><guid>https://codemario318.github.io/post/designing-data-intensive-application/3/1/</guid><description>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/cover.png" alt="Featured image of post 3.1. 저장소와 검색: 데이터베이스를 강력하게 만드는 데이터 구조" />&lt;p>가장 기본적인 수준에서 데이터베이스는 &lt;strong>데이터 저장&lt;/strong>, &lt;strong>데이터 제공&lt;/strong> 두 가지 작업을 수행한다.&lt;/p>
&lt;p>애플리케이션 개발자가 처음부터 자신의 저장소 엔진을 구현하기보다는 사용 가능한 여러 저장소 엔진 중에 애플리케이션에 적합한 엔진을 선택하는 작업이 필요하다.&lt;/p>
&lt;p>따라서 특정 &lt;strong>작업부하&lt;/strong>(workload) 유형에서 좋은 성능을 내게끔 저장소 엔지을 조정하려면 저장소 엔진이 내부에서 수행되는 작업에 대해 대략적인 개념을 이해할 필요가 있다.&lt;/p>
&lt;p>특히 &lt;strong>트랜잭션&lt;/strong> 작업부하에 맞춰 최적화된 저장소 엔진과 &lt;strong>분석&lt;/strong>을 위해 최적화된 엔진 간에는 큰 차이가 있으므로, 우선 &lt;strong>관계형 데이터베이스&lt;/strong>와 &lt;strong>NoSQL&lt;/strong>이라 불리는 데이터베이스에 사용되는 저장소
엔진에 대해 간략히 살펴보고, &lt;strong>로그 구조&lt;/strong>(log-structured) 계열 저장소 엔진(B-tree 같은)과 &lt;strong>페이지 지향&lt;/strong>(page-oriented) 계열 저장소 엔진을 검토한다.&lt;/p>
&lt;h2 id="로그와-색인">로그와 색인&lt;/h2>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>db_set &lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">,&lt;/span>&lt;span class="nv">$2&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt;&amp;gt; database
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">db_get &lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> grep &lt;span class="s2">&amp;#34;^&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">,&amp;#34;&lt;/span> database &lt;span class="p">|&lt;/span> sed -e &lt;span class="s2">&amp;#34;s/^&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">,//&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> tail -n &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>위는 매우 간단한 데이터베이스로 키-값 저장소를 함수 두 개로 구현되었으며, 기본적인 저장소 형식은 매 라인마다 쉼표로 구분된 키-값 쌍을 포함한 텍스트 파일이다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>db_set&lt;/code>을 호출할 때마다 파일의 끝에 추가하므로 키를 여러 번 갱신해도 값의 예전 버전을 덮어 쓰지 않음&lt;/li>
&lt;li>최신 값을 찾기 위해 파일에서 키의 가장 마지막 항목 확인 (&lt;code>tail -n 1&lt;/code>)&lt;/li>
&lt;/ul>
&lt;p>일반적으로 &lt;strong>파일 추가&lt;/strong> 작업은 매우 효율적이기 때문에 &lt;code>db_set&lt;/code> 함수는 매우 간단한 작업의 경우 꽤 좋은 성능을 보이는데, 이 때문에 많은 데이터베이스는 내부적으로 추가 전용(append-only) 데이터
파일인 &lt;strong>로그&lt;/strong>(log)를 사용한다.&lt;/p>
&lt;ul>
&lt;li>실제 데이터베이스는 동시성 제어, 디스크 공간 최적화, 요류 처리 등 여러 많은 문제를 다루지만 기본 원리는 같음&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>로그(log)&lt;/strong> 란?&lt;br>
일반적인 의미로 연속된 추가 전용 레코드&lt;/p>
&lt;/blockquote>
&lt;p>&lt;code>db_set&lt;/code>에 비해 &lt;code>db_get&lt;/code> 함수는 데이터베이스에 많은 레코드가 존재하는 경우 성능이 매우 좋지 않다.&lt;/p>
&lt;ul>
&lt;li>키를 찾을 때마다 키가 있는지 찾기 위해 전체 데이터베이스 파일을 처음부터 끝까지 스캔해야함(&lt;code>O(n)&lt;/code>)&lt;/li>
&lt;/ul>
&lt;p>이 때문에 데이터베이스에서 특정 키의 값을 효율적으로 찾기 위해서는 다른 데이터 구조 즉 &lt;strong>색인&lt;/strong>이 필요하다.&lt;/p>
&lt;p>색인의 일반적인 개념은 어떤 &lt;strong>부가적인 메타데이터를 유지하는 것&lt;/strong> 이다.&lt;/p>
&lt;p>이 메타데이터는 이정표 역할을 해서 원하는 데이터의 위치를 찾는데 도움을 주며, 동일한 데이터를 여러 가지 다양한 방법으로 검색하고 싶다면 데이터의 각 부분에 다양한 색인이 필요하다.&lt;/p>
&lt;p>색인은 기본 데이터(primary data)에서 파생된 &lt;strong>추가적인&lt;/strong> 구조로, 많은 데이터베이스는 색인의 추가와 삭제를 허용한다.&lt;/p>
&lt;ul>
&lt;li>데이터베이스의 내용에는 영향을 미치지 않음&lt;/li>
&lt;li>질의 성능에만 영향을 줌&lt;/li>
&lt;/ul>
&lt;p>이러한 추가적인 구조로 인해 특히 쓰기 과정에서 오버헤드가 발생하며, 위 예시의 로그처럼 단순히 파일에 추가하는 작업이 가장 간단한 쓰기 작업이므로 성능을 앞서기 어렵다.&lt;/p>
&lt;p>어떤 종류의 색인이라도 데이터를 쓸 때마다 색인을 갱신하기 때문에 대개 쓰기 속도를 느리게 만든다.&lt;/p>
&lt;p>이러한 성능 저하는 저장소 시스템의 중요한 트레이드 오프이며 색인을 잘 선택했다면 &lt;strong>읽기 질의 속도가 향상&lt;/strong>되지만, &lt;strong>쓰기 성능은 저하&lt;/strong> 된다.&lt;/p>
&lt;p>때문에 보통 데이터베이스는 자동으로 색인을 생성하지 않으므로, 개발자나 관리자가 애플리케이션의 질의 패턴에 대한 지식을 통해 수동으로 색인을 선택해야한다.&lt;/p>
&lt;h2 id="해시-색인">해시 색인&lt;/h2>
&lt;p>디스크 상의 데이터를 색인하기 위해 인메모리 데이터 구조를 사용할 수 있다.&lt;/p>
&lt;p>단순히 파일에 추가하는 방식으로 데이터 저장소를 구성한다고 가정할때, 가장 간단하게 가능한 색인 전략은 &lt;strong>키를 데이터 파일의 바이트 오프셋에 매핑해 인메모리 해시 맵을 유지하는 전략&lt;/strong>이다.&lt;/p>
&lt;p>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img.png"
width="766"
height="387"
srcset="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_hu10802d0377ceddec2b44e8f20547cd27_67858_480x0_resize_box_3.png 480w, https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_hu10802d0377ceddec2b44e8f20547cd27_67858_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="인메모리 해시맵으로 키-값 쌍의 로그 저장하기"
class="gallery-image"
data-flex-grow="197"
data-flex-basis="475px"
>&lt;/p>
&lt;ul>
&lt;li>파일에 새로운 키-값 쌍을 추가할 때마다 방금 기록한 데이터의 오프셋을 반영하기 위해 해시 맵도 갱신&lt;/li>
&lt;li>값을 조회하려면 해시 맵을 사용해 데이터 파일에서 오프셋을 찾아 해당 위치를 구하고 값을 읽음&lt;/li>
&lt;/ul>
&lt;p>이 방식은 매우 단순해 보이지만 실제로 많이 사용하는 접근법이다.&lt;/p>
&lt;ul>
&lt;li>해시 맵을 전부 메모리에 유지하기 때문에 사용 가능한 램에 모든 키가 저장된다는 조건을 전제로 고성능으로 읽기, 쓰기를 보장&lt;/li>
&lt;li>값은 한 번의 디스크 탐색으로 디스크에서 적재할 수 있기 때문에 사용 가능한 메모리보다 더 많은 공간을 사용&lt;/li>
&lt;/ul>
&lt;p>이러한 저장소 엔진은 각 키의 값이 자주 갱신되는 상황에 매우 적합하다.&lt;/p>
&lt;ul>
&lt;li>쓰기가 많지만 고유 키는 많지 않은 작업부하&lt;/li>
&lt;li>ex) 키가 특정 영상의 URL, 값이 영상의 재생 횟수 인 경우&lt;/li>
&lt;/ul>
&lt;p>하지만 파일에 항상 추가만 할 경우 결국 디스크 공간이 부족해지며, 이를 위해 특정 크기의 &lt;strong>세그먼트&lt;/strong>(segment)로 로그를 나누는 방식이 좋은 해결책으로 사용된다.&lt;/p>
&lt;ul>
&lt;li>특정 크기에 도달하면 세그먼트 파일을 닫고, 새로운 세그먼트 파일에 이후 쓰기를 수행&lt;/li>
&lt;li>세그먼트 파일들에 대해 컴팩션(compaction)을 수행할 수 있음
&lt;ul>
&lt;li>로그에서 중복된 키를 버리고 각 키의 최신 갱신 값만 유지하는 것&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_1.png"
width="765"
height="293"
srcset="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_1_hu0bae992a8b87768d26a804495794cdcd_41454_480x0_resize_box_3.png 480w, https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_1_hu0bae992a8b87768d26a804495794cdcd_41454_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="키-값 갱신 로그를 컴팩션하여 각 키의 최신 값만 유지"
class="gallery-image"
data-flex-grow="261"
data-flex-basis="626px"
>&lt;/p>
&lt;p>컴팩션은 보통 세그먼트를 더 작게 만들기 때문에 컴팩션을 수행할 때 동시에 여러 세그먼트들을 병합할 수 있다.&lt;/p>
&lt;ul>
&lt;li>이 때 세그먼트가 쓰여진 후에는 절대 변경할 수 없기 때문에 병합할 세그먼트는 새로운 파일로 만듦&lt;/li>
&lt;li>고정된 세그먼트의 병합과 컴팩션은 백그라운드 스레드에서 수행할 수 있음&lt;/li>
&lt;li>컴팩션을 수행하는 동안 이전 세그먼트 파일을 사용해 읽기와 쓰기 요청의 처리를 정상적으로 계속 수행할 수 있음&lt;/li>
&lt;/ul>
&lt;p>병합 과정이 끝난 이후에는 일기 요청은 이전 세그먼트 대신 새로 병합한 세그먼트를 사용하게끔 전환하고, 이전 세그먼트 파일을 삭제한다.&lt;/p>
&lt;p>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_2.png"
width="767"
height="401"
srcset="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_2_hu7c9ff548df2e8f3ee88f72377b5e69a1_71238_480x0_resize_box_3.png 480w, https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_2_hu7c9ff548df2e8f3ee88f72377b5e69a1_71238_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="컴팩션과 세그먼트 병합을 동시에 수행"
class="gallery-image"
data-flex-grow="191"
data-flex-basis="459px"
>&lt;/p>
&lt;p>각 세그먼트는 키를 파일 오프셋에 매핑한 자체 인메모리 해시 테이블을 갖게된다.&lt;/p>
&lt;ul>
&lt;li>키의 값을 찾으려면 최신 세그먼트 해시 맵을 먼저 확인&lt;/li>
&lt;li>키가 없다면 두 번째 최신 세그먼트 등을 확인&lt;/li>
&lt;/ul>
&lt;p>병합 과정을 통해 세그먼트 수를 적게 유지하므로 조회할 때 많은 해시 맵을 확인할 필요가 없다.&lt;/p>
&lt;hr>
&lt;p>이러한 아이디어를 실제로 구현하려면 세부적으로 많은 사항을 고려해야 한다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>파일 형식&lt;/strong>
&lt;ul>
&lt;li>CSV는 로그에 가장 적합한 형식은 아님&lt;/li>
&lt;li>바이트 단위의 문자열 길이를 부호화한 다음 원시 문자열(이스케이핑할 필요 없이)을 부호화하는 바이너리 형식을 사용하는 편이 더 빠르고 간단함&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>레코드 삭제&lt;/strong>
&lt;ul>
&lt;li>키와 관련된 값을 삭제하려면 데이터 파일에 특수한 삭제 레코드(툼스톤(tombstone))를 추가해야함&lt;/li>
&lt;li>로그 세그먼트가 병합될 때 툼스톤은 병합 과정에서 삭제된 키의 이전 값을 무시&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>고장(Crash) 복구&lt;/strong>
&lt;ul>
&lt;li>데이터베이스가 재시작되면 인메모리 해시 맵은 손실&lt;/li>
&lt;li>전체 세그먼트 파일을 처음부터 끝가지 읽고 각 키에 대한 최신 값의 오프셋을 확인해서 각 세그먼트 해시 맵을 복원 가능&lt;/li>
&lt;li>하지만 세그먼트 파일이 크면 해시 맵 복원은 오랜 시간이 걸리 수 있음(서버 재시작을 고통스럽게함)&lt;/li>
&lt;li>비트캐스크 같은 경우 각 세그먼트 해시 맵을 메모리로 조금 더 빠르게 로딩할 수 있게 스냅숏을 디스크에 저장해 복수 속도를 높임&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>부분적 레코드 쓰기&lt;/strong>
&lt;ul>
&lt;li>로그에 레코드를 추가하는 도중 데이터베이스가 죽을 수 있음&lt;/li>
&lt;li>비트캐스크 파일은 체크섬을 포함하고 있어 로그의 손상된 부분을 탐지해 무시할 수 있음&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>동시성 제어&lt;/strong>
&lt;ul>
&lt;li>쓰기를 엄격하게 순차적으로 로그에 추가할 때 일반적으로 하나의 쓰기 쓰레드만 사용&lt;/li>
&lt;li>데이터 파일 세그먼트는 추가 전용이거나 불변(immutable)이므로 다중 스레드로 동시에 읽기 가능&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>장점&lt;/strong>&lt;/p>
&lt;p>추가 전용 로그는 같은 키의 여러개의 쓰기가 발생하므로 비효율 적으로 보일 수 있다.&lt;/p>
&lt;p>예전 값을 새로운 값으로 덮어써 정해진 자리에 파일을 갱신하는 방법도 있지만, 추가 전용 설계는 여러 측면에서 장점이 있다.&lt;/p>
&lt;ul>
&lt;li>추가와 세그먼트 병합은 순차적인 쓰기 작업이므로 보통 무작위 쓰기보다 훨씬 빠름
&lt;ul>
&lt;li>특히 HDD에서 두드러지며, 일부 확장된 순차 쓰기는 SSD에서도 빠름&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>세그먼트 파일이 추가 전용이나 불변이면 동시성과 고장 복구는 훨씬 간단
&lt;ul>
&lt;li>이전 값 부분과 새로운 값 부분을 포함한 파일을 나누어 담으므로, 값을 덮어 쓰는 동안 데이터베이스가 죽는 경우에 대해 걱정할 필요가 없음&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>오래된 세그먼트 병합은 시간이 지남에 따라 조각화되는 데이터 파일 문제를 피할 수 있음&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>단점&lt;/strong>&lt;/p>
&lt;p>하지만 해시 테이블 색인 또한 제한 사항이 있다.&lt;/p>
&lt;ul>
&lt;li>해시 테이블은 메모리에 저장해야 하므로 키가 너무 많으면 문제가 발생함
&lt;ul>
&lt;li>디스크에 해시 맵을 유지할 수 있지만 디스크 상의 해시ㅣ 맵에 좋은 성능을 기대하기 어려움
&lt;ul>
&lt;li>무작위 접근 I/O가 많이 필요함&lt;/li>
&lt;li>디스크가 가득 찼을 때 확장하는 비용이 비쌈&lt;/li>
&lt;li>해시 충돌 해소를 위해 추가 로직 필요&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>점위 질의(Range query)에 적합하지 않음
&lt;ul>
&lt;li>키 값이 연속적이어도 쉽게 스캔할 수 없어 모든 개별 키를 조회해야함&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="ss테이블과-lsm-트리">SS테이블과 LSM 트리&lt;/h2>
&lt;p>각 로그 구조화 저장소 세그먼트는 키-값 쌍의 연속이다.&lt;/p>
&lt;p>이 쌍은 쓰여진 순서대로 나타나므로, 로그에서 같은 키를 갖는 값 중 나중의 값이 이전 값보다 우선 한다는 점만 제외하면 파일에서 키-값 쌍의 순서는 문제가 되지 않는다.&lt;/p>
&lt;p>이를 개선하기 위해 키-값 쌍을 키로 정렬하는 방법을 고려할 수 있다.&lt;/p>
&lt;p>이처럼 키로 정렬된 형식을 &lt;strong>정렬된 문자열 테이블&lt;/strong>(Sorted String Table), 짧게 &lt;strong>SS테이블&lt;/strong>이라 부른다.&lt;/p>
&lt;ul>
&lt;li>각 키는 각 병합된 세그먼트 파일 내에 한 번만 나타나함
&lt;ul>
&lt;li>컴팩션 과정은 이미 이를 보장&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>SS테이블은 해시 색인을 가진 로그 세그먼트보다 몇 가지 큰 장점이 있다.&lt;/p>
&lt;hr>
&lt;p>&lt;strong>세그먼트 병합은 파일이 사용 가능한 메모리보다 크더라도 간단하고 효율적&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_3.png"
width="729"
height="483"
srcset="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_3_hu7c722957ba7cfb7350c21df09c1076e8_90335_480x0_resize_box_3.png 480w, https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_3_hu7c722957ba7cfb7350c21df09c1076e8_90335_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="SS 테이블 세그먼트 병합하고 각 키의 최신 값만 유지"
class="gallery-image"
data-flex-grow="150"
data-flex-basis="362px"
>&lt;/p>
&lt;ol>
&lt;li>입력 파일을 함께 읽고 각 파일의 첫 번째 키를 확인(정렬된 순서에 따라)&lt;/li>
&lt;li>가장 낮은 키를 출력 파일로 복사한 뒤 이 과정 반복&lt;/li>
&lt;/ol>
&lt;ul>
&lt;li>이 과정에서 새로운 병합 세그먼트 파일이 생성되며, 새로 만든 세그먼트 파일도 역시 키로 정렬&lt;/li>
&lt;li>각 세그먼트는 일정 기간 동안 쓰여진 모든 값이 포함되므로, 여러 세그먼트의 같은 값이 포함될 수 있지만, 가장 최근에 만들어진 값이 최신&lt;/li>
&lt;li>따라서 다중 세그먼트가 동일한 키를 포함하는 경우 가장 최근 세그먼트의 값을 유지하고 오래된 세그먼트의 값은 버림&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>파일에서 특정 키를 찾기 위해 메모리에 모든 키의 색인을 유지할 필요가 없음&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_4.png"
width="732"
height="369"
srcset="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_4_huc566f05f79666e2208f2a7a43b254dbf_70982_480x0_resize_box_3.png 480w, https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_4_huc566f05f79666e2208f2a7a43b254dbf_70982_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="인메모리 색인을 가진 SS테이블"
class="gallery-image"
data-flex-grow="198"
data-flex-basis="476px"
>&lt;/p>
&lt;p>&lt;code>handiwork&lt;/code> 키를 찾으려 하지만 세그먼트 파일에서 키의 정확한 오프셋을 알지 못한다고 가정해도, &lt;code>handbag&lt;/code>과 &lt;code>handsome&lt;/code> 키의 오프셋을 알고 있다면, 이미 정렬되어있다는 특성을 활용하여 두 키 사이에 있다는 사실을 알 수 있다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>handbag&lt;/code> 오프셋으로 이동해 &lt;code>handiwork&lt;/code>가 나올 때까지 스캠하면 됨(키가 존재하지 않는 경우 찾을 수 없음)&lt;/li>
&lt;li>일부 키에 대한 오프셋을 알려주는 인메모리 색인은 여전히 필요&lt;/li>
&lt;li>하지만 색인 내용이 희소할 수 있는 경우, 수 킬로파이트 정도는 매우 빠르게 스캔할 수 있기 때문에 세그먼트 파일 내 수 킬로바이트당 키 하나로 충분&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>압축&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>읽기 요청은 요청 범위 내에서 여러 키-값 쌍을 스캔해야 하기 때문에 해당 레코드들을 블록으로 그룹화하고 디스크에 쓰기 전 압축함&lt;/li>
&lt;li>희소 인메모리 색인의 각 항목은 압축된 블록의 시작을 가리키게 됨&lt;/li>
&lt;li>디스크 공간을 절약할 수 있고, 이로 인해 I/O 대역폭 사용을 줄일 수 있음&lt;/li>
&lt;/ul>
&lt;h3 id="ss테이블-생성과-유지">SS테이블 생성과 유지&lt;/h3>
&lt;p>유입되는 쓰기가 임의 순서로 발생하기 때문에 데이터를 키로 정렬하려면 고민이 필요하다.&lt;/p>
&lt;p>디스크 상에 정렬된 구조를 유지하는 일은 가능하지만 메모리에 유지하는 편이 훨씬 쉽다.&lt;/p>
&lt;ul>
&lt;li>레드 블랙 트리, AVL 트리 등&lt;/li>
&lt;/ul>
&lt;p>이런 데이터 구조를 이용하면 &lt;strong>임의 순서로 키를 삽입하고 정렬된 순서로 해당 키를 다시 읽을 수&lt;/strong>있다.&lt;/p>
&lt;ul>
&lt;li>쓰기가 들어오면 인메모리 균형 트리 데이터 구조에 추가
&lt;ul>
&lt;li>이 인메모리 트리는 멤테이블(memtable)이라고도 함&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>멤테이블이 보통 수 메가바이트 정도의 임곗값보다 커지면 SS테이블 파일로 디스크에 기록
&lt;ul>
&lt;li>트리가 이미 키로 정렬된 키-값 쌍을 유지하므로 효율적으로 수행 가능&lt;/li>
&lt;li>새로운 SS테이블 파일은 데이터베이스의 가장 최신 세그먼트가 됨&lt;/li>
&lt;li>SS테이블을 기록하는 동안 쓰기는 새로운 멤테이블 인스턴스에 기록&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>읽기 요청을 제공하려면 먼제 멤테이블에서 키를 찾고 없다면 디스크 상의 가장 최신 세그먼트에서 부터 차례대로 확인&lt;/li>
&lt;li>가끔 세그먼트 파일을 합치고 덮어 쓰여지거나 삭제된 값을 버리는 병합과 컴팩션 과정을 수행
&lt;ul>
&lt;li>이 과정은 백그라운드에서 처리&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>이 계획은 데이터베이스가 고장나면 &lt;strong>아직 디스크로 기록되지 않고 멤테이블에 있는 가장 최신 쓰기는 손실되는 문제&lt;/strong>가 있다.&lt;/p>
&lt;p>이를 대비하기위해 매번 쓰기를 즉시 추가할 수 있게 분리된 로그를 디스크 상 유지해야한다.&lt;/p>
&lt;ul>
&lt;li>이 로그는 멤테이블을 복원할 때만 필요하므로 순서가 정렬되지 않아도 문제되지 않음&lt;/li>
&lt;/ul>
&lt;p>멤테이블을 SS테이블로 기록하고 나면 해당 로그는 버릴 수 있다.&lt;/p>
&lt;h3 id="ss테이블에서-lsm-트리-만들기">SS테이블에서 LSM 트리 만들기&lt;/h3>
&lt;p>여기에 기술된 알고리즘은 기본적으로 레벨DB(LevelDB)와 록스DB(RocksDB), 그리고 다른 애플리케이션에내장하기 이ㅜ해 설계된 키-값 저장소 엔진 라이브러리에서 사용한다.&lt;/p>
&lt;p>구글의 빅테이블(Bigtable)논문에서 영감을 얻은 카산드라와 HBase에서도 유사한 저장소 엔진을 사용한다.&lt;/p>
&lt;p>원래 이 색인 구조는 &lt;strong>로그 구조화 병합 트리&lt;/strong>(Log-Structured Merge-Table, LSM 트리)란 이름으로 발표되었으며, 이 색인 구조는 로그 구조화 파일 시스템의 초기 작업의 기반이 됐다.&lt;/p>
&lt;p>&lt;strong>정렬된 파일 병합과 컴팩션 원리를 기반으로 하는 저장소 엔진&lt;/strong>을 &lt;strong>LSM 저장소 엔진&lt;/strong>이라 부른다.&lt;/p>
&lt;h3 id="성능-최적화">성능 최적화&lt;/h3>
&lt;p>LSM 트리 알고리즘은 데이터베이스에 존재하지 않는 키를 찾는 경우 느릴 수 있다.&lt;/p>
&lt;ul>
&lt;li>멤테이블을 확인한 다음 키가 존재하지 않는 사실을 확인하기 전 까지 오래된 세그먼트까지 거슬러 올라야함(디스크 I/O 발생 가능)&lt;/li>
&lt;/ul>
&lt;p>이런 종류의 접근을 최적화하기 위해 저장소 엔진은 보통 &lt;strong>블룸 필터&lt;/strong>(Bloom filter)를 추가적으로 사용한다.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>블룸 필터(Bloom filter)?&lt;/strong>&lt;br>
집합 내용을 근사한(approximating) 메모리 효율적 데이터 구조&lt;/p>
&lt;/blockquote>
&lt;p>블룸 필터는 키가 데이터베이스에 존재하지 않음을 알려주므로 존재하지 않는 키를 위한 불필요한 디스크 읽기를 많이 절약할 수 있다.&lt;/p>
&lt;hr>
&lt;p>SS테이블을 압축하고 병합하는 순서와 시기를 결정하는 다양한 전략이 있다.&lt;/p>
&lt;p>가장 일반적으로 선택하는 전략은 &lt;strong>크기 계층 컴팩션&lt;/strong>(size-tiered compaction)과 &lt;strong>레벨 컴팩션&lt;/strong>(leveled compaction)이다.&lt;/p>
&lt;ul>
&lt;li>크기 게층 컴팩션
&lt;ul>
&lt;li>상대적으로 새롭고 작은 SS테이블을 상대적으로 오래됐고 큰 SS테이블에 연이어 병합&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>레벨 컴팩션
&lt;ul>
&lt;li>키 범위를 더 작은 SS테이블로 나누고 오래된 데이터는 개별 &amp;ldquo;레벨&amp;quot;로 이동&lt;/li>
&lt;li>컴팩션을 점진적으로 진행해 디스크 공간을 덜 사용&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>여러 중요한 세부 사항이 있지만 LSM 트리의 기본 개념은 간단하고 효과적이다.&lt;/p>
&lt;ul>
&lt;li>백그라운드에서 연쇄적으로 SS테이블을 지속적으로 병합하는 것&lt;/li>
&lt;/ul>
&lt;p>이 개념은 데이터셋이 가능한 메모리보다 훨씬 크더라도 여전히 효과적이고, 데이터가 정렬된 순서로 저장돼 있다면 범위 질의를 효율적으로 수행할 수 있다.&lt;/p>
&lt;ul>
&lt;li>최소에서 최대까지 모든 키를 스캔&lt;/li>
&lt;/ul>
&lt;p>이 접근법의 디스크 쓰기는 순차적이기 때문에 LSM 트리가 매우 높은 쓰기 처리량을 보장할 수 있다.&lt;/p>
&lt;h2 id="b-트리">B 트리&lt;/h2>
&lt;p>지금까지 설명한 로그 구조화 색인이 점점 보편화되고 있지만 가장 일반적인 색인 유형은 아니며, 가장 널리 사용되는 색인 구조는 &lt;strong>B 트리&lt;/strong>(B-Tree)로 구조가 &lt;strong>로그 구조화 색인&lt;/strong>과는 상당히 다르다.&lt;/p>
&lt;p>B 트리는 SS테이블과 같이 키로 정렬된 키-값 쌍을 유지하기 때문에 키-값 검색과 범위 질의에 효율적이지만, 설계 철학이 매우 다르다.&lt;/p>
&lt;ul>
&lt;li>로그 구조화 색인
&lt;ul>
&lt;li>데이터베이스를 일반적으로 수 메가바이트 이상의 가변 크기를 가진 &lt;strong>세그먼트&lt;/strong>로 나누고 항상 순차적으로 세그먼트를 기록&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>B 트리
&lt;ul>
&lt;li>전통적으로 4KB 크기의 고정 크기 &lt;strong>블록&lt;/strong>이나 &lt;strong>페이지&lt;/strong>로 나누고 한 번에 하나의 페이지에 읽기 또는 쓰기 수행&lt;/li>
&lt;li>디스크가 고정 크기 블록으로 배열되기 때문에 근본적으로 하드웨어와 조금 더 밀접한 관련이 있음&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>B 트리의 각 페이지는 주소나 위치를 통해 식별할 수 있으며, 이러한 방식으로 하나의 페이지가 다른 페이지를 참조할 수 있다.&lt;/p>
&lt;ul>
&lt;li>포인터와 비슷하지만 메모리 대신 디스크에 있음&lt;/li>
&lt;/ul>
&lt;p>이러한 페이지 참조는 페이지 트리를 구성하는 데 사용할 수 있다.&lt;/p>
&lt;p>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_5.png"
width="766"
height="432"
srcset="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_5_hu276247de4dbbe3c9ef294590e9d78f86_61955_480x0_resize_box_3.png 480w, https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_5_hu276247de4dbbe3c9ef294590e9d78f86_61955_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="B 트리 색인을 이용한 키 검색"
class="gallery-image"
data-flex-grow="177"
data-flex-basis="425px"
>&lt;/p>
&lt;p>한 페이지는 B 트리의 &lt;strong>루트&lt;/strong>(root)로 지정되며, 색인에서 키를 찾으려면 루트에서 시작한다.&lt;/p>
&lt;p>페이지는 여러 &lt;strong>키&lt;/strong>와 &lt;strong>하위 페이지의 참조&lt;/strong>를 포함되며, 각 하위 페이지는 &lt;strong>키가 계속 이어지는 범위&lt;/strong>를 담당하고 참조 사이의 키는 &lt;strong>해당 범위의 경계가 어디인지&lt;/strong> 나타낸다.&lt;/p>
&lt;p>최종적으로는 개별 키(&lt;strong>리프 페이지&lt;/strong>(leaf page))를 포함하는 페이지에 도달하게되며, 이 페이지는 각 키의 값을 포함하거나 값을 찾을 수 있는 페이지의 참조를 포함하게된다.&lt;/p>
&lt;ul>
&lt;li>B 트리의 한 페이지에서 하위 페이지를 참조하는 수를 &lt;strong>분기 계수&lt;/strong>(branching factor)라 부름&lt;/li>
&lt;li>실제로 분기 계수는 페이지 참조와 범위 경계를 저장할 공간의 양에 의존적(보통 수백개)&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_6.png"
width="767"
height="438"
srcset="https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_6_hu89ce2a7004a0b1a5586f285cb94aff3a_56106_480x0_resize_box_3.png 480w, https://codemario318.github.io/post/designing-data-intensive-application/3/1/img_6_hu89ce2a7004a0b1a5586f285cb94aff3a_56106_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="페이지 분리로 커진 B 트리"
class="gallery-image"
data-flex-grow="175"
data-flex-basis="420px"
>&lt;/p>
&lt;p>이 알고리즘은 트리가 계속 &lt;strong>균형&lt;/strong>을 유지하는 것을 보장한다.&lt;/p>
&lt;ul>
&lt;li>n개의 키를 가진 B 트리의 깊이가 항상 &lt;code>O(log n)&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>대부분 데이터베이스는 B 트리의 깊이가 3 ~ 4 정도면 충분하므로 검색하려는 페이지를 찾기 위해 많은 페이지 참조를 따라가지 않아도 된다.&lt;/p>
&lt;ul>
&lt;li>분기 계수 500의 4KB 페이지의 4단계 트리는 256TB까지 저장할 수 있음&lt;/li>
&lt;/ul>
&lt;h3 id="신뢰할-수-있는-b-트리-만들기">신뢰할 수 있는 B 트리 만들기&lt;/h3>
&lt;p>B 트리의 기본적인 쓰기 동작은 새로운 데이터를 디스크 상의 페이지에 덮어쓰며, 덮어쓰기가 페이지 위치를 변경하지 않는다고 가정한다.&lt;/p>
&lt;ul>
&lt;li>페이지를 덮어쓰더라도 페이지를 가르키는 모든 참조는 온전히 남음&lt;/li>
&lt;li>LSM 트리와 같은 로그 구조화 색인은 파일에 추가 할 뿐(더 이상 쓸모 없는 파일은 삭제) 같은 위치의 파일은 변경하지 않음&lt;/li>
&lt;/ul>
&lt;p>디스크의 페이지를 덮어 쓰는 일은 실제 하드웨어가 처리한다.&lt;/p>
&lt;ul>
&lt;li>하드드라이브
&lt;ul>
&lt;li>디스크 헤드를 적절한 곳으로 옮기고, 회전하는 플래터의 올바른 위치가 돌아올 때까지 기다린 다음 적합한 섹터에 새로운 데이터를 덮어씀&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>SSD
&lt;ul>
&lt;li>SSD가 저장소 칩의 상당한 블록을 한번에 지우고 다시 쓰기를 수행&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>일부 동작은 여러 다양한 페이지의 덮어쓰기를 필요로 한다.&lt;/p>
&lt;p>예를 들어, 삽입 때문에 페이지가 너무 많아져 페이지를 나눠야 하는 경우 분할된 두 페이지를 기록하고 두 하위 페이지의 참조를 갱신하게끔 상위 페이지를 덮어쓰기를 해야한다.&lt;/p>
&lt;ul>
&lt;li>이러한 처리 중 일부 페이지만 기록하고 데이터베이스가 고장난다면 결국 색인이 훼손되므로 위험함&lt;/li>
&lt;/ul>
&lt;p>데이터베이스가 고장 상황에서 스스로 복구할 수 있도록 일반적으로 디스크 상에 &lt;strong>쓰기 전 로그&lt;/strong>(write-ahead log, WAL, 재실행 로그, redo log)라는 데이터 구조를 추가해 B 트리를 구현한다.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>쓰기 전 로그&lt;/strong>&lt;br>
트리 페이지에 변경된 내용을 적용하기 전 모든 B 트리의 변경 사항을 기록하는 추가 전용 파일&lt;br>
데이터베이스가 복구될 때 일관성 있는 상태로 B 트리를 복원하는 데 사용됨&lt;/p>
&lt;/blockquote>
&lt;p>같은 자리의 페이지를 갱신하는 작업은 추가적인 골칫거리다.&lt;/p>
&lt;p>다중 스레드가 동시에 B 트리에 접근할 때 동시성 제어를 하지 않는다면 스레드가 일관성이 깨진 상태의 트리에 접근할 수 있다.&lt;/p>
&lt;p>동시성 제어는 보통 &lt;strong>래치&lt;/strong>(latch, 가벼운 잠금)로 트리의 데이터 구조를 보호한다.&lt;/p>
&lt;ul>
&lt;li>로그 구조화 접근 방식에서는 유입 질의의 간섭 없이 백그라운드에서 모든 병합을 수행하고 가끔 원자적으로 새로운 세그먼트를 이전 세그먼트로 바꾸기 때문에 훨씬 간단함&lt;/li>
&lt;/ul>
&lt;h3 id="b-트리-최적화">B 트리 최적화&lt;/h3>
&lt;p>B 트리는 오랫동안 사용된만큼 많은 최적화 기법이 있다.&lt;/p>
&lt;ul>
&lt;li>페이지 덮어 쓰기와 고장 복구를 위한 WAL 유지 대신, (LMDB 같은)일부 데이터베이스는 쓰기 시 복사 방식(copy-on-write scheme)을 사용함
&lt;ul>
&lt;li>변경된 페이지는 다른 위치에 기록하고 트리에 상위 페이지의 새로운 버전을 만들어 새로운 위치를 가르키게 함&lt;/li>
&lt;li>동시성 제어에도 유용&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>페이지에 전체 키를 저장하는 것이 아니라 &lt;strong>키를 축약&lt;/strong>해 쓰면 공간을 절약할 수 있음
&lt;ul>
&lt;li>트리 내부 페이지에서 키가 키 범위 사이의 경계 역할을 하는 데 충분한 정보만 제공하면 됨&lt;/li>
&lt;li>페이지 하나에 키를 더 많이 채우면 트리는 더 높은 분기 계수를 얻어 트리 깊이 수준을 낮출 수 있음(B+ 트리라 부르기도 함)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>일반적으로 페이지는 디스크 상 어디에나 위치할 수 있기 때문에(키 범위가 가깝다고 가까운 위치애 페이지를 배치하지 않음) 질의가 정렬된 순서로 키 범위의 상당 부분을 스캔해야 한다면 모든 페이지에 대해 디스크 찾기가 필요하기 때문에 페이지 단위 배치는 비효율적임
&lt;ul>
&lt;li>많은 B 트리 구현에서 리프 페이지를 디스크 상에 연속된 순서로 나타나게끔 트리를 배치하려 시도하지만 트리가 커지면 순서를 유지하기 어려움&lt;/li>
&lt;li>LSM 트리는 병합하는 과정에서 저장소의 큰 세그먼트를 한 번에 다시 쓰기 때문에 디스크에서 연속된 키를 서로 가깝게 유지하기 더 쉬움&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>트리에 포인터를 추가
&lt;ul>
&lt;li>각 리프 페이지가 양쪽 형제 페이지에 대한 참조를 가지면 상위 페이지로 다시 이동하지 않아도 순서대로 키를 스캔할 수 있음&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>프랙탈 트리(fractal tree) 같은 변형은 디스크 찾기를 줄이기 위해 로그 구조하 개념을 일부 빌렸음
&lt;ul>
&lt;li>기하학의 프랙탈과는 아무 의미 없음&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="b-트리와-lsm-트리-비교">B 트리와 LSM 트리 비교&lt;/h2>
&lt;p>일반적으로 B 트리가 LSM 트리보다 구현 성숙도가 더 높지만 LSM 트리도 그 성능 특성 때문에 관심을 받고 있다.&lt;/p>
&lt;p>LSM 트리는 보통 쓰기에서 더 빠르고, B 트리는 읽기에서 더 빠르다고 여겨진다.&lt;/p>
&lt;ul>
&lt;li>LSM 트리는 각 컴팩션 단계에 있는 여러 가지 데이터 구조와 SS테이블을 확인해야하므로 읽기가 보통 더 느림&lt;/li>
&lt;/ul>
&lt;p>하지만 벤치마크는 보통 결정적이지 않고 작업부하의 세부 사항에 민감하므로 정확한 비교를 위해 실제 필요한 작업부하로 시스템을 테스트해야한다.&lt;/p>
&lt;h3 id="lsm-트리의-장점">LSM 트리의 장점&lt;/h3>
&lt;p>&lt;strong>B 트리 색인&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>모든 데이터 조각을 최소한 두 번 기록해야한다. (쓰기 전 로그, 트리 페이지)&lt;/li>
&lt;li>해당 페이지 내 몇 바이트만 바뀌어도 한 번에 전체 페이지를 기록해야 하는 오버헤드&lt;/li>
&lt;li>일부 저장소 엔진은 전원에 장애가 발생했을 때 일부만 갱신된 페이지로 끝나지 않게 동일한 페이즈를 두 번 덮어씀&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>로그 구조화 색인&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>SS테이블의 반복된 컴팩션과 병함으로 인해 여러 번 데이터를 다시 씀
&lt;ul>
&lt;li>쓰기 증폭(write amplification)
&lt;ul>
&lt;li>데이터베이스에 쓰기 한 번이 데이터베이스 수명 동안 디스크에 여러 번의 쓰기를 야기하는 효과&lt;/li>
&lt;li>SSD는 수명 동안 블록 덮어쓰기 횟수가 정해져 있으므로 주의해야함&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>쓰기가 많은 애플리케이션에서 성능 병목은 데이터베이스가 디스크에 쓰는 속도일 수 있는데, 이 경우 쓰기 증폭은 성능 비용이다.&lt;/p>
&lt;ul>
&lt;li>저장소 엔진이 디스크에 기록할수록 디스크 대역폭 내 처리할 수 있는 초당 쓰기는 점점 줄어듦&lt;/li>
&lt;/ul>
&lt;p>LSM 트리가 상대적으로 쓰기 증폭이 더 낮고, 트리에서 여러 페이지를 덮어쓰는 것이 아닌 순차적으로 컴팩션된 SS 테이블을 쓰기 때문에, 보통 B 트리보다 쓰기 처리량을 높게 유지할 수 있다.&lt;/p>
&lt;ul>
&lt;li>특히 하드디스크는 순차 쓰기가 임의 쓰기 보다 훨씬 더 빠르다.&lt;/li>
&lt;/ul>
&lt;p>LSM 트리는 압축률이 더 좋다.&lt;/p>
&lt;ul>
&lt;li>보통 B 트리보다 디스크에 더 적은 파일을 생성한다.&lt;/li>
&lt;/ul>
&lt;p>B 트리 저장소 엔진은 파편화로 인해 사용하지 않는 디스크 공간 일부가 남는다.&lt;/p>
&lt;ul>
&lt;li>페이지를 나누거나 로우가 기존 페이지에 맞지 않을 때 페이지의 일부 공간은 사용하지 않게됨&lt;/li>
&lt;li>LSM 트리는 페이지 지향적이지 않고 주기적으로 파편화를 없애기 위해 SS테이블을 다시 기록하기 때문에 저장소 오버헤드가 더 낮다.(레벨 컴팩션 사용시 특히 더)&lt;/li>
&lt;/ul>
&lt;p>대다수의 SSD 펌웨어는 내장 저장소 칩에서 임의 쓰기를 순차 쓰기로 전환하기 우해 내부적으로 로그 구조화 알고리즘을 사용한다.&lt;/p>
&lt;ul>
&lt;li>그래서 저장소 엔진의 쓰기 패턴이 SSD에 미치는 영향은 분명하지 않음&lt;/li>
&lt;li>하지만 낮은 쓰기 증폭과 파편화 감소는 SSD의 경우 훨씬 유리함&lt;/li>
&lt;/ul>
&lt;p>데이터를 더 밀집해 표현하면 가능한 I/O 대역폭 내에서 더 많은 읽기와 쓰기 요청이 가능하다.&lt;/p>
&lt;h3 id="lms-트리의-단점">LMS 트리의 단점&lt;/h3>
&lt;p>로그 구조화 저장소의 단점은 &lt;strong>컴팩션 과정&lt;/strong>이 때로는 진행 중인 읽기와 쓰기의 성능에 영향을 준다는 점이다.&lt;/p>
&lt;p>저장소 엔진은 컴팩션을 점진적으로 수행하고 동시 접근의 영향이 없게 수행하려 하지만, 디스크가 가진 자원은 한계가 있으므로 디스크에서 비싼 &lt;strong>컴팩션 연산이 끝날 때까지 요청이 대기해야하는 상황&lt;/strong>이 발생하기 쉽다.&lt;/p>
&lt;p>처리량과 평균 응답 시간이 성능에 미치는 영향은 대개 작으나 로그 구조화 저장소 엔진의 상위 백분위 질의의 응답 시간은 때때로 길다.&lt;/p>
&lt;p>반면 B 트리의 성능은 로그 구조화 저장소 엔진보다 예측하기 쉽다.&lt;/p>
&lt;hr>
&lt;p>또 다른 컴팩션 문제는 높은 쓰기 처리량에서 발생한다.&lt;/p>
&lt;p>디스크의 쓰기 대역폭은 유한하기 때문에 초기 쓰기(로깅(logging)과 멤테이블을 디스크로 방출(flushing))와 백그라운드에서 수행되는 컴팩션 스레드가 이 대역폭을 공유해야한다.&lt;/p>
&lt;p>빈 데이터베이스에 쓰는 경우 전체 디스크 대역폭을 초기 쓰기만을 위해 사용할 수 있지만, 데이터베이스가 커질수록 컴팩션을 위해 더 많은 디스크 대역폭이 필요하다.&lt;/p>
&lt;p>쓰기 처리량이 높음에도 컴팩션 설정을 주의 깊게 하지 않으면 컴팩션이 유입 쓰기 속도를 따라갈 수 없다.&lt;/p>
&lt;ul>
&lt;li>디스크 상에 병합되지 않은 세그먼트 수는 디스크 공간이 부족할 때 까지 증가할 수 있음&lt;/li>
&lt;li>더 많은 세그먼트 파일을 확인해야 하기 때문에 읽기 또한 느려짐&lt;/li>
&lt;/ul>
&lt;p>보통 SS테이블 기반 저장소 엔진은 컴팩션이 유입 속도를 따라가지 못해도 유입 쓰기의 속도를 조절하지 않으므로 이런 상황을 감지하기 위한 명시적 모니터링이 필요하다.&lt;/p>
&lt;hr>
&lt;p>B 트리의 장점은 &lt;strong>각 키가 색인의 한 곳에만 정확하게 존재한다는 점&lt;/strong>이다. 하지만 로그 구조화 저장소 엔진은 다른 세그먼트에 같은 키의 다중 복사본이 존재할 수 있다.&lt;/p>
&lt;p>이런 측면 때문에 강력한 트랜잭션 시맨틱(semantic)를 제공하는 데이터베이스에는 B 트리가 훨씬 매력적이다.&lt;/p>
&lt;ul>
&lt;li>많은 관계형 데이터베이스에서 트랜잭션 격리(transactional isolation)는 키 범위의 잠금을 사용해 구현한 반면 B 트리 색인에서는 &lt;strong>트리에 직접 잠금을 포함&lt;/strong>시킴&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>B 트리는 데이터베이스 아키텍처에 아주 깊게 뿌리내렸고, 많은 작업부하에 대해 지속적으로 좋은 성능을 제공하므로 금방 사라질 가능성은 거의 없으며, 새로운 데이터 저장소에서는 로그 구조화 색인이 점점 인기를 얻고 있다.&lt;/p>
&lt;p>사용 사례에 적합한 저장소 엔진의 유형을 결정하기 위한 빠르고 쉬운 규칙은 없기 때문에 테스트를 통해 경험적으로 결정하는 방법도 나쁘지 않다.&lt;/p>
&lt;h2 id="기타-색인-구조">기타 색인 구조&lt;/h2>
&lt;p>지금까지는 키-값 색인을 살펴봤다.&lt;/p>
&lt;p>대표적인 기-값 색인 예시는 관계형 모델의 &lt;strong>기본키(primary-key)&lt;/strong> 색인이다.&lt;/p>
&lt;ul>
&lt;li>기본키로 관계형 테이블에서 하나의 로우를, 문서 데이터베이스에서 하나의 문서를, 그래프 데이터베이스에서 하나의 정점을 고유하게 식별&lt;/li>
&lt;li>데이터베이스에서 다른 레코드는 기본키로 로우/문서/정점을 참조할 수 있다.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>보조 색인(secondary index)&lt;/strong> 을 사용하는 방식도 매우 일반적이다.&lt;/p>
&lt;ul>
&lt;li>관계형 데이터베이스에서 &lt;code>CREATE INDEX&lt;/code> 명령을 통해 보조 색인을 생성할 수 있음&lt;/li>
&lt;li>보조 색인은 보통 효율적인 조인을 수행하는 데 결정적인 역할을 함&lt;/li>
&lt;/ul>
&lt;p>보조 색인은 키-값 색인에서 쉽게 생성할 수 있는데, 기본키 색인과의 주요 차이점은 키가 고유하지 않다는 점이다.&lt;/p>
&lt;ul>
&lt;li>같은 키를 가진 많은 로우(문서, 정점)이 있을 수 있음&lt;/li>
&lt;/ul>
&lt;p>이 점은 두 가지 방법으로 개선될 수 있다.&lt;/p>
&lt;ul>
&lt;li>색인의 각 값에 일치하는 로우 식별자 목록을 만들기&lt;/li>
&lt;li>로우 식별자를 추가해 각 키를 고유하게 만들기&lt;/li>
&lt;/ul>
&lt;p>어느 쪽이든 보조 색인으로 B 트리와 로그 구조화 색인 둘 다 사용할 수 있다.&lt;/p>
&lt;h3 id="색인-안에-값-저장하기">색인 안에 값 저장하기&lt;/h3>
&lt;p>색인에서 키는 질의가 검색하는 대상이지만, 값은 다음의 두 가지 중 하나에 해당한다.&lt;/p>
&lt;ul>
&lt;li>질문의 실제 로우&lt;/li>
&lt;li>다른 곳에 저장된 로우를 가르키는 참조&lt;/li>
&lt;/ul>
&lt;p>후자의 경우 로우가 저장된 곳을 &lt;strong>힙 파일&lt;/strong>(heap file)이라 하고 특정 순서 없이 데이터를 저장함&lt;/p>
&lt;p>힙 파일 접근은 여러 보조 색인이 존재할 때 데이터 중복을 피할 수 있기 때문에 일반적인 방식이다.&lt;/p>
&lt;ul>
&lt;li>각 색인은 힙 파일에서 위치만 참조하고 실제 데이터는 일정한 곳에 유지함&lt;/li>
&lt;/ul>
&lt;p>힙 파일 접근 방식은 키를 변경하지 않고 값을 갱신할 때 꽤 효율적이다.&lt;/p>
&lt;ul>
&lt;li>새로운 값이 이전 값보다 많은 공간을 필요로 하지 않으면 레코드를 제자리에 덮어쓸 수 있음&lt;/li>
&lt;/ul>
&lt;p>하지만 새로운 값이 많은 공간을 필요로 한다면 힙에서 충분한 공간이 있는 새로운 곳으로 위치를 이동해야한다.&lt;/p>
&lt;ul>
&lt;li>이러한 경우 모든 색인이 레코드의 새로운 힙 위치를 가르키게끔 갱신하거나 이전 힙 위치에 전방향 포인터를 남겨야함&lt;/li>
&lt;/ul>
&lt;p>색인에서 힙 파일로 다시 이동하는 일은 읽기 성능에 불이익이 너무 많기 때문에, 어떤 상황에서는 색인 안에 바로 색인된 로우를 저장하는 편이 바람직 할 수 있다.&lt;/p>
&lt;p>이를 &lt;strong>클러스터드 색인&lt;/strong>(clustered index)라고 하며, MySQL의 InnoDB 저장소 엔진에서는 테이블의 기본키가 언제가 클러스터드 색인이고 보조 색인은 (힙 파일의 위치가 아닌) 기본키를 참조한다.&lt;/p>
&lt;p>클러스터드 색인과 비 클러스터드 색인 사이의 절충안을 커버링 색인(covering index)이나 포괄열이 있는 색인(index with included column)이라 한다.&lt;/p>
&lt;ul>
&lt;li>색인 안에 테이블의 컬럼 일부를 저장&lt;/li>
&lt;li>색인만 사용해 일부 질의에 응답이 가능함(색인이 질의를 커버했다고 말함)&lt;/li>
&lt;/ul>
&lt;p>모든 종류의 데이터 복제와 마찬가지로 클러스터드 색인과 커버링 색인은 읽기 성능을 높일 수 있지만 추가적인 저장소가 필요하고 쓰기 과정에 오버헤드가 발생한다.&lt;/p>
&lt;p>또한 애플리케이션 단에서 복제로 인한 불일치를 파악할 수 없기 때문에 데이터베이스는 트랜잭션 보장을 강화하기 위해 별도의 노력이 필요하다.&lt;/p>
&lt;h3 id="다중-컬럼-색인">다중 컬럼 색인&lt;/h3>
&lt;p>지금까지 설명한 색인은 하나의 키만 값에 대응하므로 테이블의 다중 컬럼에 동시에 질의를 해야한다면 충분하지 않다.&lt;/p>
&lt;p>다중 컬럼 색인의 가장 일반적인 유형은 &lt;strong>결합 색인&lt;/strong>(concatenated index)이라고 한다.&lt;/p>
&lt;ul>
&lt;li>하나의 컬럼에 다른 컬럼을 추가하는 방식으로 하나의 키에 여러 필드를 단순히 결합&lt;/li>
&lt;li>필드가 결합하는 순서는 색인 정의에 명시&lt;/li>
&lt;/ul>
&lt;p>이 방법은 &lt;code>(성, 이름)&lt;/code>을 키로 전화번호 값으로 하는 색인을 제공하는 전화번호부와 유사하다.&lt;/p>
&lt;ul>
&lt;li>순서가 정렬돼 있으므로 특정 성을 가진 모든 사람, 특정 성 이름 조합을 가진 모든 사람을 찾을 때도 이 색인을 활용할 수 있음&lt;/li>
&lt;li>하지만 특정 이름을 가진 모든 사람을 찾을 때는 쓸모 없음&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>다차원 색인은 한 번에 여러 컬럼에 질의하는 조금 더 일반적인 방법이다.&lt;/p>
&lt;ul>
&lt;li>지리 공간 데이터 등&lt;/li>
&lt;/ul>
&lt;p>레스토랑 검색 웹 사이트에 각 레스토랑의 위도와 경도를 포함한 데이터베이스가 있다고 가정하면, 사용자가 지도에서 레스토랑을 찾을 때 웹 사이트는 사용자가 현재 보는 네모단 지도 영역 내 모든 레스토랑을 찾아야 하므로 다음과 같은 이차원 범위 질의가 필요하다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">restaurants&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">latitude&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">51&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">4946&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">latitude&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">51&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">5079&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">longitude&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">1162&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">longitude&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">1004&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>표준 B 트리나 LSM 트리 색인은 이런 유형의 질의에 효율적으로 응답할 수 없다.&lt;/p>
&lt;ul>
&lt;li>위도 범위 안의 모든 레스토랑이나 경도 범위 안의 모든 레스토랑을 줄 수는 있지만 둘을 동시에 주진 못함&lt;/li>
&lt;/ul>
&lt;p>한 가지 방법은 이차원 위치를 공간 채움 곡선(space-filling curve)을 이용해 단일 숫자로 변환한 다음 일반 B 트리 색인을 사용하는 것이다.&lt;br>
좀 더 일반적인 방법은 R 트리처럼 전문 공간 색인(specialized spatial index)을 사용하는 것이다.&lt;/p>
&lt;p>다차원 색인의 활용은 지리학적인 위치에만 국한되지 않는다.&lt;/p>
&lt;ul>
&lt;li>색상(RGB), 날씨(날짜, 기온)&lt;/li>
&lt;/ul>
&lt;h3 id="전문-검색과-퍼지-색인">전문 검색과 퍼지 색인&lt;/h3>
&lt;p>지금까지 설명한 모든 색인은 정확한 데이터를 대상으로 키의 정확한 값이나 정렬된 키의 값의 범위를 질의할 수 있다고 가정하므로, 철자가 틀린 단어와 같이 &lt;strong>유사한&lt;/strong> 키에 대해서는 검색 할 수 없다.&lt;/p>
&lt;p>이처럼 &lt;strong>애매모호한&lt;/strong>(fuzzy) 질의에는 다른 기술이 필요하다.&lt;/p>
&lt;hr>
&lt;p>전문 검색 엔진은 일반적으로 특정 단어를 검색할 때 해당 단어의 동의어로 질의를 확장한다.&lt;/p>
&lt;p>그리고 단어의 문법적 활용을 무시하고 동일한 문서에서 서로 인접해 나타난 단어를 검색하거나 언어학적으로 텍스트를 분석해 사용하는 등 다양한 기능을 제공한다.&lt;/p>
&lt;p>아파치 루씬은 문서나 질의의 오타에 대처하기 위해 특정 편집 거리(edit distance) 내 단어를 검색할 수 있다.&lt;/p>
&lt;ul>
&lt;li>편집 거리 1은 한 글자가 추가되거나 삭제되거나 교체됐음을 의미&lt;/li>
&lt;/ul>
&lt;p>루씬은 용어 사전을 위해 SS테이블 같은 구조를 사용한다. 이 구조는 작은 인메모리 색인이 필요하며, 이 색인은 키를 찾는데 필요한 정렬 파일의 오프셋을 질의에 알려주는 데 사용한다.&lt;/p>
&lt;p>레벨DB에서 이 인메로리 색인은 일부 키의 희소 컬렉션이지만, 루씬에서 인메모리 색인은 여러 키 내 문자에 대한 &lt;strong>유한 상태 오토마톤&lt;/strong>(finite state automaton)으로 &lt;strong>트라이&lt;/strong>(trie)와 유사하다.&lt;/p>
&lt;ul>
&lt;li>이 오토마톤은 &lt;strong>레벤슈타인 오토마톤&lt;/strong>(levenshtein automaton)으로 변환할 수 있다.&lt;/li>
&lt;li>특정 편집 거리 내에서 효율적인 단어 검색을 제공&lt;/li>
&lt;/ul>
&lt;p>그 밖의 퍼지 검색 기술은 문서 분류 및 머신러닝의 방향으로 진행되고 있다.&lt;/p>
&lt;h3 id="모든-것을-메모리에-보관">모든 것을 메모리에 보관&lt;/h3>
&lt;p>지금까지 설명한 데이터 구조는 모두 디스크 한계에 대한 해결책 이었다.&lt;/p>
&lt;p>디스크는 메인 메모리와 비교해 대루기 어렵다.&lt;/p>
&lt;ul>
&lt;li>자기 디스크와 SSD를 사용할 때 읽기와 쓰기에서 좋은 성능을 원한다면 주의해서 데이터를 디스크에 배치해야 함&lt;/li>
&lt;/ul>
&lt;p>이런 불편함을 참을 수 있는 이유는 디스크의 장점 때문이다.&lt;/p>
&lt;ul>
&lt;li>지속성: 디스크 내용은 전원이 꺼져도 손실되지 않음&lt;/li>
&lt;li>가격: 램보다 훨씬 더 저렴&lt;/li>
&lt;/ul>
&lt;p>하지만 과거에 비해 메인 메모리의 가격이 매우 저렴해졌고, 데이터셋의 대부분은 그다지 크지 않기 때문에 &lt;strong>메모리에 전체를 보관하는 방식&lt;/strong>도 고려할 수 있게 되었으며, 이러한 이유로 &lt;strong>인메모리 데이터베이스&lt;/strong>가 개발되었다.&lt;/p>
&lt;hr>
&lt;p>맴캐시드 같은 일부 인베모리 키-값 저장소는 장비가 재시작되면 데이터 손실을 허용하는 캐시 용도로만 활용되었으나, 다른 인메모리 데이터베이스는 지속성을 목표로 한다.&lt;/p>
&lt;ul>
&lt;li>배터리 전원 공급 RAM 같은 특수 하드웨어 사용&lt;/li>
&lt;li>디스크에 변경 사항의 로그를 기록&lt;/li>
&lt;li>디스크에 주기적인 스냅숏 기록&lt;/li>
&lt;li>다른 장비에 인메모리 상태를 복제&lt;/li>
&lt;/ul>
&lt;p>인메모리 데이터베이스가 재시작 되는 경우 디스크나 네트워크를 통해 복제본에서 상태를 다시 적재해야 한다.&lt;/p>
&lt;p>디스크는 전적으로 지속성을 위한 추가 전용 로그로 사용되고, 읽기는 전적으로 메모리에서 제공되기 때문에 디스크에 기록하더라도 여전히 인메모리 데이터베이스이다.&lt;/p>
&lt;hr>
&lt;p>직관에 어긋나지만 인메모리 데이터베이스의 &lt;strong>성능 강점은 디스크에서 읽지 않아도 된다는 사실 때문은 아니다.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>디스크 기반 저장소 엔진도 운영체제가 최근에 사용한 디스크 블록을 메모리에 캐시하기 때문에 충분한 메모리를 가진 경우에는 디스크에서 읽을 필요 없음&lt;/li>
&lt;li>오히려 인메모리 데이터 구조를 디스크에 기록하기 위한 형태로 부호화하는 오버헤드를 피할 수 있어 더 빠를 수도 있음&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>성능 외에도 인메모리 데이터베이스는 또 다른 재미있는 영역으로서 디스크 기반 색인으로 구현하기 어려운 데이터 모델을 제공한다.&lt;/p>
&lt;ul>
&lt;li>레디스
&lt;ul>
&lt;li>우선순위 큐와 셋 같은 다양한 데이터 구조를 데이터베이스 같은 인터페이스로 제공&lt;/li>
&lt;li>메모리에 모든 데이터를 유지하기 때문에 고현이 비교적 간단&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>최근 연구에 따르면 인메모리 데이터베이스 아키텍처가 디스크 중심 아키텍처에서 발생하는 오버헤드 없이 가용한 메모리보다 더 큰 데이터셋을 지원하게끔 확장할 수 있다.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>안티 캐싱(anti-caching)&lt;/strong>&lt;br>
메모리가 충분하지 않을 때 가장 최근에 사용하지 않은 데이터를 메모리에서 디스크로 보내고 나중에 다시 접근할 때 메모리에 적재하는 방식으로 동작&lt;/p>
&lt;/blockquote>
&lt;p>운영체제가 가상 메모리와스왑 파일에서 수행하는 방식과 유사하지만 데이터베이스는 전체 메모리 페이지보다 개별 레코드 단위로 작업할 수 있기 때문에 OS보다 효율적으로 메모리를 관리할 수 있다.&lt;/p>
&lt;p>하지만 이 접근 방식은 여전히 전체 색인이 메모리에 있어야 한다.&lt;/p></description></item></channel></rss>