isyumi_netブログ

isyumi_netがプログラミングのこととかを書くブログ

最強のサーバーサイド・フレームワーク『イシュミ・デルティスト』

 

Diff as a Service

差分を扱うのが得意なサーバーサイドフレームワークを作りました。

 https://github.com/rikuTanide/isyumi_deltist 

概要

僕はFirestoreにViewを作る!

問題意識

Firestoreはどこまで正規化するべきか問題があります。
バックエンドを書いている時の僕はできるだけ正規化したい派です。
しかし、クライアントを書いている時の僕はそのまま表示できる形式で渡してほしい派です。
また、僕は「非正規形のデータを直接いじるプログラムを書くと絶対間違える」という硬い信念を持っています。
なので、非正規形データは正規形データから純粋関数で展開するべきだと思います。

解決策案

単純な解決策

正規化したデータと非正規化したデータを別々にFirestoreに保存する。正規形データが変更されたら非正規形データをまるごと更新する
豪快にやっちゃいます。
データ量が少ないなら一番簡単な方法です。
しかし、データ量が増えてきたら無理です。read/writeの遅延や課金が大きくなります。

現実的な解決策

正規化したデータと非正規化したデータを別々にFirestoreに持つ。正規形データが変更されたら非正規形データを更新する 。ただし、どの正規形データへの更新がどの非正規データに伝播するか細かくプログラミングする

今の所の最善策だと思います。

このやり方には2つの欠点があります。
一つはテーブルとビューの依存関係を洗い出すのがめんどくさいことです。どのテーブルへの変更がどのビューのどの行に影響するかを確実に洗い出すのは難しいことです。

もう一つは、最適化をしたいならコードを沢山書かないといけないことです。例えば商品ごとの売上なら、売上が発生するごとに売上一覧を集計して出すより、前回の計算結果に今回の売上を加算したほうが速いです。
このような最適化の余地はたくさんありますがめんどくさいです。

夢想的な解決策

全部オンメモリでビューを計算する

まず、全てのテーブルを元に全てのビューをつくる関数を一つ用意します。
メモリに現在のテーブルを載せておき、どこかに変更が発生したらその関数を実行します。
そして前回作ったビューと今回のビューの差分をチェックしてFirestoreを更新しに行けばいいかと思います。
この方法の最大の欠点はメモリ使用量です。
EC2でも、2~4GBよりたくさんのメモリを使えるインスタンスは高いです。

最終解決策 イシュミ・デルティスト

そこで僕は、フレームワークを作りました。

プログラマはこの2つをします。

  • 正規化したテーブルのスキーマDSLで記述
  • テーブルを非正規形に展開する宣言をDSLで記述

フレームワークはこれをします。

  • 起動時
    • JSONなどでエクスポート出来る形式でビューを実体化する
  • テーブルの変更イベント受信時
    • ビューを差分更新する
    • どんな差分が発生したのかを出力する

テーブルとビューのデータはディスクに保存してあるのでメモリをほとんど使いません。
また、どのビューのどの行がどのテーブルのどの行に依存しているかについて細かい依存グラフを作成しているので、最短距離で差分更新できます。

あとは、このフレームワークが返してきたDIFF情報をそのままFirestore APIに書き込めばOKです。
Viewができた!

使い方

デルティストは単純なステートマシンです。
ReduxのReducerに似ています。
Tableへの書き込みイベントを引数にビューの差分が帰ってくる関数です。
返ってくるのは新しいビューではないです。
その中で変更があった行だけが返ってきます。
疑似コードですがこんな感じに動きます。

売上一覧テーブルに鉛筆の売上を挿入したら「月ごとの売上合計ビュー」と「商品ごとの売上ビュー」が1行ずつ変更された
void main() {
  var db = new Database();
  var diff = db.insert("Sales", {productID: 'pencil', date: '2019-09-01' });
  print(diff); 
  
}

出力
[
 {
   viewName: 'SalesParMonth' ,
   data: {year: 2019, month: 8, sum: 1000},
 },
 {
   viewName: 'SalesParProducts',
   data: {productID: 'pencil', productName: '鉛筆', sum: 500 }
 }
] 

Firestoreで使うことを想定していますが、入出力は単純な構造体なので何にでも使えます。
ブラウザからWebSocketで直接つなぐこともできますし、MySQLにMaterialized Viewを作る用途で使うこともできます。

