2022年6月3日金曜日

フロントエンド DB で運用コストゼロ

最近はサーバーレスの SQLite が人気みたいですが、個人的には sql.js-httpvfs が好きです。 個人開発で運用コストをゼロにしたいなら、こちらのほうが気楽です。

sql.js-httpvfs は Accept-Ranges を利用して、 DB のすべてのデータを fetch することなく、必要な時にバイト単位での fetch を実現します。 つまり DB をフロントエンドに置いた運用開発ができます。 バイト単位での fetch を実現する Accept-Ranges の仕組みは、フロントエンド新時代を支える技術になる気がしています。

静的 DB はフロントエンドへ移行する

ほとんどの開発において DB / ネットワークが最初にボトルネックになります。 昔から bytes-level fetch ができればフロントエンドに DB を置けるのにとは思っていたので、 Accept-Ranges fetch は超機能だなあと当時は思ったものです。 この Accept-Ranges 周りのサポートが最近一気に活性化してきました。

ちなみにこの技術を応用して フロントエンド で SQLite を動かす技術は 2019年に確立されていましたが実装も進化してきて、 また 2022年に主要なブラウザで使えるようになりました。 これによってバックエンドの DB がフロントエンドに移行する、新時代が来るかも知れません。 特に read-only で public な DB はバックエンドに置く必要がなくなります。 これはとても効率が良くて、うまくサービスを分割して公開すれば、だいたい無料で運用できるはずです。 多少の手間はありますが、迂闊にバックエンドを使って採算が取れず、コストが理由でサービスを供養するよりずっと良いです。

今のところ一番良さげな実装は sql.js-httpvfs ですが、 近い将来より良いものが出てくるかも知れません。 本当は KVS、しかも純粋な HashMap のように O(1) で使えたり、それに近い演算で処理できるライブラリがあれば嬉しいのですが、今のところないように見えます。 強い人がそのうち作ってくれるんじゃないかな…かな…。 まあデータ数が小さければ RDB の B+tree Index も KVS も大差ないので 私は sql.js-httpvfs を拝借しています。

とりあえず素振りした

そんな Early Stage に見える技術を使って、サクッと 8こほどアプリ / API を試作してみました。 いくつかテーマを付けて作ってみると、多少の課題もありました。 いま気付いている課題は、Prepared Statement と、コネクションの貼り直しですが、まあ何とかなります。 そのうち解決されたら良いですが、どうなるかな。

フロントエンド上では RDB の使い方は異なる

sql.js-httpvfs は SQLite の DB を 10MB ごとにブロック化して Virtual File System 化している部分以外は、基本的に RDB の仕組みの延長線上で動きます。 しかしフロントエンド上では、使い方を多少考えないといけません。 たいていの B+tree Index の探索は1〜3つのブロックにまず収まり、1〜5回の fetch でレコードの位置がわかります。 ただレコードの中身は様々なブロックに点在していることが多いため、 毎回 fetch しないといけません。これは帯域負荷がそれなりに掛かります。 つまり正規化された DB ではフロントエンド上で範囲検索に弱いことが問題になってきます。

みんな大好き RDB の正規化は、レコードへのアクセスがほどほどに早いことを前提にした概念です。 ネットワーク上やフロントエンド上ではバッドノウハウです。 解決方法は簡単で、教科書通りの正規化を捨てて、 事前に利用用途に応じたレコードの最適化をすれば良いだけです。 もっとわかりやすく言えば、KVS っぽく使いましょうということです。 たとえば範囲検索は結果の配列を JSON.stringify して、一つのレコードに放り込めば良いです。 レコードを一つにすれば一回の Accept-Ranges fetch でデータを取得できるので、大量の fetch を発行せず効率的にデータ取得ができます。 まあレコードはカンマ区切りでも何でも良いんですが、色々なデータを突っ込むことを想定して、JSON でいいかな。 ちなみに数値配列だと効率が悪すぎるので、Blob にすべきと思います。 一般性のあるカラムにしたいなら msgpack を使うのも良さそうです。

似たようなシステムは頑張れば自作もできなくはないです。 ただ sql.js-httpvfs で作れば、仕組みは雑でも完成度 8割のものができます。 何も考えなくてもバイナリでレコード跨ぎが簡単にできるところが強い。 KVS 的に一度に複数レコードのデータを取得する DB に無理やり変えれば、数回の fetch で範囲検索できる検索システムが雑に完成します。 ネットワーク上でも B+ Tree の効率はかなり良いです。 B+Tree の検索コストは log N / log m (m=leaf nodes) なのですが、log m の部分がネットワーク上では強い。 1000万データ 1GB くらいまでは耐えられそうです。 範囲検索も DB の作り直しを惜しまなければどうにでもなりそう。

DB の進化に期待

sql.js-httpvfs で遊んでみると、 DB はもうバックエンドだけの技術とは呼べない状態です。 バックエンドとフロントエンドの区分けとして重要なのは、 I/O の頻度や、セキュリティ、public/private の概念と言えます。 ほとんど静的なオープンデータはフロントエンド上に置いても良いのではないでしょうか。

ちなみに read/write が混在するなら、Cloudflare D1、PlanetScale、FaunaDB、Supabase、Upstash のようなサーバーレス DB、 もしくは Firebase や VPS のようなサービスを利用するしかないと思います。 オンラインストレージ、write が時系列順にしか発生しないなら静的なレコードに退避とか、多少の抜け道もありますけどね

個人的には最初に述べたように KVS っぽいもっとシンプルな実装が欲しいと思っていますが、時間は掛かる気がします。 雑に色々と考えてみたのですが、コンパクトにしようとすると Minimal Perfect Hash とか Succinct Trie (ただし本当に使いたいのは Trie Map) なども考える必要がありそうです。 でもあまり改善はしなそうかな。また色々頑張っても、データ量が増えると結局は最初のハッシュ関数のデータが結構大きくなり、それがフロントエンドではネックになります。 最初の重いハッシュ関数をレイヤーにして振り分けることも考えました。 1億データ以上なら結構差は出そうですが、それ以下だと fetch 回数が 1回減るくらいに思えて、速度差は微妙です。 初期ロードが早くなって 150ms 早くなるくらい? そして 1億データなんてのは、フロントエンド上だと使わない気がする。

やはりつよつよな人が作ってくれた実装を使いたいです。 10年前から実装面では実物がなかなか出てこない世界なので、当分は実装があることが大切で、常に安心感のある SQLite が使われる気がします。

0 件のコメント: