読者です 読者をやめる 読者になる 読者になる

テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(writeboost report) update_sb_record_intervalを使うとバグる

github.com

ジョウピンというのは中国名だろうか. dm-writeboostを1週間ほど動作させたら, 再起動時にエラーが出たとGitterで報告してきた.

CPUが大変不安定ということなので私の第一感はそれによって何か起こったというものだったが, 実際にはただのコードのバグだったことが分かった. 彼に, wb_metaツールを使ってキャッシュデバイスを解析するように依頼し, 週末には返事が来なかったがようやく返事が返ってきた. それを以って色々と考えると, やはりコードのバグだった.

writeboostは, SSDの先頭1MB領域をsuperblockと名付け, そのうち先頭512Bをsuperblock header, 尻尾の512Bをsuperblock recordと読んでいる. headerは初期化時から永遠に固定されるものだが, recordは動的に変化し得るものとして役割が異なる.

sb recordには現在, 「一体どのIDまでライトバックしましたか」という情報が入っている. これによって, 再起動後に「すでにライトバックしたものを再度ライトバックすること」を防げる. もっとも, この情報は単なるヒントであり, もし省略された場合は, flushされたIDの先頭から「最低ここまではライトバックされたはずだ」という位置を推定する. infer_last_writeback_idにこの処理が書かれている.

static int infer_last_writeback_id(struct wb_device *wb)
{
    int r = 0;

    u64 record_id;
    struct superblock_record_device uninitialized_var(record);
    r = read_superblock_record(&record, wb);
    if (r)
        return r;

    atomic64_set(&wb->last_writeback_segment_id,
        atomic64_read(&wb->last_flushed_segment_id) > wb->nr_segments ?
        atomic64_read(&wb->last_flushed_segment_id) - wb->nr_segments : 0);

    /*
    * If last_writeback_id is recorded on the super block
    * we can eliminate unnecessary writeback for the segments that were
    * written back before.
    */
    record_id = le64_to_cpu(record.last_writeback_segment_id);
    if (record_id > atomic64_read(&wb->last_writeback_segment_id))
        atomic64_set(&wb->last_writeback_segment_id, record_id);

    return r;
}

問題は, returnの前の「もしsb recordに書かれたlast_writeback_segment_idが推定したものより新しければそれを採用する」という部分で起こる. この採用自体は良いのだが, セグメントのメモリ上の状態がライトバックされたあとの状態になってないということが問題を引き起こす. 具体的には, そのセグメントをflush用に獲得しようとした時, 「セグメントはクリーンである」という不変条件を満たせない. writeboost-test-suite上にリプロデューサは作った.

    Memory(Sector.M(128)) { backing =>
      Memory(Sector.M(32)) { caching =>
        Writeboost.sweepCaches(caching)
        Writeboost.Table(backing, caching).create { s =>
          s.bdev.write(Sector(0), DataBuffer.random(Sector.M(64).toB.toInt))
          s.dropTransient()
          s.dropCaches()
          assert(s.status.lastFlushedId === s.status.lastWritebackId)
          s.dm.message("update_sb_record_interval 1")
          Thread.sleep(5000) // wait for updating the sb record
        }
        // this should not cause kernel panic
        Writeboost.Table(backing, caching).create { s =>
        }
      }
    }

実直には, このifに入った時に, セグメントの状態をライトバックされた風に帰着させるというのが考えられるが, ミスを誘発しやすい気がするのと, ロジックの重複が気になるため, より良い方法がないか考える.

なぜ起こったかというと, update_sb_record_intervalを使っている人がいなかったからだろう. 私自身, 「当然動くだろう」と鷹をくくってたため, テストが抜けていたくらいだ. 叩いてくれるユーザには感謝したい.