테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

키-값: Redis·RocksDB·etcd

키로 값을 바로 찾는 단순한 모델이다. 메모리 캐시, 임베디드 저장 엔진, 합의 기반 설정 저장소로 쓰임새가 갈린다.

목차 (8)
  1. Redis: 메모리 위의 자료구조 서버
  2. Redis: RDB·AOF 영속성과 복제
  3. Redis Cluster: 16384개의 해시 슬롯
  4. Redis 라이선스와 Valkey
  5. RocksDB: 앱 안에 넣는 LSM 저장 엔진
  6. RocksDB: compaction 방식 고르기
  7. etcd: 합의로 지키는 설정 저장소
  8. 고르는 법

Redis: 메모리 위의 자료구조 서버

키-값 데이터베이스의 질문은 하나다. "이 키의 값은?" 조인도 복잡한 조건도 없으니 키만 알면 곧바로 값에 닿고, 그만큼 빠르고 나누기 쉽다. 같은 모델 위에서도 데이터를 메모리에 두느냐, 디스크에 두느냐, 여러 대가 합의하느냐에 따라 쓰임새가 완전히 달라진다. 이 노트의 세 엔진이 그 세 갈래다.

Redis는 모든 데이터를 메모리에 두는 서버다. 값이 바이트 덩어리에 그치지 않고 자료구조라는 점이 특징이다. 문자열, 해시, 리스트, 집합, 점수로 정렬되는 정렬 집합, 추가 전용 로그인 스트림 같은 자료형마다 전용 명령이 있어, "순위표에 점수를 더하고 상위 10명을 달라" 같은 일을 서버 안에서 한 번에 처리한다.

Redis 실행 구조: 여러 클라이언트의 소켓을 I/O 스레드가 읽고 파싱하며 응답을 쓴다. 명령은 메인 스레드 하나가 줄 선 순서대로 하나씩 실행해 메모리의 자료구조(문자열·해시·리스트·집합·스트림)를 바꾼다클라이언트소켓소켓소켓소켓I/O 스레드읽기 · 파싱응답 쓰기읽기 · 파싱응답 쓰기메인 스레드 1개INCR viewsHSET user:7 …ZADD rank …GET views메모리문자열해시리스트집합 · 정렬 집합스트림명령은 하나씩 끝까지 실행된다 → 명령 하나는 원자적이다대신 오래 걸리는 명령 하나(KEYS *, 큰 집합 연산)가 모두를 기다리게 한다
Redis의 명령 실행은 스레드 하나다. 네트워크 읽기·쓰기는 I/O 스레드에 나눌 수 있지만, 자료구조를 만지는 것은 언제나 메인 스레드 하나가 줄 선 순서대로 한다.

명령은 메인 스레드 하나가 이벤트 루프로 하나씩 처리한다. 락이 필요 없어 명령 하나하나가 저절로 원자적이고, 메모리 접근만 하므로 한 명령은 마이크로초 단위로 끝난다. 병목은 대개 CPU가 아니라 네트워크와 시스템 호출이라 6.0부터는 소켓 읽기·파싱과 응답 쓰기를 I/O 스레드에 나눠 맡길 수 있게 했다. 반대로 말하면 큰 키 하나를 지우거나 KEYS *처럼 전체를 훑는 명령은 그동안 다른 모든 클라이언트를 세운다. 여러 명령을 묶을 때는 MULTI/EXEC 트랜잭션이나 Lua 스크립트를 쓰며, 이것도 실행되는 동안 다른 명령이 끼어들지 않는다는 뜻의 원자성이지 롤백은 없다.

Redis: RDB·AOF 영속성과 복제

메모리에만 두면 꺼질 때 다 사라진다. Redis는 두 가지 영속성을 준다. RDB는 정한 간격마다 메모리 전체를 파일 하나로 떠 두는 스냅샷이다. Redis가 fork()로 자식 프로세스를 만들면 운영체제의 copy-on-write 덕에 자식은 그 순간의 메모리를 그대로 보며 파일을 쓰고, 부모는 계속 요청을 받는다. AOF는 쓰기 명령을 받는 대로 파일 끝에 붙이는 로그로, 다시 켤 때 처음부터 재생한다.

