RDBMSを自作して学ぶ:第15章 さらに学ぶために
前回は、作ったデータベースをPostgreSQL・SQLite・MySQL(InnoDB)と比べ、対応関係と省略した機能を整理しました。今回はシリーズの最終回です。ここまでの内容をさらに深めるための書籍・講義・論文、実際のデータベースのソースコードを読むときの道しるべ、そして分散データベースの世界へ進むための道筋を紹介します。
書籍と講義
Database Internals(Alex Petrov、O’Reilly、2019年)
ストレージエンジンと分散システムの2部構成で、データベースの内部を解説した本です。第1部では、B木の実装上の工夫、バッファ管理、WALとリカバリ、LSM木などを扱っていて、本シリーズの第3〜6章と第12章の内容をさらに掘り下げられます。第2部では、レプリケーション・合意形成・分散トランザクションを扱っているので、分散データベースに進む準備にもなります。日本語訳は『詳説 データベース』(オライリー・ジャパン)です。
Designing Data-Intensive Applications(Martin Kleppmann、O’Reilly、2017年)
データを扱うシステム全体の設計を、データモデル、ストレージ、レプリケーション、パーティショニング、トランザクション、ストリーム処理まで幅広く解説した本です。第7章のトランザクションの章は、分離レベルごとに起きる異常を具体例でていねいに説明していて、本シリーズの第10・11章と合わせて読むと理解が深まります。日本語訳は『データ指向アプリケーションデザイン』(オライリー・ジャパン)です。
Database System Concepts(Silberschatz・Korth・Sudarshan、McGraw-Hill)
データベースの定番の教科書です。関係代数とSQL、問い合わせの処理と最適化、トランザクション、同時実行制御、リカバリ(ARIESを含む)までが体系的にまとまっています。本シリーズで「本シリーズでは省略します」と書いた部分の多くは、この本で理論から学べます。
CMU 15-445/645 Database Systems
カーネギーメロン大学の、データベースの内部構造を扱う講義です。講義動画と資料が公開されています。バッファプール、B+木、クエリの実行、同時実行制御、リカバリと、本シリーズとほぼ同じ順番で進みます。課題では、BusTubという教育用のデータベース(C++)に、これらのコンポーネントを実装していきます。本シリーズをGoで一通り作った後にBusTubの課題に挑戦すると、同じテーマを別の設計で実装し直すことになり、よい復習になります。
Architecture of a Database System(Hellerstein・Stonebraker・Hamilton、2007年)
プロセスモデル、パーサ、オプティマイザ、ストレージ、トランザクションといった、実際のデータベースを構成する要素を1本の論文で概観した解説です。本シリーズで作った部品が、実際のデータベースの中でどう組み合わされているかを俯瞰するのに向いています。
論文
本シリーズで扱ったテーマの原典や、重要な論文を紹介します。どれも、データベースの教科書で必ず参照される論文です。
ストレージとインデックス
- R. Bayer, E. McCreight, “Organization and Maintenance of Large Ordered Indices”, Acta Informatica, 1972 ── B木を提案した論文
- D. Comer, “The Ubiquitous B-Tree”, ACM Computing Surveys, 1979 ── B木とその変種(B+木を含む)の解説。第6章の理論的な背景を知るのに最適
- P. Lehman, S. B. Yao, “Efficient Locking for Concurrent Operations on B-Trees”, ACM TODS, 1981 ── 並行アクセスに強いB-link木。PostgreSQLのB+木(nbtree)のもとになっている
- P. O’Neil, E. Cheng, D. Gawlick, E. O’Neil, “The Log-Structured Merge-Tree (LSM-Tree)”, Acta Informatica, 1996 ── 書き込みをまとめて順に書き出すことで、B+木より書き込みに強くしたデータ構造。RocksDBなど多くのストレージエンジンが採用している
クエリ処理
- P. Selinger ほか, “Access Path Selection in a Relational Database Management System”, SIGMOD, 1979 ── IBMのSystem Rで開発された、コストベースのクエリ最適化の原点
- G. Graefe, “Volcano — An Extensible and Parallel Query Evaluation System”, IEEE TKDE, 1994 ── 第8章で実装したイテレータモデルのもとになったシステム
トランザクションとリカバリ
- K. Eswaran, J. Gray, R. Lorie, I. Traiger, “The Notions of Consistency and Predicate Locks in a Database System”, Communications of the ACM, 1976 ── 2相ロックで直列化可能性が保証されることと、ファントムと述語ロックの問題を示した論文
- H. Berenson ほか, “A Critique of ANSI SQL Isolation Levels”, SIGMOD, 1995 ── SQL標準の分離レベルの定義のあいまいさを指摘し、スナップショット分離を定式化した論文
- C. Mohan ほか, “ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging”, ACM TODS, 1992 ── 第12章で参考にしたリカバリ手法。IBM DB2やMicrosoft SQL Serverのリカバリの基礎になっている
- M. Cahill, U. Röhm, A. Fekete, “Serializable Isolation for Snapshot Databases”, SIGMOD, 2008 ── SSI(Serializable Snapshot Isolation)。PostgreSQLのSERIALIZABLEはこの手法をもとに実装されている
OSSのソースコードを読む
自分でデータベースを作った後は、実際のデータベースのソースコードが格段に読みやすくなっています。「バッファプールはどこだろう」「ページLSNはどう使われているだろう」と、探すべきものがわかっているからです。
SQLite
SQLiteはC言語で書かれた、小さく読みやすいデータベースです。公式のソースコードはFossilというバージョン管理システムで管理されていますが、GitHubにもミラー(https://github.com/sqlite/sqlite)があります。
| ファイル | 内容 | 本シリーズとの対応 |
|---|---|---|
src/btree.c |
B+木(テーブルとインデックス) | 第5・6章 |
src/pager.c |
ページキャッシュ、ロールバックジャーナル、トランザクション | 第4・12章 |
src/wal.c |
WALモード | 第12章 |
src/parse.y |
SQLの文法(パーサ生成器Lemonで処理される) | 第7章 |
src/where.c |
クエリプランナ(WHERE句の解析とインデックスの選択) | 第8章 |
src/vdbe.c |
バイトコードを実行する仮想マシン | 第8章 |
SQLiteは、設計ドキュメントも充実しています。
- Architecture of SQLite(https://www.sqlite.org/arch.html)── 全体の構成
- Database File Format(https://www.sqlite.org/fileformat2.html)── ファイル・ページ・B+木のバイト単位の形式
- Write-Ahead Logging(https://www.sqlite.org/wal.html)── WALモードの動作
- The Bytecode Engine(https://www.sqlite.org/opcode.html)── VDBEの命令の一覧
PostgreSQL
PostgreSQLは規模の大きいプロジェクトですが、ディレクトリの構成が機能ごとに整理されていて、目的のコードを探しやすくなっています。こちらもGitHubにミラー(https://github.com/postgres/postgres)があります。
| ディレクトリ・ファイル | 内容 | 本シリーズとの対応 |
|---|---|---|
src/backend/storage/smgr/md.c |
ファイルへのページの読み書き | 第3章 |
src/backend/storage/buffer/bufmgr.c、freelist.c |
共有バッファとクロックスイープ | 第4章 |
src/backend/storage/page/bufpage.c |
ページと行ポインタの操作 | 第5章 |
src/backend/access/heap/heapam.c |
ヒープへの挿入・削除・更新 | 第5章 |
src/backend/access/nbtree/ |
B+木(nbtree) | 第6章 |
src/backend/parser/gram.y |
SQLの文法(Bison) | 第7章 |
src/backend/optimizer/ |
コストベースのプランナ | 第8章 |
src/backend/executor/nodeSeqscan.c、nodeIndexscan.c、nodeNestloop.c |
各オペレータ | 第8章 |
src/backend/storage/lmgr/lock.c、deadlock.c |
ロックとデッドロック検出 | 第11章 |
src/backend/access/transam/xlog.c、xlogrecovery.c |
WALとリカバリ | 第12章 |
PostgreSQLのソースコードには、ディレクトリごとに設計を解説した README が置かれていることがあります。例えば src/backend/access/nbtree/README はB+木の並行制御、src/backend/storage/lmgr/README はロックマネージャ、src/backend/access/transam/README はトランザクションとWALの設計を説明していて、コードを読む前の地図として非常に役立ちます。
コードリーディングのコツ
1. 知っている機能から入る
いきなり全体を読もうとせず、「INSERTしたときにページのどこにタプルが入るか」のように、本シリーズで実装した機能に対応する部分から読み始めます。自分の実装と比べながら読むと、「なぜこうしているのか」という疑問が自然に湧き、それが理解の手がかりになります。
2. テストとドキュメントから逆引きする
SQLiteは test/ ディレクトリに大量のテストを持っています。関数がどう使われるかは、テストから逆引きするとわかりやすいことがあります。
3. 変更の履歴をたどる
コードがなぜ今の形になったのかは、変更の履歴に残っています。PostgreSQLのコミットメッセージは、変更の理由がていねいに書かれていることが多いです。
git log -p --follow src/backend/access/nbtree/nbtsearch.c # ファイルの変更履歴
git blame src/backend/access/nbtree/nbtsearch.c # 各行を最後に変更したコミット
4. デバッガで処理を追う
デバッグ用にビルドして、デバッガで実際の処理の流れを追うのが、最も確実な理解の方法です。例えばSQLiteなら次のようにします(macOSでは gdb の代わりに lldb を使います)。
./configure --enable-debug
make
gdb --args ./sqlite3 test.db "INSERT INTO t VALUES (1)"
(gdb) break sqlite3BtreeInsert
(gdb) run
次のステップ ── 分散データベースへ
1台のマシンを超えるときに起きる問題
本シリーズのデータベースは1台のマシンで動きます。現代のWebサービスでは、データを複数のサーバーに複製したり分割したりすることが一般的で、そこでは1台のときにはなかった問題が現れます。
レプリケーション(複製)
同じデータを複数のサーバーに持たせ、1台が故障してもサービスを続けられるようにします。PostgreSQLのストリーミングレプリケーションは、第12章で実装したWALを別のサーバーに送り、そこでREDOすることで複製を作っています。
プライマリ スタンバイ
│ WALを書く │
└──── WALを送る ──────────────▶│ WALをREDOする
複製が複数あると、「どのサーバーが最新か」「書き込みをどのサーバーで受け付けるか」をサーバー同士で合意する必要が出てきます。これが**合意形成(コンセンサス)**の問題で、PaxosやRaftといったアルゴリズムが使われます。
シャーディング(分割)
1台に収まらないほどデータが増えたら、テーブルをキーの範囲などで分割し、別々のサーバーに置きます。すると、複数のサーバーにまたがるトランザクションを原子的にコミットする必要が生じます。代表的な方法が**2相コミット(2PC: Two-Phase Commit)**です。第11章の2相ロック(2PL)とは名前が似ていますが、まったく別の仕組みです。
CAP定理
ネットワークが分断されたとき、分散システムは「すべてのノードが一貫したデータを返す(Consistency)」と「すべてのリクエストに応答し続ける(Availability)」を同時には満たせない、という定理です。分断耐性(Partition tolerance)の頭文字と合わせてCAP定理と呼ばれます。分散データベースは、分断が起きたときにどちらを優先するかを選んで設計されています。
学習ロードマップ
本シリーズ(単一ノードのRDBMS)
│
▼
LSM木とキーバリューストア(RocksDB、Pebble)
│
▼
レプリケーションと合意形成(Raft)
│
▼
分散トランザクション(2PC)とシャーディング
│
▼
NewSQL(Spanner、CockroachDB、TiDB)のアーキテクチャ
読んでおきたいOSSと論文
OSS
- etcd(https://github.com/etcd-io/etcd)── Raftによる合意形成を使った分散キーバリューストア。Goで書かれていて、Raftの実装は独立したライブラリとしても使える
- Pebble(https://github.com/cockroachdb/pebble)── CockroachDBのストレージエンジンとして使われている、Go製のLSM木ベースのキーバリューストア。第12章のWALと比べながら読むと面白い
- CockroachDB(https://github.com/cockroachdb/cockroach)── PostgreSQLのプロトコルに対応した分散SQLデータベース。GoogleのSpannerの設計を参考にしている。Goで書かれている
- TiDB(https://github.com/pingcap/tidb)── MySQLのプロトコルに対応した分散SQLデータベース。SQL層はGo、ストレージ層のTiKVはRustで書かれていて、TiKVはRocksDBとRaftを使っている
CockroachDB・etcd・Pebble・TiDBのSQL層はGoで書かれているので、本シリーズを読み通した方にとっては、コードを読み始めるハードルが低いはずです。
論文
- D. Ongaro, J. Ousterhout, “In Search of an Understandable Consensus Algorithm”, USENIX ATC, 2014 ── Raftの論文。理解しやすさを第一の目標に設計された合意形成アルゴリズム
- J. Corbett ほか, “Spanner: Google’s Globally-Distributed Database”, OSDI, 2012 ── 原子時計とGPSを使った時刻API(TrueTime)で、世界規模の分散トランザクションを実現した
- A. Verbitski ほか, “Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases”, SIGMOD, 2017 ── データベースのサーバーからストレージへはREDOログだけを送り、ページの再構築をストレージ側で行う設計
- G. DeCandia ほか, “Dynamo: Amazon’s Highly Available Key-value Store”, SOSP, 2007 ── 一貫性よりも可用性を優先したキーバリューストア。コンシステントハッシングによるデータの分散
シリーズを振り返って
本シリーズでは、Goでリレーショナルデータベースを一から実装してきました。
| 部 | 章 | 作ったもの |
|---|---|---|
| 第1部 基礎 | 第1・2章 | 全体像の整理、プロジェクトの準備 |
| 第2部 ストレージエンジン | 第3〜6章 | DiskManager、BufferPoolManager、ヒープファイル、B+木 |
| 第3部 SQLエンジン | 第7〜9章 | Lexer・Parser、Planner・Executor、カタログ |
| 第4部 トランザクション | 第10〜12章 | TransactionManager、LockManager、WALとリカバリ |
| 第5部 まとめと発展 | 第13〜15章 | CLI、ベンチマーク、実際のデータベースとの比較 |
WriteAt でファイルにページを書くところから始めて、最後には、kill -9 で強制終了してもコミットしたデータが失われないデータベースが完成しました。
業務でデータベースを使うときの見え方も、きっと変わっているはずです。COMMIT が返ってくるまでの数ミリ秒の間にWALのfsyncが行われていること。EXPLAIN に出てくる Seq Scan と Index Scan が、第8章で作ったSeqScanとIndexScanと同じ考え方であること。主キーで検索すると速いのは、B+木が数回のページ読み込みで目的の行にたどり着けるからであること。デッドロックのエラーが返ってきたとき、データベースの中では待機グラフの閉路が見つかっていること。
データベースは「魔法のブラックボックス」ではありません。一つひとつは、ここまで自分の手で作ってきたような、理解できる仕組みの積み重ねです。このシリーズが、データベースの内側に興味を持つきっかけになれば幸いです。最後まで読んでいただき、ありがとうございました。