見出し画像

Datomic Cloudで実現するデータ指向アプリケーションデザイン

これは何か

Clojure Advent Calendar 2020の記事です。職場でDatomic Cloudを使ってプロトタイピングした経験かTipsとかを淡々と書き下ろした内容となっております。長文ですが、これを機にDatomicに興味を持って頂けると嬉しいです。

データ指向アプリケーションとは

ここで言うアプリケーションはデータ(実績やファクト)を蓄積、表示、加工、運用、場合によっては分析につなげることを価値として提供しているものを指しております。少し広めの定義ですので、昨今のSaaSやクラウドアプリケーションの大半が対象となっておりますが、その中でDatomicは強い整合性や一貫性を担保するシステムに特化したデータベース兼アプリケーションアーキテクチャーです。

システムやアーキテクチャには向き不向きがあると考えております。無論Datomicも例外ではありません。銀の弾丸が存在しない限り我々はユースケースに基づいてツールを選択しないといけないのですが、その中でも汎用的なデータベースとしてDatomicは十分パフォーマンスを発揮できると思い、記事に仕立て上げました。

Datomic Cloudとは

Datomic(正確にはDatomic on-prem)は本来次世代型のデータベースとして発足したのでデータベースとして紹介されていることが多いのですが、運用しやすいようにAWS上で動くフルマネージドサービスとして開発されたのがDatomic Cloudです。

Latency Comparison Numbers (~2012)
----------------------------------
L1 cache reference                           0.5 ns
Branch mispredict                            5   ns
L2 cache reference                           7   ns                      14x L1 cache
Mutex lock/unlock                           25   ns
Main memory reference                      100   ns                      20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy             3,000   ns        3 us
Send 1K bytes over 1 Gbps network       10,000   ns       10 us
Read 4K randomly from SSD*             150,000   ns      150 us          ~1GB/sec SSD
Read 1 MB sequentially from memory     250,000   ns      250 us
Round trip within same datacenter      500,000   ns      500 us
Read 1 MB sequentially from SSD*     1,000,000   ns    1,000 us    1 ms  ~1GB/sec SSD, 4X memory
Disk seek                           10,000,000   ns   10,000 us   10 ms  20x datacenter roundtrip
Read 1 MB sequentially from disk    20,000,000   ns   20,000 us   20 ms  80x memory, 20X SSD
Send packet CA->Netherlands->CA    150,000,000   ns  150,000 us  150 ms

Notes
-----
1 ns = 10^-9 seconds
1 us = 10^-6 seconds = 1,000 ns
1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns

Credit
------
By Jeff Dean:               http://research.google.com/people/jeff/
Originally by Peter Norvig: http://norvig.com/21-days.html#answers

Contributions
-------------
'Humanized' comparison:  https://gist.github.com/hellerbarde/2843375
Visual comparison chart: http://i.imgur.com/k0t1e.png

一般的なDBとアプリケーションがネットワークを超えて通信するモデルでは検索時にネットワークIOがボトルネックとなっている場合がありますが、Datomic Cloudは全ての検索はなるべく近場で実行する多段キャッシュを前提としたアーキテクチャーとなっているため、検索に必要なデータがメモリーに乗る場合はネットワークIOを必要とせずインメモリで完結することが可能です。

Datomicを試したい方のためのリンク集

ここから少しずつ技術的な要素が増えてきますのでその前にまずは役立つ資料をまとめておきます。

本家資料:
Datomic with Rich Hickey
What Is Datomic Cloud?

日本語の資料:
Datomic on AWS
week-of-datomic

ここから難易度が徐々に上がっていって、最終的にはデータモデリングのTips集みたいになるのでDatomicに慣れてない方はまずちゃんとした入門資料から始めるといいかと思います。

おさらい:Datomicにおけるデータとは

一般的なリレーショナルデータベースやドキュメントストアと違い、Datomicは属性(Attribute)と属性を束ねるエンティティ(Entity)を使うことによって底レイヤーの型を担保しつつ柔軟なデータ構造を表現しております。