Redis의 RDB와 AOF: RDB는 Redis가 fork한 자식 프로세스가 copy-on-write로 메모리를 나눠 보며 dump.rdb 스냅샷을 쓰므로, 꺼지면 마지막 스냅샷 이후의 변경을 잃는다. AOF는 쓰기 명령을 파일 끝에 이어 쓰고 기본 설정에서 1초마다 fsync하므로 잃는 것은 최대 약 1초다. AOF는 base 파일과 증분 파일, 목록 파일(manifest)로 나뉜다RDB 스냅샷Redis 부모계속 요청 처리fork한 자식copy-on-writedump.rdb스냅샷스냅샷꺼짐잃는 것: 마지막 스냅샷 이후 전부파일이 작고 재시작·백업이 빠르다AOF (append-only file)SET a 1INCR aHSET u …DEL b쓰기 명령마다 파일 끝에 붙인다 →baseincrmanifest다시 쓰기(rewrite) 뒤 7.0부터 여러 파일로 나뉜다fsync: 1초마다 (기본 everysec)잃는 것: 최대 약 1초 (always면 0)둘을 함께 켜면 재시작 때 AOF를 쓴다
RDB는 작고 빠르게 복구되지만 마지막 스냅샷 이후를 잃는다. AOF는 기본 설정에서 1초 이내만 잃는다. 둘 다 켜는 것이 권장 조합이다.

AOF의 내구성은 fsync 정책이 정한다. 기본값 everysec는 1초마다 백그라운드 스레드가 fsync하므로 최악에 1초 분량을 잃고, always는 명령마다 fsync해 잃지 않지만 훨씬 느리다. AOF는 계속 길어지므로 Redis는 현재 상태를 만드는 가장 짧은 명령들로 파일을 다시 쓴다(rewrite). 7.0부터는 이 결과가 base 파일, 그 뒤의 증분 파일, 둘을 묶는 manifest로 나뉜다.

복제는 기본이 비동기다. 복제본이 처음 붙으면 주 서버가 RDB를 만들어 보내고(전체 동기화), 그 뒤로는 쓰기 명령의 흐름을 그대로 보낸다. 잠깐 끊겼다 다시 붙으면 복제 ID와 오프셋으로 놓친 부분만 백로그에서 받아 간다. 주 서버가 쓰기에 응답한 뒤 복제본에 닿기 전에 죽으면 그 쓰기는 사라진다. WAIT 명령으로 복제본 몇 대가 받았는지 기다릴 수 있지만, 그래도 강한 일관성을 약속하지는 않는다. 장애 때 복제본을 승격하는 일은 Sentinel이나 아래의 Cluster가 맡는다.

Redis Cluster: 16384개의 해시 슬롯

한 대의 메모리를 넘으면 데이터를 나눠야 한다. Redis Cluster 명세는 키 공간을 해시 슬롯 16384개로 나눈다. 키의 슬롯은 CRC16(키)를 16384로 나눈 나머지이고, 슬롯 구간을 마스터마다 나눠 맡는다. 노드를 더하면 키를 하나하나 재해시하지 않고 슬롯 단위로 옮긴다.

Redis Cluster 해시 슬롯: 키마다 CRC16 값을 16384로 나눈 나머지가 슬롯 번호이고, 슬롯 0~16383을 마스터 A·B·C가 구간으로 나눠 맡는다. 중괄호 해시 태그가 같은 두 키는 중괄호 안만 해시하므로 같은 슬롯, 같은 마스터에 들어간다. 마스터마다 복제본이 있다user:3슬롯 2648user:2슬롯 6777{user:1}:cart슬롯 10778{user:1}:orders슬롯 10778order:1슬롯 14374슬롯 0–5460마스터 A + 복제본슬롯 5461–10922마스터 B + 복제본슬롯 10923–16383마스터 C + 복제본슬롯 = CRC16(키) mod 16384, {중괄호}가 있으면 그 안만 해시한다여러 키를 한 명령에 쓰려면 같은 슬롯이어야 한다 — 해시 태그로 묶는다
키 다섯 개의 실제 슬롯 번호(CRC16 mod 16384로 계산). {user:1}이라는 해시 태그를 공유하는 두 키는 중괄호 안만 해시하므로 같은 슬롯에 들어간다.

클라이언트는 슬롯 → 노드 표를 들고 있다가 키를 맞는 노드로 바로 보낸다. 표가 낡았으면 노드가 MOVED 응답으로 바른 노드를 알려 주고, 슬롯을 옮기는 중이면 ASK로 잠시 다른 노드에 물어보게 한다. 여러 키를 한 명령에 쓰는 MGET이나 트랜잭션은 키가 모두 같은 슬롯일 때만 된다. 그래서 함께 다룰 키에는 {user:1}처럼 같은 해시 태그를 붙인다. 노드끼리는 별도 버스로 상태를 주고받다가 마스터가 죽으면 그 복제본 하나를 승격하는데, 복제가 비동기이므로 명세 스스로 "응답을 받은 쓰기도 잃을 수 있다"고 밝힌다.

