seekdb 数据库架构图(源码导览)

The State Store for AI Agents · MySQL-compatible · Hybrid Vector + Full-text Search

基于 OceanBase 内核演进 Embedded / Server / 分布式三态 Change Stream 异步索引流水线 两级 HNSW FORK/MERGE COW 沙箱

一、seekdb 是什么

seekdb 是 OceanBase 团队推出的、面向 AI Agent 状态存储 (state store) 的多模态数据库:同时承载 向量 / 全文 / 关系 三类数据,对外完全兼容 MySQL 协议,可作为嵌入式库单机 Server分布式集群运行。 它继承自 OceanBase 的 SQL 优化器与 LSM-Tree 存储引擎,并针对 Agent 的 「流式写 + 毫秒级检索 + 并发查询」工作负载,重写了索引构建路径,并提供 Kernel-level Copy-on-Write 沙箱。

性能定位:在 VectorDBBench Streaming 场景下,seekdb 在持续写入 500 行/秒的同时取得 1,523 QPS、并发 P99 21.7 ms(Milvus 的 10.7×;P99 抖动比 1.1× vs ES/Milvus 的 ~10×)。 本质原因是「写路径不碰索引」+「查询永远只扫两层索引」。

二、整体分层架构

从客户端协议到底层物理存储,seekdb 由六个逻辑层组成。每一层既在嵌入模式下作为函数调用栈存在,也能在 Server 模式下被网络与 RPC 框架横向切分到多节点:

