Clojure
datomic

この記事は、2017年のClojure/conjで、Cognitect社のStuart Hallowayが発表した、Datomic Made Easy Datomic in the Cloudを元に書き起こしたものです。
2017/11/23時点で、まだ正式に発表されていないので、Sneak Previewという位置づけでお読みください。

また、Datomicについては、Rich Hickey on Datomic, CAP and ACIDを参考にしています。

Datomicとは

Datomicは2012年に、Clojureの開発者であるRich Hickeyによって設計されたデータベースシステムです。Cloud上で使うことを念頭に分散データベースとして設計されていますが、多くのNoSQLソリューションと異なり、書き込みに関してはACID(原子性 Atomic, 一貫性
Consistent, 独立性 Isolated, and 永続性 Durable)な特性を備えています。

トランザクション、ストレージ、クエリの分離

Datomicは従来のACID特性を備えたSQLデータベースが、単一のサーバで動作する前提で設計されているのとは対照的に、データの書き込みを担当するトランザクション、データの永続化を保証するストレージ、データの問合せを行うクエリの3機能を分離することで、下記の特徴を獲得しています。

ストレージの抽象化

AWS Dynamo DB, Cassandra, MySQL, PostgresなどのSQL DB, メモリを利用することができます。

書き込みのACID特性

書き込みに関しては、単一のTransactorが担当します。ただし、Transactorはスタンバイ・インスタンスを持つことが可能で、最小限の中断でフェイルオーバーします。フェイルオーバーの際は遅延が発生するだけで、データロスは発生しません。

Datomicは直近のトランザクションを保持するlogと、バックグラウンドプロセスが定期的に更新するindexという2つの木データ構造を持ちます。どちらも結果一貫性の特性を持つストレージに格納することができます。但し、ルート木を保持するポインタは原子性と即時一貫性が必要で、DynamoDBであればConditional put, SQLであればdbトランザクションを利用しています。これにより、Datomicのデータ書き込みは原子性、一貫性、永続性を保証しつつ、ストレージ層に分散データベースを利用することが可能になっています。

独立性に関しては、書き込みをするプロセスがシステム内に1つしか存在しないため、常にSerializedであることが自明です。

問合せのスケーラビリティ

クエリの処理は、Transactorとは別のプロセスとして稼働する、PeerまたはClientServer(以下Peerと総称)が担当します。複数のPeerプロセスがシステムに参加することが可能なため、スケールアウトが可能になります。

Transactorは、Peerから書き込みリクエストを受取り、データをストレージにACIDトランザクションを用いて書き込んだ時点でトランザクション完了とし、新しい情報を全てのPeerのメモリ上に複製します。また、複数のPeerはメモリ上に保持している最新の情報+キャッシュに加え、更にデータが必要になった場合はTransactorを経由せず直接ストレージにアクセスします。データはイミュータブルであるため、複製されたストレージからの読み出しが可能であり、スケーラブルです。Peerとストレージ層の間にMemcachedを挟んで、Peerが追加、再起動した際にキャッシュを持たないコールドインスタンスとしてパフォーマンスの低下を防ぐことも可能です。

問合せの一貫性と独立性

最新のトランザクションが見えるタイミングはPeer間で異なる可能性がありますが、Datomicでは下記のように扱うことができます。

Datomicの問合せは、まずシステムから現時点、または過去のデータベースのスナップショットをdb関数を呼び出すことで取得し、そのスナップショットに対して1つ以上の問合せを実行するというモデルになっています。これにより、複数の問い合わせ間のデータの一貫性と後続のトランザクションからの独立性が保証されます。通常は個々のPeerが各々の時間軸を持っていることになり、Peer間でトランザクションがメモリに追加されるタイミングが異なることによる弊害はありません。

もしPeer間で最新かつ統一したDBスナップショットを取得する必要がある場合はdb関数の代わりにsync関数を未来のタイムスタンプを引数として呼び出し、設定したタイムスタンプの後に発生するトランザクションを待つことで実現可能です。過去のトランザクションに関しては、個々のPeerがas-of関数に同じ時間情報を渡すことで、過去の一時点のデータをPeer間で統一して取得することができます。

DatomicとCAP定理

