第8章: ストレージ

実行中のシステムのすべてのデータはメモリに存在します——高速で揮発性で有限です。プロセスがクラッシュしたりサーバーが電源を失ったりするとそのデータは消滅します。ストレージサービスは永続的なデータ保存を提供します:サービスに書き込まれたデータがプロセスの再起動、ハードウェア障害、電源障害にも耐えるという保証です。ストレージはステートフルなシステムが構築される基盤です。

ストレージエンジンの設計は競合する要求のバランスを取ることを含みます。読み取りは高速であるべきです(理想的にはメモリから提供)。書き込みは耐久性があるべきです(確認前に安全にディスクに保存)。スペースは効率的に使用されるべきです(古いデータはコンパクション)。そしてシステム全体がクラッシュ後に迅速に回復すべきです。私たちの実装はこれらの各懸念に古典的な技術で対処します:ライトアヘッドログです。

インターフェース

storage/src/lib.rs ストレージサービスは4つのプロシージャを提供します。getとputがデータの読み書きの基本的なキーバリューインターフェースを形成します。deleteはキーを削除しscanはプレフィックスに一致する複数のエントリを取得します。

pub const GET_PROCEDURE: ProcedureId = 1;
pub const PUT_PROCEDURE: ProcedureId = 2;
pub const DELETE_PROCEDURE: ProcedureId = 3;
pub const SCAN_PROCEDURE: ProcedureId = 4;

#[derive(Debug, Serializable, Deserializable)]
pub struct GetArgs {
    pub key: String,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct GetResult {
    pub value: String,
    pub found: i32,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct PutArgs {
    pub key: String,
    pub value: String,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct DeleteArgs {
    pub key: String,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct ScanArgs {
    pub prefix: String,
    pub limit: i32,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct ScanResult {
    pub entries: String,
}

実装

storage/src/engine.rs ストレージサービスの心臓部はそのStorageEngineです。エンジンは高速読み取りのためのインメモリHashMap、耐久性のためのライトアヘッドログ(WAL)、効率的なリカバリのためのスナップショットメカニズムを維持します。この組み合わせはSQLiteからPostgreSQLまでのデータベースに見られる古典的なパターンです。

pub struct StorageEngine {
    data: HashMap<String, String>,
    wal_path: PathBuf,
    snapshot_path: PathBuf,
    operations_since_snapshot: usize,
}

const COMPACTION_THRESHOLD: usize = 1000;

ライトアヘッドログが耐久性の鍵です。putでもdeleteでもすべての書き込み操作はインメモリデータ構造が更新される前にまずディスク上のWALファイルに追記されます。これにより書き込みを確認した直後にプロセスがクラッシュしてもログから操作を回復できることが保証されます。

リカバリは2段階でインメモリ状態を再構築します:まず最新のスナップショット(存在する場合)をロードし、次にスナップショット後に書き込まれたWALエントリをリプレイします。時間の経過とともにWALは操作の蓄積により成長します。コンパクションは現在のインメモリ状態のスナップショットを書き込み、WALを切り詰めることでこれを解決します。

設計の議論

ライトアヘッドログパターンは強い耐久性保証を提供します:データは書き込みが確認される前にディスクに書き込まれます。しかし私たちの実装は単一のディスク上の単一のファイルに書き込みます。本番のストレージサービスはディスクやマシンの障害に耐えるためにデータを複数のサーバーにレプリケートします。

インメモリのHashMapはO(1)の読み取りを提供しますが総データサイズを利用可能なメモリに制限します。より大きなデータセットに対してはストレージエンジンはLSMツリーやBツリーなどのディスク上のデータ構造を使用します。

scan操作のプレフィックスベースのフィルタリングは多用途のプリミティブです。名前空間化、範囲クエリ、列挙などのパターンを可能にします。ハイライトシステムはこのパターンを使用してユーザーごとページごとのハイライトをhl:{user_id}:{page_slug}のようなキーの下に格納します。