ディスクを使うのでLambdaやGAEなどで使うことはできません。
EC2インスタンスが一つ必要です。

ロードバランスやSPOF回避の仕組みは何も考えていません。

実装方法

デルティストはビューを作る途中の仮想テーブルも全てビューとみなし実体化してディスクに保存しています。
デルティストでは3個以上のテーブルやビューをJOINする方法はありません。JOINは2個に限ります。
3個以上のテーブルやビューをJOINしたければパラマス式トーナメントにします。
そしてそれぞれを実体化します。
例えば4個のテーブルをJOINしてビューを作るなら、合わせて3つビューが実体化されることになります。
下の図の場合、本当にほしいビューがView3ならこうなります。

Table1 -┐
        |-View1┐
Table2 -┘      |-View2-┐
Table3 --------┘       |-View3-
Table4 ----------------┘

テーブルが更新されると、ビューに行変更イベントを通知します。
受け取るのはテーブルに直接依存しているビューだけです。
変更イベントを受け取ったビューは、そのイベントを元に自分を変更しないといけないかどうかを判断します。
もし変更があった場合は自身を書き換えてから行変更イベントを作成しディスパッチします。
それを再帰的に行うと全てのビューが更新されます。
この時の重要なのが、最小コストでビューを更新出来るようにすることです。
例えば、TweetsテーブルとUsersテーブルをJOINしてTimelineビューを作るとします。Tweetsテーブルに一行挿入されたらUsersテーブルからそのTweetのuserIDと一致するUsersテーブルの行だけを取得してJOINしてTimelineビューに挿入します。
逆にUsersテーブルから行が削除されたらTimelineビューの中でuserIDが一致するツイートだけを削除します。

実体化したビューはRocksDBというデータベースで保存します。
RocksDBはKVSです。
TableとViewのPrimary Key == KVSのKeyです。
DBサーバーではなくCのライブラリとして使えるので運用が楽です。
デルティストは1万行の中の1行を書き換えるような処理が多いので、RocksDBのLSM-Treeというアルゴリズムと相性がいい(らしい)です。詳しくは知りません。

大変だった点

DartC++インテロップ

始めてDartC++インテロップを書きました。大変でした。

DSLの文法設計

どういう文法だったらプログラマにわかりやすくて間違えにくいか一生懸命考えました。
データを設定する時に文字リテラルでテーブル名やカラム名を指定するようなことはしたくなかったです。

// 嫌な例
row.set("Tweets" , "UserID", 10);

これは誤字脱字が発生します。
このように書きたいです。

row.set(tweets.userID, 10);

この時型を間違ったらコンパイルエラーにしたいです。

row.set(tweets.userID, "10"); // 第二引数がintじゃないのでエラー

Dartの型システムの強さに救われました。

ビュー更新の実行計画

アルゴぢからが足りないため、大変苦労しました。
やることは各演算のオペランドが増えたときと減った時にそれぞれ前回の結果からどう変わるかを1個ずつプログラミングすることです。
例えばMax関数なら

  • Insert
    • 入って来た数が既存のMaxより大きい -> Maxを更新
    • 入って来た数が既存のMaxより小さい -> 無視
  • Delete
    • 消した数が既存のMaxと等しい -> 二番目をMaxに昇格
    • 消した数が既存のMaxより小さい -> 無視

みたいなコードをいっぱい書きます。

インデックス

RocksDBはRDBのようなインデックスの機能がありません。
その代わりPrefix Seekという機能があります。キーのビット列が前方一致する行を全て列挙してくれます。
つまり主キー以外の属性で行を取得したいなら、その属性をキー、テーブルの主キーをバリューにしたKVSを別に作っておく必要があります。
例えば、Timelineビューの主キーはtweetIDだったとします。
ユーザー名が変更された時にTimelineビューを更新するにはuserIDをキー、tweetIDをバリューにしたKVSが必要です。
どんなインデックスが必要なのか全て洗い出すのが大変でした。

進捗

今のところ完成している機能は

  • INNER JOIN
  • UNION
  • SELECT

