Rustでリレーショナルデータベースを自作したときの成果と反省と学び

はじめに

この記事では、個人プロジェクトとしてRust言語でリレーショナルデータベースを開発した経験(もう五ヶ月も前...)について、その成果と反省、得た学びを共有します。

DBMSを自作した理由

自分がDBMSの自作に着手したのは、『Designing Data-Intensive Applications』という本の内容を深く理解するためでした。

この本は、データシステムの設計と運用において最も大切な「信頼性」、「拡張性」、「保守性」を保証する方法論を、豊富な文献を引用しつつ、理論と実践の橋渡しを巧みに行いながら、丁寧に説明している名著です。読んだことがない人は速攻購入してくだい。本当にいい本です。

この本は、データベースの内部構造に関する話も豊富に含まれていたので、「データベース自作してみようか...」という気持ちになりました。

Rustを採用した理由

データベースの実装のついでに、Rustも詳しくなりたかったので、Rustを実装言語として採用しました。

結構、安直で申し訳ないですが、それが真実です...。

参考にしたデータベース

PostgreSQLMySQLなどの商業用のデータベースは、大概、数千万行のコード量になります。 現実的に考えると、これらのデータベースのソースコードを読んで実装を勉強するのは無理なので、教育用のデータベースを教材にする必要がありました。

参考にする教育用のデータベースの候補には、CMUのデータベースの講義で使われるbustubDatabase Design and Implementationという教科書で実装が解説されているSimpleDBがありましたが、教科書がある方が自分のレベルや学習方法と合うと思い、SimpleDBを参考にすることにしました。

実装したデータベース

OxideDBというデータベースを実装しました。

「Rustは錆」→「錆は鉄の酸化物」→「酸化は英語でOxide」ということで、OxideDBと名付けました。

命名自体は結構気に入っていますが、DBを入れると被りやすい命名アンチパターンを踏んでいるらしく、次は、無関係な一音節の単語2つを繋げる「The Pavlo Database Naming Method」という方法を採用していこうと思います。

実装とテストをあわせて、14,000行ほどの規模のソフトウェアになりました。

CRUD操作も基本的なSQLで実行できて、インデックスも張ることができます。

これが実装できた自分を誇らしく思います。

成果

ソフトウェア開発のコツが掴めた

ソフトウェア開発は抽象化の連続」という言葉が好きなのですが、まさにこの言葉の意味をこの身を持って理解することができました。

初期段階では、実装に必要なすべてのコードの詳細を頭に入れておかなければ、一行も書けなかったのですが、 開発が進むにつれ、処理を適切に抽象化しインターフェース化したら、その瞬間に具体的な実装の詳細を忘れても、問題なくコードが書けるようになりました。

人間の脳は忘却後に抽象化するプロセスを辿りますが、ソフトウェア開発ではこのプロセスを逆転させるだけで効率化できることに気付けたのは自分にとって大きな成果だと感じています。

トランザクションを理解できた

データベースにおけるデータの一貫性を守るための機構であるトランザクションの仕組みを、実装を通して理解できました。 この経験によって、「ラッチ」、「ロック」、「WAL」、「トランザクション分離レベル」などの、ドキュメントで頻繁に登場するものの直感的に理解しにくい概念を実装ベースで理解することができました。

自信を育むことができた

DB自作は労力的にはコンパイラ自作やOS自作と比べても遜色ないので、そのレベルのことを学部生のうちにできたことは自信に繋がりました。

多少、難しいエンジニアリングも「まぁ、データベース作ったことあるし。」で片付いて臆することがなくなりました。

反省

途中でデータベース実装がただのコードの翻訳作業となった

SimpleDBのソースコードを参考にしながらRustでデータベースを実装した訳ですが、途中で実装作業が、コードの翻訳作業になりました。

途中から、ソースコードの実装上の意味などを深く考えずに、ただ流れ作業でJavaのコードをRustに変換していました。

データベースの実装の知識がない状態では、学習のために多少コードの写経や翻訳作業を強いられるのは仕方のないことですが、学習の目的を見失わないようにするべきでした。

ラッチの粒度が粗い

データベースの機能は実装できたものの性能が絶望的にないです。 具体的には、データベース内でディスクにバイト列を読み書きするFileManagerや、ヒープ上でデータの読み書きをして低速なディスクI/Oの代わりにメモリI/Oでデータの読み書きを行うBufferManagerなどのコンポーネントや、他の全てのコンポーネントArc<Mutex<>>でラップされている影響でデータの読み書きが並列化できていないです。