메모리가 maxmemory에 닿으면 축출 정책(eviction policy)이 정한 대로 한다. 기본값 noeviction은 새 쓰기를 오류로 거부하고, 캐시로 쓸 때는 allkeys-lru나 allkeys-lfu로 덜 쓰는 키를 지운다. 이때 LRU는 모든 키를 줄 세우지 않고 몇 개를 표본으로 뽑아 그중 가장 오래된 것을 지우는 근사다.

Redis 라이선스와 Valkey

Redis는 오랫동안 BSD 라이선스였다. 2024년 3월 20일 Redis사는 7.4부터 RSALv2와 SSPLv1 이중 라이선스로 바꾼다고 발표했다. 둘 다 소스는 공개하지만 클라우드 사업자가 그대로 관리형 서비스로 파는 것을 막는 조건이 있어 OSI 기준의 오픈 소스가 아니다. 8일 뒤인 3월 28일 리눅스 재단이 Valkey를 발표했다. 마지막 BSD 판인 Redis 7.2.4에서 갈라져 나온 포크로, 기존 메인테이너 일부와 여러 클라우드 회사가 참여했고 지금도 BSD-3 라이선스다.

Redis 라이선스 변화: Redis는 2009년부터 BSD-3였다가 2024년 3월 20일 7.4부터 RSALv2와 SSPLv1 이중 라이선스로 바뀌었다. 2024년 3월 28일 리눅스 재단이 마지막 BSD 판인 7.2.4에서 갈라진 Valkey를 발표했고 Valkey는 BSD-3를 유지한다. 2025년 5월 1일 Redis 8부터 AGPLv3가 세 번째 선택지로 더해졌다RedisValkey마지막 BSD 판2009BSD-3처음 공개2024.3.20RSALv2 / SSPLv17.4부터2025.5.1+ AGPLv3Redis 8부터 셋 중 고름2024.3.28Valkey (BSD-3)7.2.4에서 갈라짐AGPLv3는 OSI 승인 오픈 소스
Redis와 Valkey의 갈림길. 2025년 5월부터 Redis는 AGPLv3를 포함한 세 라이선스 중 하나를 고르게 하고, Valkey는 BSD로 따로 발전한다.

약 1년 뒤 Redis 8부터 AGPLv3가 세 번째 선택지로 더해졌다(2025년 5월 1일). AGPLv3는 OSI가 인정하는 오픈 소스 라이선스지만, 고쳐서 네트워크 서비스로 제공하면 고친 소스를 공개해야 하는 강한 카피레프트다. 그래서 지금은 "Redis 8(AGPLv3 등 셋 중 선택)"과 "Valkey(BSD)"가 나란히 있고, 2024년 이후의 새 기능은 두 쪽이 각자 만든다. 명령과 프로토콜은 대부분 호환되지만 갈라진 뒤에 생긴 기능은 서로 다를 수 있으니, 관리형 서비스를 고를 때 어느 쪽인지 확인한다. (2026년 10월 기준)

RocksDB: 앱 안에 넣는 LSM 저장 엔진

RocksDB는 서버가 아니라 라이브러리다. Facebook(지금의 Meta)이 Google의 LevelDB를 바탕으로 빠른 SSD와 많은 코어에 맞춰 다시 만든 임베디드 키-값 저장소이고, 정렬된 바이트 키와 바이트 값을 Put·Get·Delete·범위 반복자로 다룬다. 다른 데이터베이스의 아래층으로 많이 쓰인다. MySQL의 MyRocks, 분산 키-값 저장소 TiKV, Kafka Streams의 상태 저장소, Ceph BlueStore의 메타데이터가 RocksDB 위에 있다.

구조는 저장 엔진 노트의 LSM 트리 그대로다. 쓰기는 WAL과 메모리의 MemTable에서 끝나고, 디스크의 정렬된 파일(SST)은 한 번 쓰면 고치지 않는다.

RocksDB 단계 그림: 쓰기는 WAL과 MemTable에 들어가고, 가득 찬 MemTable은 불변이 되어 L0 SST 파일로 flush되며, compaction이 L0와 L1을 병합한다. 읽기는 MemTable부터 아래 레벨까지 차례로 찾는다메모리MemTable불변 MemTableWAL (디스크)디스크의 SST 파일L0L1L2범위 겹침① WAL 기록② MemTable에 넣기새 SST병합 정렬 후 다시 쓰기① 메모리부터② 블룸 필터로 거르며 아래 레벨로
1 / 5