DatomicはEric BrewerのCAP定理ではどのタイプに位置づけられるのでしょうか。Datomicは書き込みと問合せで違う性質を持っているため、各々に分けて考えるのが適切と思われます。

  • 書き込み:一貫性+可用性
    • transactorは単一プロセスのため、そもそもネットワークの分断というコンセプトが当てはまらない。
    • 可用性は前述のスタンバイシステムにより、単一障害点となることを回避している。
  • 問合せ:一貫性+可用性+分断耐性
    • Peerは最新のトランザクションにアクセスすることが可能なため、即時一貫性といえる。
    • Datomic on AWSではストレージにSSD=>EFS=>Dynamo DB=>S3というフォールバックが構築できるため、高い可用性を持つ。複数のPeerをALBを経由してルーティング可能なため、Peer層にも単一障害点を持たない。
    • 問合せはイミュータブルなデータにのみ行われるため、ネットワークが分断しても2つのネットワーク間でデータの不整合が起こることはない。

このトピックについては、Exploiting Loopholes in CAPという発表で詳しく説明されています。

テーブル構造からの決別

Datomicはデータを固定された5つのフィールドからなるデータ構造に格納します。

Entity Attribute Value Tx Added
42 :person/firstName "研二" 1001 true
42 :person/lastName "中村" 1001 true
43 :address/province "東京都" 1001 true
44 :person/firstName "Rich" 1002 true
44 :person/lastName "Hickey" 1002 true
45 :address/state "NY" 1002 true

上記の例では、単一のトランザクション(id 1001)で、EntityID 42でpersonに関するキーと値のペアが、EntityID 43でaddressに関するキーと値のペアが記録されたことを意味します。

Datomicデータベース内において、属性(Attribute)はユニークなキーを持っていなければなりません。名前空間付きキーワードの名前空間を便宜的に属性のグルーピングに使用することができます。

SQLのテーブルでいうところの行を追加するには、同じ組み合わせの属性を、別のEntityIDに紐付けて格納することになります。追加のタイミングが異なれば、新しいトランザクションを実行し、別のTx IDが割り振られることになります。

データは一度格納されると、上書きされることはありません。既存のEntityIDとAttributeで新しい値を追加すれば上書きに相当する、「新しい事実」を記録することが可能です。このため、手作業の会計では仕訳帳にペンで仕訳を書き込み、間違えていれば消しゴムで消す代わりに修正仕訳を新たに起こすように、修正があれば追記することで、過去時点の事実を変えることなく、時系列なデータ変化を記録することができるようになります。その結果、「過去の事実」を、データが「腐る」心配をすることなく、分散システムに持たせることができるようになるのです。

また、テーブル構造を持たないことで、従来のSQLデータベースが苦手としていた、疎なデータや階層データを無理なくモデリングすることが可能になっています。

疎なデータは、SQLで単純にモデリングすると、多数のカラムをほとんどNULLが占めているテーブルになります。DatomicはデータをEntity, Attribute, Valueで保持しているため、そのような無駄が発生しません。

階層データについては次項で説明します。

スキーマ

Datomicは多くのNoSQLソリューションと異なり、予めスキーマを定義する必要があります。
SQLのデータ定義言語では、テーブル定義の中にフィールドの属性定義がありますが、Datomicではテーブルの概念はなく、フィールドの属性定義がAttributeの定義に相当します。

{:db/ident :person/firstName
 :db/valueType :db.type/string
 :db/cardinality :db.cardinality/one
 :db/doc "A person's first name"}

本稿では詳説しませんが、スキーマでは他にEnum, Partition, Indexを定義することができます。

上で述べた階層データについては、スキーマに用意されている、他のエンティティを参照する:db/ref, one-to-oneかone-to-manyかを指定する:db/cardinality, 参照しているエンティティが独立性を持つのか、従属しているのかを示す:db/isComponentを使うことで自然に定義することが可能です。

Datalogによる問合せ

ストレージを抽象化した結果、問合せ言語は従来のSQLに縛られる必要がなくなりました。Datomicは、SQLと同様、関係代数に基づいたDatalog言語と、GraphQLに類似したPull API、エンティティを取得するEntity APIをクエリ機構として採用しています。DatalogはClojureで記述されているため、Clojureコードとシームレスに連携します。Datalogについては、Learn Datalog Todayが優れたチュートリアルです。Datalogの特徴として、アプリケーションのメモリ上のデータとDatomicのデータ構造に対して同じアプローチで操作することが可能な点が挙げられます。DatalogはPrologを問合せに特化させたサブシステムと考えることができます。