flowchart TB subgraph CLIENT["客户端 / SDK 层"] PY["Python SDK
pyseekdb"] MY["MySQL 客户端
JDBC / mysql-cli / SQLAlchemy"] EMB["嵌入式 C API
seekdb_embed.h"] JNI["Android JNI
seekdb_jni"] end subgraph PROTO["接入 / 协议层 · src/observer"] MP["MySQL 协议
obmp_query / obmp_stmt_*"] NET["网络框架
ob_srv_network_frame"] DLV["请求派发
ob_srv_deliver"] OMT["多租户调度 OMT
observer/omt/ob_multi_tenant"] end subgraph SQL["SQL 引擎 · src/sql"] PARSE["Parser"] RESOLVE["Resolver"] OPT["Optimizer"] REWRITE["Rewrite"] CG["Code Generator"] EXEC["Executor + DTL"] PC["Plan Cache"] end subgraph TX_DAS["数据访问 / 事务 · src/sql/das + storage/tx"] DAS["DAS
Data Access Service"] TRX["ObTransService
MVCC + 2PC"] GTS["GTS Timestamp Service"] end subgraph STORAGE["存储引擎 · src/storage"] LS["LS 日志流
storage/ls"] MT["Memtable
storage/memtable"] SST["Block SSTable
storage/blocksstable"] COMP["Compaction
storage/compaction"] DDL["DDL / Direct Load
storage/ddl"] FTS_S["FTS
storage/fts"] end subgraph PLAT["平台 / 集群服务"] PALF["LogService PALF
src/logservice"] RS["RootServer
src/rootserver"] SHARE["share 公共服务
change_stream / vector_index / hybrid_search"] PLUG["Plugin / PL / ObJIT"] end PY --> EMB JNI --> EMB EMB -. in-process .-> SQL MY --> MP MP --> NET --> DLV --> OMT --> SQL PARSE --> RESOLVE --> REWRITE --> OPT --> CG --> EXEC EXEC --> DAS DAS --> TRX DAS --> MT DAS --> SST DAS --> FTS_S MT --> SST COMP --> SST MT --> LS LS --> PALF EXEC --> SHARE DDL --> SHARE RS --> LS RS --> SHARE PLUG --> SQL
读路径与写路径的关键差异:写入只经过 Memtable → CLOG/PALF 即返回; 索引构建(向量 HNSW、FTS、物化视图增量)由 Change Stream 异步在背后消费 redo 完成。读路径则在 DAS 中并行下推到 Memtable + SSTable + Delta 索引 + Snapshot 索引 并做行级融合。

三、三种部署形态

flowchart LR subgraph EMBED["Embedded 模式 · in-process"] direction TB APP1["App 进程
Python / Java / Go / C / Android"] LIB["libseekdb_embed
oceanbase_static"] APP1 -.链接.-> LIB LIB --> KER1["完整 OB 内核
不走网络"] end subgraph SERVER["单机 Server 模式"] direction TB CLI1["MySQL Clients"] OBS1["observer 进程
:2881 MySQL · :2886 RPC"] KER2["Sys Tenant + User Tenant"] CLI1 -- MySQL 协议 --> OBS1 --> KER2 end subgraph CLUSTER["OceanBase 分布式集群"] direction TB PROXY["OBProxy / Sharding-JDBC"] O1["observer #1
RS/Leader"] O2["observer #2"] O3["observer #3"] PROXY --> O1 PROXY --> O2 PROXY --> O3 O1 <-- Paxos --> O2 O2 <-- Paxos --> O3 O1 <-- Paxos --> O3 end

3.1 嵌入模式(Embedded)

3.2 单机 Server 模式

3.3 OceanBase 分布式模式

四、源码顶层目录

路径角色关键内容
src/observer/接入层 + 进程入口MySQL 协议、OMT 多租户、嵌入式 API、main.cpp
src/sql/SQL 引擎parser/resolver/optimizer/code_generator/executor/das/dtl/plan_cache
src/storage/存储引擎LSM-Tree (memtable+blocksstable)、tx、ls、compaction、ddl、fts、向量列存、mview
src/logservice/复制日志服务PALF Paxos、apply/replay/archive/restore/cdc/logfetcher
src/rootserver/集群大脑(仅 sys 租户)DDL service、freeze、balancer、fork_table、mview、direct_load
src/share/跨模块公共服务change_stream、vector_index、hybrid_search、cache、ai_service、catalog、schema
src/pl/存储过程 (PL/SQL & MySQL Routines)resolver/compile/code_generator/package/pl_cache/sys_package
src/plugin/插件机制动态加载分词器、UDF、Storage Engine 等
src/objit/表达式 JIT基于 LLVM 的 expression / PL 字节码 JIT
src/diagnose/诊断 / Lua 脚本调试探针、问题诊断脚本
deps/第三方oblib、easy、libobcdc 等
unittest/ mittest/ test/测试体系mysqltest、单元测试、模块集成测试
tools/工具集deploy 脚本、obadmin、ob_admin、benchmark 等

五、Observer 接入与调度层

Observer 是整个进程的大门。它负责接收客户端请求、按租户调度执行、并把请求转换成 SQL 引擎可识别的格式。代码位于 src/observer/

flowchart TB subgraph IN["网络入口"] NF["ObSrvNetworkFrame
libeasy/easy_io 线程"] DLV["ObSrvDeliver
按租户/优先级派发"] end subgraph PROTO["MySQL 协议 · observer/mysql"] CONN["obmp_connect
登录握手"] QRY["obmp_query
文本协议"] PREP["obmp_stmt_prepare/execute
二进制协议"] PIPE["obmp_packet_sender
结果集回包"] end subgraph OMT["OMT 多租户 · observer/omt"] MTNT["ObMultiTenant"] TNT["ObTenant
每租户独立线程池/资源"] WK["ObThWorker
工作线程"] RQ["ObRetryQueue
重试队列"] end subgraph CORE["核心服务句柄"] SRV["ObServer 单例"] SVC["ObService
RPC 处理"] SCHU["ObServerSchemaUpdater"] LIVE["ObSrvLiveSession"] end NF --> DLV --> OMT OMT --> CONN OMT --> QRY OMT --> PREP QRY --> SQL_ENG[("SQL 引擎")] PREP --> SQL_ENG PIPE --> NF SRV --- SVC SRV --- MTNT SRV --- SCHU

5.1 关键文件

文件职责
main.cpp / ob_server.cpp进程入口、Server 生命周期、全局单例初始化
ob_srv_network_frame.cpp基于 libeasy 的网络框架,监听 MySQL/RPC 端口
ob_srv_deliver.cpp请求按租户、按优先级派发到 OMT 队列
ob_srv_xlator.cppRPC 包到内部 processor 的路由
mysql/obmp_*.cppMySQL 协议命令的逐条 processor(query/prepare/execute/init_db/auth 等)
mysql/ob_query_driver.cpp查询统一驱动:同步/异步、流式/缓冲
omt/ob_multi_tenant.cpp多租户管理器,租户的添加/卸载/资源分配
omt/ob_tenant.cpp单租户对象(自有线程池、自适应工人池)
omt/ob_th_worker.cppworker 线程,绑定到租户上下文执行 SQL
embed/c/seekdb_embed.cpp嵌入式 C 入口;本地直接驱动 SQL Engine
embed/python/ob_embed_impl.cppPython pybind11 绑定
virtual_table/所有 __all_virtual_*oceanbase.gv$* 视图实现
table_load/Direct Load API 入口(旁路写)

5.2 多租户 (OMT) 设计

六、SQL 引擎

SQL 引擎位于 src/sql/,是 OceanBase 历经多年沉淀的核心,seekdb 在其上扩展了向量、FTS、混合检索算子与 FORK/MERGE 语义。

flowchart LR REQ["SQL 文本"] --> PARSER subgraph PIPELINE["执行流水线"] direction LR PARSER["Parser
parser/ flex+bison"] --> RESOLVER["Resolver
resolver/"] RESOLVER --> REWRITE["Rewrite
rewrite/"] REWRITE --> OPTIM["Optimizer
optimizer/"] OPTIM --> CG["Code Generator
code_generator/"] CG --> PLAN["Physical Plan"] PLAN --> EXEC["Executor
engine/executor"] end PLAN -- 缓存 --> PC[("Plan Cache
plan_cache/")] PC --> EXEC EXEC --> DAS["DAS Layer
das/"] EXEC --> DTL["DTL 数据传输
dtl/ 分布式"] DAS --> STORAGE[("Storage")] EXEC --> PRIV["Privilege Check
privilege_check/"] EXEC --> SESSION["Session 管理
session/"] EXEC --> MONITOR["Monitor / Trace
monitor/"]

6.1 子模块一览

子目录能力
parser/Lex/Yacc 词法语法,输出 ParseNode AST
resolver/语义分析、列绑定、权限基础校验;按 DML/DDL/DCL/TCL 分子目录
rewrite/查询改写(视图展开、子查询消除、谓词下推、Outer-Join 重排)
optimizer/基于成本的优化器 (CBO);支持并行、分布式 plan、向量/FTS hint
code_generator/把逻辑 plan 翻译成物理算子(ObOpSpec)
engine/40+ 算子实现:aggregate / basic / cmd / dml / expand / expr / join / 等
executor/物理 plan 执行驱动;分布式调度
plan_cache/语句指纹 → Plan 复用
das/Data Access Service:把 SQL 算子下推到存储;含 ob_text_retrieval_opob_vector_index_lookup_op
dtl/Data Transfer Layer:分布式算子间数据流
monitor/执行 trace、慢日志、real-time stat
printer/反解析 SQL(schema 重建、explain 输出)
privilege_check/权限检查

6.2 SQL 执行链路

sequenceDiagram participant C as Client participant MP as obmp_query participant SQL as ObSql participant RS as Resolver participant OPTI as Optimizer participant EX as Executor participant DAS as DAS Layer participant ST as Storage C->>MP: COM_QUERY "SELECT ..." MP->>SQL: stmt_query() SQL->>SQL: Parse → ParseNode SQL->>RS: resolve RS->>SQL: stmt(逻辑 AST) SQL->>OPTI: rewrite + plan OPTI-->>SQL: Physical Plan SQL->>EX: open(plan) EX->>DAS: scan / dml ops DAS->>ST: 行级读写(含向量/FTS lookup) ST-->>DAS: rows DAS-->>EX: rows EX-->>MP: result set MP-->>C: MySQL packets

6.3 向量 / 全文相关的 DAS 算子

七、存储引擎

位于 src/storage/,是经典的 LSM-Tree + MVCC 架构。一个表被分成多个 tablet,多个 tablet 隶属于一个 LS (Log Stream),LS 是复制和事务的最小单位。

flowchart TB subgraph LSMAP["LS · Log Stream"] direction TB LSMETA["ObLS
ls_meta + state"] LSDDL["ObLSDDLLogHandler"] LSFR["ObFreezer
转储"] LSREPL["ObLSReservedSnapshotMgr"] end subgraph TBLT["Tablet · 行存/列存"] direction TB META["meta_mem
tablet 元信息"] MT["Memtable
memtable/"] SST["SSTable
blocksstable/"] LOB["LOB
lob/"] MV["Mview
mview/"] DDLKV["DDL KV
ddl/ob_tablet_ddl_kv"] HNSW["HNSW EmbedMgr
ddl/ob_hnsw_embedmgr"] end subgraph BSL["blocksstable 物理层"] BLKMGR["ObBlockManager
本地宏块管理"] BLKSTORE["blockstore/
分层存储 / S3"] BF["BloomFilter Cache"] ENC["encoding / cs_encoding
编码压缩"] IDX["index_block
多级索引块"] end subgraph COMP["compaction/ 合并"] direction TB MNF["Minor Freeze
memtable→mini sstable"] MJF["Major Merge
合并基线"] FLT["filter / 谓词下推"] end subgraph CKPT["checkpoint/"] CKE["ObCheckpointExecutor"] DCK["ObDataCheckpoint"] FCK["ObFreezeCheckpoint"] end LSMETA --> TBLT MT --> SST COMP --> SST SST --> BSL HNSW -. 列存 .-> BSL LSFR --> COMP CKE --> CKPT CKPT --> LSMETA

7.1 关键概念

对象说明
LS(Log Stream)复制和事务最小单元;含若干 tablet;一个 LS 对应一组 PALF 日志
Tablet表/分区/索引在一个 LS 内的物理实例;行存或列存
Memtable内存增量;行存 B+Tree / Hash;MVCC 版本链
Mini/Minor SSTablememtable 转储产物;按时间窗口合并
Major SSTable基线数据;定期全量合并
HEAP / 行存 / 列存HEAP = 无主键堆表;普通索引组织表用 IOT;分析列用 column_store

7.2 模块清单

子目录职责
access/统一的 scan / get / multi_get / multi_scan 接口
blocksstable/SSTable 宏块/微块格式、读写器、行/列编码、bloom filter
blockstore/本地/远端 (S3、对象存储) 块存储抽象
checkpoint/多维度检查点(数据 / 冻结)
column_store/列存 (CG: Column Group) 实现,分析型查询用
compaction/Mini/Minor/Major Merge 调度,及谓词/重排
concurrency_control/表锁、行锁、ROW LOCK
ddl/DDL 增量与回放、Direct Load、Tablet split/fork、HNSW EmbedMgr
direct_load/旁路写 (skip memtable) 加速大批量装载
fts/全文检索内嵌的分词器、词典、参数化 helper
high_availability/副本迁移、Rebuild、Restore
lob/大对象 (LOB) 列存储
ls/LS 对象、状态机、handler 集合
memtable/Memtable 实现 + MVCC + Multi-source Data
meta_mem/tablet/ls 元信息内存管理
meta_store/持久化元信息
multi_data_source/事务多源数据(DDL / TableLock / TXBuffer)
mview/物化视图刷新
tx/分布式事务、GTS、2PC、版本号

八、事务子系统

位于 src/storage/tx/。seekdb 完整继承 OceanBase 的 MVCC + 2PC + Paxos 事务设计。

flowchart LR subgraph TX["ObTransService"] BEG["Begin TX"] --> WR["写入 memtable
挂载到 tx_ctx"] WR --> CMT{"Commit?"} CMT -- 单分区 --> ONE["ObOnePhaseCommitter"] CMT -- 多分区 --> TWO["2PC
Prepare→Commit"] ONE --> CLOG["CLOG 落日志"] TWO --> CLOG CLOG --> PALF[("PALF Paxos")] PALF --> CB["end_trans_cb"] end GTS["ObTimestampService / GTS
全局时间戳"] --> TX GTSL["ObGtsSource / Local Cache"] --> GTS DLK["deadlock_adapter
分布式死锁"] TX --- DLK

九、LogService(PALF 复制日志)

位于 src/logservice/,是分布式一致性、灾备、CDC 的统一底座。

flowchart TB subgraph PALF["PALF · Paxos Append-only Log File"] direction TB LH["LogHandler
对外 API"] LE["LogEngine
日志读写"] LCM["LogConfigMgr
配置变更"] LSW["LogSlidingWindow
同步窗口"] LELE["election/
Paxos 选举"] LCAC["LogCache"] LBM["LogBlockMgr
本地块"] LFR["fetch_log_engine
追赶/补日志"] end subgraph SVC["围绕 PALF 的服务"] APPLY["applyservice
commit→memtable callback"] REPLAY["replayservice
followers 重放"] ARC["archiveservice
归档到对象存储"] REST["restoreservice
从归档恢复"] CDC["cdcservice / logfetcher
对外抽 CDC 流"] DD["data_dictionary
schema 字典"] LC["leader_coordinator
主选举协调"] RC["rcservice
角色/容灾"] end LH --> LE --> LBM LH --> LSW LH --> LCM --> LELE APPLY --> LH REPLAY --> LH CDC --> LFR ARC --> LE REST --> LFR

9.1 子目录

子目录职责
palf/Paxos 实现,提供强一致 append-only 日志
applyservice/Leader 把日志 commit 后回调 memtable,完成本地可见
replayservice/Follower 顺序重放 redo
archiveservice/把 redo 归档到 NAS / S3 / OSS
restoreservice/物理备份恢复
cdcservice/ logfetcher/ logrouteservice/OBCDC:把 redo 翻译成行级变更流
data_dictionary/嵌入到日志流里的 schema 字典快照
leader_coordinator/多 LS 选主 / 切主调度
rcservice/角色变更(主备切换、停机维护)
seekdb 的关键复用:Change Stream(异步索引)也走 PALF 的 logfetcher 接口订阅 redo,因此对外的 CDC 接口与内部异步索引共享同一份日志读取基础设施。

十、RootServer(集群大脑)

仅 sys 租户启动,位于 src/rootserver/。负责 schema、调度、备份、DDL 串行化、Fork/Merge、合并冻结等。

flowchart TB RS["ObRootService"] --> DDL["ObDDLService
所有 DDL 入口"] DDL --> DDLT["ddl_task/
DDL 任务追踪与重做"] RS --> FRZ["freeze/
major freeze 调度"] RS --> LB["ObLoadBalancer
partition 负载均衡"] RS --> SCH["ObServerLocalityCache"] RS --> FORK["fork_table/
ObForkDatabase/TableService"] RS --> MV["mview/
物化视图调度"] RS --> DI["direct_load/
大批量装载调度"] RS --> BACK["backup/
备份调度"] DDL --> IDX["ObIndexBuilder
建索引
含 vector/FTS"] DDL --> LOB["ObLobMetaBuilder
ObLobPieceBuilder"] DDL --> CCL["ob_ccl_ddl_service
限流规则"] DDL --> AI["ob_ai_model_ddl_service
AI 模型注册"]

十一、share 公共服务

src/share/跨 SQL、Storage、RS 的横切层,集中存放公共服务、跨节点 RPC、缓存、AI 集成。

子目录角色
schema/ inner_table/所有 __all_xxx 内部表、schema getter/cache
cache/统一 KV cache(schema cache、row cache、block cache)
location_cache/tablet/LS → server 的位置缓存与 RPC 探测
io/统一 IO 框架,分级 IO 调度
change_stream/ ★异步索引流水线(Fetcher / Dispatcher / Worker / Plugin)
vector_index/ ★HNSW / IVF 向量索引引擎适配层(接 VSAG)
hybrid_search/ ★JSON 风格混合查询入口(Pinecone-like API)
ai_service/外部 LLM/Embedding 模型代理(HTTP 调用 OpenAI 等)
catalog/外部数据源 catalog(Iceberg/Hive/外表)
aggregate/ datum/向量化执行所需的 datum、聚合算子原语
ash/ index_usage/ longops_mgr/ASH 活动会话、索引使用统计、长任务监控
backup/备份策略、备份目录
compaction/合并 schedule 配置 / 异常追踪
deadlock/分布式死锁检测器
diagnosis/诊断信息收集
external_table/外表(S3/CSV)读取
allocator/租户内存子分配器
client_feedback/给 OBProxy 的反馈信息(路由优化)
config/所有配置项注册中心

十二、Change Stream 异步索引流水线 ★ Agent 性能关键

位于 src/share/change_stream/。这是 seekdb 区别于 OceanBase 的核心新增模块, 也是「写路径不碰索引」、「查询永远只扫两层索引」得以成立的根因。

12.1 概念

普通 LSM 数据库写入时同步构建索引(向量/全文/物化视图),引起 P99 抖动;seekdb 让事务只写 redo, 索引由独立流水线从 PALF 异步消费 redo 构建。

flowchart LR subgraph WRITE["写路径"] DML["INSERT/UPDATE"] --> MT["Memtable + Tx"] MT --> CLOG["PALF Redo Log"] CLOG --> RET["Commit OK 立即返回"] end subgraph CS["Change Stream 异步流水线 · src/share/change_stream"] direction LR FET["ObCSFetcher
单线程,按 LSN 顺序消费 PALF"] FET -- "已 commit 事务" --> RING["Ring Buffer
每事务一条 ObCSTxInfo"] RING --> DIS["ObCSDispatcher
切片到 worker"] DIS --> SUB["ObCSExecSubTask
按 heap_pk 切片"] SUB --> EX["ObCSExecutor
LinkQueue 线程池"] EX --> PLUG["ObCSPlugin 实例池
每 batch 创建"] PLUG --> AIDX["Async Index Plugin
ob_cs_plugin_async_index"] AIDX --> DELTA["Delta HNSW / FTS
内存增量索引"] end CLOG --> FET DELTA --> Q[("查询路径合并")]

12.2 关键组件

线程模型职责
ObChangeStreamMgr租户级 MTL 单例持有 Fetcher / Dispatcher / Worker 生命周期
ObCSFetcher单线程从 PALF 顺序拉 redo,按 tx_id 组装;commit 时推入 ring buffer。事务级零拷贝,rollback to savepoint 用 [to_seq, from_seq) 区间裁剪
ObCSDispatcher单线程消费把单事务拆成多个 ObCSExecSubTask(按 heap_pk 切片,邻近行落到同一 worker)
ObCSWorker / ObCSExecutor多线程 LinkQueue 池并发执行子任务;自适应线程数
ObCSExecCtxper-batch共享事务句柄、refresh_scn、Plugin 实例集;任务结束统一回收
ObCSPluginper-batch 工厂创建插件式接口:向量异步索引、未来可挂 FTS、Mview Refresh、自定义流
ObCSPluginAsyncIndex把 redo 行写入 delta HNSW / IVF buckets

12.3 行可见性 / Rollback 支持

12.4 关键文件

ob_change_stream_mgr.{h,cpp}租户级管理器,MTL 注入点
ob_change_stream_fetcher.{h,cpp}PALF 消费者,事务组装
ob_change_stream_dispatcher.{h,cpp}事务切片、可见性
ob_change_stream_worker.{h,cpp}LinkQueue 工人池
ob_change_stream_plugin.{h,cpp}Plugin 注册表
ob_cs_plugin_async_index.{h,cpp}异步索引插件实现

十三、向量索引(两级 HNSW / IVF)

实现位于 src/share/vector_index/ + src/storage/ddl/ob_hnsw_embedmgr.* + sql/das/ob_vector_index_lookup_op.cpp。底层算法库通过 VSAG(OceanBase 开源的向量索引内核)实现。

13.1 两级索引架构

flowchart LR subgraph WR["写 / 异步构建"] DML2["INSERT/UPSERT"] --> RDLOG["Redo"] RDLOG --> CS2["Change Stream"] CS2 --> DELTA2["Delta HNSW
in-memory,可立即查询"] end subgraph SP["周期性 Snapshot"] DELTA2 -- rotate --> SNAP["Snapshot HNSW
持久化到 SSTable"] DELTA2 -. 保留新增 .-> DELTA2 end subgraph QRY["查询路径"] Q["ANN Query"] --> DASOP["ObVectorIndexLookupOp"] DASOP --> SDELTA["search delta HNSW"] DASOP --> SSNAP["search snapshot HNSW"] SDELTA --> MERGE["结果归并
+ scalar/FTS 过滤"] SSNAP --> MERGE MERGE --> ROW["回表取完整行"] end
为什么 P99 抖动只有 1.1×?
• 索引数量固定为 2,并发查询永远只对两个索引各做一次 ANN,不会随写入累积膨胀的段数发散;
• Delta HNSW 内存常驻,并提供细粒度读锁,新写入毫秒级可见;
• Snapshot HNSW 通过 SSTable 落盘,重启可加载,不必重建。

13.2 关键类

类 / 文件职责
ObPluginVectorIndexService租户级向量索引服务总入口(vector_index/)
ObPluginVectorIndexMgrtablet → adapter 注册表
ObPluginVectorIndexAdaptor单 tablet 上 delta+snapshot 双 HNSW 的封装;FilterInterface 注入 bitmap 谓词下推
ObVectorQueryAdaptorResultContext查询请求 context(topK、ef、过滤位图)
ObPluginVectorIndexSchedulerdelta→snapshot 切换、合并的后台调度器
ObVectorIndexIvfCacheMgr / ObIvfAsyncTaskExecutorIVF 系列索引的缓存与异步构建
ObVsagMemContext / ObVsagSearchAllocVSAG 内存子分配器
ObVectorEmbeddingHandler向量字段类型 / 编码 / 持久化 handler
ObHnswEmbedMgr位于 storage/ddl/,把 HNSW 段写成 SSTable
ObHybridVectorRefreshTaskvector + 标量混合刷新任务
ob_vector_index_lookup_op.cppSQL 执行端的 ANN lookup 算子

13.3 支持的索引参数

类型常用参数
HNSWM (default 16)、ef_construction、ef_search、distance (l2/ip/cosine)、lib=vsag
IVF_FLAT / IVF_SQ8 / IVF_PQnlist、nprobe、训练样本数;通过 ob_vector_kmeans_ctx 训练
BQ / HNSW_BQBinaryQuant 量化变体

十四、全文检索(FTS)

分词器与查询入口分布在 src/storage/fts/share/ob_fts_index_builder_util.*

flowchart TB subgraph DDL_FTS["建索引 · ob_fts_index_builder_util"] CRT["CREATE FULLTEXT INDEX ... WITH PARSER xxx"] --> BLD["FTS Builder"] BLD --> TBLS["doc_id 倒排表 + doc_word 表 + fts_index 表"] end subgraph WR_FTS["写路径"] INS["INSERT 文档"] --> MT2["Memtable"] MT2 --> CSF["Change Stream
异步分词+倒排"] CSF --> INVTBL[("倒排表 SSTable")] end subgraph QR_FTS["查询路径"] MATCH["MATCH(content) AGAINST(...)"] --> TRET["ob_text_retrieval_op"] TRET --> PARSE2["FT Parser
ik/ngram/beng/whitespace"] PARSE2 --> SCAN["倒排扫描+评分 BM25"] SCAN --> RET2["doc_id 列表"] end

14.1 内置分词器

Parser说明
ik中文 IK 分词(细粒度+智能模式),位于 storage/fts/ik/
ngram / ngram2n-gram 通用切分
beng英文基础分词(小写化、停用词)
whitespace纯空白切分(标记型场景)
plugin parsers通过 plugin 框架挂入第三方分词

14.2 关键文件

ob_fts_plugin_helper.*分词器插件统一接入(ObFTParser / ObFTParseHelper)
ob_fts_struct.*分词、文档结构定义
ob_fts_stop_word.*停用词管理
share/ob_fts_index_builder_util.*FTS 三个内部表的构建/重建
sql/das/ob_text_retrieval_op.cppSQL 执行端倒排扫描算子

十五、Hybrid Search 混合检索

src/share/hybrid_search/ 提供了 Pinecone 风格的 JSON 查询入口,把 向量 + 全文 + 标量 翻译成一条 SQL Plan。

flowchart LR REQ2["JSON Hybrid Query"] --> PARSE3["ob_query_parse
解析"] PARSE3 --> RQT["ObQueryRequest
结构体"] RQT --> TRX2["ob_query_translator
翻译为 SQL"] TRX2 --> SQL2[("SQL Plan")] SQL2 --> EXEC2["ob_hybrid_search_executor"] EXEC2 --> VEC["Vector Lookup"] EXEC2 --> FTSO["FTS Retrieval"] EXEC2 --> SCA["Scalar Filter / Sort"] VEC --> MRG["执行计划内归并"] FTSO --> MRG SCA --> MRG MRG --> OUT["结果集"]

关键文件:ob_hybrid_search_executor.cppob_query_translator.cppob_query_parse.cppob_query_request.cppob_request_base.cpp

十六、FORK / MERGE COW 沙箱 ★ Agent 探索安全网

实现位于 src/rootserver/fork_table/ + share/ob_fork_table_util.* + storage/ddl/ob_tablet_fork_task.*。这是 seekdb 区别于普通 DB 的另一个核心新特性。

16.1 概念

「FORK」并不真正复制数据;它在 schema 层为目标库/表创建新的 ID,并把所有 tablet 标记为 共享基线 + 写时复制 (Copy-on-Write) 模式。Agent 在沙箱里读写时:

flowchart LR subgraph BEFORE["Step 1 · FORK 之前"] direction TB MAIN1["main DB · agent_state"] T1["tablet T1
SSTable基线"] MAIN1 --> T1 end subgraph FORKED["Step 2 · FORK DATABASE"] direction TB MAIN2["main DB"] SB["sandbox_42"] SHTAB["共享基线 SSTable
引用计数"] MAIN2 --> SHTAB SB --> SHTAB end subgraph WRITE["Step 3 · sandbox 写入"] direction TB MAIN3["main"] SB3["sandbox_42"] SHB["共享基线"] SBN["sandbox 私有
memtable/SSTable"] MAIN3 --> SHB SB3 --> SHB SB3 --> SBN end subgraph MERGE["Step 4 · MERGE / DROP"] direction TB MM["MERGE TABLE STRATEGY FAIL/THEIRS/OURS"] DD["DROP DATABASE 直接丢弃"] MM --> MAIN4["已更新 main"] DD --> MAIN5["main 保持不变"] end

16.2 执行流程

sequenceDiagram participant U as User SQL participant RS as RootServer / ObDDLService participant FK as fork_table service participant SCH as Schema Service participant LS as Storage / LS U->>RS: FORK DATABASE src TO dst RS->>FK: ObForkDatabaseService FK->>SCH: 复制 schema:新 db + 新 table_id + tablet_id FK->>LS: ObTabletForkTask 标记 tablet 为 COW 共享基线 LS->>LS: 引用计数 +1(不拷贝数据) FK-->>U: OK(秒级返回) Note over U,LS: sandbox 上的写入只生成新 memtable/SSTable U->>RS: MERGE TABLE ... STRATEGY THEIRS RS->>FK: 合并任务,按 strategy 解决冲突 FK->>LS: 把 sandbox 增量打回 main tablet

16.3 关键文件

rootserver/fork_table/ob_fork_database_service.cppFORK DATABASE 入口
rootserver/fork_table/ob_fork_table_service.cppFORK TABLE 入口
rootserver/fork_table/ob_fork_table_task.cppDDL Task:跨节点协调
rootserver/fork_table/ob_fork_table_info_builder.cpp构建 fork 后的 schema/tablet 映射
rootserver/fork_table/ob_fork_table_helper.cpp跨模块辅助:MERGE 策略 (FAIL/THEIRS/OURS)
share/ob_fork_table_util.cpp共享工具:可见性、版本判定
storage/ddl/ob_tablet_fork_task.cpptablet 层的 COW 落地
storage/ddl/ob_table_fork_info.cpptablet fork 元信息
MERGE STRATEGY 含义
FAIL:行冲突即报错,回滚整个 merge;
THEIRS:以 sandbox 内容为准覆盖主库;
OURS:以主库内容为准,sandbox 冲突行丢弃。

十七、Plugin 框架

src/plugin/ 提供动态加载机制(FT Parser、UDF、Storage engine 等)。

flowchart LR CFG["INSTALL PLUGIN xxx
plugin 表注册"] --> MGR["ObPluginMgr"] MGR --> DLOPEN["ObPluginDlHandle
dlopen 动态库"] DLOPEN --> ENT["ObPluginEntryHandle
读符号 entry"] ENT --> ADP["Adaptor
plugin/adaptor"] ADP --> IF["Interface
plugin/interface"] IF --> CONS["内核消费者
FTS / UDF / Storage"]
plugin/sys/插件加载/卸载/管理器、handle 抽象
plugin/interface/各类插件的 C++ 接口契约
plugin/adaptor/把插件 API 适配给内核
plugin/export/ include/对外头文件、ABI

十八、PL 引擎 & ObJIT

PL 引擎

位于 src/pl/。实现 MySQL/Oracle 兼容的存储过程、函数、Package、Trigger。

  • parser/ — PL 词法/语法
  • ob_pl_resolver.* — 语义分析
  • ob_pl_compile.* / ob_pl_code_generator.* — 编译为 PL 指令
  • pl_cache/ — Routine/Package 缓存
  • ob_pl_package_*.* — Package 状态、依赖追踪
  • sys_package/ — 内置系统包(DBMS_*、UTL_*)
  • pl_recompile/ — 失效后重编译
  • diagnosis/ — PL 诊断

ObJIT

位于 src/objit/,基于 LLVM 的 JIT 框架,把表达式、PL 字节码即时编译成本地指令,加速热点计算。

  • 独立 CMake 子项目,可单独打 rpm
  • 提供给 SQL Expression、PL VM 调用
  • 支持表达式 fold、循环 unroll

诊断

src/diagnose/ 提供 Lua 脚本环境,运行时可注入采样、追踪逻辑。

十九、典型数据流

19.1 流式写 + 立即可查(Agent Memory)

sequenceDiagram participant App as Agent App participant MP as obmp_query / pyseekdb participant SQL as SQL Engine participant TX as TX + Memtable participant PALF as PALF participant CS as Change Stream participant VEC as Vector Index Adaptor App->>MP: INSERT INTO memory embedding,content VALUES ... MP->>SQL: parse → plan SQL->>TX: write → memtable TX->>PALF: append redo PALF-->>TX: majority commit TX-->>App: OK(不等索引) PALF->>CS: ObCSFetcher 拉取 redo CS->>VEC: AsyncIndex Plugin 写 Delta HNSW Note over App,VEC: 通常 ~ms 后即对 ANN 查询可见 App->>MP: SELECT ... ORDER BY l2_distance(...) APPROXIMATE MP->>SQL: plan with vector_index_lookup SQL->>VEC: query delta + snapshot HNSW VEC-->>SQL: top-K SQL-->>App: rows

19.2 混合检索一条 SQL 多模态下推

flowchart LR Q3["SELECT id, title, l2_distance(emb, ?) dist
FROM docs
WHERE MATCH(content) AGAINST(quarterly report)
AND author_id = 42 AND created_at > 2026-01-01
ORDER BY dist APPROXIMATE LIMIT 10"] Q3 --> OPT3["Optimizer 生成多模态计划"] OPT3 --> VEC3["Vector ANN"] OPT3 --> FTS3["FTS 倒排"] OPT3 --> SCAL["标量索引/范围"] VEC3 --> JOIN3["执行端内归并
(无客户端拼接)"] FTS3 --> JOIN3 SCAL --> JOIN3 JOIN3 --> TOPK["Top-K + 回表"]

19.3 FORK / MERGE 沙箱生命周期

sequenceDiagram participant U as Agent participant RS as RootServer participant LS as Storage_tablets U->>RS: FORK DATABASE agent_state TO sandbox_42 RS->>LS: 创建共享 tablet 引用,标记 COW RS-->>U: OK(秒级) U->>U: USE sandbox_42; 大量 INSERT / UPDATE U->>RS: MERGE TABLE sandbox_42.memory INTO agent_state.memory STRATEGY THEIRS RS->>LS: 把 sandbox 私有增量打回 main RS-->>U: OK Note over U,RS: 若失败:DROP DATABASE sandbox_42 即丢弃所有变更

二十、组件能力总表

组件所在路径核心能力关键类 / 入口
Observer (协议接入)src/observer/MySQL 协议、嵌入 API、WorkerObServer, obmp_query, ObSrvDeliver
OMT 多租户observer/omt/租户隔离、线程池ObMultiTenant, ObTenant, ObThWorker
SQL Enginesrc/sql/Parse/Plan/Exec/CacheObSql, ObResultSet, ObDASRef
DASsql/das/跨存储/节点的统一数据访问ObDataAccessService, ObDasScanOp
Storagesrc/storage/LSM-Tree + MVCCObLS, ObTablet, ObMemtable, ObSSTable
事务 / GTSstorage/tx/2PC、MVCC、GTSObTransService, ObTimestampService
LogService PALFsrc/logservice/Paxos 强一致复制日志LogHandler, LogEngine, election/
RootServersrc/rootserver/DDL、负载、SchemaObRootService, ObDDLService
Change Streamshare/change_stream/异步索引流水线 ★ObChangeStreamMgr, ObCSFetcher, ObCSDispatcher
Vector Indexshare/vector_index/两级 HNSW / IVF ★ObPluginVectorIndexService, ObPluginVectorIndexAdaptor
FTSstorage/fts/多语种全文检索ObFTParser, IK/ngram/beng
Hybrid Searchshare/hybrid_search/JSON 多模态查询入口ObHybridSearchExecutor
FORK / MERGErootserver/fork_table/COW 沙箱 ★ObForkDatabaseService, ObTabletForkTask
Direct Loadstorage/direct_load/旁路写大批量数据ObDirectLoadMgr
Mviewstorage/mview/ rootserver/mview/物化视图刷新ObMviewService
Pluginsrc/plugin/动态加载分词器/UDFObPluginMgr
AI Serviceshare/ai_service/外部 LLM/Embedding 代理ObAIServiceExecutor
PL Enginesrc/pl/存储过程 / 包ObPL, ObPLPackage
ObJITsrc/objit/LLVM 表达式 JITObJit
嵌入式 APIobserver/embed/C/Python/JNI in-process APIseekdb.h, ob_embed_impl.cpp
一句话总结 seekdb 架构:在 OceanBase 的 LSM-Tree + Paxos + 多租户 SQL 内核之上,新增了三件「为 Agent 而生」的关键能力——Change Stream 异步索引流水线两级 HNSW 向量索引FORK/MERGE COW 沙箱; 再通过 嵌入式 C/Python API 让它能像 SQLite 一样 in-process 运行, 也能在 Server / 分布式模式下水平扩展。

附 A · LSM-Tree 存储引擎深入

A.1 一行数据的一生

flowchart LR subgraph IN["写入路径"] direction TB DML["INSERT/UPDATE/DELETE"] --> CTX["ObStoreCtx
事务上下文 + snapshot_version"] CTX --> KV["ObMTKVBuilder
构造 key/value"] KV --> QE["ObQueryEngine
双索引 Hash + KeyBTree"] QE --> ROW["ObMvccRow
版本链头节点"] ROW --> NODE["ObMvccTransNode
新版本挂链头"] NODE --> CLOG["redo: ObMemtableMutator"] end subgraph FREEZE["转储 / 冻结"] direction TB ACT["Active Memtable"] -->|freeze| FROZ["Frozen Memtable
只读"] FROZ -->|dump dag| MINI["Mini SSTable"] end subgraph COMPACT["合并 Compaction"] direction TB MINI2["多个 Mini"] -->|Minor Merge| MINOR["Minor SSTable
仍多版本"] MINOR -->|每日 Major Merge| MAJOR["Major SSTable
基线 单版本"] end IN --> FREEZE --> COMPACT

A.2 LSM 三层物理结构

层级位置形态生命周期
Active Memtable内存Hash 点查 + KeyBTree 范围 + 多版本链写入直到 freeze 触发
Frozen Memtable内存同上但只读,等待 dump转储后销毁
Mini SSTable磁盘Macro 2MB → Micro 16KB,行存或列存等待 Minor Merge
Minor SSTable磁盘多版本合并,保留快照内活跃版本下一次 Major Merge 前
Major SSTable (基线)磁盘单版本,每行保留 snapshot 可见的最新下一轮 Major Merge

A.3 与 RocksDB 的差异

维度RocksDBseekdb / OB
层级L0..L6 七层 Leveled仅 3 类:Memtable / Minor 增量 / Major 基线
写放大10×+,每层都重写"基线 + 累积增量",写放大显著更低
合并触发后台异步,时机不可控每日全集群同步 Major Freeze,所有分区在同一全局快照点合并
多版本标识seq numberSCN (=GTS 时间戳),全局单调
读路径L0..LN 逐层 Bloom 兜底Memtable + Minor + Major 三段融合;Bloom + Row Cache + Block Cache 三级 cache
分区无原生 partitiontablet 是物理分区单位,跨副本由 PALF Paxos 保证一致

A.4 Macro / Micro Block 物理布局

flowchart TB subgraph MACRO["Macro Block · 默认 2MB"] direction TB HDR["Macro Header
checksum / 列描述 / micro index"] M1["Micro Block 1
16KB"] M2["Micro Block 2"] M3["Micro Block N"] IDXB["Micro Block Index
每微块首键 + offset"] HDR --> M1 --> M2 --> M3 --> IDXB end subgraph SST["SSTable 由多个 Macro 组成 + 多级索引"] direction LR L1["L0 root index"] --> L2["L1 mid index"] --> L3["leaf macro"] end

A.5 读路径的"行融合"

sequenceDiagram participant SQL as DAS scan participant MEM as Active Memtable participant FRZ as Frozen Memtable participant MIN as Minor SST participant MAJ as Major SST baseline SQL->>MEM: get(key, snapshot_scn) MEM-->>SQL: row v3 SQL->>FRZ: get(key, snapshot_scn) FRZ-->>SQL: row v2 SQL->>MIN: get(key, snapshot_scn) MIN-->>SQL: row v1 SQL->>MAJ: get(key) MAJ-->>SQL: row v0 baseline SQL->>SQL: ObRowFuse 按版本从新到旧融合
遇 DELETE 标记立即终止
「DELETE 实际是怎么落的?」
写入只生成一个 type=DELETE 的 ObMvccTransNode(tombstone);读路径融合到 tombstone 即终止;Major Merge 才真正物理回收。

附 B · MVCC 实现深入

B.1 三个核心数据结构

flowchart TB subgraph MT["ObMemtable"] direction TB QE["ObQueryEngine
Hash + KeyBTree 双索引"] QE --> R1["ObMvccRow key=A
list_head_ → 链头"] QE --> R2["ObMvccRow key=B"] R1 --> N1A["ObMvccTransNode v3
tx=T7 scn=120"] N1A --> N1B["ObMvccTransNode v2
tx=T5 scn=110"] N1B --> N1C["ObMvccTransNode v1
tx=T2 scn=100"] end TXC["ObMemtableCtx
本事务 callback list"] TXC -. 反向引用 .-> N1A TXC -. 关联 .-> R2
对象角色关键字段
ObQueryEnginememtable 索引KeyBTree (range scan) + ObMtHash (point get)
ObMvccRow一行的版本链头list_head_、latest_compact_node_、max_trans_version_、latch_(spin lock)
ObMvccTransNode一个版本tx_id_、trans_version_、scn_、seq_no_、prev_/next_、type_ = NORMAL/COMPACT/DELETE
ObMemtableCtx事务级 ctxcallback_list_:commit/rollback 时回写所有挂在版本链上的节点
ObMvccAccCtx访问 ctxsnapshot_version_、tx_id_、读写模式

B.2 写入:mvcc_write 的并发控制

sequenceDiagram participant T as Tx T 写入者 participant QE as QueryEngine participant R as ObMvccRow T->>QE: lookup(key) QE-->>T: row 存在或新建 T->>R: 获取 latch_ (轻量 spin) T->>R: 检查 WW 冲突
链头 tx_id != self 且仍 ACTIVE alt 冲突 R-->>T: conflict_tx_id 返回 T->>T: 走 LockWaitMgr 排队等待 else 无冲突 T->>R: new ObMvccTransNode
tx_id=T, scn=MAX (未提交) R->>R: 插入到 list_head_ 前 T->>T: callback 加入 ObMemtableCtx end T->>R: 释放 latch_
关键点:写入时 scn = MAX。这是「未提交版本」的标记,对所有其他事务的读都不可见(因为没有任何快照版本 ≥ MAX)。

B.3 提交:「延迟打版本号」

  1. 事务 T 发起 commit;从 GTS 拿到全局严格单调的 commit_version (SCN)
  2. 写 redo 到 PALF;多数派落盘后认定 commit 成功。
  3. 遍历 ObMemtableCtx.callback_list_:把所有该事务挂上去的 TransNode 的 trans_version_ 从 MAX 改写为 commit_version。
  4. 从此该版本对所有 snapshot_version ≥ commit_version 的读可见。

B.4 回滚 / Rollback to Savepoint

B.5 读:可见性判定算法

flowchart TB RQ["Read with snapshot_scn=S, reader_tx=R"] --> LOOK["QueryEngine lookup key"] LOOK --> ROW["ObMvccRow"] ROW --> ITER["从 list_head_ 向后遍历 TransNode"] ITER --> JUDGE{"判定该 node"} JUDGE -- "tx_id==R 且 seq_no 未 rollback" --> SEE["可见 (自己刚写的)"] JUDGE -- "tx_id != R 且 trans_version_ <= S 且 COMMITTED" --> SEE JUDGE -- "ACTIVE 或 trans_version_ > S" --> SKIP["跳过 → 看下一个"] SKIP --> ITER SEE --> RET["返回该版本数据 / 或 tombstone"]

B.6 并发原语

ObRowLatchper-row 自旋锁,纳秒级,仅在写入挂链/链头检查时持有
QSync / Hazard PointerKeyBTree 用 epoch-based reclaim,读不加锁也安全
ObLockWaitMgrWW 冲突时挂等;不轮询;锁释放唤醒
分布式死锁share/deadlock/ 基于消息环检测

B.7 GTS:MVCC 的心脏

B.8 Compact Node(链路压缩)

当一行的版本链超过 INDEX_TRIGGER_COUNT (=500) 个节点,会异步在链上插入一个 COMPACT 类型 TransNode,把若干历史版本压成一个完整的快照点,缩短后续读取路径。

B.9 ELR · Early Lock Release

OceanBase 的优化:commit 流程内一旦 redo 多数派落盘,立即释放行锁(甚至先于 callback 改写完所有 TransNode)。ObMvccRow 上的 max_elr_trans_version_ 记录了 ELR 后的最大版本,避免脏读。

「为什么写不阻塞读?」三个理由:(1) 写新增链头 + 旧版本仍可见;(2) 读不申请行锁,只做版本比较;(3) latch_ 只在挂链瞬间持有,纳秒级,不与读冲突(读通过 RCU/Hazard 安全遍历)。

附 C · 向量索引层架构深入

C.1 一个向量列在内核里到底有几张「表」?

看似一条 VECTOR INDEX idx_vec (emb) WITH (TYPE=hnsw),seekdb 实际在内核创建 5 张辅助 tablet

tablet角色形态
data_tablet原表行存(用户表)普通行存 SSTable
rowkey_vidrowkey → vid 映射普通二级索引
vid_rowkey (delta_buffer)vid → 向量 + 行键的增量缓冲"delta" 部分,对应 Delta HNSW 的真身
vbitmap (vid bitmap)已删除/活跃 vid 位图支持过滤推下 + 删除标记
snapshot"快照"索引数据序列化后的 Snapshot HNSW 图结构,写到 SSTable

这 5 张表绑定在一起,由 ObPluginVectorIndexAdaptor 统一管理,对外只暴露「一个向量索引」概念。

C.2 Adaptor 把 5 张表黏成「两级 HNSW」

flowchart TB USER["用户表
data_tablet"] -->|insert| RWK["rowkey_vid
分配 vid"] RWK -->|新 vid + emb| DELTA["vid_rowkey delta_buffer
= Delta HNSW (内存图)"] DELTA -. 周期 rotate .-> SNAP["snapshot
= Snapshot HNSW (持久化图)"] DEL["DELETE 行"] --> BMAP["vbitmap
标 vid 失效"] subgraph QUERY["查询"] ANN["ANN(query_vec, K)"] --> SAD["search delta HNSW"] ANN --> SAS["search snapshot HNSW"] SAD --> FBM["filter by vbitmap
剔除已删 vid"] SAS --> FBM FBM --> MRG["合并 K + 距离重排"] MRG --> LOOK["按 vid 回 rowkey_vid → 回 data_tablet 取行"] end

C.3 关键对象

对象作用
ObPluginVectorIndexService租户级单例 (MTL);管理本租户全部向量索引的生命周期
ObPluginVectorIndexMgrper-LS 管理器;持有 tablet_id → Adapter 的注册表
ObPluginVectorIndexAdaptor一个向量索引实例;持有 5 个 tablet_id、delta/snapshot 句柄、写锁、引用计数
ObVectorQueryAdaptorResultContext一次 ANN 查询的 ctx:topK / ef_search / 过滤 bitmap / 结果数组
ObVsagMemContextVSAG 内存上下文,托管所有 HNSW 节点分配
ObVsagSearchAlloc查询期独立 allocator,避免污染主分配器
ObHnswBitmapFilter实现 VSAG 的 FilterInterface,把 SQL 标量谓词转成 bitmap 在 ANN 内部过滤
ObPluginVectorIndexScheduler后台调度:delta 满 → snapshot 切换、HNSW 重建、合并
ObHnswEmbedMgr位于 storage/ddl,把 HNSW 序列化字节流写成 SSTable 微块

C.4 写路径:从 INSERT 到 Delta HNSW 出现一个点

sequenceDiagram participant U as INSERT participant TX as Tx + Memtable participant PALF as PALF participant CS as Change Stream participant AD as VectorIndexAdaptor participant DH as Delta HNSW U->>TX: 写 data_tablet + rowkey_vid (vid 自增) TX->>PALF: redo append PALF-->>U: commit OK (返回客户端) PALF->>CS: ObCSFetcher 拉 redo CS->>AD: AsyncIndex 插件投递 (vid, emb_vector) AD->>DH: add_node(vid, vec)
VSAG hnsw_index->add() Note over DH: 该 vid 立刻对 ANN 可见

C.5 Delta → Snapshot 切换 (相当于向量索引的 minor freeze)

查询永远只扫两个索引:无论写了多少数据,活跃 HNSW 数量恒为 2 (delta + snapshot)。这是 P99 抖动 1.1× 的根因。

C.6 查询路径:Hybrid 过滤如何下推

flowchart LR PLAN["SQL 计划
WHERE author=42 AND MATCH(...) ORDER BY l2(emb,?)"] --> DAS["ObVectorIndexLookupOp"] DAS --> SCALAR["1) 先评估标量谓词
得到候选 rowkey 集合"] SCALAR --> VID["2) 转成 vid bitmap"] VID --> FILT["3) 注入 ObHnswBitmapFilter"] FILT --> DELTA2["4) ANN @ delta HNSW (带 filter)"] FILT --> SNAP2["4) ANN @ snapshot HNSW (带 filter)"] DELTA2 --> MRG2["5) 合并 top-K"] SNAP2 --> MRG2 MRG2 --> LOK2["6) 回 rowkey_vid 取 rowkey,回主表取整行"]

关键:filter 是下推到 HNSW 图遍历内部的,而非 ANN 出 K 后再过滤——后者会因被过滤掉太多而出现「召回不足」需要放大 ef_search 的恶性循环。

C.7 支持的算法族

类型调用引擎关键参数
HNSWVSAG hnswM, ef_construction, ef_search
HNSW_BQVSAG hnsw + binary quant同上 + bq 量化层
IVF_FLATseekdb 自管nlist, nprobe;用 ObVectorKmeansCtx 训练
IVF_SQ8同上 + 标量量化nlist, nprobe
IVF_PQ同上 + 乘积量化nlist, m (子空间数), nbits

IVF 系列的桶(centroid)数据走 ObVectorIndexIvfCacheMgr + ObIvfAsyncTaskExecutor 异步训练与刷新。

附 D · HNSW 算法与工程实现

D.1 算法直觉:跳表 + 邻居图

HNSW (Hierarchical Navigable Small World) 把 ANN 看作「在一张图上做最短路径搜索」:

D.2 图的层次结构

flowchart TB subgraph L2["Layer 2 · 最稀疏 (e.g. 1% 节点)"] E["entry point"] L2A["o"] --- L2B["o"] E --- L2A end subgraph L1["Layer 1"] L1A["o"] --- L1B["o"] L1B --- L1C["o"] L1A --- L1C end subgraph L0["Layer 0 · 全部节点 + 全部邻居 (M_max 条边)"] A0["o"] --- B0["o"] B0 --- C0["o"] A0 --- D0["o"] D0 --- C0 end E -. 同一节点不同副本 .-> L1A L1C -. 同一节点 .-> A0

D.3 关键参数

参数含义取值经验
M每个节点保留的双向邻居上限 (高层) 16(默认),越大召回越高、内存越大
M_max0底层 Layer 0 的邻居上限通常 2 × M
ef_construction建图时的候选集大小200–512,影响构图质量
ef_search查询时维护的候选集大小topK 的 2–10 倍;越大召回越高、延迟越大
mL (= 1 / ln(M))新节点最高层的指数分布参数由 M 决定,无需调

D.4 插入算法(伪流程)

sequenceDiagram participant U as Insert(vec) participant H as HNSW Index U->>H: 随机一个最高层 l = floor(-ln(uniform()) * mL) H->>H: 从 entry_point 在 layer L..l+1 贪心下行,每层只保留 1 个最近邻 H->>H: 在 layer l..0 上做 ef_construction 的 beam search 拿候选集 H->>H: 在候选中用启发式选 M 个邻居 (避免聚簇) H->>H: 双向连边;若邻居超 M 上限则收缩 alt l > 当前最高层 H->>H: 更新 entry_point 为新节点 end

D.5 查询算法

  1. 从 entry_point 出发,沿 layer L → 1 贪心下行:每层用 ef=1 的 best-first 搜索,找到该层离 query 最近的点。
  2. 到达 layer 0,把上一层的最近点作为起点,做 ef_search 大小的 best-first 搜索(维护两个堆:candidates 最小堆 + W 最大堆)。
  3. 循环:从 candidates 弹出最近点 c;若 dist(c, q) > W 堆顶 → 提前停止;否则展开 c 的邻居,未访问过的点计算距离,放入 candidates 和 W。
  4. 最终返回 W 中的 top-K。
flowchart LR Q["query vec"] --> EP["entry_point @ Top Layer"] EP --> G1["greedy descent
逐层只保留 1 近邻"] G1 --> L0Q["到达 Layer 0"] L0Q --> BFS["beam search
ef_search 大小候选集"] BFS --> FLT["可选: BitmapFilter 跳过被过滤 vid"] FLT --> OUT["top-K"]

D.6 seekdb 的工程化要点

问题解决
动态写入下 entry_point 失效VSAG 内部用版本号;Adapter 同时维护 delta 与 snapshot,新写入只入 delta,避免破坏 snapshot 图结构
删除HNSW 不支持删点;用 vbitmap tablet 标记软删除,查询时 FilterInterface 过滤;Major Merge 时随 Snapshot 重建
过滤后召回坍塌不在 ANN 出 K 后过滤,而是用 ObHnswBitmapFilter 在 best-first 搜索每次展开邻居时就过滤;并自适应放大 ef_search
持久化Snapshot HNSW 整图序列化为字节流,按 16KB micro block 切分,写到 snapshot tablet 的 SSTable;重启 mmap 加载
并发查询整图只读时无锁;只有 add/refresh 期间 Adapter 持写锁,写时其他查询自动落到旧版本
内存隔离ObVsagMemContext 把 VSAG 内部 alloc 全部托管到租户 MemoryContext,超限被熔断
距离函数L2 / IP / Cosine 三选一;SIMD (AVX2/Neon) 内联

D.7 复杂度

插入期望 O(log N) × M 邻居筛选
查询期望 O(log N + ef_search × M)
内存每点 ~ M × 4 字节 邻居指针 × 层数;典型 384 维 + M=16 ≈ 1.6KB / 点
「为什么 ef_search 调大就慢、调小召回就低?」
ef_search 是 best-first 的候选集大小:候选越大,越不容易陷入局部最优(召回↑),但每次都要计算距离并维护堆(延迟↑)。本质是图搜索的 beam width 调参。
陷阱题「HNSW 支持范围 + 向量混合查询吗?」
HNSW 本身不支持。要么前过滤(pre-filter,先标量再 ANN,seekdb 的做法),要么后过滤(post-filter,召回多了再裁),要么用 IVF + 倒排(部分场景)。seekdb 把过滤位图传给 FilterInterface,所以是带 filter 的图搜索

附 E · Change Stream 正确性与背压

E.1 为什么 Fetcher 是单线程

PALF 是顺序日志。索引构建必须保证同一行的事件按顺序消费:先 INSERT 后 UPDATE 必须按这个顺序进入 HNSW,否则最终状态错。所以 Fetcher 单线程顺序拉日志,并按 tx_id 聚合,只在事务 commit 时整事务下推。

E.2 同事务多 DML 的顺序

E.3 Rollback to Savepoint 的正确性

flowchart LR TX["事务 T"] --> S1["INSERT a seq=10"] S1 --> SP["SAVEPOINT sp1"] SP --> S2["UPDATE b seq=20"] S2 --> S3["DELETE a seq=30"] S3 --> RB["ROLLBACK TO sp1
产生 rollback 区间"] RB --> S4["COMMIT"] S4 --> CS2["Change Stream 看到所有 redo + rollback 区间"] CS2 --> SEL["对 seq_no 命中 rollback 区间的行跳过"] SEL --> ONLY["最终只把 INSERT a 投到 HNSW"]

这保证索引不会出现「曾发生又被回滚」的脏数据。

E.4 背压

Fetcher → Dispatcher固定容量 ring buffer;满则 Fetcher 暂停拉 PALF(PALF 自带保留期,不丢日志)
Dispatcher → WorkerLinkQueue 自适应线程数;Worker 慢则 Dispatcher 减速
Worker → 索引VSAG 内存超限触发熔断 → 主动 freeze delta → 切 snapshot,腾空内存

E.5 refresh_scn 与可见性

「索引落后数据多少?」
正常 ~ ms 级;高写入压力下取决于 worker 数与向量维度。但不会无限落后——内存到上限就强制 freeze + snapshot 切换,把队列腾空。

附 F · 写路径全链路时序

sequenceDiagram participant C as Client participant OB as observer / OMT worker participant SQL as ObSql Parser-Optimizer participant DAS as DAS Insert Op participant TX as TransService participant MT as Memtable QueryEngine participant PALF as PALF Paxos participant CS as ChangeStream participant VEC as Vector Adaptor C->>OB: INSERT INTO t VALUES emb OB->>SQL: parse + plan SQL->>DAS: das_insert_op DAS->>TX: trans_begin / join DAS->>TX: get_store_ctx snapshot_scn DAS->>MT: mvcc_write key,val MT->>MT: 找/建 ObMvccRow MT->>MT: 抢 latch_, 检查 WW 冲突 MT->>MT: 挂 ObMvccTransNode scn=MAX MT->>TX: callback 入 ObMemtableCtx TX->>PALF: append redo PALF->>PALF: 多数派落盘 PALF-->>TX: on_commit scn=X TX->>MT: 回填 callback 的 trans_version_ = X TX-->>OB: end_trans_cb OB-->>C: OK Packet ACK Note over PALF,VEC: 以下完全异步,不阻塞客户端 PALF->>CS: ObCSFetcher 顺序拉 redo CS->>CS: 按 tx_id 组装 + 等 commit CS->>VEC: Dispatcher → Worker → AsyncIndex Plugin VEC->>VEC: add_node vid,vec → Delta HNSW

F.1 关键耗时分解

阶段典型耗时说明
SQL 解析 + plan10–100 μs命中 plan cache 可 < 5 μs
mvcc_write 挂链1–5 μslatch 自旋 + 链头比较
PALF append 多数派1–5 ms跨节点 RTT 主导
客户端 ACK1–6 ms= 上面之和(SLA 关键)
异步索引落地1–几十 ms取决于负载与维度,不阻塞客户端

F.2 嵌入式模式下的差异

附 G · 常见问题回答

Q1 · seekdb 怎么做到写入和查询并发还不抖动?

  1. 写不碰索引:DML 只写 memtable + PALF redo;索引由 Change Stream 异步消费。
  2. 索引层数恒为 2:Delta + Snapshot;查询永远只对两个 HNSW 做 ANN,不会随写入累积膨胀。
  3. 读不加行锁:MVCC 用版本链 + SCN 比较解决可见性,避免读写竞争。
  4. 资源隔离:OMT 给每个租户独立 worker pool + memstore 配额。

Q2 · 刚 INSERT 完为什么 APPROXIMATE 查询有可能查不到?

因为向量索引是异步构建的。解法:

Q3 · FORK DATABASE 怎么做到秒级、零拷贝?

  1. RootServer 在 schema 层创建新 db + 新 table_id + 新 tablet_id(纯元数据,秒级)。
  2. 每个新 tablet 标记 COW,引用原 tablet 的 SSTable 宏块,仅引用计数 +1,不复制数据。
  3. 沙箱写入只写沙箱自己的 memtable / SSTable。读时融合「共享基线 + 沙箱增量」。
  4. MERGE 按 STRATEGY 解冲突(FAIL/THEIRS/OURS)打回主库;DROP 直接释放引用,引用计数归零时回收宏块。

Q4 · DELETE 一个向量,HNSW 里这点为什么不能立刻拿掉?

HNSW 是双向邻居图,删点会让邻居出现悬挂边,破坏 small-world 性质。所以 seekdb 用 tombstone: DELETE 时写一个 DELETE TransNode + 把 vid 设入 vbitmap;查询时 FilterInterface 跳过; 只有 Snapshot 重建 / Major Merge 时才物理去除节点。

Q5 · HNSW 的 M 调大有什么后果?

经验:M=16 通用够用;高维稀疏数据可调到 32–48。

Q6 · Hybrid 查询里标量过滤掉 90%,ANN 还能召回吗?

后过滤 (post-filter)先 ANN 取 topK,再过滤——容易召回崩溃(K 个候选可能全被过滤掉)
seekdb 做法 (内嵌过滤)把谓词转 bitmap 注入 HNSW FilterInterface;图搜索每次邻居展开时即过滤;自适应放大 ef_search 直至找到 K 个真正有效结果

因此即便选择率 ≤ 10%,召回率依然能保持。

Q7 · 事务 commit 之后,follower 还没重放完,从 follower 读会怎样?

Q8 · seekdb 比 Milvus / ES 在 Streaming 场景快的本质?

  1. 索引段数不发散:Milvus 每次刷盘产生一个 segment,写久了几百段,并发查询时 N×M 段 × 线程争抢 CPU;seekdb 索引数永远是 2。
  2. 写路径与索引解耦:seekdb 写 commit 不等索引;ES 的 refresh 是同步阻塞。
  3. MVCC 不抢锁:高并发下读完全无锁;ES 的 segment 锁、Milvus 的 growing segment lock 都会成为瓶颈。
  4. 同一存储 + 同一执行器:标量、向量、全文用同一套 SQL 计划下推到存储,比客户端 N+1 拼接快一个数量级。

Q9 · GTS 挂了集群还能写吗?

Q10 · 一条 SQL 同时含 vector + fulltext + scalar 怎么执行?

Optimizer 生成一个 plan

  1. 标量索引/范围查询先产生候选行集合 → 转 bitmap;
  2. 把 bitmap 注入 ANN 的 FilterInterface(前过滤),向量索引出 topK;
  3. 同时倒排表按词命中产生 doc_id 集合;
  4. 执行器内部按 SQL ORDER BY 合并打分(向量距离 + BM25 + 标量 score),最终 top-K 回主表取整行。

关键:所有过滤都在存储/索引层完成,结果集没有客户端拼接,这是性能/正确性优势的核心。

你怎么验证一致性?答:(1) Major Merge 时做全局校验和 (column_checksum) 对比三副本;(2) ddl_checksum 保证 schema 变更后数据不丢;(3) PALF 自带 Paxos 多数派认定 commit;(4) ObLockWaitMgr + 分布式死锁检测器保证并发正确。