Put(k, v). 먼저 WAL 파일 끝에 이어 쓰고, 메모리의 MemTable(기본은 스킵 리스트)에 넣으면 끝이다. 디스크의 정렬된 파일은 건드리지 않아 쓰기가 빠르다.

LSM 트리의 쓰기는 메모리와 순차 로그에서 끝나고, 정리는 뒤에서 compaction이 한다. 읽기는 그 대가로 여러 곳을 위에서부터 찾아 내려간다.

Column family는 한 DB 안의 독립된 키 공간이다. 저마다 MemTable과 SST 파일, 설정(압축, compaction 방식)을 따로 갖지만 WAL은 함께 쓴다. 그래서 여러 column family에 걸친 쓰기 묶음(WriteBatch)도 원자적으로 커밋되고, 성격이 다른 데이터(예: 자주 바뀌는 메타데이터와 큰 값)를 한 DB 안에서 다르게 튜닝할 수 있다.

RocksDB: compaction 방식 고르기

LSM의 성능은 compaction이 정한다. 같은 데이터를 몇 번 다시 쓰는지(쓰기 증폭), 지워질 옛 값이 얼마나 자리를 차지하는지(공간 증폭), 키 하나를 찾으려 몇 군데를 봐야 하는지(읽기 증폭)는 하나를 줄이면 다른 하나가 는다. RocksDB는 compaction 방식을 골라 이 균형을 정하게 한다.

RocksDB compaction 방식 비교: Leveled는 레벨마다 크기가 약 10배씩 커지고 한 레벨 안의 파일 범위가 겹치지 않아 읽기와 공간 낭비가 적지만 같은 데이터를 여러 번 다시 쓴다. Universal은 비슷한 크기의 정렬된 묶음끼리 합쳐 다시 쓰기가 적지만 공간을 더 쓴다. FIFO는 병합하지 않고 가장 오래된 파일을 지운다Leveled (기본)UniversalFIFOL1L2L3아래로 갈수록 약 10배작은 묶음합친 묶음비슷한 크기끼리 합친다버림새 파일오래된 것부터 지운다LeveledUniversalFIFO다시 쓰기(쓰기 증폭)공간 낭비(공간 증폭)찾을 곳 수(읽기 증폭)크다작다작다작다크다중간거의 없음정한 크기크다
세 compaction 방식이 쓰기·공간·읽기 증폭을 서로 다르게 맞바꾼다. 어느 것이 낫다기보다 일의 성격에 맞춰 고른다.
  • Leveled(기본): 레벨마다 전체 크기를 약 10배씩 키우고, L1부터는 한 레벨 안에서 파일 범위가 겹치지 않게 유지한다. 키 하나는 레벨마다 많아야 한 파일에 있어 읽기가 싸고 공간 낭비도 적지만, 같은 데이터가 레벨을 내려갈 때마다 다시 쓰인다.
  • Universal: 비슷한 크기의 정렬된 묶음끼리만 합친다(tiered). 다시 쓰기가 적어 쓰기가 아주 많은 일에 맞지만, 합치는 동안 옛 묶음과 새 묶음이 함께 있어 최악에는 공간이 두 배까지 든다.
  • FIFO: 합치지 않고 전체 크기가 한도를 넘으면 가장 오래된 파일을 통째로 지운다. 일정 기간만 두는 로그·지표처럼 지우기를 시간이 대신해 주는 데이터에 쓴다.

etcd: 합의로 지키는 설정 저장소

etcd는 작지만 절대 틀리면 안 되는 데이터를 위한 키-값 저장소다. 노드 3대나 5대가 Raft 합의로 같은 순서의 로그를 나눠 갖기 때문에, 과반이 살아 있는 한 어느 노드에 물어도 같은 답(선형화 가능한 읽기)을 준다. Raft 자체의 동작은 복제·샤딩·합의 노트에 있다. 대표 사용자는 Kubernetes다. 클러스터의 모든 객체(파드, 서비스, 설정)가 etcd에 들어 있고, kube-apiserver만 etcd와 직접 이야기한다.