開発中・調整中なのが

  • Optional型
  • 生成列
  • OUTER JOINとLEFT JOIN
    • COALESCE
  • GROUP BY
    • MAX
    • MIN
    • COUNT
    • AVERAGE
    • ARRAY
  • RANK
    • DESC
    • ASC
  • WHERE
  • UPDATEの高速化(Insert/Deleteに比べてUpdateは最適化可能性が多すぎるのでだいぶサボってる)

構想中なのが

  • Infinite ScrollのためのSelect機能
  • 休眠ユーザーを識別して計算量をサボる機能

です。
引き続きがんばります。

mozaic bootcampに行ってきました

mozaic bootcampとは

講義形式のトレーニングです。講師はJxckさん矢倉さんです。
4/28-5/1の4日間、10:00-17:00の時間のスケジュールでした。
Webの仕様を中心に、Web系開発者が知っておくべきことが詰め込まれていました。
mozaic.fmが4日間朝から夕方まで続く感じです。

場所

一休さんのオフィススペースを貸していただきたました。一休さん本当にありがとうございました。

形式

大雑把に

  1. ncコマンドでHTTPを手書きしてexample.comにHTTP Requestしてみる
  2. TCPサーバーを立て、そこにブラウザでアクセスして、送られてくるHTTP Requestを見てみる
  3. Wiresharkで通信を見てみる
  4. 重要なポイントについて仕様を読む
  5. Jxckさんから解説がある
  6. 仕様に他のオプションが書いてあれば、その動きを試してみる
  7. 以上を踏まえて、「〇〇(←ありがちな要件)を正しく実装するにはどうするべきか」を問われるので、自分なりに実装してみる

というサイクルでした。
質問はその場で聞くことも出来るし、所定のチャットに書き込むこともできました。
自分は同期的に聞いておかないとわからなくないそうなことは前者、補足事項のようなことは後者というように使い分けていました。

内容

  • HTTPのスタートライン( GET /foo HTTP/1.1 のこと)
  • HTTPの文法
  • Content-Length
  • Content-Type
  • Content-Encoding
  • Keep-Alive
  • Transfer-Encoding
  • HTTP Status Code
  • リダイレクト
  • noreferrer
  • HTMLのForm
  • キャッシュ
  • Cookie
  • Origin
  • Same-Origin Policy
  • RESTFul
  • Mixed Contents
  • 認証
  • TLS

などでした。
最終日は、ここまで学んだことを総動員して「正しいサインアップ・ログインフォームを作る」という実技演習がありました。
これが一番楽しかったです。

何ではないか

特定の言語・フレームワークについての解説ではないです。
最新の機能についての解説でもないです。
とりあえず動くものを作って楽しもうという感じでもないです。

きっかけ

僕はmozaic.fmのリスナーなので、mozaic.fmで知りました。
僕は最近、自分がとっくに初心者を脱しているものの、その先のステップで伸び悩んでいるという自覚がありました。
特に、僕のように独学でやってきた人は、「特定のパターンに落とし込めばWebアプリなんか簡単に作れてしまうが、しかし網羅的な知識はない」という人が多いと思います。
世の中には初心者向けの教材はたくさんありますが、中級者以上のための教材はあまり多くありません。
色々模索していた時にこのbootcampを知り、その内容の説明から「自分にちょうどいい難易度ではないか?」と思い応募してみました。

雰囲気

ガチンコ! ファイトクラブみたいなノリだったらやだなと思っていましたが、Jxckさんも矢倉さんも他の参加者のみなさんも優しかったので安心しました。
まず、他の参加者の方がとても真剣だったので、いい場所に来たなと思いました。それが一番大事なことだったかもしれません。
手を動かす→話を聞く→手を動かす、のサイクルだったので集中が途切れず最後までついていくことができました。
初日はみんな緊張していましたが、矢倉さんがいい感じにJxckさんに質問を挟んで場をほぐしてくれました。
真剣さと和やかさが7:3くらいで、とてもちょうどよかったと思います。

飲み会

最終日の後にみんなで飲み会に行きました。普段の収録の裏話や、他のpodcastの話、各技術コミュニティの話、URLはどうなっていくか、キャリアの悩み、ビザの話、好きな言語の話などをしました。

感想