Datalogはこのような見た目になります。

(d/q '[:find ?t ?title
       :in $ ?artist-name
       :where
         [?a :artist/name ?artist-name]
         [?t :track/artists ?a]
         [?t :track/name ?title]]
     db
     "John Lennon")

このクエリは、「コネクションから取得したデータベースのスナップショットDBをDB用の変数$に、John
Lennonを?nameに代入した状態で、属性:artist/nameの値にJohn Lennonを持つエンティティ?aと、そのエンティティを:track/artistsの値に持つエンティティ?tと、エンティティ?tの中で属性:track/nameの値を?titleとし、エンティティ?tと?titleを返す」と解釈され、以下のようなデータが取得できます。

#{[809240558083034 "I Found Out"] [998356558092610 "New York City"] [809240558082263 "Out the Blue"] [809240558090128 "Cold Turkey"] [809240558146599 "Nutopian International Anthem"
] [809240558146603 "You Are Here"]...

この結果から、Pull APIを使って、クエリの対象となったデータを取得していきます。まず、最初の曲 "I Found Out"を値に持つエンティティを取得します。

user=> (d/pull db '[*] 809240558083034)
{:db/id 809240558083034
 :track/artistCredit "John Lennon"
 :track/artists [{:db/id 527765581346058}]
 :track/duration 217893
 :track/name "I Found Out"
 :track/position 3}

:track/artistsには:db/ref型で:db/cardinalityがmanyなデータが入っているので、参照先のエンティティを取得します。

user=> (d/pull db '[*] 527765581346058)
{:artist/country {:db/id 17592186045580}
 :artist/endDay 8
 :artist/endMonth 12
 :artist/endYear 1980
 :artist/gender {:db/id 17592186045420}
 :artist/gid #uuid "4d5447d7-c61c-4120-ba1b-d7f471d385b9"
 :artist/name "John Lennon"
 :artist/sortName "Lennon, John"
 :artist/startDay 9
 :artist/startMonth 10
 :artist/startYear 1940
 :artist/type {:db/id 17592186045423}
 :db/id 527765581346058}

:artist/nameには"John Lennon"が入っています。最初のDatalogはこの関係性を逆にたどって、"I Found Out"という曲をデータから抽出していることになります。ちなみに、:artist/gender, :artist/typeなどはEnumで、Enumの値は:db/identという、ユニークな値を意味する型を持つデータとして表現されています。

user=> (d/pull db '[*] 17592186045423)
{:db/id 17592186045423 :db/ident :artist.type/person}
user=> (d/pull db '[*] 17592186045420)
{:db/id 17592186045420 :db/ident :artist.gender/male}

上記のPull API例ではパターンにワイルドカードを使用していますが、ここに様々なパターンマッチングを適用することで、複雑なデータを柔軟に取得できます。例えば、John Lennonのアルバム"Live Peace in Toronto 1969"に収録されている楽曲を取得するには、DatalogとPull APIを組み合わせて下記のように記述できます。

user=> (d/q '[:find [(pull ?r [{:release/media [{:medium/tracks [:track/name {:track/artists [:artist/name]}]}]}])...]
  #_=>  :in $ ?artist-name ?album
  #_=>  :where
  #_=>  [?a :artist/name   ?artist-name]
  #_=>  [?t :track/artists ?a]
  #_=>  [?t :track/name    ?title]
  #_=>  [?m :medium/tracks ?t]
  #_=>  [?r :release/media ?m]
  #_=>  [?r :release/name  ?album]]
  #_=>  db "John Lennon" "Live Peace in Toronto 1969")
[{:release/media [{:medium/tracks [{:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "Blue Suede Shoes"}
                                   {:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "Money"}
                                   {:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "Dizzy Miss Lizzy"}
                                   {:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "Yer Blues"}
                                   {:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "Cold Turkey"}
                                   {:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "Give Peace a Chance"}
                                   {:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "Don't Worry Kyoko (Mummy's Only Looking for Her Hand in the Snow)"}
                                   {:track/artists [{:artist/name "John Lennon"}
                                                    {:artist/name "The Plastic Ono Band"}]
                                    :track/name "John John (Let's Hope for Peace)"}]}]}]

このように、Datomicでは、DatalogとPull APIを組み合わせて効率よくデータを問い合わせることができます。また、DatalogもPull APIもClojureのデータ構造を使用しているため、クエリをプログラムで生成したり変形することも容易です。

Peer/ClientServer

ここまで、"Peer"と"ClientServer"をまとめて説明してきましたが、ここでその違いについて説明します。

Peer

Peerとは、問合せを行うノードのことです。Peerは自作のアプリケーションとライブラリに加え、Transactorとストレージへのアクセスに必要なライブラリをClasspathに含める必要があります。データをメモリ上に持つため、高速な問合せが期待できる一方、依存関係が重くなるという欠点があります。また、頻繁に起動を繰り返す、AWS Lambdaのようなモデルではキャッシュが効くまで時間がかかるため、適していないという問題がありました。

Client

そこで、2016年に追加されたのがClientというアクセス方法です。Peerの代わりにClientServer(別名PeerServer)を起動させ、ClientServerに対してクライアントが軽量ライブラリを用いてコネクションをHTTPベースで張って、問合せをClientServer上で行い、結果を送り返すというモデルです。メモリ上での問合せに比べ、パフォーマンスの低下は起こりますが、マイクロサービスやAWS Lambdaにより適した接続方法です。

従来のDatomicのAWSへのデプロイ方法

さて、Datomicは当初よりAWSなどクラウド環境をターゲットに設計されていましたが、Transactor, Peer/ClientServer, ストレージの3要素を組み合わせて実現するシステムとしてのソリューションであったため、パフォーマンスと高可用性を担保したシステムの構築には、AWSの管理知識とふるまいの理解が不可欠でした。また、Peerは自作アプリケーションと一体化しているため、RDSやElastiCacheのような、PaaSとして、明確に線引きをすることができませんでした。

Datomic on AWS Marketplace

AWS Marketplaceとは

Cognitect社は当初DatomicのPaaS版を構築するつもりでしたが、既存の顧客からのヒアリングの結果、いざトラブルが起こった際に、サービスレベルアグリーメントで縛られているPaaSよりも、自分の手で対応できる方が望ましいと考えていることがわかりました。そこで、AWSのCloudFormationを用いて、オートスケーリング、高可用性、セキュリティ、監視、ロードバランスなどのベストプラクティスを焼き込んだテンプレートをAWS Marketplaceで提供することで、顧客は自分のAWSアカウント内に定義したVPCの中に、ワンボタンでシステムを構築できるようになります。

AWS Marketplaceは、ソフトウェアベンダーやSIerがAMIをライセンス付き、または既存のライセンス持ち込みで利用できるようにするプログラムです。請求は他のAWSサービスと一緒に、AWSからの請求書に対して支払うことになります。

Solo Topology (開発・テスト向け構成)

  • 開発、テスト、CI、あるいはホビープロジェクトに適している。
  • AWSセキュリティ、監視、ログが統合されている。
  • データは、Dynamo DB, EFS, S3の三箇所に保存され、S3を凌ぐデータ保全を実現できる。
  • 踏み台サーバも提供されるので、開発者は、SSH+Socksを使って、VPCの外側からDatomicにアクセスすることが可能。
  • 24x7で稼働させて、月額約31米ドル。 solo_topology.png

プログラミングモデル

  • Clientモデルをサポート。Peerモデルはサポートされない。
  • Clientモデルは前述の通り、マイクロサービスに適している。
  • ライブラリはApache Licenseで配布される。
  • 同期・非同期モデルの両方をサポートする。
  • Transducerが利用可能である。
  • 1つのEC2インスタンス上で、TransactorとClientServerが稼働している。

監視・ロギング

  • Datomicのアプリレベルの指標が、AWS Cloud Watchに統合されているので、DevOpsが慣れた手段で監視することが可能。
    cloudwatch_metrics.png

  • ログもCloud Watchに統合されている。
    cloudwatch_log.png

  • タグを活用した管理も可能。

セキュリティ

  • AWSのHMACを利用した認証なので、他のAWSサービスと同等のセキュリティを提供する。
  • データはClientServerのRESTエンドポイントに到達した時点で、AWSのKMS(Key Management System)を利用して暗号化される。
  • ネットワークはVPCで隔離されている。
  • 通常はサードパーティ製品用にIAM権限を定義できないが、その問題を回避しており、Datomic用の権限をIAMを使って管理できる。

データ保存

  • 一般的に、データ保存の特性別に選択されるAWSのストレージ技術は以下の通り。
    • 低遅延...EC2(メモリまたはSSD)
    • 低コスト...S3, EFS
    • ACID...Dynamo DB
    • 信頼性...Dynamo DB, S3
  • Datomicは、リード・ライトが分離されており、イミュータブルな値と、DBコネクションなど可変な値についてはmanaged ref(atom, agent)が適用できる。
  • Datomic on AWSは、Dynamo DB, S3, EFS, SSDの全てを組み合わせることができる。
  • 問合せはSSDへアクセスし、失敗すれば、EFS、Dynamo DB、S3へとフォールバックすることができる。
  • Dynamo DBへ書き込むことでACID特性は保たれるが、実際のデータはS3など別のデータ構造に格納できるので、Dynamo DBにはポインタの格納のみを行い、負荷を数十分の1まで下げることができる。
  • データは、S3、Dynamo DB、EFSの全てに書き込むことができるので、S3の99.999999999%に勝る永続性を得ることができる。またデータが破損した場合、異なるストレージ間でデータ修復を自動的に行なってくれる。

自動スケーリング

  • 2017年6月に発表された、Dynamo DBのAuto scaling機能を使って、インポートなどバーストライトが発生した場合、自動的にキャパシティを引き上げ、その後引き下げることが可能。
  • しきい値を設定して、一定額以上課金されないようにするかわりにライトリクエストをバックプレッシャーコントロールすることも可能。 DatomicAutoScale.png

Production Topology (本番システム向け構成)

  • 上記で紹介したSolo Topologyの機能に加えて、
  • 全ての要素がクラスタリングされており、単一障害点がない。
  • 従来はPeer, Transactorの異なるタイプのプロセスが混在していたが、Datomic on AWSでは、それらが統一され、一種類のノードでクラスターを構成する。
  • 高可用性のために、AWS ALB(Application Load Balancer、第2世代のELB)を利用している。
  • ノードは、ASG(Auto Scaling Group)で管理されているため、負荷に応じて自動的にインスタンスを追加、縮退する。
  • データ分析、Webアクセスなどの目的別にクエリグループを異なるASGに割り当てることが可能。 prod_topology.png
  • 本番システム管理に必要な監視項目を更に追加してある。 prod_metrics.png
  • 分散システムの多くは、将来シャーディングを始めるときのために、予めデータをグルーピングしておく必要があるが、Datomicではそのような考慮は不要。
  • Datomic内部のデータである、pending requestがCloud Watchに統合されているため、それを基にASGを制御することができる。

リリース時期

  • 2017年第四四半期(〜12月)
  • 当初はクエリグループは実装されていないが、程なく追加される予定。

Q&A

  • Clientは既存のものと異なるのか?
    • ほとんど同一。コネクション関数が、multimethodを使ってon premiseとon AWSで異なる振る舞いを実装する点だけが異なる。
  • Peerモデルは利用可能なのか?
    • Clientモデルのみ。現在Proを使っているユーザーの9割はProを使い続けると予想している。一つにはOracleなど、オンプレミスのDBにロックインしているケースが多いし、Peerモデルの長所を活かした実装をしているケースも多いため。
  • AWSのクロスリージョンはサポートしているか?
    • 現時点ではNo。一つのリージョンを選択し、専用のAMIを起動する必要がある。但し、クロスリージョンのサポートは非常に容易なはずで、ユーザーは是非その機能をリクエストしてほしい。
  • Database (transaction) Functionは利用可能なのか?
    • Proと同じ形でサポートする予定はない。clojure.specを利用したバリデーション、S3からjarファイルをclasspathにロードする仕組みなどを検討している。初期リリースには含まれない見通し。
  • Cloud版でも単一のTransactorが書き込みを行っているのか?
    • まず、よくある誤解を解いておきたいのだが、Transactorがハートビートを使っているのは、パフォーマンス最適化のためで、仮にハートビート機構がうまく動かずに複数のTransactorが同時に書き込みをしても、データベースでの条件付き書き込み処理によって1つのみ成功し、他のトランザクションは失敗するだけである。クラウド版では、consistent hashmapを用いて、transactorとDBの組み合わせを一意に管理しているので、複数のDBに対して効率よく書き込むことが可能になっている。

謝辞

京極さん@iku000888、片山さん@shinichyに査読していただきました。ありがとうございます。