Cloud NativeなServerless DBを開発した - 超α版

はじめに

GCPのCloudRunやAWSのLambdaのようなFaaSはアプリケーションのデプロイ先にとても便利です。

一方で、サーバレスなのでストレージを何とかしないといけないのですが、やはりRDBは使いたい。本気の業務システムならここでCloudSQLだとかAWS Auroraが出てきてこいつらは問題無くこれらのサーバレスなアプリから接続できます。

ただし、高い。大事なことなのでもう一度言います。高い。

KVS系は無料枠があるのでそっちを使えば良いですが、やはりRDBが。。。となるととたんに選択肢が無くなります。たんに実行したいのが手元の管理アプリとかだとちょっともったいないですよね?

アプリ側がIaaSやステートフルなコンテナならsqliteを動かすのがこういう用途では多かったと思います。そう、私はサーバレスでもsqlite的なことがしたいのです!

という分けでまずはプロトタイプを作ってみました。

今回はこのプロトタイプをベースに何を作りたいのかを話していきたいと思います。

コンセプト & アーキテクチャ

主なコンセプトは以下の3つです。

  • 利用している時以外は稼働しないサーバレスDB
  • RDBでありSQLをサポート。でもACIDには目をつむる
  • JDBCで接続できJPAなどから活用できる

まずサーバレスDBという事が必須の要件です。これによってsqliteをローカルに置いていた時のような手軽な運用を取り戻せます。

つづいて、DataStoreやDynamoのようなKVSも良いのですが、やはりRDBは欲しいです。特に既存のOSSを移植したいと思ったときにSQLが使えることは重要でしょう。ただし、用途が実験用または個人向けという事を加味してACIDの完全性は目を瞑ります。

最後にJDBCで接続できること。これは上記とほぼ同じですがRDBなのだからアプリから見たI/FはJDBCであるべきです。RESTやgRPCをアプリから使ってというのは実装が容易そうなのですが既存のライブラリと整合性が無くなるので、完全では無くてもJDBCのサポートは重要と考えます。

上記の3つをコンセプトにして以下のようなアーキテクチャーにしました。

  • DBエンジンはCloud Runで動作
  • ストレージはGCSで動作
  • DBエンジンのI/FはWeb API
  • JDBC側でDBのAPIサーバと会話してアプリからはJDBCに見せる

図にすると以下のような感じです。

f:id:pascal256:20200316125559p:plain

ストレージの実体は可用性とコストを考えてオブジェクトストレージにしています。これをDBエンジンが都度読み書きする事で永続化しています。

とりあえず超ナイーブに実装したのでSQLのリクエストがある度にDBのフルロードとフルストアが発生しますが、ここは何かしらの工夫が可能が気がします。

現在はDBエンジンはh2dbのラッパーです。Javaだから取り扱い安いというのもありますがh2dbは多くのDBの互換モードも備えてるのでそれも活用できるかもです。

JDBCドライバとサーバはHTTPSの通信なので完全にトランザクションが切れています。なので、必然auto-commitのみの運用になります。WebSocketとか使えば何とかなるかもですが現状は未検討。むしろ性能観点ではgRPCに対応したいですね。

一応、JDBCドライバとして実装してるので以下のようなコードがそのまま動きます。逆に言えば今はこのコードを動かすための実装しかまだしてないですが。。。

var url = "jdbc:serverlessdb://http://localhost:8080/mydb";

Class.forName("cn.orz.pascal.serverlessdb.jdbc.ServerlessDriver");
try (var con = DriverManager.getConnection(url); var st = con.createStatement()) {
    st.executeUpdate("CREATE TABLE IF NOT EXISTS sample_tbl (name varchar(255))");
    st.executeUpdate("INSERT INTO sample_tbl(name) values('Nanoha')");

    try (var rs = st.executeQuery("SELECT name FROM sample_tbl")) {
        while (rs.next()) {
            System.out.println("rs[1]=" + rs.getString(1));
        }
    }

    try (var rs = st.executeQuery("SELECT count(1) FROM sample_tbl")) {
        while (rs.next()) {
            System.out.println("count=" + rs.getInt(1));
        }
    }
}

URLで設定したDBを無ければ新規で作ったりもするので、テストの場合にはかなり便利かと思います。

パフォーマンス

さて、パフォーマンスはどんなものでしょうか? またベンチマークテストをするレベルではないので単体リクエストを投げたときのログを出します。

2020-03-16 13:05:08.444 JST2020-03-16 04:05:08,443 INFO [ser.profile] (executor-thread-1) tracelog: getBucket(ms): 39.420
2020-03-16 13:05:08.572 JST2020-03-16 04:05:08,571 INFO [ser.profile] (executor-thread-1) tracelog: readDbFiles(ms): 127.543
2020-03-16 13:05:08.579 JST2020-03-16 04:05:08,578 INFO [ser.profile] (executor-thread-1) tracelog: execute(ms): 0.658
2020-03-16 13:05:08.697 JST2020-03-16 04:05:08,697 INFO [ser.profile] (executor-thread-1) tracelog: storeDbFiles(ms): 117.519
2020-03-16 13:05:08.697 JST2020-03-16 04:05:08,697 INFO [ser.profile] (executor-thread-1) tracelog: executeQuery(ms): 293.730

これはデータ件数が数件しかない非常に小さなデータだという事に注意してください。それでもデータのREAD/WRITEに240msくらいかかっており処理速度の大半を占めています。 データが大きくなったときにどういう傾向になるのかとか、実感速度としてどの程度遅いかは今後の計測ですね。まあ、原理的にはあまり期待できない気がします。

リードレプリカを作るのは容易なので、リードヘビーなアプリなら読み込みをスケールさせることも理屈上は可能な気がしますが、それもデータサイズが影響してくるので真面目に作るとシャーディングが必要になりそうです。

きっと真面目に作るとSpannerクローンみたいになってくるのでそこを真面目に作ってはダメでしょう。

今後の展望

まずはちょうど欲しかったREDMINE的なチケットシステムを実装しながら機能を拡充して、標準的なAPIは実装させておきたいです。

その上で、既存のMetabaseみたいなOSSツールをサーバレスに改造してみたいとは思ってたので、そういった場合のコンフィグ格納先として使えると良いなぁ、と思ったり。上手くいくかは分かりませんが。

あと、管理APIは実装しないとですね。DBを消したりとか。

sqlite3的な気軽なDB運用はやはりサーバレスになってもしたいので、しばらく開発は続けてみたいと思います。

それでは、Happy Hacking!

参考