そもそも僕は基本的なことをだいたい知っていると思っていました。
しかし、今回これに参加したことで、自分が如何にWebの基本をわかっていなかったか思い知らされました。
僕ぐらいのレベル感の人が、まさかHTTPのスタートライン(GET / HTTP/1.1)やFormタグやCookieに知らないことが詰まっているなんて思いもよらないです。
僕は、特定の1パターンを知っているだけで、それ以外のところを全く知りませんでした。
結構クリティカルな思い違いに気づいたこともありました。
ある意味とても不安になりました。
しかし、仕様の読み解き方をみっちり教わったので、これからは自分で乗り越えていけるだろうという感触も得ました。

素晴らしさ

まず、4日間という長さです。体力的にも結構きついし、それだけのスケジュールを抑えるのは普通の人には大変なことだと思います。(幸か不幸か僕はスケジュールを抑えるのに何の苦労もありませんでした)。しかし、Webを使って何かの機能を正確に作ろうと思ったら、Cache-ControlとCookieとSame-Origin Policyの正確な知識は確実に必要です。そしてそれを学ぶにはどうしても4日間は必要だった思います。そこを頑張り抜いた価値は合ったと思います。

次に、必ず仕様を元に説明される点が良かったです。自分が教える側だと、「こうしたら動くから」と説明してしまうことが多いです。しかし、それでは他の選択肢を知る機会がなく、欲しい機能が仕様に含まれているにもかかわらず自分で再実装してしまったりするでしょう。逆に、「〇〇ヘッダーを入れておけば安全」と思いこんでしまって深刻な脆弱性を作ってしまうことにつながります。必ず、まず仕様を読むということが大事なんだと学びました。

更に、他の参加者さんがいたことが大きかったと思います。というのも、一人で話を聞いていると、本当はわかってなくてもわかった気になってしまうことが多いからです。そんな時に、他の参加者さんが言語化しにくい疑問点を言葉にして聞いてくれるので「そうだ、僕はそこがわかってなかった」と気づくことがあります。

最後に、あまり本題の邪魔はしない範囲で、こういう機会に普段から気になっていたことを聞けるのがいいと思います。
僕は、「普通の中小企業は自前でPassword管理をせずOAuthやFirebaseを使ったほうがいいと思うがどう考えるべきか」「ブラウザにはDigest認証やクライアント証明書などのセキュアそうな認証機能があるが、いまいち流行ってないのはなぜか」と言うようなことを聞けました。

次回参加者へ

次があるのかどうか全くわからないですが、もしあったときのためのアドバイスをしたいと思います。
まず、このための言語の勉強はほとんど不要だと思います。今回はRubyを使いましたが、最低限の文法(ifとか forとかpとか)がわかれば大丈夫です。
このためにHTTPについて予習する必要も特にないと思います。
一番大事なのは普段からたくさんWebアプリを作って引き出しを増やしておくことだと思います。

将来サーバーもフロントエンドも一つの言語で書くようになる。

要するに

f(RDB , ユーザーイベント) = UI

序文

僕がWebアプリケーション開発を始めてから6年くらいずっと思っていたことです。
いわゆるIsomorphicの話ではないです。サーバーもフロントエンドも同じ言語で書くと楽だよねというようなことではないです。

本質的な主張

プログラマアセンブラを書く時はレジスタの使い方を指示します。C言語を書くときはコンパイラが自動的に割り当ててくれます。一般的に、普通のプログラマーのスキルと比べれば、コンパイラに任せたほうがパフォーマンスが高いとされています。
同じように、サーバーサードプログラムとフロントエンドプログラムを分けず一つの言語で一つのアプリケーションを書き、機械的にサーバーとフロントの処理の分担を導出したほうが良いのではないかという主張です。

凄く極端な例

フロントエンドのソースコードの中に直接SQLが書かれていたらビビります。普通に考えてとんでもない脆弱性です。しかし、もしそのSQLが動的に実行されるのではなく、webpackの中でprepared statement化されて、実際のRPCはSQL通し番号と引数をやり取りしているだけであれば脆弱性ではないです。
このように、フロントエンドのコードを正にしてサーバーサイドの処理を生成することができます。
突き詰めれば、「最終的に画面に何を表示したいのか」を厳密に宣言出来るなら書式は何でもよく、そこから実装を導出できるはずです。

職業柄思うこと

