Why huge RDBMS ultimately don’t work
DOA がアンチパターンな理由
データ・オリエンテッド・アプローチ
多くの企業では単一の巨大な RDB を使用してシステムを運用していることが多いと思います。
これはデータ・オリエンテッド・アプローチ(以下 DOA)と呼ばれる考え方をもとにシステムを構築してきたからではないかと推察します。
DOA については下記のサイトで詳細が説明されていました。
DOA 自体は 1990 年代に広く広まった考え方の一つで、上記の図にもあるとおり「データは変わりにくい」という特性を元に、「プログラムとデータは独立」という考え方を前提にしています。
私が SI プロジェクトに携わっていた際、まさにこの ER 図を中心した考え方で開発していました。
まず、データベースとして所持すべきテーブル、テーブルが所有すべき列を ER 図を使用して列挙していきます。
次に、列に対応したプロパティを以下のような Java のファイルに落とし込み、setter/getter を付けて Java クラスファイルの完成という流れで開発していきます。この作業はプログラマでなくても Excel を使える人であれば誰でも作ることが可能です。
こうして作られた Java クラスファイルは Plain Old Java Object(POJO)と呼ばれます(.NET の場合は POCO)。
この POJO と呼ばれるオブジェクトとリレーショナルデータベースのテーブルを自動的にマッピングする ORM (オブジェクト/リレーショナル・ マッピング) ツールである Java Persistence API (JPA) を使用して、データベースへの永続化を実施します(このツールを使うために Java クラスファイルには setter/getter しか存在しません)。
この後の作業としては、どのプロパティがデータベースの主キーになるのかを表す @id を付与したり、他テーブルとのつながりを示す@OneToMany 等のアノテーションを付与していきます。
上記のクラスファイルの使用を想定している Book テーブルをシステム A で使うケースを考えてみましょう。
以下のようなアーキテクチャになると思います。
- Book クラスは3層モデルにおけるデータ層
- Business Logic は3層モデルにおけるアプリケーション層
- User Interface は3層モデルにおけるプレゼンテーション層
3層モデル自体にも問題があり、そちらに言及したブログは以下になります。
仮に同じ Book テーブルをシステム B でも使いたいとなるとどうなるでしょうか。DOA アプローチは「プログラムとデータは独立」という考え方を元にしているため、Book テーブルをシステム A、システム B 両方が参照していても問題はないアプローチです。
DRY 原則に違反しているのでは?
DRY 原則は Don’t Repeat Yourself の略です。DRY 原則は以下を意味します。
「すべての知識はシステム内において、単一、かつ明確な、そして信頼できる表現になっていなければならない」
簡単に言えば、同じ知識をあちこちに書かず、1箇所にまとめろということです。プログラミングに関して守るべきとされている原則の中でも特に重要なものとされています。
アプリケーションを構成するコードはすべて、保守を必要とします。どのコードも将来バグになる危険性を秘めています。重複があると、コードベースは不必要に大きくなり、それにつれて、バグが生じる危険性も高まります。また、システムの構造は、そう意図していないにもかかわらず複雑になってしまいます。重複によりコードベースが大きくなれば、開発に携わる人間がシステム全体を完全に理解することも難しくなります。特に困るのはコードに変更を加える時です。どこかに変更を加えた場合、それとロジック等が重複している箇所にも同様の変更が必要かどうか確認しなければなりません。DRY原則を守れば、そういう事態に陥らずに済みます。DRY原則を守るとは、言い換えれば「すべての知識はシステム内において、単一、かつ明確な、そして信頼できる表現になっていなければならない」という条件を満たすことです。
例えば、本を構成する 「発売日」 に関連するロジックを考えてみます。
- 「発売日」は未来の時間を設定することはできない。
Book クラスには大抵のケースでは setter/getter しか実装されませんから、発売日の仕様をチェックするコードは Business Logic 部分に実装されます。Business Logic はシステム A / システム B 両方に出現しますから、同じ知識が複数箇所に渡って重複出現することになります。(同一システム内の話ではないため)私の拡大解釈ではありますが、DRY 原則に違反しているのではないかと考えています。
例えば、仕様が変わったとします。
- 「発売日」は未来の時間を設定可能
- 「発売日」が未来の場合、「予約」を実施することができる
「発売日」の仕様が変わったとしたら、システム A にもシステム B にも手を入れる必要があります。少し観点を変えると「発売日」の仕様が変わったら、システム A、システム B どちらかのシステムの改修漏れが発生するかもしれません。繋がっているシステムが2システム以上存在することも考えられます。
Book クラスと Business Logic を共有ライブラリにすれば解決するじゃないかと言われるかもしれませんが、 DOA は「プログラムとデータは独立」という考え方を元にしているため、仮にシステム A とシステム B で共通部分をライブラリ化したとしても Book テーブルを扱う第3者が登場することは避けられません。DOA アプローチをとっている限り、避けようのない問題です。
おさらいすると DOA アプローチの問題点は
- 同じデータを参照するために似たようなロジックが複数システムに散らばる
- どのデータをどのロジックが参照しているか不明
→ 言い換えるとデータ & ロジックに対する責務が未定義 - 変更漏れが発生するリスクを高める
RDB としての問題点
DOA アプローチの問題点として取り上げた上記は、技術的負債と呼ばれており、システムリリース当初は問題が全く感じられないものです。徐々にスパゲッティ化を招き、気づいたら普通の運用でさえも高コストになってしまっている類のものです。
逆に、これから取り上げるものはシステムリリース直後から直面する可能性のある問題です。
大前提 — RDB に対する書き込みと読み込みの要件は異なる
- RDB における大部分のワークロードは読み込み
- 読み込みを高速化しようとするとインデックス化が必要
- インデックス化すればするほどストレージ領域が増加
- インデックス化すればするほど書き込みの速度は劣化
問題点1
誰かがとても重いクエリを RDB に対して発行すると、繋がっているシステム全てがスローダウンする。
問題点2
RDB のスケールが垂直方向のみ。
Web やアプリケーションサーバーをいくら水平方向にスケールしても、最終的にデータを取得する部分の RDB がスケールしにくい構造となっており、ボトルネックとなる。
問題点3
RDB のベンダーによっては垂直スケールが非常に高コスト。
こういった問題点を抱えてまでもシングル DB を続けるメリットはあるでしょうか。
解決策
解決策は「データ」と「ロジック」が誰のものか責務を持たせることです。そして「データ」と「ロジック」に責務を持たせると、自然とマイクロサービスアーキテクチャになります。以下を読むことをお勧めします。
上記のマイクロサービスアーキテクチャへの分解手順を用いると、「データ」と「ロジック」は責務で分解されるので、どのサービスがどの「データ」と「ロジック」を担当しているのかが一意に決まります。同じ「データ」が複数サービスで出現することはあり得ません(あったとしたらサービスの分解の仕方を間違えています)。もし他のサービスの責務のデータを自分のサービスで利用したい場合は、責務のあるサービスから API でデータを引っ張るだけです。
「発売日」の仕様が変わったとしても、改修するのは1サービスのみになります。
加えて RDB に関する問題も解決できます。
なぜなら、パフォーマンスを必要としているサービスだけ水平方向へのスケールが可能な RDB 以外のデータソースを選択するという策をとれるからです。誰かが仮に特定の RDB に対して重いクエリを発行したとしても、他のサービスには影響を与えません。
DOA アプローチのメリット
1DB にいろんなデータが詰まっていてそこさえ見ればなんでもわかるというようなことを言っている人がいます。逆にいうとメリットはこれくらいしか思いつきません。
しかしマイクロサービスアーキテクチャを用いていてもデータを1箇所でみる方法はあります。
- Tableau を使う
複数 DB にアクセスでき、データ同士の結合が可能 - 仮想データベースを使う(Presto / SQL Server BDC / Talend 等)
複数 DB を結合することができます - ÇQRS + ES を使う
ETL を使ってバッチ的にデータを1箇所に集中させて溜め込むのはお勧めしません。データソースに誤りがあって、データソースを直したとしても即座にデータが反映されるわけではないからです。もしかしたら1日前のデータを見ることになるかもしれません。理由は以下のエントリをみてもらえればわかると思います。
最後の CQRS + ES だけ簡単に記載します。
CQRS + ES
CQRS + ES は Command & Query Responsibility Segregation + Event Sourcing の略です。コマンドとクエリの責務を分離するという考え方です。
CQRS は CQS — Command & Query Separation という考え方から生まれました。CQS は「質問(クエリ実行)をしたことで答えを変化させてはならない」が端的に CQS を表しています。
- 状態変化を起こすのはコマンドだけ
- 値を返すのはクエリだけ
上記が CQS です。
CQRS は CQS を発展させ、書き込みと読み込みの責務を分離して、読み込み用のモデルを別に作るという考え方です。書き込みと読み込みのデータソースは別にすることも可能なため、上述した書き込みと読み込みの相反した要件を分離することができるようにります。
- RDB における大部分のワークロードは読み込み
- 読み込みを高速化しようとするとインデックス化が必要
- インデックス化すればするほどストレージ領域が増加
- インデックス化すればするほど書き込みの速度は劣化
イベントソーシングは、将棋における棋譜を全部取得するようなイメージです。
- △5六歩
- ▲8四歩
- △7六歩
- ・・・
RDB に起こったチェンジイベント(どのテーブルのどのデータが何から何に変わった)とドメインイベント(上記の棋譜そのもの)をすべて収集して最新の状態を作るのがイベントソーシングです。
最後に
DOA アプローチのような共有データベースは今ではアンチパターンです。
ただ、複数のコンポーネントが一つのDBに依存してしまうことは危険な徴候だと言えます。これは、マイクロサービスアーキテクチャーにおいて「共有データベース」アンチパターンと呼ばれているものです。DBに依存している複数のアプリケーション間による依存スパゲッティの問題で、例えば、システムの変更に伴って、アプリケーションコードと、データベース定義の変更を行ったら、そのデータベースに依存している別のアプリケーションが動かなくなってしまった、というような話です。ちなみに、「共有データベース」アンチパターンは、昔はよく使われていたパターンです。はてなでも複数サービスから一つのユーザーデータベースを参照する、という作りになっているところがあります。時代の流れに伴い、昔は良いと言われていたことがアンチパターンに変化することはこの業界ではよくあることです。他には「データサイズが肥大しやすく、分割も難しい」といった意見もあります。これも実際その通りで、RDBの制約は便利なので、ちゃんとアプリケーションを設計すればドメイン分割可能で別のデータベースに入れられるようなデータであっても、ついつい、単一のデータベースに入れてしまいがちです。そうなると、データサイズが肥大してしまいますが、多くのRDBはその性質上マスターデータベースの分割が難しい構造になっており、サーバー1台の力に頼らざるをえない部分があります。