etcd 쓰기 단계 그림: kube-apiserver의 PUT을 리더가 받아 Raft 로그에 붙이고 팔로워 둘에 복제한다. 과반이 기록하면 커밋되고, 상태 머신에 적용되며 revision이 42로 오른다. 그 변경이 watch 중인 컨트롤러에 이벤트로 전달된다kube-apiserver컨트롤러watch 중리더Raft 로그MVCC 저장소(bbolt)revision 41revision 42팔로워 1같은 로그, 같은 순서팔로워 2같은 로그, 같은 순서팔로워 2는 아직 (늦어도 괜찮다)PUT3대 중 2대 기록 → 커밋적용: (키, rev 42) 새 판 저장watch 이벤트: PUT, rev 42
1 / 5

kube-apiserver가 PUT /registry/pods/default/web을 보낸다. 쓰기는 늘 리더가 받는다. 팔로워에 닿으면 리더에게 넘긴다.

etcd는 Raft로 모든 노드가 같은 순서의 로그를 갖게 하고, 그 로그를 MVCC 저장소에 적용하며 revision을 하나씩 올린다. watch는 그 revision의 흐름을 구독하는 것이다.

etcd의 데이터 모델은 MVCC다. 쓰기가 커밋될 때마다 클러스터 전체에 하나뿐인 revision이 1씩 오르고, 키의 새 판은 (키, revision)으로 B+트리 파일 저장소(bbolt)에 쌓인다. 그래서 "revision 1000 시점의 값"을 읽거나, 값이 내가 읽은 revision 그대로일 때만 바꾸는 트랜잭션(compare-and-swap)을 쓸 수 있다. watch는 이 revision 흐름을 구독하는 API다. 끊겼다 다시 붙어도 마지막으로 본 revision 다음부터 받으므로 변경을 놓치지 않는다. Kubernetes 컨트롤러가 "원하는 상태"와 "지금 상태"를 맞추는 방식이 이 watch 위에 서 있다. 시간이 지나면 끝나는 lease를 키에 붙여 리더 선출이나 생존 신호에도 쓴다.

옛 판은 compaction으로 지우기 전까지 남고, compaction 뒤에도 파일의 빈자리는 defrag를 해야 돌아온다. etcd는 큰 데이터를 담는 용도가 아니다. 공식 한도는 요청 하나 1.5MiB, 저장소 기본 2GiB이고 권장 최대는 8GiB다. 쓰기마다 과반의 디스크 fsync를 기다리므로 디스크 지연이 곧 쓰기 지연이다.

고르는 법

키-값 엔진 고르기: 잃어도 다시 만들 수 있는 캐시·세션인가? 예면 Redis / Valkey. 작지만 모든 노드가 같은 값을 봐야 하나? 예면 etcd. 직접 만드는 시스템의 로컬 저장 엔진인가? 예면 RocksDB. 모두 아니면 원본 데이터는 관계형·문서 DB에 둔다잃어도 다시 만들 수 있는 캐시·세션인가?예Redis / Valkey아니오작지만 모든 노드가 같은 값을 봐야 하나?예etcd아니오직접 만드는 시스템의 로컬 저장 엔진인가?예RocksDB아니오원본 데이터는 관계형·문서 DB에 둔다
키-값 엔진을 고르는 순서. 먼저 데이터를 잃어도 되는지, 모든 노드가 같은 값을 봐야 하는지를 묻는다.
기준Redis / ValkeyRocksDBetcd
형태네트워크 서버앱에 넣는 라이브러리3·5대 클러스터 서버
데이터 위치메모리(디스크는 복구용)디스크(SSD), LSM디스크, B+트리(bbolt)
값자료구조(해시·정렬 집합·스트림 등)바이트, 정렬된 키로 범위 조회바이트, 판(revision)과 watch
일관성비동기 복제, 장애 때 최근 쓰기 유실 가능한 프로세스 안, 복제는 위층 몫Raft 합의, 선형화 가능
크기메모리 한도(Cluster로 나눔)디스크 한도까지 TB 단위수 GiB(권장 8GiB 이하)
잘 맞는 곳캐시, 세션, 순위표, 속도 제한, 큐다른 DB·서비스의 저장 계층, 쓰기가 많은 로컬 저장설정, 서비스 발견, 리더 선출, 분산 락

"빠른 키-값"이 필요하면 먼저 데이터를 잃어도 되는지 묻는다. 다시 만들 수 있는 캐시라면 Redis(또는 Valkey)가 거의 늘 답이다. 원본 데이터라면 Redis를 주 저장소로 쓰기 전에 유실 창을 받아들일 수 있는지 따지고, 작지만 모든 노드가 같은 값을 봐야 하는 조정용 데이터라면 etcd 같은 합의 저장소를 쓴다. 직접 저장 엔진을 품은 시스템을 만든다면 RocksDB가 출발점이다.