そんなの無理だろor無意味だろと思うかもしれませんが、僕はこれを毎日頭の中でやっていて、大変つらいと思っているので、なんとか自動化してほしいというお気持ちはご理解いただきたいです。

自分はいつもDB〜サーバーサイド〜フロントエンドを一人で設計・開発・運用していることが多いです。
DB〜サーバーの間にキャッシュ用のKVSが挟まることが多いです。
また、サーバー〜フロントエンドの間にFirebaseを挟むことも多いです。

最初に画面に何を表示したいか考えます。次に、DBにどうやってデータが入っているか考えます。そこから、その間をつなぐ実装を逆算していきます。

考えることは

  • この画面に必要十分なデータはRDBのどの範囲か
    • ≒サーバー→フロントエンドの通信を最小化するには
    • IndexedDBに入れて使いまわすか
  • この画面を表示する度にRDBにクエリを発行して大丈夫か
    • ダメならどんなキャッシュを用意しなければいけないのか
    • そのキャッシュを更新する条件は何か
  • WebSocketなどを使って更新を通知する必要があるか

などです。

ここで僕は2つのことを思います。一つは、計画立案自体を自動化出来るのではないかということです。もう一つは、この計画を元にソースコードを生成できるはずではないかということです。

上の「考えていること」は関数の最適化と言えます。
関数の最適化とはこういうことを言っています。まず最も素朴な関数を書きます。この関数はパフォーマンスがとても悪いです。しかし簡単に書けます。この関数を書き損じることはほぼないと考えます。その関数の挙動を変えないように式変形していきます。それによってパフォーマンスを向上させていきます。これを関数の最適化と呼ぶことにします。
ここでは、まず画面をRDB全体を引数に画面を返す関数を想像し、それを通信量とクエリ効率の観点から脳内変形してパフォーマンスを向上させています。
僕は、こういうことこそコンピュータに任せるべきだと考えています。自動化により計画立案ミスを防ぐこともがきます。また、人間は複雑な最適化を嫌がりますが、コンピュータなら嫌がらずにパフォーマンスのいいキャッシュ戦略を立案してくれると思います。実際、僕もキャッシュの種類を増やすことをとても嫌がります。管理が煩雑になり、その後の開発効率を落とすからです。しかし、コンピュータに立案させておけば、ビジネスロジックを変更する度に再立案させればいいので、気が楽です。また、自分のプロダクトでは効率のいい差分更新ではなく効率の悪い洗い替えをしているところがたくさんあります。めんどくさいからです。コンピュータならちゃんとやってくれると思います。

次に、コードの自動生成です。
自分はサーバーサイドもフロントエンドも書き慣れているので、上記の計画が立ったらその後に書くコードを隅々まで想像できます。僕に出来るんだから他の人にはもっといいコードが想像できるはずです。そうであれば、その計画→コードの変換を定式化しておくことで、コードの自動生成ができると思います。

統一言語はどんなものか

まず、SQLDDLを書くということは無くなりません。
また、統一言語はVDOM的なテンプレートエンジンを持っています。そのテンプレートを書いた時点であるページの型が定まります。
統一言語はSQLのテーブルの型とページの型の2つを読み込みます。
そして、画面ごとにDB上の全データを該当ページのVDOMに変換する純粋関数を書きます。どんなにパフォーマンスが悪くてもいいので最も簡単でわかりやすい関数を書きます。
この言語で書いたWebアプリケーション定義をコンパイルすると、フロントエンドで実行されるJSとサーバーサイドで実行されるバイナリが出力されます。そのコードは、サーバー↔フロントエンドの通信の最適化やKVSを使ったクエリ結果のキャッシュなどが合理的に実装されています。

ユーザーイベントの扱いも重要です。
ユーザーイベントもReact的なものと同じ方針で扱います。
ユーザーイベントを値としてStoreに保存しておき、その値も上の関数の引数になります。実行時にその処理をサーバーサイドでやるのか、フロントエンドでやるのか、楽観的UIは必要か、などはコンパイラが考えてくれます。

よく表を絞り込みできるUIがあります。絞り込み条件をサーバーに送ってサーバーが絞り込みをしたほうがいいのか、該当しそうなデータを全部フロントエンドに送ってフロントエンドで絞り込みをしたほうがいいのか、悩むことが多いと思います。総合的にはサーバーサイドでやったほうがいいが、フロントエンドでやったほうがめんどくさくないからです。
でも、この仕組みなら、どちらを選んでもほぼ同じコストで実現できるので、常に合理的な方を選ぶことができます。

