Skip to content

GC 无法回收空间:所有扇区 dirty 状态为 FALSE 时的问题 #382

@BurnerK

Description

@BurnerK

GC 无法回收空间:所有扇区 dirty 状态为 FALSE 时的问题

问题描述

当 FlashDB KV 数据库空间不足时,垃圾回收(GC)过程无法回收空间,因为 GC 只处理 dirty=2(FDB_SECTOR_DIRTY_TRUE)或 dirty=3(FDB_SECTOR_DIRTY_GC)状态的扇区。但在某些场景下,所有扇区都保持 dirty=1(FDB_SECTOR_DIRTY_FALSE)状态,导致 GC 跳过所有扇区,无法释放空间。

根本原因分析

  1. 扇区 dirty 状态初始化:当扇区被格式化时(例如 GC 后或初始化时),它们被设置为 FDB_SECTOR_DIRTY_FALSE(dirty=1)。

  2. 扇区 dirty 标记失败:当调用 fdb_kv_set_blob 时,如果 new_kv_ex 因空间不足而失败,函数会立即返回 FDB_SAVED_FULL,而不会调用 del_kv 来标记扇区为脏。这意味着:

    • 旧的 KV 可能已被标记为 FDB_KV_PRE_DELETE,但它们的扇区从未被标记为脏
    • 包含已删除/垃圾 KV 的扇区保持 dirty=1 状态
  3. GC 处理限制do_gc 函数只处理 dirty=2dirty=3 的扇区:

    if (sector->check_ok && (sector->status.dirty == FDB_SECTOR_DIRTY_TRUE || sector->status.dirty == FDB_SECTOR_DIRTY_GC)) {
        // 处理 GC
    }

    这导致所有 dirty=1 的扇区被跳过,即使它们包含可以回收的垃圾 KV。

复现步骤

  1. 用参数填满 KV 数据库,直到接近满载
  2. 持续更新参数(例如 fdb_kv_set_blob
  3. 当空间不足时,new_kv_ex 失败
  4. GC 被触发,但由于所有扇区都是 dirty=1,所有扇区被跳过
  5. GC 完成但没有释放任何空间
  6. 后续的 fdb_kv_set_blob 调用继续失败,返回 FDB_SAVED_FULL(err=7)

预期行为

GC 应该能够从包含垃圾 KV 的扇区回收空间,即使这些扇区没有被标记为脏。这在空间不足且没有脏扇区可用时尤其重要。

实际行为

GC 跳过所有 dirty=1 的扇区,即使它们包含垃圾 KV,导致:

  • do_gc: SKIP sector=0x..., dirty=1, store=2, DIRTY_TRUE=2, DIRTY_GC=3
  • gc_collect_by_free_size: GC completed, free_size=XX(但实际上没有释放空间)
  • new_kv: FAILED after GC, size=XX, KV full

建议的解决方案

修改 do_gc 函数,使其在空间不足时也能处理处于 USINGFULL 状态的非脏扇区,因为它们可能包含垃圾 KV:

static bool do_gc(kv_sec_info_t sector, void *arg1, void *arg2)
{
    // ... 现有代码 ...
    bool should_gc = false;

    /* 处理脏扇区(正常情况) */
    if (sector->check_ok && (sector->status.dirty == FDB_SECTOR_DIRTY_TRUE || sector->status.dirty == FDB_SECTOR_DIRTY_GC)) {
        should_gc = true;
    }
    /* 当空间不足时,也处理非脏的 USING/FULL 扇区(它们可能包含垃圾) */
    else if (sector->check_ok && sector->status.dirty == FDB_SECTOR_DIRTY_FALSE 
             && (sector->status.store == FDB_SECTOR_STORE_USING || sector->status.store == FDB_SECTOR_STORE_FULL)) {
        should_gc = true;
    }

    if (should_gc) {
        // ... 现有的 GC 处理代码 ...
    }
}

这确保了:

  • 有效的 KV(FDB_KV_WRITEFDB_KV_PRE_DELETE)被正确移动到新空间
  • 垃圾 KV 被跳过,扇区被格式化以释放空间
  • 空扇区不会被不必要地处理

安全性分析

此修改是安全的,因为:

  1. 有效的 KV 通过现有的 move_kv 逻辑被正确移动
  2. 只处理 USINGFULL 扇区,不处理 EMPTY 扇区
  3. 垃圾 KV 被正确跳过和清除
  4. GC 过程已经受到锁保护

额外说明

此问题是在生产环境中发现的,当时参数更新开始间歇性失败。通过调用恢复默认值函数可以暂时解决问题,但一段时间后会再次出现。

日志示例

显示问题的日志输出示例:

[KV_TRACE] do_gc: SKIP sector=0x00000000, check_ok=1, dirty=1, store=2, DIRTY_TRUE=2, DIRTY_GC=3
[KV_TRACE] do_gc: SKIP sector=0x00001000, check_ok=1, dirty=1, store=1, DIRTY_TRUE=2, DIRTY_GC=3
[KV_TRACE] do_gc: SKIP sector=0x00002000, check_ok=1, dirty=1, store=2, DIRTY_TRUE=2, DIRTY_GC=3
[KV_TRACE] gc_collect_by_free_size: GC completed, free_size=39
[KV_TRACE] new_kv: FAILED after GC, size=39, KV full

相关代码位置

  • 文件:fdb_kvdb.c
  • 函数:do_gc(约第 1190 行)
  • 相关函数:gc_collect_by_free_sizedel_kvset_kv

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions