RocksDB
- TTL eviction: 한번 eviction 한다고 용량이 정리되지 않고, 다시 불러온 뒤 iteration 을 돌려야 정리되는 듯 함
B) RocksDB 재설치
aurochs 용 RocksDB 컴파일
rm -rf /usr/lib/librocksdb.so*
git clone -b v4.6.1 --depth 1 https://github.com/facebook/rocksdb.git rocksdb.git
cd rocksdb.git
make shared_lib && make install-shared INSTALL_PATH=/usrC) RocksDB 란 무엇인가?
“RocksDB 는 Facebook(현 Meta) 에서 개발한 오픈소스 고성능 임베디드 Key-Value 데이터베이스 라이브러리입니다.”
여기서 핵심 키워드는 **‘임베디드 (Embedded)’**와 ‘데이터베이스 (Database)’ 입니다.
- 임베디드 (Embedded) 라이브러리: Redis 나 MySQL 처럼 별도의 서버로 동작하는 것이 아니라, 애플리케이션 코드에 라이브러리 형태로 포함되어 직접 실행됩니다. 이로 인해 네트워크 통신 오버헤드가 없어 매우 빠른 로컬 데이터 처리가 가능합니다.
- Key-Value 데이터베이스: 단순한 캐시가 아니라, 데이터를 디스크 (주로 SSD) 에 영속적으로 (persistently) 저장하는 데이터베이스입니다. 애플리케이션이 종료되거나 서버가 재시작되어도 데이터는 안전하게 보존됩니다.
가장 중요한 기술적 특징은 LSM Tree (Log-Structured Merge-Tree) 자료구조를 기반으로 한다는 점입니다. 이는 쓰기 작업에 매우 최적화된 구조입니다.
- 쓰기 과정 (간략히):
- MemTable: 모든 쓰기 요청은 일단 메모리에 있는 MemTable 에 기록됩니다 (그래서 빠릅니다).
- SSTable: MemTable 이 가득 차면, 정렬된 상태로 디스크에 있는 SSTable(Sorted String Table) 파일로 옮겨집니다.
- Compaction: 백그라운드에서 여러 계층 (Level) 의 SSTable 파일들을 효율적으로 병합하여 읽기 성능을 최적화하고 공간을 절약합니다.
D) 기본적인 Key-Value Cache 대비 RocksDB 를 사용하는 이유
이 질문의 핵심은 “Cache”와 “Database”의 근본적인 차이를 이해하고 있는가 입니다.
“저희가 일반적인 Key-Value 캐시 대신 RocksDB 를 사용한 이유는, 단순히 빠른 접근 속도뿐만 아니라 데이터의 영속성, 대용량 데이터 처리, 그리고 쓰기 중심의 워크로드라는 요구사항을 만족해야 했기 때문입니다.”
아래 표와 같이 두 기술의 특징을 명확히 비교하여 설명할 수 있습니다.
| 항목 | 일반 Key-Value Cache (예: Redis, Memcached) | RocksDB |
|---|---|---|
| 주요 목적 | 데이터의 일시적 보관 (캐싱) | 데이터의 영속적 저장 (데이터베이스) |
| 데이터 저장소 | 주로 메모리 (In-Memory) | 주로 디스크 (SSD/Flash Storage) |
| 데이터 영속성 | 휘발성 (Volatile) (재시작 시 데이터 소멸) | 영속성 (Persistent) (재시작 시 데이터 유지) |
| 처리 가능 용량 | RAM 크기에 제한 | 디스크 크기까지 확장 가능 (RAM 보다 훨씬 큰 용량) |
| 아키텍처 | 독립된 서버 (Standalone Server) | 애플리케이션에 내장되는 라이브러리 (Embedded) |
| 주요 강점 | 극도로 빠른 읽기/쓰기 속도 (메모리 기반) | 매우 높은 쓰기 처리량, 대용량 데이터 관리 |
이러한 차이점을 바탕으로 RocksDB 를 선택한 이유를 아래와 같이 구체적인 시나리오로 설명할 수 있습니다.
-
데이터 영속성 보장이 필요했습니다.
“저희 서비스는 사용자의 상태 정보나 중간 계산 결과 등을 단순히 빠르게 접근하는 것을 넘어, 시스템 장애나 재시작 시에도 데이터가 유실되지 않도록 보장해야 했습니다. 인메모리 캐시는 이런 영속성을 보장할 수 없었기에, 디스크 기반의 영속성을 가진 RocksDB 가 적합했습니다.”
-
RAM 용량을 초과하는 대용량 데이터를 다뤄야 했습니다.
“처리해야 할 데이터의 크기가 수십, 수백 기가바이트에 달해 가용 RAM 안에 모두 저장하는 것은 불가능했습니다. RocksDB 는 데이터를 SSD 에 저장하므로 RAM 용량에 구애받지 않고 대용량 데이터를 안정적으로 관리할 수 있었습니다.”
-
쓰기 작업이 매우 빈번한 환경이었습니다.
“저희 시스템은 초당 수천 건 이상의 로그나 이벤트 데이터가 지속적으로 발생하는 쓰기 중심 (Write-Heavy) 의 워크로드를 가집니다. RocksDB 의 LSM Tree 아키텍처는 쓰기 요청을 메모리에 먼저 기록하고 디스크에는 순차적으로 기록하므로, 랜덤 쓰기 (Random Write) 에 대한 부하가 적어 이러한 환경에서 뛰어난 성능을 보였습니다.”
-
낮은 지연 시간 (Low Latency) 이 중요했습니다.
“데이터베이스와 통신하기 위한 네트워크 지연 시간을 최소화해야 했습니다. RocksDB 는 애플리케이션에 내장되는 라이브러리 형태이므로 별도의 네트워크 호출 없이 함수 호출만으로 데이터에 접근할 수 있어, 필수적인 지연 시간을 줄일 수 있었습니다.”
E) RocksDB vs. sqlite
inode 소모와 파일 시스템의 부하 때문에 SQLite 가 RocksDB 보다 훨씬 나은 선택이었을 것입니다.
이제 inode 키워드를 중심으로 자세히 설명해 드리겠습니다.
E.1) 1. Inode 란 무엇인가? (문제의 근원)
리눅스/유닉스 파일 시스템에서 inode(Index Node) 는 개별 파일이나 디렉터리에 대한 모든 메타데이터를 저장하는 데이터 구조입니다.
-
inode 정보: 파일의 권한, 소유자, 크기, 생성/수정 시간, 그리고 가장 중요하게는 데이터가 디스크의 어느 블록에 저장되어 있는지에 대한 포인터를 담고 있습니다.
-
핵심: 모든 파일과 디렉터리는 자신만의 고유한 inode 를 가집니다. 파일 시스템은 디스크 공간과 별개로 inode 의 총개수가 정해져 있으며, 작은 파일이 아무리 많아져도 inode 를 모두 소진하면 더 이상 파일을 생성할 수 없습니다.
E.2) 왜 RocksDB 에서 성능 저하가 발생했는가? (inode 관점)
질문 주신 “아이템별로 DB 를 두고 피드백을 적재”하는 아키텍처가 결정적인 원인입니다.
RocksDB 는 단일 파일 데이터베이스가 아닙니다. 하나의 RocksDB “인스턴스”는 실제로는 여러 파일로 구성된 디렉터리입니다.
- MANIFEST 파일: 데이터베이스의 상태 및 버전 정보를 관리합니다.
- WAL (Write-Ahead Log) 파일: 쓰기 요청을 먼저 기록하여 내구성을 보장합니다.
- 다수의 SSTable (.sst) 파일: 실제 데이터가 정렬되어 저장되는 불변 (immutable) 파일들입니다. 데이터가 쌓이고 Compaction 이 발생할 때마다 이 파일들은 계속 생성되고 삭제됩니다.
상황 시뮬레이션:
-
아이템 100 만 개: 추천 시스템에 100 만 개의 아이템이 있다고 가정합니다.
-
RocksDB 인스턴스 생성: 아이템별로 RocksDB 인스턴스를 생성하면, 100 만 개의 RocksDB 디렉터리가 생깁니다.
-
파일 개수 폭증: 각 RocksDB 인스턴스가 최소 5~10 개의 파일 (WAL, MANIFEST, 여러 SSTable 등) 을 가진다고 가정하면, 시스템에는 순식간에 500 만 ~ 1,000 만 개의 파일이 생성됩니다.
-
inode 고갈 및 파일 시스템 과부하:
-
inode 소모: 파일 개수가 수백만, 수천만 개로 늘어나면서 파일 시스템의 inode 를 급격히 소진시킵니다.
-
파일 시스템 성능 저하: 운영체제 (OS) 가 특정 파일을 찾으려면 거대한 디렉터리 구조를 탐색하고 수많은 inode 를 읽어야 합니다. 이는 파일 열기 (open), 읽기 (read), 쓰기 (write) 등 모든 파일 관련 시스템 콜의 속도를 저하시킵니다. 이것이 바로 시스템 전체가 느려지는 근본적인 원인입니다.
-
-
OS 파일 디스크립터 한계: 각 프로세스가 열 수 있는 파일의 개수 (file descriptor limit) 는 정해져 있습니다. 수많은 RocksDB 인스턴스를 동시에 관리하려면 이 한계에 부딪히기 매우 쉽습니다.
결론적으로, RocksDB 의 LSM Tree 아키텍처 자체의 문제가 아니라, RocksDB 의 동작 방식을 고려하지 않은 “하나의 아이템 = 하나의 인스턴스” 설계가 파일 시스템 수준의 병목을 일으킨 것입니다.
E.3) 왜 이 상황에서는 SQLite 가 더 나았을까?
동일한 “아이템별 DB” 아키텍처를 가정했을 때, SQLite 는 이 문제를 훨씬 잘 피해갈 수 있습니다.
SQLite 는 진정한 단일 파일 데이터베이스입니다.
- 하나의 SQLite 데이터베이스는 기본적으로 단 하나의 파일(.db 파일) 로 모든 것을 관리합니다. (WAL 모드를 사용하면 -wal, -shm 파일이 추가로 생기지만, RocksDB 와는 비교할 수 없을 정도로 적습니다.)
상황 비교:
-
RocksDB: 100 만 개 아이템 → ~1,000 만 개 파일 → inode 고갈, 파일 시스템 마비
-
SQLite: 100 만 개 아이템 → 100 만 개 파일 → 파일 수는 여전히 많지만 RocksDB 에 비해 1/10 수준으로 inode 부담이 훨씬 적고 관리가 용이합니다.
따라서 해당 아키텍처에서는 SQLite 가 파일 시스템에 주는 부담이 훨씬 적어 더 안정적이고 예측 가능한 성능을 보였을 것입니다.
E.4) 아키텍처 비교 및 올바른 설계
| 항목 | 잘못된 설계 (Item per RocksDB) | 더 나은 설계 (Item per SQLite) | 최적의 설계 (Single RocksDB/SQLite) |
| 구조 | 1 아이템 = 1 RocksDB 인스턴스 | 1 아이템 = 1 SQLite DB 파일 | 1 개의 DB 인스턴스 내에서 모든 데이터 관리 |
| 파일/inode 수 | N (아이템 수) * M (파일/인스턴스) → 극도로 많음 | N (아이템 수) → 많음 | 상수 (적음) |
| 문제점 | inode 고갈, 파일 시스템 전체 성능 저하, OS 한계 도달 | inode 부담은 덜하지만, 여전히 파일 관리가 복잡함 | OS 및 파일 시스템 자원을 효율적으로 사용 |
| 데이터 관리 | 수백만 개의 DB 를 애플리케이션이 직접 관리 (불가능에 가까움) | 수백만 개의 DB 파일을 관리 (복잡함) | 단일 DB 내에서 Key 설계를 통해 관리 (효율적) |
E.4.1) 최적의 설계: 단일 DB 인스턴스와 Composite Key
사실 이 문제의 가장 이상적인 해결책은 하나의 거대한 RocksDB (또는 SQLite) 인스턴스만 사용하는 것입니다.
-
Key 설계: 아이템 ID 를 Key 의 일부로 사용합니다.
- 예: [ItemID]:[FeedbackTimestamp] 또는 [ItemID]:[UserID] 형태의 **복합 키 (Composite Key)**를 사용합니다.
-
데이터 조회: 특정 아이템의 모든 피드백을 가져오려면 RocksDB 의 Prefix Scan 기능을 사용합니다. ItemID 를 prefix 로 지정하여 해당 아이템으로 시작하는 모든 Key-Value 쌍을 효율적으로 스캔할 수 있습니다.
이 방식을 사용하면, 수억 개의 피드백이 있더라도 파일 시스템은 단 몇십, 몇백 개의 파일만 관리하면 되므로 inode 문제는 완벽하게 해결되며 RocksDB 의 Compaction 메커니즘도 전체 데이터에 대해 최적으로 동작할 수 있습니다.
E.5) 요약
과거 추천 시스템에서 아이템별로 RocksDB 인스턴스를 생성하여 피드백을 저장했을 때, 초기에는 빨랐지만 아이템 수가 증가함에 따라 시스템 전체 성능이 급격히 저하되는 문제를 겪었습니다.
그 원인은 inode 관점에서 설명할 수 있습니다. RocksDB 는 단일 파일이 아니라, 다수의 SSTable 파일 등으로 구성된 디렉터리 구조입니다. 따라서 아이템마다 RocksDB 인스턴스를 생성하는 것은 수백만 개의 아이템에 대해 수천만 개의 파일을 생성하는 결과를 낳았습니다. 이는 파일 시스템의 inode 를 빠르게 소진시키고, 모든 파일 작업에 대한 OS 수준의 오버헤드를 유발하여 시스템을 마비시키는 원인이 되었습니다.
만약 동일한 아키텍처를 유지해야 했다면, 단일 파일로 동작하는 SQLite 가 훨씬 나은 선택이었을 것입니다. SQLite 는 아이템당 하나의 파일만 생성하므로 inode 와 파일 시스템에 가해지는 부하가 RocksDB 에 비해 훨씬 적기 때문입니다.
하지만 이 경험을 통해 근본적으로는 “하나의 아이템 = 하나의 DB 인스턴스”라는 아키텍처 자체가 문제였음을 배웠습니다. 최적의 방법은 단일 RocksDB 인스턴스를 두고, ItemID 를 포함한 복합 키 (Composite Key) 를 사용하여 모든 피드백을 저장하는 것이었습니다. 이 방식은 RocksDB 의 Prefix Scan 기능을 활용해 아이템별 조회를 효율적으로 처리하면서도, 파일 시스템의 부하를 최소화하여 확장성과 성능을 모두 잡을 수 있는 올바른 설계입니다.