Unlimited Process Works - 無限にスケールするためのアーキテクチャープロトタイプ

「行くぞDocker! プロセスの貯蔵は十分か!?」

はじめに

さて、以前顧客向けシステムに最適なDBは何だろう? と思ってこんな記事を書きました。

koduki.hatenablog.com

で、その中でも顧客向けシステムはユーザ毎にDB作れば良いんじゃね? Dockerでいけるんじゃね? ということ書いたんですがシステムアーキテクチャを考える上でのプロトタイプとして作ってみました。

github.com

システム名は[Heaven’s Feel]II.lost butterfly見たいけど近所に無いから見れないので、出始めのコメントを思いついて勢いで付けた次第です。後悔はしていない。

基本コンセプト

READMEにも書いてますが、基本コンセプトはCGIのようにリクエスト毎にDockerを立ち上げてそのコンテナでsqlliteをユーザ毎に作成してバインドするとう作りになっています。 Dispatcherが同じユーザなら同じDBにアサインする形ですね。

その上で、参照専用のDBとしてGlobal DBをpostgresqlで持っています。 ここにそれぞれのユーザのDBの内容を非同期で連携していきます。その上でマテリアライズドビュー等を使用して集計したり扱い易い構造にします。

https://cacoo.com/diagrams/OFL4JoIEKUjLIooU-30F44.png

sqliteだと分散トランザクションがー、とか大容量データでの性能大丈夫? とか色々感じるかも知れないですが、 一般的に顧客データは顧客が増えるとレコードが増えますがユーザ毎では数MBとか数百MBで収まるケースも多いです。 また、顧客を超えて読み書きすることは(マスタデータ以外)ありません。

不正検知やレポートを作る作業をはじめとした横断的な集計をする作業はGlobal DBを利用することで高速に実行できます。 そのため、sqlite側でDB間の一貫性を保つ必要はないので分散トランザクションなどの処理は不要です。 これらの作業は大抵はタイムスロットがあるので一定の時間より後のデータで整合性が保証されてるならリアルタイムのそれは問題にならないため、遅延連携が可能となります。

ユースケース

このシステムは超簡単な銀行システムなので以下のことができます。

  • 自分のアカウントに入金
  • 自分のアカウントの中身を表示
  • 全員の残高ランキングを取得

まあ、セキュリティ的にヒドい銀行ですがそれはサンプルということでw

テーブル構造

基本的にはまんまシンプルなのですが、ポイントの一つはIDにシーケンスではなくUUIDを使っていることです。 UUIDを使うことでシーケンスの発行で競合することがありません。加えて、sqlite側と連携先のpostgresql側で同じIDが使えるので整合性チェックも容易になります。 連携が容易にしやすいようにsqlite側ではアップデートをせずにInsertのみで利用する使い方を想定しています。

sqlite

CREATE TABLE account (
        id UUID,
        amount BIGINT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
);

PostgreSQL

CREATE TABLE account (
    id UUID,
    name TEXT,
    amount BIGINT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

CREATE MATERIALIZED VIEW v_account_summary AS 
    SELECT name, SUM(amount) FROM account GROUP BY name;

性能特性

レイテンシー

まずDispatcherですがリクエスト毎にDocker起動させてるのでオーバーヘッドが凄いです。あらゆるリクエストが2秒くらいかかります。 いくら極限のレイテンシーは要求されないとは言え流石に遅すぎですね。これをちゃんとすると多分FaaSになるのかな? でちゃんとする方法は例えばFast Containerを導入するとかになるかも知れないです。

hb.matsumoto-r.jp

ただ、致命的な点としてsqliteのために永続化層が必要です。ステートフルなのでプロセスの再利用が難しいことを考えるとデータロードだけでもレイテンシーを稼ぐ必要があります。 そのためのアイデアとしては2つ

  • KVSなど高速なストレージに乗せる
  • パーティションなどで階層化してすぐ使うデータはメモリなど高速なストレージに置いて、残りは遅延で読み込む

sqliteのdbファイルをKVSに置くのが一番良い気がしますね。それで容量の問題が出るならパーティーション化とか階層化するしかないのかな、と。 なんとなくですがGoogle SpanerとかもKVSの上に構築されたRDBっぽいので、似たような発想なのではないかと。もちろん仕組みはもっと高度でしょうが。

スケーリング

Workerは現在は純粋なプロトタイプv1なのでコンテナをローカルに作ってるため1マシンを超えてスケールしないですが、実際はk8sやswarmを使って複数マシンを使うことでスケールさせる事ができると思います。 Dispatcherは特にステートを持たないのでこれはスケールアウトさせることができます。 ASync ETLは本来的にはKafkaなどを手前に置くことになると思いますがこの状態ならスケールするのではないかと。 Global DBはトラディショナルなRDBなのでスケールの問題がありますが、ユーザトランザクションからの書き込みはないのでリードレプリカで対応できます。

負荷試験を実施してないので断定は出来ないのですが、理論上はこの構造ならかなりのところまで金の弾丸でスケールするんじゃないかな、と。

メンテナンス

メンテナンスですがユーザDBのスキーマを変えるにはGlobal DBとAsync ETLにも変更が必要なのでまあ大変ですね。 とは言え、基本的にはユーザDBを徐々に変更していって、最終的にGlobal DBを変えるという手順が使えるので全体的なメンテナンスコストはオーバーヘッド分ありますが、ここのメンテナンスはそこまで大変でもないかと想定します。

アプリとDBがセットのコンテナをリリースして入れ替えていくだけなので、MySQLとかでsハーディングしてる時より小さくアップデートできて楽かも。

まとめ

とりえず無限にスケールするアーキテクチャというのをコンセプトにシステムを組んでみました。 素朴に作ってみた感じでは無限にスケールはなんとかなりそうだけど、せめてレイテンシーが1秒を切らないと使い物にならないだろうという感じなので、ちゃんとしたミドルウェアを使うのが当面は良さそうです。仮に問題が解決してもこの構造で運用するのは多くのケースでオーバーテクノロジーな気もしますし。

ただ、こうして自分でコンセプトモデルを作ることでスケールのためにアプリケーションやミドルウェアが備えるべき要件もクリアになりましたし、ミドルウェア側の振る舞いもある程度想像ができるようになったので目的は達したかな、と。

勉強にはなるので暇を見てもうちょっと改造はしてみたいなー。 それではHappy Hacking!