アクセス権限はどうするの?

あんまり考えていません。確かに、どこまでの処理をサーバー側でやってどこからフロントエンドでやるかをコンピュータに決めさせているので、予想外に秘密の情報をフロントエンドに送ってしまう可能性はあります。適当にアノテーションを入れたりすると思います。

RDBMSを作ろうと思う

Cloud Datastoreのラッパーを作りたい

自分は普段GAEのCloud DatastoreというKVSを使います。
KVSは多くの場合とても速いですが、RDBほどやりたいことを直接実現できない場合があります。
特に集計系の操作が苦手です。
例えば画面に「月ごとの売上」を表示するのは一筋縄では行かないです。

個別のケースを最適化することは可能です。
上記の例であれば「月ごとの売上テーブル(Datastore用語ではKind)」のようなものを作って、売上データを保存する時に同時に月ごとの売上テーブルに加算したりします。
しかし、これらの処理をいちいち作るのは大変であり、アルゴリズムの使い回しができず、コードの保守も大変です。
そこで、Cloud Datastoreのラッパーフレームワークを作りたいなと思っていました。
KVSでありがちな困りパターンとその解決戦略を全て実装しておき、各アプリケーションは戦略を選ぶだけで自動的にいい感じにしてくれるものを作りたいなと思っていました。

RDBを作ってみたい

普通に考えて無理では?

話は変わります。PostgreSQLMySQLのような立派なRDBを作ってみたいと思いました。理由は特にないですが、プログラマーに生まれたからにはCPUとOSと言語とRDBを作ってみたくなるものではないかと思います。「俺の考えた最強のRDB」を作りたいのです。
自分のRDBに入れたい超便利な機能のアイディアもあります。例えば僕はFirebaseが大好きなのでFirebaseと連携できる機能を作りたいです。
しかし、RDBというのはとてもむずかしいものだと思いました。
RDBチューリング賞を何人も出している一大学問ジャンルであります。データベースの理論に精通していなければいけないと思います。
また、実装面でも

に精通していないと既存のRDBより出来のいいものを作るのは不可能だと思っていました。

用途を限れば出来るかも

しかし、クラウドで使う前提のRDBを作るだけなら簡単なのではないかと思いました。
まず、特定のOSで動けばいいので、互換性のためのコードが要りません。
GCPAWSにはCloud DatastoreやDynamoDBといったシンプルなKVSが用意されています。
これにRDBの皮を被せればいいのではないかと思いました。
これらのKVSはすでに

といった必要な機能を全て備えています。
また、GCPAWSにはそれぞれGAEとLambdaといったサーバーレスアプリケーション実行環境を用意しています。プロセスが落ちた場合に備えたりラウンドロビンを実装したりする必要がありません。
後はKVS上に参照整合制約やドメイン制約(いわゆる型)を実装するだけだと思います。

作る!

以上の2つの話から「GAE上で動きCloud DatastoreにRDBの皮をかぶせるラッパーフレームワークを作ればいい」という結論に達しました。

〇〇ではダメなのか

クラウドRDBホスティングサービスではダメなのか

例えばGCPのCloud SQLAWSのRDSがあります。
これらは、プログラマがこれまでと同じようにPostgreSQLMySQLを使い続けたいと言ったから始まったマネージドサービスです。
そもそも、PostgreSQLMySQLができたときには、今のようなクラウドコンピューティングはありませんでした。ということはクラウドで動かすなら不要な機能はいっぱいあると思います。
想像ですが、これらのホスティングサービス内で動いているRDBは、オリジナルの実装とは全然違うはずです。PostgreSQLプロトコルを喋りPostgreSQLと同じ出力をする全然別の何かが動いているはずです。
ところが、自分はRDBの機能の殆どを使ったことがないです。SQLの文法もほとんど使ったことがないです。もちろん、プログラマとしてずっとRDBを使ってきました。それでも、全機能の内使ったことがある機能は5%くらいじゃないかなと思います。
これはとても非効率なことです。プログラマPostgreSQLがほしいと言ったから、クラウド事業者はPostgreSQLと全く同じ動きをする全然別のものを作り、しかしプログラマはその機能の内殆どを使っていないのです。だったら最初から既存のRDBとは全然違う、機能も互換性も絞ったものを使ってもいいじゃんという気持ちになりました。

