
kioku-mesh #3 - kioku-mesh의 내부 이해 — Zenoh와 RocksDB와 SQLite index
요약
kioku-mesh의 내부 아키텍처를 분석하여 Zenoh, RocksDB, SQLite 간의 데이터 흐름과 역할을 설명합니다. Zenoh와 RocksDB를 신뢰할 수 있는 단일 원천(Source of Truth)으로, SQLite를 효율적인 검색을 위한 읽기 캐시로 설계한 비대칭 구조를 다룹니다.
핵심 포인트
- Zenoh와 RocksDB는 데이터의 영속성을 보장하는 단일 원천임
- SQLite는 SQL 기반의 복잡한 검색 쿼리를 처리하기 위한 읽기 캐시 역할
- 쓰기 경로는 Zenoh를 거쳐 RocksDB로 흐르며, 읽기는 로컬 SQLite를 활용함
- SQLite 데이터가 손실되어도 RocksDB를 통해 복구가 가능한 구조
이번 스코프
연재 제4회에서 메쉬(Mesh)를 구성할 때, 아무것도 모르는 상태에서 init --mode hub부터 시작해도 동작은 하지만, 트러블슈팅(Troubleshoot) 시에 "지금 고장 난 곳은 어느 계층인가?"를 판단할 수 없습니다. 이번에는 그 부분을 명확히 하겠습니다.
이번 회차에서 다루는 것은 다음 3가지 포인트입니다.
- Zenoh + RocksDB가 신뢰할 수 있는 단일 원천(Source of Truth)이며, SQLite는 읽기 캐시(Read Cache)이다.
- 쓰기는 Zenoh → SQLite 순으로 흐른다.
local에서hub/spoke로 전환하면 SQLite의 위치가 바뀐다.
전체상 다시 보기
에이전트(Agent) 입장에서 보면, 쓰기와 읽기의 경로가 다릅니다.
- 쓰기: 에이전트 →
kioku-mesh-mcp→zenohd→ RocksDB - 읽기: 에이전트 →
kioku-mesh-mcp→ 로컬 SQLite
왜 비대칭으로 설계했는지에 대한 이야기는 다음과 같습니다.
왜 Zenoh + RocksDB가 Source of Truth인가
메쉬(Mesh)의 본질은 "여러 호스트 간에 동일한 관측(Observation)을 볼 수 있게 하는 것"입니다. 이를 성립시키는 계층은 다음을 모두 갖추고 있어야 합니다.
- 스토리지 백엔드(Storage Backend)에 영속화(Persistence)할 수 있어야 한다.
kioku-mesh는 이를 Zenoh + Zenoh Storage Backend (RocksDB)로 실현하고 있습니다.
Zenoh는 Key-Value 형식의 분산 Pub/Sub이며, Key는 계층적 경로(Hierarchical Path)입니다. kioku-mesh는 관측(Observation)에 다음과 같은 Key를 할당합니다 (docs/Spec.md §3).
mem/obs/{agent_family}/{client_id}/{pc_id}/{session_id}/{observation_id}
mem/tomb/{agent_family}/{client_id}/{pc_id}/{session_id}/{observation_id}
mem/obs/...가 관측(Observation) 본체mem/tomb/...는 동일한 경로 구조를 가진 툼스톤(Tombstone, 논리 삭제 마커)- 검색 시의 Identity 필터 (
agent_family/client_id/pc_id/session_id)는 그대로 이 경로 계층에 대한 필터링이 됩니다.
그리고 zenoh-backend-rocksdb가 mem/obs/**와 mem/tomb/**를 RocksDB에 영속화합니다. 클러스터 전체에서 정답인 것은 RocksDB 상의 값이며, 각 호스트의 SQLite는 거기서부터 다시 만들 수 있는 이차 자료(Secondary Data)라는 관계입니다. 특정 호스트의 SQLite가 고장 나더라도 RocksDB만 있으면 복구할 수 있습니다.
왜 SQLite가 별도로 존재하는가
"Source of Truth가 RocksDB라면, 검색도 거기에 직접 수행하면 되지 않을까?"라고 생각할 수 있지만, kioku-mesh는 의도적으로 분리했습니다. 이유는 두 가지입니다.
1. 검색 쿼리가 SQL에 적합하기 때문
에이전트가 사용하는 search는 다음과 같은 "전형적인 SQL 쿼리"입니다.
- 전체 텍스트와 유사한 부분 일치(Partial Match)
memory_type/importance/subject/project/created_at를 통한 필터링- 건수 제한 및 최신순 정렬
이는 RocksDB의 Key-Value 인터페이스에 직접 태우는 것보다, SQLite에서 솔직하게 작성할 수 있는 형태를 띠고 있습니다.
SQLite 측의 스키마도 obs_index 테이블이 observation_id/project/created_at/memory_type/importance/subject/summary/payload_json/deleted_at를 가지며, (project, created_at DESC)와 (created_at DESC) 인덱스를 생성하고 있습니다 (docs/Spec.md §6). 검색은 이곳으로 들어옵니다.
2. 로컬에서 완결되는 "빠른 읽기"가 필요하기 때문
에이전트의 대화 중에 매번 Zenoh로 왕복한다면, 로컬 참조임에도 불구하고 불필요한 지연(latency)이 발생합니다. 쓰기(write)의 후속 작업으로 채워지는 읽기 캐시(read cache)로서 SQLite를 가짐으로써, write는 반드시 Zenoh를 통하고(메시(mesh)에 전파하기 위해), read는 SQLite 로컬에서 완결되는(빠른) 역할 분담이 성립합니다. SQLite를 WAL 모드로 열고, busy_timeout=5000으로 다루며, 256 upsert마다 wal_checkpoint(TRUNCATE)를 수행하는 등의 운영상의 튜닝도 포함되어 있습니다.
쓰기는 Zenoh → SQLite 순으로 흐른다
docs/Spec.md §6의 흐름을 자세히 풀어서 설명하면 다음과 같습니다.
- 에이전트가
save_observation을 호출한다 kioku-mesh-mcp가 Zenoh에mem/obs/...를put한다- Zenoh로의
put이 성공한 후 SQLite에upsert한다 - 다른 피어(peer)로부터의 쓰기는
kioku-mesh-mcp가mem/obs/**/mem/tomb/**를 구독(subscribe)하여 SQLite로 가져온다
순서가 "Zenoh → SQLite"인 이유는, 메시(mesh)에 전파되지 않은 상태에서 로컬만 거짓된 검색 결과를 반환하지 않기 위해서입니다. 역으로 말하면, SQLite에 존재하는 관측(observation)은 반드시 Zenoh를 통해 확인한 적이 있는 관측이라는 의미가 됩니다.
kioku-mesh-mcp가 이를 listen 하고 있습니다. 따라서 "Host A의 Claude Code가 save" → "Host B의 Codex CLI가 search"하는 과정이, Codex CLI 측에서 아무런 특별한 조치를 하지 않아도 통하게 됩니다.
기동 시의 rebuild 정책
SQLite가 읽기 캐시라고는 하지만, 프로세스를 장시간 실행한 순간 "mesh 상에서 늘어난 observation이 보이지 않는" 상황이 발생하면 곤란합니다. 그래서 kioku-mesh는 기동 시에 rebuild_from_zenoh를 실행하는 옵션을 가지고 있으며, 수명이 짧은 프로세스는 skip하고, 장시간 실행되는 프로세스는 실행하는 방식으로 구분하여 사용합니다 (docs/Spec.md §7).
| 기동 형태 | 기본값 |
|---|---|
kioku-mesh CLI | rebuild를 skip (one-shot 기동을 빠르게) |
kioku-mesh-mcp | rebuild를 실행 (기동 시점에 한 번만 비용을 지불) |
우선순위는 --rebuild (명시적) → MESH_MEM_FORCE_REBUILD=1 → MESH_MEM_SKIP_REBUILD=1 → 기본값 순입니다. "메시를 대상으로 kioku-mesh search를 쳤는데 몇 건이 부족한 것 같다"라고 느껴질 때는 MESH_MEM_FORCE_REBUILD=1을 사용하세요.
삭제와 tombstone
delete가 곧바로 물리적 삭제가 아니라 tombstone을 작성할 뿐인 것 또한 메시를 의식한 설계입니다.
- 어떤 peer가
delete를 수행한다 →mem/tomb/.../<id>에deleted_at을 채운다 - 물리적 삭제는
gc --retention-days N을 실행했을 때, 일정 시간이 경과한 tombstone을 대상으로 수행한다
"한쪽에서 지웠는데 다른 한쪽에서 다시 살아나는" 현상이 발생하지 않도록, 삭제 또한 Zenoh 상의 정규 이벤트로서 흐르게 합니다.
local과 hub/spoke에서 SQLite의 위치가 달라진다
지금까지의 내용은 Zenoh 기반의 hub/spoke 모드에 대한 이야기이며, local 모드는 이를 단순화한 것입니다.
docs/Spec.md §6에 명시된 대로, SQLite의 위치는 모드에 따라 나뉩니다.
- Zenoh backend (
hub/spoke):MESH_MEM_INDEX_DB가 있으면 해당 경로를 사용하고, 없으면state_dir()/index.db를 사용합니다.:memory:지정도 가능합니다. - Local backend (
local):state_dir()/local/index.db로 고정됩니다.MESH_MEM_INDEX_DB는 영향을 주지 않습니다.
즉, init --mode local --force로 로컬 운용하던 SQLite와, init --mode hub --force...
로 local 모드에서 저장한 메모를 hub 모드로 전환했을 때 보이지 않게 된 것은 바로 이 때문이며, 파일이 사라진 것이 아닙니다. 마이그레이션 (Migration) 수단은 존재하므로, 필요한 시점에 별도로 다루겠습니다.
트러블슈팅 시 어떤 계층을 의심해야 하는가
메시 (Mesh) 운용에 들어가면 문제가 발생할 수 있는 지점이 분산됩니다. 이번에 분석한 구조를 기억해 두면 문제 격리 (Isolation)를 빠르게 할 수 있습니다.
- 저장했는데 메시의 다른 피어 (Peer)에서 보이지 않는다 → Zenoh의 레플리케이션 (Replication) (
zenohd) - 메시 상에 존재해야 할 관측값이 목록에 나오지 않는다 → 기동 시 리빌드 (Rebuild) 스킵 (
MESH_MEM_FORCE_REBUILD=1) - 삭제해도 사라진 느낌이 들지 않는다 → 툼스톤 (Tombstone)이 전파되지 않았거나, 아직
gc(Garbage Collection)를 실행하지 않음
kioku-mesh doctor는 이러한 경계 지점들을 종합적으로 체크해 주므로, 우선적으로 실행해야 할 명령어는 doctor입니다.
차회 예고
제4회에서는 드디어 실제 기기를 사용하여 다음과 같은 과정을 진행합니다.
init --mode hub로 허브 (Hub) 1대를 세우기--listen으로 주소 설계하기init --mode spoke --connect로 스포크 (Spoke) 연결하기- 양방향 레플리케이션 (Replication)을
save/search로 확인하기
이번 회차의 핵심인 "신뢰할 수 있는 원천 (Source of Truth)은 Zenoh+RocksDB, 읽기는 SQLite"라는 점을 머릿속에 넣어두면, 절차의 의미를 더 쉽게 이해할 수 있습니다.
참고 링크
- kioku-mesh (GitHub)
- 리포지토리 내:
docs/Spec.md(§3 Zenoh key 설계 / §6 SQLite 인덱스 / §7 rebuild 정책) - 연재 제1회: kioku-mesh란 무엇인가
- 연재 제2회: kioku-mesh를 Claude Code와 Codex CLI에 MCP로서 연결하기
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기