論理削除も物理削除もできる新しい考え方

一般的なリレーショナルデータベースは物理削除や上書きをしてしまうと過去に実績が復元できなくなるため、クリティカルなシステムでは何かしらの形で論理削除を実装しないといけない場合があります。ただSQLを前提としたリレーショナルデータベースではSQLやアプリケーションロジックの肥大化やデータの蓄積に伴うパフォーマンスの低下などのトレードオフがあったため安易に実装できない問題がありました。以前 @t_wada さんの「SQLアンチパターン 幻の第26章」でも物理削除と論理削除のトレードオフを解決するための手法として提案されておりましたが、DatomicではAttribute毎の設定によって追記型のデータと上書き型のストレージモデルを選択できます。

デフォルトでは全項目追記型のストレージになっており、過去のデーターをアクセスできるAPIが用意されているので、データモデリングの際には特に変更履歴テーブルやCDCを意図的に実装しなくても過去の状態を再現することや差分を抽出することが可能です。その際の底レイヤーストレージとしてAWSのDynamoDBと多段キャッシュを採用しており、高度なスケーラビリティとパフォーマンスを両立させることが可能になっております。

リレーショナルでもスキーマレスでもない新しい考え方:Duck Typing

古典的なSQLを前提としたリレーショナルデータベースはデータの構造をテーブルとカラムで表現しするように設計されております。テーブルには決まった項目が存在し、項目には決まった情報が格納されているのでOOP言語のクラスと似たような感覚で扱っているプロジェクトも多いと思いますが、Datomicでは柔軟にデータ構造を表現するためにあえてEntity-Attribute-Value(略してEAV)に近いEntity-Attribute-Value-Time(略してEAVT)構造を提供しつつその上でリレーショナルなデータ構造をサポートしております。

{:db/id               1
 :character/name      "Donald Duck"
 :animal/sound-sample "quack.mp4"}

{:db/id               2
 :item/name           "A plastic toy duct"
 :item/sku            "821f7325-0788-4e85-9a87-e48ea4d9e0b3"
 :animal/sound-sample "quack.mp4"}

SQLではEAVはアンチパターンとして知られておりますが、Datomicではそれを高速に検索できるようなインディックスとAttributeレベルの制約、Entityレベルの制約がつけられるのでテーブルと同等の安定性を担保しつつそれ以上に柔軟な表現力を持つことが可能になります。

Attributeレベルの制約を担保するには

Datomicのスキーマ自身もデータで定義されており、必要であればアプリケーションロジックに応じて拡張することができます。

{:db/ident       :order-item/quantity
 :db/doc         "Quantity of the item"
 :db/valueType   :db.type/long
 :db/cardinality :db.cardinality/one}

例えばこちらECシステムのオーダーのアイテム数を表現すための項目です。データベースが提供しているプリミティブな型はLong/Int64ですが、アプリケーションロジックとしてマイナスの数量を受け付けないように制限する場合は「Attribute Predicates」という仕組みを使います。

{:db/ident       :order-item/quantity
 :db/doc         "Quantity of the item"
 :db/valueType   :db.type/long
 :db/cardinality :db.cardinality/one
 :db.attr/preds  clojure.core/nat-int?}


;; resources/datomic/ion-config.edn にて許可することを忘れずに
{:app-name "my-app"
 :allow    [clojure.core/nat-int?]}

もちろんclojure.core以外にも自作の関数が使えるので自由にアプリケーションのドメインロジックを表現することが可能です。

Entityレベルの制約を担保するには

Attributeを横断したビジネスロジックなどを表現するには「Entity Predicates」を使います。

{:db/ident        :reservation/start-at
 :db/doc          "予約開始日時"
 :db/valueType    :db.type/instant
 :db/cardinality  :db.cardinality/one}

{:db/ident        :reservation/end-at
 :db/doc          "予約終了日時"
 :db/valueType    :db.type/instant
 :db/cardinality  :db.cardinality/one}

