データベースとその複雑さがもはや不要な理由
(blog.redplanetlabs.com)- バックエンドの複雑さの原因は、特定製品の欠陥というより、データベースが長く維持してきた概念的構造がアプリケーション設計を制約し続けている点にある
- データベースはグローバルな可変状態のように振る舞い、固定されたデータモデル、制限のあるスキーマ、正規化と非正規化の緊張関係を開発者に背負わせる
- 提案されている代替案は、イベントソーシングとマテリアライズドビューを組み合わせ、ソースデータとインデックスビューを分離し、インデックスをデータモデルではなく永続性のあるデータ構造として扱う方法である
- Red Planet LabsのRamaは、depots、ETL、PStates、query topologiesにより、データの取り込み・処理・インデックス化・クエリと、デプロイ・監視を1つのプラットフォームにまとめる
- Twitter規模のMastodon再実装の事例では、Ramaは1万行・9人月で実装され、比較対象であるTwitterのコンシューマー向け製品は100万行・約200人年を要したと比較されている
データベース複雑性の出発点
- 問題の焦点は、特定のデータベース製品のAPI、運用上の難点、恣意的な制限ではなく、今日まで維持されてきた集合的な概念構造にある
- より良い方法が登場して初めて従来方式の問題が明確になり、代替案としてevent sourcingとmaterialized viewsの組み合わせを提示している
グローバルな可変状態としてのデータベース
- プログラマーはグローバル変数の使用を最小限にするよう学ぶが、データベースも本質的にはグローバルな可変状態である
- データベースには、通常のグローバル変数より扱いにくい面がある
- 相互作用が複数のシステムに広がっており、状態の推論が難しい
- 状態が永続性を持つため、バグを修正しても破損したデータが自動的に復旧しない
- 破損範囲を正確に把握したり、完全に修正したりするのが難しい場合がある
- バックアップに戻す、またはバックアップの一部をマージする方法は最適な解決策ではない
- イベントソーシングとマテリアライズドビューを使うと、ソースログからビューを再計算できるため、破損したインデックス状態を修正する余地が生まれる
固定されたデータモデルの限界
- データベースはkey/value、document、relational、column-oriented、graphのようなデータモデルを中心に設計されている
- 単一のデータモデルですべてのユースケースを支えるのは難しいため、企業は複数のデータベースを併用することが多い
- より一般的なインデックス抽象化は、データモデルではなくデータ構造である
- Key/value: map
- Document: map of maps
- Relational: map of maps、secondary indexesは追加のmap
- Column-oriented: map of sorted maps
- 永続性のあるデータ構造はディスクに保存され、非常に大きくなり得るほか、ネストしたデータ構造も表現できる
- インデックスをデータ構造の組み合わせとして指定すれば、既存のデータモデルだけでなく、より多様なインデックス形態を作れる
- アプリケーションのドメインモデルをデータベースに合わせて歪めるのではなく、ストレージの形をドメインモデルに合わせられれば、基礎的な複雑性は減る
正規化と非正規化の緊張関係
- リレーショナルデータベースの利用者は、最終的に正規化と非正規化の間の選択に直面する
- 正規化された保存は明確な真実のソースを作り、不整合の可能性を減らすが、JOINが増えてクエリコストが大きくなり得る
- 性能のために同じ情報を複数の方法で保存すると、バグがあったときにデータ不整合が生じる可能性がある
- RDBMSアーキテクチャは、真実のソースと高速なクエリのためのインデックスストアを同じデータストアにまとめている
- 解決策は、2つの役割を分離する構造である
- 1つのサブシステムは真実のソースを表現する
- もう1つのサブシステムは、そのソースから任意の数のインデックスストアをマテリアライズする
- 2つ目のシステムがソースデータからインデックスを再計算できれば、不整合を修正できる
制限のあるスキーマとドメイン表現
- データベースは、保存可能な値の種類が製品ごとに大きく異なる
- あるデータベースはbyte arrayのようなblobだけを許可する
- あるデータベースは整数、浮動小数点数、文字列、日付など複数の型を許可する
- ドメインオブジェクト内部のネストした情報を直接クエリしたり集計したりできるよう、第一級の表現として保存することはまれである
- データベースの実装言語とアプリケーション言語が異なるため相互運用が難しく、Postgres向けのprotobuf extensionのような拡張も煩雑で制限がある
- ORMはドメイン表現とデータベース表現をマッピングするが、抽象化が漏れて問題を引き起こすことがある
- 理想的なドメイン表現とは異なる方法でデータをインデックス化する必要がある場合、アダプターコードが必要になり、効率的に実行できるクエリも制限され得る
複雑なデプロイとアラカルトモデル
- 完全なバックエンドはデータベースだけで構成されるわけではなく、データベース、処理システム、監視ツール、スケジューラーなど多くのツールが必要になる
- 大規模バックエンドでは数十個のツールを組み合わせる必要があり、アプリケーション更新はマイグレーション、コード更新、インフラ変更のオーケストレーションに膨らむ
- 本番対応には十分なテレメトリーが必要だが、ツールごとに収集方法が異なるため、1つの監視ダッシュボードに集約することも別のエンジニアリング課題になる
- 現在主流の開発方式は、各アーキテクチャ部分に最適なツールを選んで組み合わせるアラカルトモデルに近い
- 独立して設計されたツール同士を連携させる作業自体が大きく、固定データモデルと制限のあるスキーマのため、ツールをアプリケーションに合わせるのではなく、アプリケーションをツールに合わせることになる
- バックエンド構成のための凝集したモデルがなかったためアラカルトモデルが定着したが、凝集したモデルでは抽象化、自動化、再利用の余地が大きくなる
バックエンドを見る単純なモデル
- バックエンドの主な機能は、新しいデータを受け取ることとそのデータに関する質問に答えることである
- 最も一般的なクエリは、バックエンドが受け取ったすべてのデータに関数を実行する形で表現できる
query = function(all data)
- 現実にはデータセットが10PBで、クエリ応答がミリ秒単位でなければならない場合があるため、実用的なシステムにはインデックスが必要である
- インデックスを追加したモデルは次のように表現される
indexes = function(data)query = function(indexes)
- 従来のバックエンドは、このモデルの構成要素を複数の狭い用途のツールに分けて実装する
- RDBMSバックエンドはRDBMSをデータとインデックスに使い、ElasticSearchのような別のデータベースを追加のインデックス化に使うことがある
- 計算はAPIサーバーのハンドラーや、キュー・ワーカー基盤のバックグラウンドジョブで行われることが多い
- 大規模バックエンドはCassandra、MongoDB、Neo4j、Kafka、Hadoop、Storm、Kafka Streamsのようなツールを組み合わせることがある
- すべての構成要素を統合的かつ汎用的に実装するツールがあれば、前述の複雑さを避けられるという結論につながる
Ramaの構成とMastodon事例
- Ramaは、こうした原則に基づいて設計されたバックエンド開発プラットフォームである
- Red Planet Labsは8月15日、Ramaを「the 100x development platform」というタグラインで発表した
- コスト削減の事例として、MastodonをTwitter規模で再実装した取り組みを提示している
- 1億のbotが毎秒3,500回投稿する
- 平均fanoutは403である
- Twitterの同等のコンシューマー向け製品は100万行・約200人年を要したと比較している
- Ramaでの実装には1万行・9人月を要した
- 実装はopen-sourceであり、complete、high-performance、production-readyだと提示されている
- Twitterは規模を実現するためにsocial graph database、in-memory timeline databaseのような特化データベースを自作し、Puppet設定だけで100万行以上という複雑なデプロイを抱えていたと比較されている
- Ramaベースの実装は、特化インフラをサブ問題ごとに新たに作る代わりに、Ramaのプリミティブ要素を組み合わせて同様の性能・スケーラビリティ問題を解いたとしている
- Mastodon実装のperformance numbersは、Twitterの数値と同等またはそれ以上だと提示されている
Ramaのプログラミングモデル
- Ramaの概念は、先に示したバックエンドモデルに直接対応している
- Depots: 任意のデータを格納する分散ログで、
dataに相当する - PStates: partitioned stateの略で、インデックスに相当する
- ETLs:
function(data)に相当する - Queries:
function(indexes)に相当する
- Depots: 任意のデータを格納する分散ログで、
- PStatesは、永続性のあるデータ構造の任意の組み合わせとして必要なだけ作成できる
- ETLとqueryはチューリング完全なデータフローAPIで表現され、計算を分散実行できる
- RamaはJava APIチュートリアルをドキュメントで提供しており、Clojure APIもある
Ramaがデータベース複雑性を減らす方法
- RamaのPStatesはデータベースに似た役割を果たすが、そのPStateを所有するETL topologyからしか書き込めない
- すべての書き込みが同じETLコード内にあるため、状態の推論が容易になる
- PStatesはイベントソーシングログ上のマテリアライズドビューとして動作する
- depot dataが真実のソースであるため、PStateは再計算できる
- データモデルの制約は、PStatesをデータ構造として指定する方法で扱う
- Mastodon実装ではprofiles、statuses、timelinesだけでも33個のPStatesを使っている
- あるPStateは10個のユースケースを処理し、あるPStateは1つのユースケースだけを支える
- PStatesはdurable、partitioned、incrementally replicatedという性質を持つ
- incremental replicationは、leader partitionの障害時にtakeoverする別のpartitionが準備されていることを意味する
- 現在のleaderで見えるものは、その後のleaderでも見えることを保証する
- 正規化と非正規化の問題は、depotsとPStatesを明示的に分離することで扱う
- 制限のあるスキーマ問題は、ドメイン表現をそのまま使う方法で緩和する
- hash map、listのような一般的なデータ構造
- Protocol Buffers
- ネストしたオブジェクト定義
- Ramaが知らない型はcustom serializerの登録で扱う
デプロイ、統合、監視
- Ramaアプリケーションはmodulesと呼ばれ、moduleは複数のdepots、ETLs、PStates、query topologiesを含められる
- Ramaはmoduleのデプロイ、更新、拡張のための組み込みメカニズムを提供し、それぞれターミナルのone-linerで実行できるとしている
- Ramaは「all or nothing」なツールではなく、他システムと統合しやすいよう設計されており、既存アーキテクチャへ段階的に導入できる
- 統合プラットフォームであるため、自身の監視も行う
- 監視データの収集
- 処理
- インデックス化
- 可視化
- cluster UI telemetryは、moduleの性能理解、問題の検知と診断、スケールするタイミングの判断に使われる
学習と導入経路
- Ramaをさらに学ぶためのリソースとして、次が提示されている
- 本番環境での新機能構築、既存システムの拡張、インフラ単純化のためにRamaを使いたい場合は、private betaに申し込める
- private betaのユーザーには、Ramaの学習だけでなく、コード作成、最適化、テストも支援するとしている
1件のコメント
Hacker News のコメント
「あとで説明するが、よりよいアプローチはイベントソーシングとマテリアライズドビュー」って、結局解決策は複雑さの増加ということなのか。まあ当然そうだろう
私たちが作ったTwitter 規模の Mastodon 実装が直接の例だ。同じ機能をスケールする形で作るために Twitter が書かなければならなかったコードより文字どおり 100 倍少なく、Mastodon の公式実装よりも 40% 以上少ないコードで済んでいる。同じツールで 2 回目だからよりうまく設計できたのではなく、根本的により優れた抽象化で作られているからだ
ただし一定の規模を超えると、あらゆることがデータエンジニアリングの問題になり、システム全体の文脈ではこの方式が相対的に単純な解決策になることもある。「単に mySQL/SQLite/Postgres を使え」という助言は、それが通用しない瞬間が来るまでは素晴らしい
Martin Fowler の元のイベントソーシング記事がそもそも書かれていなければ、みんなにとってそのほうがよかったと本気で思っている。99% の場合には悪いアイデアだと思う
私が見落としているのかもしれないが、この記事では同時実行性、分離、制約といった概念が完全に抜け落ちているように見える。それに「クエリトポロジー」というものは宣言的でもなく、クエリ計画/最適化の責任を作者に押しつけているように見えるのだが、本当にこれをよりよい開発者体験だと見なしているのか?
そしてそのとおり、Rama のクエリ方式ははるかに優れたアプローチだ。複雑なクエリプランナーが必要になるのは、たいていデータのインデックス方法の限界、特に正規化と非正規化の間の緊張関係に起因する。Rama では、クエリに必要な形であらかじめ用意された複数のビューを堅牢にマテリアライズするのが容易だ
Rama の概念をやさしく紹介するチュートリアルはここにある: https://redplanetlabs.com/docs/~/tutorial1.html
「どんな単一のデータモデルも、すべてのユースケースをサポートすることはできない」という話について言えば、理論上は、物のタプルと関係で正確にモデル化できないドメインや有限なドメイン集合は存在しない
実務上、特定のデータベース/スキーマの範囲は通常、1つのビジネスや問題領域に限られるが、型が不適切に重なり合ってさえいなければ、これも大した問題にはならない。名前にさえ気をつければ、Web小売業者と保険会社を同じスキーマに入れても十分に機能する
すべてを正確に1つのデータベースに入れることは超能力だ。私がこれを強く主張する最大の理由は、複数のデータストアにまたがる分散トランザクションを避けるためだ。すべてのビジネスが1つのトランザクションシステム内で起こるなら、セマンティクスは劇的に単純になる
人々が実際にデータを使い始めると、100万ドルの大型DBサーバーは、安価なデータベースを重複して並べたものよりずっと安い。開発者であれアナリストであれ経営陣であれ、全員の時間が節約されるし、おそらく一部のDBAだけが例外かもしれない
理論上それがすべてだとしても、もう1つの継続的な問題は実装だ。最近の開発者の多くは、データベースがどう動作し、どう使うべきかをよく理解しておらず、ひどい使い方をしている。そこでこの事実から逃れようとして、NoSQLのような新しいデータベースを作り、頭をあまり使わず接着コードだけを増やそうとした。それもひどい結果だと分かった
ほどなく流れはまた変わり、「ただ大きなデータベースを1つ」は再び流行ではなくなり、別の「単純さ」のパラダイムが出てくるだろう。マイクロサービスの時のように、「1つの大きなデータベース」の代わりに「多数の小さなデータベース」が単純だと見なされるかもしれない。歴史を理解しなければ、繰り返すしかない
一般的なプログラミングでも、1つのデータ構造やデータ構造の組み合わせがすべてのユースケースをサポートできないのと同じだ。時にはマップが必要で、時にはリスト、集合、組み合わせ、あるいはまったく別の何かが必要になる
当然、データ構造は多様で、性能も必要で、あまりに重要なため一般の人が触れられないボトルネックになり、すべての変更は極めて安全でなければならない。セキュリティもある。誰かが開発バグのセクションから、人事や役員専用の財務セクションへ移る方法を見つけたらどうするのか。汎用ERMシステムを実装したことがある人なら、統合システムがどれほど難しく苦痛を伴うか分かるだろう
ただし、これは極端な例だ。この理想を追い求めて、人々がどこまで行き、どうやったのかが気になる。1つのシステムで運営されるビジネスを見たことがない。個人の知識管理システムはどうだろうか。すべてがぴったり収まるのか。今でも素早い作業にはスプレッドシート、自由形式にはテキストファイルを使っているのではないか
バズワード集のように見える。世界最大級の企業の1つで何年もデータベースを扱ってきたが、トポロジーという言葉を聞いたことがない
これで節約できる時間があるとしても、Javaとこのフレームワークを学ぶのに消えてしまうだろう。データベース自体には何の問題もない
ここでテキストの表を作ろうとしたが、ページ表示が完全に壊れたのでスクリーンショットとして上げた: https://imgur.com/a/XtwSkyx
Ramaを5歳児に説明するように言うと、何なのか? ドキュメントも分かりにくかった: https://redplanetlabs.com/docs/~/index.html
「パラダイムシフト」や「プラットフォーム」みたいな流行語は抜きにしてほしい。図が必要なら、もっと明確に説明している文章を読みたい
Ramaはクラスタとして実行され、複数のアプリケーションは「モジュール」という形でそのクラスタにデプロイされます。深く詳細なテレメトリも組み込まれています。
Ramaのプログラミングモデルはイベントソーシングとマテリアライズドビューです。Ramaアプリケーションを作るときは、必要なだけ多くのインデックスを、望む形で、複数の永続データ構造の組み合わせとしてマテリアライズします。インデックスは分散データフローAPIでマテリアライズされます。
Ramaは既存のものとあまりにも違うので、これくらいが私にできる高レベルな説明です。基礎を学ぶのに最適な資料はrama-demo-galleryで、さまざまなユースケースにRamaを適用する、短く最後まで動作する詳細コメント付きの例が入っています。どれもスケーラブルでフォールトトレラントです: https://github.com/redplanetlabs/rama-demo-gallery
「Depot」という追記専用イベントログがあり、その上に任意のビューを作り、それを「P-States」と呼びます。Ramaソフトウェアはこれらのビューの低レイテンシ更新を約束します。この上に作られたアプリケーションはビューにクエリし、Depotに新しいイベント/コマンドを送信します。
1〜10TB規模のライブイベントデータセット上に柔軟なマテリアライズドビューエンジンを作る1年がかりのプロジェクトをやっていて、別のプロジェクトに移る前には、私たちのアーキテクチャもおおむねインデックスのある場所へコードを送るというアイデアに収束しつつありました。
Ramaにはかなり惹かれますが、まったく非合理的な理由で、JVMのせいで採用しない気がします。単にJava/JVMが嫌いなのです。このアーキテクチャが別の環境に移植されるといいのですが。
職場では読み取りモデルと書き込みモデルを分離しています。書き込みモデル、つまり信頼できる唯一の情報源は、不変条件/制約まで備えた従来型のリレーショナルなドメインモデルで、ORMに慣れている大半の開発者にとって推論するのは難しくないと思います。
ほぼすべてのコマンドはイベントも作成し、共有ドメインイベントキューに発行します。読み取りモデルは、イベントを消費するワーカーがそれぞれ必要な方法でビューを作り、作り直すこともできます。たとえばユーザー管理サービスは信頼できる唯一の情報源であり、他のサービスは複雑なUIを表示するためのビューサービスとして、ユーザーサービスや他サービスのイベントに基づいて独自の読み取りモデル/インデックスを作ります。これがなければ、途方もないJOINや遅いサービス間API呼び出しが必要になっていたでしょう。
技術的にはイベントのリプレイは可能です。実際、プラットフォームコードのバグのために過去3年分の全イベントをリプレイしたこともあります。ただ、本当に必要だったことはほとんどないと思います。バグのためにビューを作り直さなければならないこともありますが、たいていは特別なスクリプトやSQLマイグレーションのような一時的なプログラム方式で処理します。私たちのアーキテクチャを正確に何と呼ぶのかは分かりませんし、誰かが「イベントソーシング」と呼ぶのを聞いたこともありません。
結局のところ、古いMySQL + RabbitMQと少しのグルーコードです。ただし、きちんとやるには、トランザクションアウトボックス、少なくとも1回の配信保証、結果整合性、正しいイベント処理順序の維持、イベントデータのバッチ化、DB管理、イベントハンドラが死んだときの処理などがあるため、決して些細ではありません。すでに実戦で検証済みの、言語非依存の構成で、PHPとGoのプロデューサ/コンシューマがどちらもいる状況で、Ramaなしだと何を見落としているのか、上の問題群をRamaがどう解くのか気になります。RamaはJava側により寄せられているように見えます。
Ramaのインデックスはずっと柔軟です。たとえば1億個の要素を持つネストした集合が必要でも簡単です。ユーザーID → フォロワーID集合のようなソーシャルグラフのインデックスがよくある例です。粒度別に分割した時系列インデックス、つまりエンティティ → 粒度 → 時間バケット → 統計も同じように簡単です。
Ramaに保存されるデータ型に制限はありません。Ramaのクエリは非常に強力で、インデックスの一部または全体にまたがるリアルタイムのオンデマンド分散クエリが容易です。アプリケーション全体にわたる深く詳細なテレメトリも組み込まれているため、別途作ったり管理したりする必要はありません。
デプロイも組み込まれています。今のやり方では、アプリケーション更新がワーカーコードやスキーママイグレーションなど複数のシステムにまたがることがあり、特に無停止を望むならかなり難しいエンジニアリング作業になります。Ramaは計算と保存をエンドツーエンドで統合しているため、アプリケーションのリリース、更新、スケーリングがすべてターミナルの1行で可能です。
Ramaははるかにスケーラブルです。これは機能面から見たRamaであり、Ramaでコーディングするときにインピーダンスミスマッチがないことがどれほど大きな違いを生むかは、言葉では表現しにくく、使ってみないと分かりません。RamaはJVM向けなので任意のJVM言語で使え、現在はJavaとClojure APIを提供しています。
イベントはどこに、どの形式で保存していますか? もっと詳しく聞きたいです。
今足りないのは、エコシステム全体に付けるかっこいい名前のようですね。
変更時点でデータを具体化すると、プロダクトが一つのことを非常に高速に行う必要がある場合にはメリットがあり得る。しかし、アトミックな書き込みで一緒に更新しなければならない複雑なトランザクションが発生したり、データを別の方法で整理する新機能を追加しようとした瞬間に厄介になる。
アプリケーション構築の部分で「単にインデックスを一つ付ければよい」というふうに軽く流しているのも、非常に不満が残る。インデックスはグローバル状態であり、1つ下の階層に移されただけである。
この文書を読んでも、対象読者が誰で、何を解決しようとしているのかが明確ではない: https://redplanetlabs.com/docs/~/why-use-rama.html#gsc.tab=0
実際の事例を一つ取り上げ、RAMAならどれほど簡単で効率的になるのかを変換して示してくれると役に立ちそうだ。
1つ目はTwitter規模のMastodon実装で、同じものをスケールさせて作るためにTwitterが書いたコードより100倍少ないコードで済む。消費者向けプロダクトだけを基準にしている。スケールしないMastodon公式実装よりもコードが40%以上少ない: https://github.com/redplanetlabs/twitter-scale-mastodon
rama-demo-galleryリポジトリにも、Ramaを非常に異なるユースケースに適用した、短く独立していて詳細なコメント付きの例が多数ある。ユーザープロフィール管理、時系列分析、アトミックで障害許容性のある銀行送金などが含まれる: https://github.com/redplanetlabs/rama-demo-gallery
実際に日間ユーザー4,000万人のWebサイトを運用しているのでなければ、「Twitter規模のMastodonクライアント」でこれが証明されたと主張することはできないように見える。実環境、それに伴うコードとインフラの変更、実際のユーザー、ネットワーク使用量などをシミュレートするのは不可能である。