既存のRDBプラグインじゃダメなのか

追加したい機能があるならプラグインを作ればいいというのはまともな発想です。
僕も最初はこれを作ろうと思いました。
PG-StromというGPUを使ってクエリをアクセラレートするPostgreSQLプラグインがあります。僕はこれを知っていたので、僕も真似しようと思いました。Firebase連携プラグインを作ろうと思ったのです。しかし、クラウドRDBマネージメントサービスが全盛の時代に、これを使ってくれる人がどれくらいいるか考えた結果断念しました。

SpannerやBigQueryではダメなのか

自分はFirebaseと連携するRDBがほしいので、公式が対応してくれない限り無理だと思います。

また、SpannerやBigQueryはいろんな分析をするためには優れていると思いますが、Webアプリケーションのバックグラウンドには向いていないのではないかと思います。
柔軟に対話的に分析をしたり、今後何に使うかわからないデータを大量に蓄積しておくにはいいサービスだと思います。しかし、多くのWebアプリはどのデータをどのように集計して使うかわかりきっている場合が多いです。事前にデータの並べ方を工夫しておけます。BigQueryのような「どんなクエリが来ても一瞬で集計してくれる」機能はオーバーキルであり、逆に課金の高さが際立ってしまいます。

作りたい機能

Firebase連携機能

先述の通り、Firebase連携機能がほしいです。
RDBのビューをFirebaseに反映できたらいいと思います。マテリアライズドビュー機能を持つDBがありますが、そのマテビューをFirebaseに書き込むようなものです。

よく、Firebaseを使ったリアルタイムなチャットのデモがあります。
でも、アレを作るのって結構大変です。
まず、セキュリティルールを正確に記述しなければいけません。
また、Firebaseは非正規化して使うのが普通なので更新系が苦手です。
新規書き込みをリアルタイムで反映させるのは簡単でも、書き込みユーザーがユーザー名を変更した時に画面上のユーザー名をリアルタイムで更新するのは諦めたりします。

RDBはこういう操作が得意です。
だから、RDBに対してCREATE VIEW文を宣言したら自動的にそのVIEWがFirebaseに反映され、DBが更新されたらリアクティブにFirebaseも差分更新されたらいいなと思いました。
この機能を作りたいです。

新しいSQL

僕はRDBSQLの世界観が好きです。
ジョー・セルコの「プログラマのためのSQL」を愛読しています。
しかし、SQLの文法はあまり好きではありません。
今のSQLの文法ではSQLが本当にやりたかったことを満たしづらいと思います。だから新しい文法を作りたいです。
詳しくはこっちです。
https://blog.isyumi.net/entry/2018/08/01/140842

定理証明

最近既存のOSSの実装を定理証明することでバグを発見するような試みを聞くことが多いです。
先日話題になったHeartbleedもその一種です。
発見される側の気持ちはわからないですが、切り口として面白いと思います。
僕は最初からこのRDBの実装に定理証明を導入したいです。
RDBは平行処理とステートマシンであり、定理証明の得意分野の一つだと思います。
最初から定理証明があるということが、コントリビュータを増やすことに効いてくると思います。
もちろん、定理証明があるかないかはユーザーにとって関係ありません。自己満足のそしりを免れられないと思います。
しかし、そもそも僕が作りたいから作るのであって僕が定理証明を入れたいなら入れるのが正義なのです。異論は認めません。

管理ツール

MySQL Workbenchのような使いやすい管理ツールを作りたいです。これがキラーアプリになると思います。
僕は普段Webフロントエンド系の開発をすることが多いので、こういうのは得意です。

課題

KVS上に参照整合制約やユニーク制約やON DELETE CASCADEは簡単に作れると思もいます。