pub struct OxideDB {
    block_size: usize,
    file_manager: Arc<Mutex<FileManager>>,
    log_manager: Arc<Mutex<LogManager>>,
    buffer_manager: Arc<Mutex<BufferManager>>,
    lock_table: Arc<Mutex<LockTable>>,
    metadata_manager: Option<Arc<MetadataManager>>,
    planner: Option<Arc<Mutex<Planner>>>,
}

これはJavaに最適化されたアーキテクチャをあまり深く考えずにRustに変換してしまったことが原因です。 LogManagerBufferManagerはディスクアクセスを行うため、ディスクアクセスを行うFileManagerインスタンスをコンストラクタに渡して依存性注入(DI)が行われています。他のインスタンスも同様に、DIのマトリョシカを行うように設計されています。

DIによって、クラス/構造体間の結合度を下げ、変更やテストが容易なコードになるのですが、このデザインパターンは、マルチスレッドのプログラムにおいて*1、「複数のインスタンスで共有する場合はArc<>、さらに、インスタンスに変更が加えれる可能性があるのであれば、Arc<Mutex<>>をラップしなければならない」というRustの言語ルールと衝突します。

今回の実装では、メモリ管理をGCに任せるJavaと、所有権を持つスコープを厳密に管理してメモリ管理をするRust、それぞれの最適なソフトウェアデザインパターンが異なることを意識せずに、元のSimpleDBの設計を忠実にRustで再現してしまったので結果的に、ラッチの粒度が粗いデータベースが出来上がってしまいました。

次回は、Rustの言語ルールに沿ってデータベースを設計することにします。

データベース開発と先人のアドバイスから得た学び

自分は、とあるデータベース自作コミュニティに参加しているのですが、そのコミュニティで頂いたアドバイスから得た学びを共有します。

ラッチの粒度の粗さは伸び代

コミュニティでラッチの粒度が粗いことを嘆いていたら、「ラッチの粒度の粗さは伸び代だよ」と慰まれました。

皆さんも自作DBに挑戦される際は、ロックの粒度に臆せず実装をしていきましょう。

ソフトウェアの設計と実装は繰り返される試行を通して洗練される

今回作ったものは、それはそれで理解の役に立ったし、経験値も積めたので、感謝とともにどっかで投げ捨てましょう」(一部に変更を加えました)

データベース実装の労力を鑑みるとかなりショッキングなアドバイスですが、結果的にためになりました。

設計は本来、その後の実装における不確実性を最小限に抑えるためのものですが、実際の開発過程では、設計の段階で想定していた通りに進むことは稀です。さらに、実装段階で設計自体に問題が見つかることも少なくないです。

そのようなケースで、ソフトウェアの可塑性の限界を超える変更を加える必要があると判断した場合、また0からやり直すのも手です。

このアドバイスがなかったら、このOxideDBに価値がほとんどない追加機能作業をして時間を無駄にするところでした。

このアドバイスをしてくれた方によると、3ラウンドくらいで良い設計になるらしいです。

あと2ラウンド頑張ります!

※ シンプルにベターなソフトウェアを目指す場合の知見です。ビジネスの話だと、既に使った工数を無駄にすることは避けるインセンティブが働くので、この考え方はトレードオフの関係にあるということに注意が必要です。

今後の展望

並行処理を勉強

今回の開発では、自分の並行処理の知識のなさが誤った実装の原因だと考えているので、詳しくなりたいです。

教科書は『並行プログラミング入門―Rust、C、アセンブリによる実装からのアプローチ』を読もうと思います。

とにかくラウンド2

ラウンド2に挑戦したいです。 大体、Rustに合う設計は頭の中にぼんやりあるので、並行プログラミングについての勉強と並行して行いたいです。

教材的なのをOSSで作りたい

もっとデータベース自作erを増やしたい所存なので、初学者が取り掛かりやすいようなハンズオン形式の教材をかなり遠い将来作りたいと考えています。

イメージとしては、rustlingと似たようなものを想定しています。

終わり

最後まで読んでくれてありがとうございました。データベース自作ぼちぼち頑張りますので、応援のほどよろしくお願いします。

ちなみに、自分が実装の際に参考にした文献はだいたい自作RDBMSやろうぜ!にあります。自作DBコミュニティの情報もあります。


修正

なるべく正しい知識を広めたいので、記事に間違った内容があれば、どのような手段でも構わないのでご連絡ください。

  • 2024/03/05
    • 「ラッチの粒度が粗い」の節で、「マルチスレッドのプログラムにおいて」という文言を追加しました。
    • ryo_gridさんのご指摘によって修正、ありがとうございました!

*1:シングルスレッドの場合はRefCell型でいけます。