SQLite 状态持久化:三库拆分与 WAL 模式
SQLite 状态持久化:三库拆分与 WAL 模式
学习目标
- 理解 Codex 的三库拆分架构和拆分理由
- 掌握 Codex 的 WAL 模式和批量写入实现
- 分析 Codex 的元数据实时提取和日志分区策略
项目实践
三库拆分架构
codex-rs/state/src/lib.rs 将状态拆分为三个 SQLite 文件:
| 数据库 | 文件 | 用途 | 访问模式 |
|---|---|---|---|
| State DB | ~/.codex/state_5.sqlite | 线程元数据、Agent 图、backfill 状态 | 读写频繁 |
| Logs DB | ~/.codex/logs_2.sqlite | 结构化日志(tracing 事件) | 写多读少 |
| Goals DB | ~/.codex/goals_1.sqlite | 线程目标(ThreadGoal)管理 | 读写中等 |
拆分理由:
- 减少锁竞争:不同数据类型的访问模式不同,拆分后互不阻塞
- 独立备份和清理:日志可以单独轮转,不影响元数据
- 故障隔离:一个库损坏不影响其他库
- 迁移独立:各库可以独立进行 schema 迁移
SQLite 配置
// 连接配置let pool = SqlitePoolOptions::new() .connect_with( SqliteConnectOptions::new() .filename(path) .journal_mode(SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) .busy_timeout(Duration::from_secs(5)) ) .await?;关键参数:
journal_mode = WAL:Write-Ahead Logging,读写不冲突synchronous = NORMAL:平衡安全与性能(每 WAL checkpoint 同步一次)busy_timeout = 5s:锁竞争时自动重试
迁移策略
Codex 使用 sqlx::migrate 管理数据库迁移:
sqlx::migrate!("./migrations") .run(&pool) .await?;向前兼容:设置 ignore_missing: true,允许旧二进制打开新迁移后的数据库。这是关键设计——升级后的数据库不会被旧版本拒绝。
日志批量写入
state/src/log_db.rs 的 LogDbLayer 实现了 tracing 事件的批量写入:
impl<S> Layer<S> for LogDbLayer { fn on_event(&self, event: &Event, _ctx: Context<S>) { // 非阻塞发送事件到 channel let _ = self.sender.try_send(LogEntry::from(event)); }}后台任务:
async fn log_writer(mut receiver: Receiver<LogEntry>, pool: SqlitePool) { let mut batch = Vec::with_capacity(LOG_BATCH_SIZE); let mut flush_interval = interval(LOG_FLUSH_INTERVAL);
loop { tokio::select! { Some(entry) = receiver.recv() => { batch.push(entry); if batch.len() >= LOG_BATCH_SIZE { flush_batch(&pool, &batch).await; batch.clear(); } } _ = flush_interval.tick() => { if !batch.is_empty() { flush_batch(&pool, &batch).await; batch.clear(); } } } }}参数:
LOG_BATCH_SIZE:128 条LOG_FLUSH_INTERVAL:2 秒- 分区限制:每线程最多 10 MiB / 1000 条
元数据实时提取
state/src/extract.rs 从 rollout JSONL 实时提取线程元数据:
pub struct SessionMeta { pub provider: String, pub cwd: PathBuf, pub git_info: Option<GitInfo>,}
pub struct TurnContext { pub model: String, pub reasoning_effort: Option<String>, pub sandbox_policy: SandboxPolicy,}
pub struct TokenCount { pub cumulative_prompt: u64, pub cumulative_completion: u64,}提取时机:在事件流处理过程中实时提取,而非事后扫描。这确保了元数据的及时性和一致性。
问题与规避
SQLite 并发锁竞争
Codex 的多重缓解措施:
- WAL 模式:读写互不阻塞
- 连接池:复用连接,减少创建开销
- 短事务:尽快提交,减少锁持有时间
- 5 秒 busy timeout:自动重试瞬态冲突
WAL 文件无限增长
Codex 没有显式处理 WAL 文件大小,依赖 SQLite 的自动 checkpoint 机制。在极端写入场景下,WAL 文件可能增长。建议生产环境配置定期 checkpoint。
设计取舍
三库拆分 vs 单库 schema 隔离
Codex 选择物理拆分为三个文件,而非在单库内使用 schema 隔离。优势:
- 真正的物理隔离,一个库的锁不影响其他库
- 可以独立备份、恢复和迁移
- 日志库可以单独清理,不影响元数据
代价是管理复杂度增加,需要维护三个连接池和迁移目录。
同步写入 vs 批量异步
Codex 对日志采用批量异步写入,对元数据采用同步写入。原因是:
- 日志是”尽力而为”的,丢失几条不影响功能
- 元数据(如线程状态)必须强一致,不能丢失
这种混合策略平衡了性能和可靠性。