一番大変そうなのはVIEWです。特定のテーブルの更新に追従して別のテーブルに実体化させておいたVIEWやFirebaseを最安経路で更新するアルゴリズムを考えるのが大変だと思います。
例えば月ごとの売上VIEWであれば、

  • 初回起動時に全ての売上
  • 売上が立ったらその月の合計を増やす
  • 売上がキャンセルされたらその月の売上を減らす
  • 売上の日付が月をまたいで変更されたら
  • 元の月の売上を減らして変更後の月の売上を増やす
  • 個数が変更されたら単価*(変更後の個数-元の個数 )を加算

を導出する。
どうやったらいいのかさっぱりわからない。

じゃあいつやるの?

現行のスケジュールでは来年の5月頃に取り組めるかと思います。

インメモリアーキテクチャ

 

 

僕はサーバーサイドのビジネスロジックの書き方について、メモリに全データを載せておいたほうがいいのでは、と考えています。

理由

クエリを書いたりキャッシュを設計したりするのがめんどくさいからです。もちろん僕は、SQLを書いたりキャッシュを設計したりすることができます。おそらくどちらかといえば上手な方だと思います。しかし、自分がやらなければいけない仕事がたくさんある中で、あまりそれに時間を使いたくないということがあります。

とりわけ、最近はパフォーマンスのためにKVSを組み合わせることが多くなりました。キャッシュ目的のKVSを設計するのは大変です。

  • そのデータがどう使われるのか
  • どのタイミングで更新されうるのか
  • マイグレーションはどうするのか

を考えぬいて正確に設計しなければいけません。

パフォーマンスのために開発時間が余計にかかることはよくあります。素朴に実装していいなら30分で終わるけどパフォーマンスのために色々キャッシュを設計して書いたので2時間かかったりします。

それであれば、お金はかかるもののメモリをいっぱい積んでいるインスタンスを借りて、ビジネスロジックに必要なデータは全部インメモリに載せておくのが一番ラクなのではと思いました。

文脈

オンラインゲームではそれをやってるらしいです。
DBにはライトスルー方式で書き込んでいるそうです。
https://www.amazon.co.jp/dp/4774145807
これを見てWebアプリケーションもそうすればいいのではないかと思いました。

メリット

  • DBからデータを取ってくる部分のコードを減らせる
  • パフォーマンスのために複雑なコードを書かなくて良くなる
  • テストコードを簡単に書ける

デメリット

  • メモリがたくさん必要でお金がかかる
  • そもそも一定以上の大規模サービスでは不可能
  • 本番環境のインスタンスの立ち上げに時間がかかる

実現方法

複数インスタンスをどうやって同期するか

RDBレプリケーションプロトコルを喋れればいいかなと思います。
バイナリログをListenするのがもっと簡単かもしれません。
Firebaseという手もあると思います。

どんなコードになるのか

サーバーのAPIは純粋関数になります。
引数はHTTPリクエストとRDB上の全データです。
このデータはImmutableです。
戻り値はHTTPレスポンスとRDBへの更新命令です。
フレームワークはDBへの変更を監視し、常に手元の変数を更新します。
HTTPリクエストが来たら関数を適用し、戻り値を得ます。
まずはDBに対して更新を試みます。
成功したらHTTPレスポンスをブラウザに返します。

パフォーマンスは大丈夫なのか

メモリ量については、大半のWebアプリにとっては大丈夫だと思います。
大半のWebアプリは数十GB以内で済むのではないかと思います。

CPUもそこそこ大丈夫ではないかと思います。
Go言語で100万件の中からFor文で条件に合う数件を抽出するコードは、だいたい数十msくらいだと思います。

将来はいろんな自動最適化が出来るようになると思っています。

更に

フロントエンドプログラミングとされているものも、サーバーに置けるかもしれません。
RDBのデータを全部インメモリで持っておくなら、フロントエンドのステートもサーバーのメモリに持っていてもいい気がします。
うまいこと楽観的UIを透過的に扱う方法があれば、これが一番ラクだと思います。
DOMベースでUIをサーバーから更新するのか、あるいはブラウザのCanvasに映像を流すのか、どちらも可能だと思います。後者は楽観的UIと相性が悪そうな気がなんとなくするのと、ドキュメントののセマンティクスが失われるので、あまり気分は良くないです。

いつからやるの?

僕は、趣味の開発を除けば、そういうのがベストプラクティスとして確立するまではやらないです。
でも、そのうちそういうフレームワークが出てきてこなれてくると思います。そうしたら始めます。