{:db/ident        :reservation/validation
 :db.entity/attrs [:reservation/start-at :reservation/end-at]
 :db.entity/preds my-app.reservation/valid?}

;; resources/datomic/ion-config.edn にて許可することを忘れずに
{:app-name "my-app"
 :allow    [clojure.core/nat-int?
            my-app.reservation/valid?]}

これによってトランザクション内で「:db/endure」を使うとDatomic内で関数を実行し、強いACID性質を担保しつつバリデーションを行うことが可能になります。

{:reservation/start-at #inst "2017-09-16T11:43:32.450-00:00"
 :reservation/end-at   #inst "2017-09-16T12:43:32.450-00:00"
 :db/ensure            my-app.reservation/valid?}

スキーマ管理とマイグレーション

データ指向アプリケーションは長期的にデータ構造と向き合うことになるのでスキーマを管理しなければなりません。一般的にはDBではなく、フレームワークがその管理ツールを提供していることが多いのですが、Datomicを想定して作られたフレームワークはまだないのでClojureで昨今流行っているDuctフレームワークの上で実装して公開しました。

具体的にはデータベースのコネクション管理、スキーママイグレーション、及びDatomic Ionのロガーの接続ですが、Ductの生態系と使い勝手を意識して作っておりますのでよかったら使ってみて下さい。

参考資料:
The Ten Rules of Schema Growth
https://github.com/hden/duct.module.datomic

ローカル開発環境

Datomicはローカルでも開発できるようにdev-localというモジュールを提供しております。上記のスキーマ管理とdev-localのインメモリーモードを使えば気軽に使い捨てられる環境が作れますので快適な開発環境を構築することができます。個人的にはSQLite3みたいな感覚で使っており、単体テスト毎環境をリセットしてテストの再現性を担保しております。

参考資料:
Local Dev and CI with dev-local

強いACID性質とパフォーマンスを両立させるためには

Datomicの設計思想として高いトランザクション分離レベル(SERIALIZABLE)を担保するために全てのトランザクションを一つのWriter(Datomic CloudではPrimary Computeと呼びます)内で実行します。数少ないWriterを長期的にブロックしないためにも大きなトランザクションではなく、最小限のアプリケーションロジックを関数(Datomic CloudではIonと呼びます)として素早く裁く構成が推奨されております。

シングルライターは一見パフォーマンスの低下要因に見えますが、DatomicはWriterとは別にRead Groupを設置することによって、内部にRead/Writeの分離がなされ、WriterがReader側の負担を担当することがなくなることによって逆にスケーラビリティやパフォーマンスの改善に繋がります。

トランザクションと:db/id

DatomicではEntityのIDとして:db/idというAttributeを提供しております、実際のトランザクションログのEAVTではEのポジションに入っている整数がそれで、Pull APIなどで[:db/id]を指定するとその値を取り出せます。

ただその値はDatomicのシステム内で管理しており、MySQLのAuto IncrementやPostgreSQLのBigSerialみたいに都合よくリセットできません。僕個人の意見として、今のところはSeedデータの作成や単体テストの再現性を担保するために別途自分で管理できるグローバルIDを作る方法が一番楽だと考えております。

{:db/ident       :system/id
 :db/doc         "Global Unique ID"
 :db/valueType   :db.type/string
 :db/unique      :db.unique/identity
 :db/cardinality :db.cardinality/one}

開発環境のマイグレーションにSeedデータを仕込んでおくと単体テストで:system/idを指定することができるので再現性を担保しやすくなります。

ちなみにIDの選定ですが、個人的には業務上分析も担当しており、IDでソートした場合はなるべく時系列で並んで欲しいのでランダムベースのUUIDではなく、シーケンシャルなCUIDを使うことが多いです。

参考資料:
https://github.com/hden/cuid

トランザクションメタデータ

一般的なトランザクションではアプリケーションのデータを扱うことが多いのですが、監査ログの視点ではWhatだけではなく、Who、How、Whyなどの情報(俗にいうメタデータ)も同時に保存するのが理想です。

