跳转到内容

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)管理读写中等

拆分理由

  1. 减少锁竞争:不同数据类型的访问模式不同,拆分后互不阻塞
  2. 独立备份和清理:日志可以单独轮转,不影响元数据
  3. 故障隔离:一个库损坏不影响其他库
  4. 迁移独立:各库可以独立进行 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.rsLogDbLayer 实现了 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 对日志采用批量异步写入,对元数据采用同步写入。原因是:

  • 日志是”尽力而为”的,丢失几条不影响功能
  • 元数据(如线程状态)必须强一致,不能丢失

这种混合策略平衡了性能和可靠性。


参考来源