一般的なリレーショナルデータベースで厳密な監査ログを実装するのはかなり大変な作業ですが、Datomicの考え方としてはトランザクション自体も一般的なデータ(一つのトランザクション自体が一つのEntity)とみなしているのでトランザクション内で「datomic.tx」というtempidを使うと自動にトランザクションにアプリケーション独自のメタデータをつけることが可能です。

(d/transact conn
            {:tx-data
             [[:db/add "datomic.tx" :my-app/transacted-by user-id]]})

これによって詳細な監査ログがかけるのできっとセキュリティー強化、トラブルシューティング、分析など各方面でお役に立てるかと思います。

参考資料:
Finding Out Who Changed What with Datomic
Exploring four Datomic superpowers

もし我々がタイムマシーンを持っていたら

データ指向アプリケーションでは長期的にデータを管理するためデータと時間との関係性を考慮せざるおえません。一般的なリレーショナルデータベースでは最新の状態を保持していることを前提としているのでもし編集履歴のことを考慮せずそのまま上書きすると過去の状態は失われるので後から特定の瞬間の状態を再現することができなくなります。

例えば金融系のシステムで変なトランザクションが走った時、レストランの予約サービスで変な予約は入った時、ポイントサービスで過去の状態に遡ってアカウントバランスを調査したりする必要は度々あるかと思いますが、その都度もし手元にタイムマシーンがあってそのトランザクションが走った瞬間、全データベースの状態を再現できれば調査は簡単ですが、あいにく青い大型猫型ロボットはまだ実用化されていないので僕たちはシステム側で工夫をしないといけません。

一般的なリレーショナルデータベースではシステムによってはトリガーを使って履歴テーブルを残したり、CDCで差分を抽出したり、またはT字型ERやバイテンポラリデータモデルでデータを設計したりしますが、それぞれ実装コスト、パフォーマンスコストなどのトレードオフが発生しますが、Datomicではそこを意識せずとも自然に編集履歴を残してくれますし、History APIを経由すれば「指定した瞬間(as-of)の状態」、「指定した時間からの差分(since)」などに気軽にアクセスできるので分析基盤に対しての差分更新や監査履歴など苦もなく対応することが可能です。

参考資料:
SQLアンチパターン 幻の第26章
Turning the database inside-out with Apache Samza

つまりDatomicは銀の弾丸なのか?

Datomic CloudはDatomic本来の強力な機能を扱いやすくAWSに特化した形で提供している素晴らしいサービスですが、アプリケーション設計において銀の弾丸が存在しないように、Datomic Cloudにはいくつかのトレードオフが発生します。

例えばデーターベースのプリミティブな型ですが、PostgreSQLの多彩な型やインデックス(例:Range、Geo、JSONB、GIN、GiST)と比べるとDatomicの型はどうしても色褪せてしまいます。その場合は差分を外部の検索エンジン(例:Elastic Search、Algolia)に逃がすことができればなんとかなりますが、流石にACID性質や線形可能なトランザクションよりグローバルなトランザクションが必要な場合、Datomic Cloudのアーキテクチャーは向いていないかもしれません。

最後に

システムの設計上どうしてもトレードオフは発生しますが、Datomic Cloudは総合的にみて素晴らしい機能とパフォーマンスを発揮できるように設計されていると思います。

個人的な視点ですが、ドメインに関係なく今後のクラウド、あるいはSaaSなどのアプリケーションは今まで以上に「データのクオリティ」や「長期的に変わる要件に柔軟に対応できること(Situated Program)」が求められる時代が来るだろうと思います。その流れの中でより多くの企業がClojureやDatomic Cloudを導入することによって知見やツールを共有できるコミュニティーを構築できたらいいなと思い、僕が感じているDatomicの特徴とよさをまとめてみました。ついつい長文になってしまいましたが、お楽しみ頂けたら嬉しいです。

この記事が気に入ったらサポートをしてみませんか?