データベースとの混沢とした戦い

18,974 文字

My chaotic journey to find the right database
Databases are hard. While building T3 chat I've got through A LOT of them from Redis to SQL. It's been a wild ride, hype...

データベースって難しいですね。SQLデータベースを立ち上げてクエリを実行するという部分は簡単なんですが、急速に変化し、さらに急速に成長しているアプリケーションのために、強力で一貫性のあるデータモデルを構築するというのは別の話です。
T3 chatを構築する中で、私は狂気じみた経験をしました。学んだことや、もっと重要なことには、その過程で犯した間違いについて共有したいと思います。というのも、これは本当に大変な旅路だったからです。データベースに関して自分が経験した完全なるカオスについて共有する機会を持ちたいと思います。そうですね、実際に過去24時間の間に事実上3回もデータベースを切り替えました。最終的な着地点には満足していますが、そこに至るまでの過程は動画にする価値があるものでした。
早速本題に入りましょう。なぜなら、ローカル側からサーバー側まで、そして両者の間に構築した狂気のような同期レイヤーまで、データモデルに関して話すべきことがたくさんあるからです。
しかし、その前に私のチームへの支払いを確実にしなければなりません。今日のスポンサーからの短いメッセージをお届けし、それからすぐに本題に入ります。
ウェブアプリの構築は以前より簡単になりましたが、安全に構築するのはまた別の話です。認証の設定は以前より簡単になったかもしれませんが、それを正しく行い、維持し、必要なものすべてを統合するのは、これまで以上に面倒になっています。
異なる認証オプションを詳しく解説した1時間半の動画をご覧いただくこともできますし、あるいは、その動画の最後で私が推奨している、そして私自身がほぼすべてのプロダクトで使用している選択肢を選ぶこともできます。私が認証に使用しているのはClerkです。
彼らは認証をこれまでになく簡単にしてくれます。私はこれほど認証の設定を楽しんだことはありませんし、他のものを使用するたびに後悔することになります。彼らはすべてを処理します。最も包括的と自称する時、彼らは本気です。
例えば、開発用プレビュー環境へのサインインのような、通常は非常に面倒な作業も。Clerkを使用していない私たちの製品ではまだそれが機能していませんが、Clerkを使用していれば、何も設定する必要なく、すぐに動作します。
これはサーバーサイドやサインイン機能だけではありません。サインインコンポーネントを提供するのはもちろんですが、ユーザープロファイルやユーザーボタンコンポーネントは本当に過小評価されています。
ほとんどのウェブサイトの隅にあるあの素敵な小さなボタン、プロフィール写真が表示されていて、クリックすると詳細情報やサインアウトなどが表示されるアレ。作るのは楽しくありません。私を含め、多くの人がそれを何度も作ってきましたが、Clerkを使用する時は違います。彼らが提供してくれるんです。
ボタンをクリックすればすべての情報が得られ、最近ではアカウント切り替え機能も追加されました。ユーザーがアカウントを切り替えられるようにしたい場合?彼らがカバーしています。ユーザーが組織を立ち上げられるようにしたい?それもカバーしています。SOC2コンプライアンスのための奇妙なMFAやSSOの問題に対処する必要がある?もちろんカバーしています。
これらの用語がよくわからない場合は、彼らが確実にカバーしてくれます。価格について心配されるのもわかります。でも、おそらく心配する必要はないでしょう。10,000ユーザーまで無料です。しかも、一度サインインして消えていったユーザーもカウントされるわけではありません。サインアップから24時間以上経過してアプリに戻ってきたユーザーのみをカウントします。
そのため、全員が離れていってしまうようなバイラルな日があっても、大金を失うことを心配する必要はありません。Clerkは私が一度も使用を後悔したことのない唯一の認証プロバイダーです。あなたも後悔することはないでしょう。
これは、T3 chatでのあなたの体験の大部分を支えているファイルです。これは私のローカルDBであるdexiファイルです。
待ってください、それがフロントエンドにあるんですか?データベースって、バックエンドにあるべきではないんでしょうか?
まあ、そうですね。説明させてください。一般的に、アプリを構築する際、すべてをクライアント側に置くことはお勧めしません。実際、私たちはReactのようなツールを使用して、すべてをブラウザに置き、サーバーにほとんど仕事をさせないという方向に行き過ぎていると思います。
しかし、他のAIチャットアプリを使用していて感じたのは、彼らがサーバー側でも、そして面白いことにクライアント側でも多すぎる処理を行っており、その結果として悪い体験になっているということです。
例えば、chat.openai.comにアクセスして、今起こった大量のポップアップは無視するとして、どれかをクリックすると、読み込みに時間がかかります。クリックして動き回ると、しばらく待つ必要があり、切り替える時にスクロールが突然ランダムに上下に動くこともあります。
まあ、少なくとも何とか動作はしますね。新しいチャットを作成すると、すべてがあちこちに移動しますが、最も重要な詳細は常にネットワークタブにあります。
ネットワークタブを開いてサイドバーをスクロールすると、ローディングスピナーが表示されます。このローディングスピナーは、リストの下部に到達するたびに新しい会話を取得するため、解決までに最大7秒かかることがあります。これらの会話はどこかのサーバーに保存されているからです。
ここを見ると、これらの異なるアイテムのすべてのデータ、3ビットコンピューターソリューション、作成日時、更新日時、そして何にも使用されていない他の値がすべて表示されています。スクロールするたびに新しいセットを取得し、更新するたびに再度すべてが表示されるのを待つ必要があります。
私のお気に入りは、更新時にサイドバーを注意深く観察することです。最初の部分が表示され、次にSoraとExplorerが表示され、その後DALLEが中央に再表示され、そして会話が流れ込み始めます。サイドバーでは5〜6の異なる状態が発生します。
完璧だとは言いませんが、私たちのアプリはそれよりもはるかに優れています。その理由は、見るべきネットワークリクエストが存在しないからです。
最近チャットを全て削除したので、以前ほど多くはありませんが、たくさんあったとしても、クライアント側でリストを仮想化するだけです。これらのイベントは、あまりにも積極的になりすぎていて後で制限する必要がある一般的なPostHogイベントに過ぎません。
チャットを開くと、レート制限をチェックするためにイベントが発生し、送信できるかどうかを確認しますが、データはすでにそこにあります。これらのナビゲーションはURLが変更されたためにイベントを送信していますが、データの取得は行われていません。すべてが即座に反応します。なぜなら、すべてがクライアント側に存在するからです。
実際、私はコンソールでそれにアクセスできるようにしています。私たちのデータベースdexiをアクセス可能にしたので、threads.toArray()を実行できます。
すみません、threadsのスペルを間違えました。コードは難しいですね。OK、const threads = await…これで私のスレッドの配列が得られます。
ここには表示されているよりもずっと多くのスレッドがあることに気付くかもしれません。ここで複雑さが始まります。なぜなら、残念ながらスレッドはタイトルだけのものではないからです。削除した時に何が起こるのか?
悲しい現実は、このローカルなものと動作する同期レイヤーを作るには、実際の削除を行うことができないということです。後で詳しく説明しますが、今のところ、ここでstatus: deletedと表示されているように、何かを削除する時、その内容を消去して削除済みとマークする必要があります。実際に削除するのではなく、それを覚えておいてください。
ともかく、ここですべてのデータを見ることができます。アプリケーションのIndexedDB(これはブラウザでデータを持つための面白い標準です)に入って、ここに存在するすべてのもののキーと値を見ることもできます。
メッセージ、プロジェクト、検索、トークン、スレッド。これらはすべてT3 chatを動かすための基本的なプリミティブです。
サインアウトした時に何が起こるでしょうか?サインアウトしました。戻ってみると、ああ、今は少しのものしかありません。オンボーディング用のスレッドがあり、オンボーディングのメッセージがあります。これらは…ああ、これらはスレッドIDのバインディングで、基本的にはインデックスのようなものです。
ウェルカム、T3 chatとは何か、作成された時間、レスポンス「T3 chatは最高のAIチャットです」。このページにある内容がすべてそこにあるのがわかりますね。
では、サインインした時はどうでしょう?チャットを取り戻すにはどうすればよいでしょうか?
私のサインインは隠しますが、ネットワークタブは表示したままにしておきます。
OK、サインインしました。すぐに入れたのは、ここに同期イベントがあるからです。同期は、必要なデータを同期するために認証された状態でサーバーに行うリクエストです。それを取得すると、そのデータすべてが返されます。
少しではありません。90キロバイトです。一部のユーザーにとっては、はるかに多くなるでしょう。しかし、これは私たちが実質的なバックアップとして保存しているデータで、あなたの状態を追跡するために使用します。
しかし、それを取得するのはこの時点だけです。更新時に再度取得しますが、今や全世界を手に入れたと言えます。あなたの全履歴があなたのローカル環境に組み込まれ、何かをクリックする時にサーバーを待つ必要はありません。
ブラウザのウェブ側をオフにすることもできます。ネットワークを完全にオフにしても、完全にナビゲートすることができます。これは本当に素晴らしいと思います。ローカルデータを使用し、ナビゲートや検索のためにネットワークの往復を必要としないツールを作れることは、本当にクールだと思います。
hell、このモデルのおかげで検索も完全にオフラインで動作します。
では、私が話していたデータベースの変更はどこにあるのでしょうか?excalidrawに入る時が来ました。データベースは2つの側面で変更されています。なぜなら、実質的に2つのデータベースを持つ必要があるからです。
ローカルソリューション(ブラウザ内であなたのデータを追跡するもの)と、サーバーソリューション(実際にそのデータを同期してバックアップし、サインインとサインアウト時に復元できるようにするもの)が必要です。複数のデバイスからのアクセスなど、人々が体験に期待するすべてのことができるようにするためです。
信じてください、今までにないほど、サーバー側が存在しないふりをしたいと思っています。サーバー寄りのReact開発者として、このアプリで地獄を見てきました。
まずはローカル側から話しましょう。これが私たちのアプリのキーとなるため、この部分を確実に成功させたかったからです。ほとんどのアプリはすべてのデータをローカルに持つ必要はありませんが、バックグラウンドで開いたままにし、検索し、チェックし、多くの時間を費やすアプリは、それから大きな恩恵を受けます。私はAIチャットアプリでかなりの時間を費やしていたので、ローカルなものを作りたいと思いました。ChatGPTやClaudeの体験の奇妙なカオスに疲れていたからです。それらは粗いですからね。
では、ここで何をしたのでしょうか?最初の探索は実際に両方を解決しようとしました。Zeroを両方の下に置いています。なぜならこれらは結びついていたからです。ZeroはReplicacheと同じ人々によって作られています。
Replicacheは、ある意味で業界標準と呼べるでしょう。バックエンドとフロントエンドで全く同じデータを持つための、長年存在してきたリアルタイム同期エンジンです。データを取得するためにネットワークを待つ必要はありません。一度データを持っていれば、操作を行うことができ、変更を同期し、他の場所で起こった変更を同期ダウンロードします。
また、これらのローカルファーストツールを見ると、リアルタイムと協調作業が言及される傾向があることに気付くでしょう。ローカルファーストは実際にはローカルファーストだけを意味することはほとんどありません。
ローカルファーストと呼ばれるものは、多くの場合、リアルタイムと協調作業を暗示しています。これらは異なる別個のものであり、常にグループ化しないでほしいと思います。それが一貫した問題となっているからです。
3つの仕様があることを注記しておきましょう。多くのツールがこれらを目指しています:

ローカルファースト
リアルタイム同期
協調作業

最初の探索はZeroでした。Zeroは、PostgreSQLサーバーとのローカル同期を簡素化することを目的とした、Replicacheの新製品です。彼らはウェブ向けの汎用同期エンジンを構築しています。
ZeroをDBの前に置き、ウェブサービスとUIのメインスレッドに至るまでバックエンドを配布します。組み込みDBのように見えるクライアントサイドAPIを取得しますが、サーバーを含むデータベース全体にまたがるハイブリッドクエリを任意に実行できます。
Reactクライアント側で、zero.query.playlist()と書き、彼らのクエリラッパー、関連トラック、関連アルバム、関連アーティストを使用し、再生回数で並べ替え、IDがIDである場合というように書きます。これは、すでに同期されているローカルデータを使用してこのクエリを実行します。
今やトラックのような情報を得るために常に待つ必要はありません。とても期待が持てるように見えます。Zeroが人々が使用する解決策の1つになる可能性は十分にあると今でも思っています。
しかし、問題もあります。私が試してみて遭遇した大きな問題は、メンタルモデルとして、データを非常に厳密に分割する必要があるということでした。
これは彼らの例です。ここにメインアプリのソースがあり、z.query.user、z.query.mediumなどを使用しています。このコードは本当に良く見え、シンプルで理にかなっています。
更新も素晴らしいです。z.mutate.message.insert()で、ランダムなメッセージ、ユーザー、メディアを入れることができます。ここに他のもの、例えば間違った型の配列を入れると、「それは間違っている、IDと名前とパートナーを持つ正しい型のものを入れる必要がある」という型エラーを出すほど賢いものです。
57を与えると、それも間違いです。ブーリアンである必要があるからです。それは全て理にかなっていて、クエリレイヤー、ミューテーションレイヤーからフルスタックの型安全性を見ることができるのは素晴らしいことです。
しかし、それをどのように実現したかを見てみましょう。drizzleのような何かに似た、定義したスキーマがここにあります。問題は、これが定義する唯一の場所ではないということです。
Dockerに移動すると、SQLで同じ形をデータベースでも定義する必要があります。これは生成されたものではなく、手書きです。これはそれで遊ぶためのデフォルト値を持つシードですね。
スキーマでは、物事が持つ権限と持たない権限を決定する権限モデルを記述する必要があります。それは単純ではありません。最悪ではありませんが、これらすべてを自分で定義する必要があります。
あなたのもの間のすべての関係を。SQLで移行を行い、それがここのコードと完全に一致することを確認するのは大変です。ユーザーが正しいバージョンにいることを確認し、これらすべてがうまく組み合わさることを確認するのは大変です。
実際、Zeroのバージョンをアップグレードする時、アップグレードについてのセクションがここにあります。デプロイしようとした時、ダウンタイムなしの更新方法がないことが指摘されました。
Zeroデプロイメントを更新すると、サービスが稼働していないためにエラーに遭遇する可能性のあるユーザーがいる可能性があり、それは最悪でした。強制的なダウンタイムなしにアップグレードや変更を行う方法はありませんでした。
良くなってきているのは嬉しいですが、私の最大の懸念はこれらのことではありません。それがとてもアルファ版で、まだ未熟だということでさえありません。
私の最大の懸念は、オープンソースではないということです。GitHubに行くことはできますが、GitHubはhello-zeroテンプレートだけです。Zeroの部分は実際にはオープンソースではありません。
LinearのクローンやExcalidrawのクローンを、ReplicacheとZeroを使用して構築した例がありますが、このリポジトリもクローズドソースです。それは単なるREADMEファイルです。
使用している技術に問題があった場合、それを修正できることを確実にしたかったのです。なぜなら、修正できない技術で地獄を見てきたからです。それは本当に苛立たしいものでした。
今では使用しているものの問題のほとんどを修正できるほど深く理解していますが(有名な最後の言葉ですが)、これらのことが私をRocky Corpから遠ざけた理由です。
Zeroは本当に新しく、とても初期の段階に感じられ、問題に遭遇した場合、彼らが修正するのを待つしかないと感じられたからです。それらの力学のどれも好きではありませんでした。
Zeroについて間違っていたようですね。これは彼らのモノレポの一部なのでしょうか?どれくらいがここにあるのかわかりません。彼らのコアな同期レイヤーの多くがオープンソースではないことは確かです。
しかし、これは私が思っていたよりもオープンです。良い発見です。Zeroにオープンソースの部分があることを知らなかったことは私の誤解でした。その情報をもう少しアクセスしやすくしなかったことについては、彼らにより多くの責任があると思います。
これはApacheライセンスですね。彼らはそれを知らなかったのでしょうか?教訓を得ました。どれだけのものが実際にここにあるのか、より詳しく調査する必要があります。
dear-client、zerach、zql…ああ、これはReplicacheを基盤に使用しているんですね。Replicacheクライアントライブラリで。実際にオープンソースなんです。この部分がオープンソースだとは知りませんでした。
実際、これを使用している人々から、この部分はオープンソースではないと言われていたので、私だけが誤解していたわけではありません。このリンクを見つけてくれたチャットに感謝します。
できる限り反対の方向に進むものが欲しかったのです。これらのことが当てはまらないものが欲しかったのです。オープンソースで、初期段階からはほど遠く、正しい形に落ち着くまでデータモデルを少し操作できるものが欲しかったのです。
しばらく探し回って、興味深い発見をしました。Dexi – IndexedDBのためのミニマリストなラッパー。なんということでしょう、このライブラリは私の人生をずっと楽にしてくれました。
DexiはIndexedDBのために作られた、比較的古い、ミニマリストなライブラリです。IndexedDBは直接扱いたくないものです。IndexedDB APIがどれほど厳しいものかを説明するのに2つの動画を使うこともできますが、ここでは私を信じてください。それは悪いものだからです。
DexiはIndexedDBがひどかったため、2012年か2013年ころから存在しています。これらのドキュメントには、Internet Explorer 8のサポートのために存在する特定の機能退行や機能について語るページがあります。
これは古いライブラリですが、今でもかなり良くメンテナンスされており、VueやSvelte用の最新のバインディングも提供しています。もちろん、私たちが気にしているのはReactです。
Dexiは比較的シンプルです。モデルがどれほど柔軟かを理解するのは難しいかもしれませんが、一度慣れると理解できます。重要なのは、IndexedDBはリレーショナルデータモデルではないということです。
SQLのようなものではまったくありません。単なるKVストアです。IndexedDBは、より多くの値とテーブルの漠然とした概念を持つことができるKVに過ぎません。
Dexiは、データがどのような形であるべきかを定義し、異なる値で検索できるように異なるインデックスを定義できるようにすることで、はるかに柔軟にします。
不必要に複雑な世界のKVであるIndexedDBに固有の多くの問題を解決します。
ここに簡単なDexiの例があります。新しいDBを作成し、バージョン1を定義します。これはバージョンを上げる方法でもあり、後でそれについて触れるかもしれません。
そして、ID、名前、年齢を持つfriendsを保存します。注目すべきは、これがデータが持たなければならない形ではないということです。KVペアの値の一部として、ここにないものを渡すことができます。
これは、より速く検索できるようにインデックスを付けるものだけです。しかし、テーブルが定義されると、そのストアには何でも入れることができます。
ここでは、friendというTypeScriptがあり、ID、名前、年齢を持っています。DBにはfriendsがあり、これはIDを主キーとするエンティティテーブルです。そしてストアfriendsはID、名前、年齢を持っています。
これで、これらすべてにアクセスできるようになりました。素晴らしい、素晴らしい進歩を遂げています。
しかし、本当に素晴らしくなるのは、実際にアプリで使用し始めてからです。データをクエリしたいコンポーネントがある場合は、liveQueryを使用します。ここで魔法が始まります。
someData()を呼び出すと、配列内のすべてのfriendsが表示されます。最初はnullです。すべて非同期だからです。しかし、friendが変更されると、このコンポーネントが更新され、再実行されます。
面白い事実として、T3 chatの更新、つまりチャットが新しいトークンを取得し、新しいコンテンツがストリーミングされる時、私は文字通りメッセージテーブルからメッセージフィールドにコンテンツを追加しているだけです。それによってコンポーネントのレンダリングが強制されます。
カスタムレンダリングループを書く必要はありませんでした。周りのすべてをメモ化するために苦労しましたが、このモデルでは、変更があった時に即座に更新できます。
しかし、選択するものには注意が必要です。ツリーの高い位置ですべてのメッセージを選択したり、メッセージに触れたりすると、ストリーミングされる各トークンがアプリ全体をレンダリングすることになるからです。
そのため、メモ化やreact-virtualを使用して、不要なレンダリングが発生しないようにするのに多くの時間を費やす必要がありました。その結果、本当にうまく動作しています。
しかし、Dexiは素晴らしく、多くの問題を解決してくれました。しかし、Dexiもいくつかの問題を引き起こしました。
前に言ったオープンソースのものが欲しいということを覚えていますか?Dexiについての私の唯一の不満は、クラウド製品を持っているものの、それがクローズドソースのクラウド製品だということです。
奇妙な支払いモデルを持つクラウド製品です。無料版では最大3ユーザーまでで、100MBのストレージを使用できます。本番版は1ユーザーあたり月額12セントで、私たちの300,000人のDiscordユーザーがいれば破産するでしょう。
しかし、オンプレミスオプションも提供しています。自分のサーバーでホストするために3,500ドルを支払うか、ソースコードを手に入れるために8,000ドルを支払うことができます。ただ、奇妙です。
問題は、これらのオプションのどれも、私たちが展開しようとしているスケールでのパフォーマンス特性について良い考えを与えてくれないということです。
また、ライブ同期、更新、そして最も重要なWebSocketに関する特定の事柄に非常に焦点を当てています。これはよく出てくる話題です。
そこで、データを保存する場所が必要になった時、まだDexiクラウドについて確信が持てなかったので、興味深い場所で問題を解決しました。
ローカルレイヤーにDexiを選んだ時、そのデータを置く場所が必要でした。そこで選んだのが面白い場所でした。これをRedis V1と呼びましょう。V0と呼ぶのが適切かもしれません。
T3 chatの同期が元々どのように機能していたか、皆さんは私をからかうべきかもしれませんが、同時に、予想よりもはるかに長く、はるかにうまく機能しました。
同期の元々の仕組みを説明するために、実際にGitHubリポジトリを開いて探してみましょう。それは本当に面白いものです。
同期の仕組みは、すべてのメッセージとスレッドを取り、superJSONで処理し、mySyncJsonToDB関数を呼び出すというものでした。
その処理方法は、テキストエンコーダーを作成し、そのJSONの文字列をgzipで圧縮し、同期エンドポイントにポストするというものでした。
あなたの同期、ストア、バックアップは、データベースの内容全体をsuperJSON(日付と時刻を失わないように)で処理し、gzipで圧縮し、キーがユーザー名またはユーザーIDで、値が圧縮されたblobであるKVにアップロードするというものでした。
同期するために、反対のことを行いました。ローカルにあるコンテンツよりも新しい場合は更新し、ローカルに存在しない場合は追加するというbulk putを行いました。
これは驚くほどうまく機能しました。このモデルがこれほど長く機能し続けたことに驚きました。これは4日前か2日前まで使用していたモデルでした。
実際、素晴らしく機能しました。テストから、多くのスレッドとメッセージをテストするためのユーティリティがありました。各スレッドに2つのメッセージがある1000のスレッドを持っていましたが、gzipで圧縮すると、そのblobのサイズは200キロバイト程度でした。
しばらくの間、これが道筋として合理的に思えました。しかし、人々は10万語のメッセージを投稿し始めました。彼らは単に何かのドキュメントをコピーしてメッセージとして貼り付け、送信するだけでした。
そして、3〜4つのメッセージで800キロバイトに近づいていました。gzip圧縮後でも2〜3メガバイトに達するユーザーもいました。
それはすべてを破壊していました。実際にかなり早く破壊していました。そして、それを分割して処理するための最善の努力にもかかわらず、これはもう実行可能ではないと認識し、Redis V1に移行する時が来たと判断しました。
実際には、まだそこまで行きませんでした。なぜなら、この恐ろしい穴から抜け出す方法を考えるのに多くの時間を費やしたからです。
Dexiクラウドについてさらに調べ、Dexiチームともっと話をしました。他のローカルファーストなものについてもさらに探索しました。
最初に見つけたのは、Tiny Baseという名前のものでした。実は、これはExpoのドキュメントから見つけました。これは本当にクールに見えました。
オープンソースで、リアクティブなデータベースのようなもので、同期もあり、ローカルファーストウェブ向けに構築されています。OK、素晴らしい、今のところ私のすべてのチェックボックスにチェックが付いています。
多くの異なるものをサポートしているのも本当にクールです。Tiny Baseが次の一手になるように見えましたが、さらに掘り下げ続けました。
そして最終的に見つけたのは、「WebSocket」という用語の一貫した使用でした。期待は明確でした。Tiny Baseの同期レイヤーには2つのオプションがあります。
オプション1はWebSocketを通じて行う、オプション2は自分で行う。これらのオプションには満足できませんでした。
以前に言及し忘れたものが1つありました。これは実際に、Dexiに完全にコミットする前に探索したものです。実際、リリース前に、信頼できる数人の人々がそれを推奨したため、T3 chatの全体をJazzで再構築しようと、ローンチ前夜に最善を尽くしました。
Jazzには多くのクールな機能があります。ここにJazzを使用して構築されたチャットアプリがあります。iframeハッシュルーター(恐ろしい)、チャット作成、チャットの読み込みなどがあります。
これらのすべてのグループとチャットの呼び出しがスキーマグローバルから来ているのがわかります。そして、正しいページにいる場合はチャット画面をレンダリングします。
では、チャット画面を見てみましょう。useCoState、chats[chatId]を使用し、ここにチャットがあります。素晴らしい、素晴らしい。
では、これの問題は何でしょうか?なぜこれを選ばなかったのでしょうか?coモデルがとても苦痛です。期待されているのは、通常はユーザーの識別子オブジェクトである1つのグローバルオブジェクトを持ち、それが他のすべてのための子を持つということです。
これは、サインアウト状態があるため、私たちには全く機能しません。実際、Jazzはサインアウト体験にまったく準備ができていませんでした。
チームと話をしました。Jazzチームは明らかに、Jazzを素晴らしいソリューションにすることに、おそらく多すぎるほど気を配っています。短時間でのオープンソース開発者との作業の中で、最高の経験の1つでした。
しかし、私たちには準備ができていませんでした。サインアウト体験のために、かすかにでも幸せなパスを持っていませんでした。そして、それをハックで解決しようとするのではなく(一生懸命試みましたが)、私は怒りをもってDexiに戻ることにしました。
そこで、ZeroからDexiへ、Jazzのセットアップに失敗してDexiに戻り、Tiny Baseを探索して、WebSocketsを扱いたくないと気付き、その同期エンジンを自分で構築する必要があること、そしてその努力はDexiでの作業を修正するのと同じくらい大きくなるだろうということに気付きました。
しかし、まだ探索は終わっていませんでした。Legend Stateは本当にクールな状態管理ソリューションです。これはReact Nativeの世界からより多く来ています。
LegendのクリエイターであるJayは、React Native用のリストコンポーネントで、状態管理のものと同じくらい有名です。これは本当に良い、本当に速いライブラリです。
Reactに非常に焦点を当てているので、あなたの目標が非常にReactっぽく、React向けに構築されたものであれば、これはそのためにあります。
状態をObservableとして作成し、React外部で設定でき、React内部でuse$を使用して呼び出すことができます。
ここには荒い部分がいくつかありました。特に、React Compilerの準備を整えるためにV3で最近行われた変更により、直感的ではない構文の変更が発生し、ドキュメントがそれを適切に示していなかったためです。
しかし、Jayも素晴らしい人物で、これらすべてについて多くの洞察を与えてくれました。Legendでプロトタイプを作成することができ、私たちの結論は、おそらく自分で作り上げた方が良いということでした。
なぜなら、ここでの同期側は、彼らのKeelとSupabaseプラグインで非常に手厚いか、あるいは非常に荒く、CRUDビルドで作業するのを待っているかのどちらかだったからです。
CRUDプラグインを読み、彼と話し、これを理解し続けるにつれて、いくつかのことが心に浮かび、いくつかの気付きがありました。
特に、避けようとしていたすべてのハック、ソフトデリートのようなものは、これらすべての同期エンジンにとって必要なものでした。
そして、すべてのオプションが同じ回避策を必要とすることに気付いた時、回避策の解決策を見つけることができなかったので、それらを回避するしかありませんでした。
Tiny Baseのクリエイターからも、Legendのクリエイターからも示唆されたように、私のニーズは十分にシンプルで、十分に特殊だったため、自分で構築した方が良いということでした。
ローカルのソフトデリートについて、直感的ではない部分について簡単に説明したいと思います。
2台のコンピュータがあり、両方ともT3 chatにサインインしているとします。複数のスレッドがあり、スレッド1、スレッド2、スレッド3と呼びましょう。これらのスレッドは両方のデバイスに存在します。
また、このデータを保存しているデータベースもどこかにあります。これで、これらのクライアントの1つが開くたびに、すべてのデータを取得し、同期もアップします。
変更が発生した時、例えばメッセージを送信した時、データ全体を同期します。ここでスレッド4を追加すると、送信時にここに追加されます。
これで、データベースにはスレッド4があり、最終的にこのクライアントを再度開いた時に、新しい更新をダウンロードし、スレッド4が追加されます。
スレッド3を削除した場合はどうなるでしょうか?スレッド3を削除すると、ここから削除されます。今、このクライアントがダウンロードを行います。OK、ここにまだないスレッドがないので、新しいデータをダウンロードする必要はありません。
しかし、同期アップする時、スレッド3が再び同期されて戻ってきます。そして、今、あなたが削除したはずのスレッドが戻ってきます。永久に削除したくなかったことを願います。楽しくありませんね。
これを修正する唯一の方法は、スレッドを実際に削除しないことです。スレッド3の代わりに、このインデックスに次のようなものを入れます。
これで、その同期が来た時、削除済みに置き換えられます。なぜなら、それがどのようなものかを示すIDが明らかにあるからです。
これがソフトデリートです。データベースから削除する代わりに、そのIDを取り、すべての値をnullにし、ステータスを削除済みにして、独自の部分的な状態を持つ他のものが、あなたが実際に意図していることは、そのものがなくなることだということを知ることができるようにします。
これを行う必要があります。ローカルファーストモデルを構築する場合、解決する必要のある他の小さなことがたくさんあります。楽しくありませんが、ご理解いただけると思います。
これらすべてを行いましたが、問題は、これらの各々を比較することが大変だったということです。特に、多くのものが入った大きなテーブルがある場合です。
10,000個のメッセージを持つ人はいませんが、1,000個以上のメッセージを持つ人はいます。1,000個のメッセージがある場合、ページを読み込むたびにそれらすべてをダウンロードする必要があります。実行可能ではありません。
また、私が常により多くのものを取得し続けることもできないということを意味します。これらの2〜3メガバイトのペイロードは十分に大きく、時間とともに請求額を積み上げていくため、T3 chatではすでに400ギガバイトのトラフィックが発生していました。
より粒度の高い方法が必要でした。全体をblobとして保存することはできません。それは最初からのハックでした。予想以上に長く続いたハックです。それを使って、とても早くリリースし、多くの変更を加えることができたことに感謝しています。
しかし、他のものを探索する時が来ました。すでに述べたように、Jazzを試して諦め、Tiny Baseを試しました。正直に言うと、Tiny Baseのコードベースを初期化することはありませんでした。ただドキュメントを掘り下げ、感心はしましたが、ここでは必要ないと判断しました。
最も近かったのはLegendでした。本当に近かったのですが、正直なところ、Legendから得られた最高の価値は、Jayと話をして、Legendのすべての部分がどのように組み合わさって機能するかを学び、自分でこれを正しく行う方法をより良く理解することでした。
最終的に落ち着いたのは、Dexiでしたが、今私がRedis実装のV1と呼んでいるものを使用してデータレイヤーを完全にオーバーホールしました。
Redis実装のV1は、かなり大きな変更でした。ユーザーごとに1つのblobを持つ代わりに、メッセージとスレッドそれぞれが独自のKVを持つRedis同期レイヤーを、メッセージとスレッドのテーブルと共に持つようにしました。
なんというPRでしょう。sync V4と呼びました。何回書き直したか分からなくなり、4という数字が十分大きく感じられたからです。
ここで、サーバー側で重要な変更を加えました。実際、その時のものを見てみましょう。
ここにあります。すべてnext.js APIルートを使用していて、GETはオフをチェックし、このgetAllThreadsForUserとgetAllMessagesForUserを呼び出し、結果をJSON化して返していました。
POSTはもう少し複雑でしたが、同じことです。投稿されたすべてのスレッドとメッセージを取得し、それぞれが大きすぎないことを確認してから、それらを同期することを約束していました。
実際の同期部分を見てみましょう。ここにあります。sync redis db。もちろん、新しいものを開きません。なぜそうするでしょうか。素晴らしい。
これで、メッセージ用とスレッド用に別々のキーがあり、syncMessagesはすべてのメッセージのためのKVマップを作成してキーを作り、値をマップし、このマップでMSETを呼び出して、あなたが送信したものでどの値も更新します。
これは最終的に機能しなくなり、分割する必要がありました。そのPRは見せる価値もありません。最後の変更は楽しいものでした。
また、キーがuserID:s:threadIDであることに注目してください。これは重要です。なぜなら、IDはすべてクライアント側で定義されているため、他の人のスレッドを上書きできないようにする必要があったからです。
例えば、私のT3 chatでURLが、分かりませんが、これがこのスレッドのIDだとします。IDが何かを知っていれば、それを上書きできることになります。
クレイジーな権限モデルを構築するか、なぜ賢いものを構築する必要があるのでしょうか?もっと巧妙な解決策を構築できます。
ここでの私のずっと巧妙な解決策は、キーの最初の部分をあなたのユーザーIDにすることでした。これで、私のものに対して何もできません。A+で素晴らしく機能します。
キー名前空間はこれを解決するための素晴らしい場所でした。チャットの皆さん、これを愚かだと呼ばないでくれてありがとうございます。
これは実際にかなりうまく機能しました。キーが確実にユーザー固有になるようにするためのキージェネレーター、すべてのメッセージを同期する関数、すべてのスレッドを同期する関数、そして反対方向で同じことを行うゲッターがありました。
クライアント側はほとんど変更されませんでした。実際、このPRでは全く変更されなかったと思います。それはすべてサーバー側でした。いいえ、gzipの圧縮を停止したので、クライアントは少し変更されました。
しかし、私が叩いていたエンドポイントは、あなたのデータのsuperJSON blobを取り、それをKVに保存するだけでした。
そしてここで、物事は崩壊し始めました。これはUpstashでの本番デプロイメントです。Upstashには非常に満足しています。素晴らしいプロバイダーで、私の人生をかなり楽にしてくれています。
数日前に興味深いことが起こったことに気付くでしょう。約1ギガバイトのデータを保存していたのが、急激に増加しました。データ転送も大幅に増加しました。
それについてのグラフィックはないと思いますが、T3 chatの最初の3週間で、1日約70ギガバイトの帯域幅を使用していました。この変更後、さらに40ギガバイトを使用しました。良くありません。
特に、チャンキングとページネーションを処理した後、パフォーマンスはかなり遅くなり始めました。一般的に、インメモリストアとしてのRedisは、保存しているデータ量が小さく、保存している全体的なキー空間が小さい場合に最も強力です。
キーの数を見ると、最も恐ろしい数字が見えます。チャット同期を持つサインイン済みユーザーごとに1つのキーから、スレッドとメッセージごとに1つのキーに変更し、数時間のうちに50,000のキーから100万近くのキーに増加しました。
ありがたいことに、まだ機能しました。私も驚きました。遭遇した問題は主に、更新とフェッチ部分のチャンキングで修正可能なものでした。
しかし、一度に数千のキーを選択する必要がある場合、Redisは正しい選択ではありません。正直に言いますと、はい、これは知っているべきでした。知らなかったわけではありません。
ただ、どのようなパフォーマンスになるのか、どのように見えるのかを確認したかったのです。最初のパスを行った時でさえ、巨大なテストデータでもかなりうまくいきました。
これを知らなかったのですが、T3 chatの初期に構築した素晴らしい機能の1つは、メッセージ履歴のインポートとエクスポートです。巨大な履歴を持つユーザーの中には、テスト用にそれらをエクスポートして送ってくれる親切な人々がいました。
そのため、実際の世界の巨大な履歴でテストすることができ、一部のユーザーが持っていたメッセージの数の多さに恐れを感じたため、Redisでこのモデルを維持することは上手くいかないことをすぐに認識しました。
では、これをどのように修正すればよいでしょうか?
実質的に、Dexiは問題ないと結論付けました。また、クライアント側のモデルは現時点で実際にかなりうまく機能していると結論付けました。
問題は、Redisが一度に100万のものを取得するために構築されていないということです。それは少数のものを本当に速く取得するために構築されています。
大量のデータを一度に処理するために構築されたものがあります。過去に多く使用し、懐かしく思っていたもの。Planet Scaleです。
Planet Scaleは以前、このチャンネルのスポンサーでした。もう長い間、彼らから支払いを受けていません。技術的には少しの株式を持っていますが、どれくらいかは確認していません。
彼らの出口戦略も分かりませんので、そこから何かを得ることはないでしょう。しかし、それに応じてバイアスを考慮してください。
Planet Scaleは非常にうまくスケールします。なぜなら、Vitessを使用して構築されているからです。これは、絶対的なシャーディングによってMySQLデータベースをスケールする方法です。
彼らの技術であるVitessは多くのものを動かしています。Planet Scale自体はBarstools を動かしていると思います。また、Intercomも動かしています。
しかし、オープンソースのVitess MySQLスケーリングソリューションは、Slackを部分的に動かし、GitHubを部分的に動かしています。元々はYouTubeを動かすために構築されました。本当に良い技術です。
それを起動した途端に人生が良くなったので、そのことを知っています。今では途方もないトラフィックを処理していますが、1秒あたりのクエリ数は比較的少ないものの、1秒あたりに選択される行数は少し狂っています。
人々がデータを取得しているため、そして取得しているデータ量は途方もないものです。特に、それらの大規模なユーザーの一部は、ページ読み込み時に20,000行を取得しています。とんでもないことです。
このデータのために移行した時、パフォーマンス特性がどうなるか確信が持てませんでした。非常に読み取りが多く、書き込みはそれほど多くないからです。
しかし、私たちのために素晴らしくスケールしてくれました。デプロイ以来、行の読み取りは最小で1秒あたり9行から、最大で200行です。
これは、なんと、Planet Scaleの月額30ドルの基本プランですべて処理されています。まったくアップグレードしていません。これをすべて問題なく処理しています。
私たちのI/Oは全く制限に達していません。このクラスターが設定されているものの範囲内で完全に収まっています。
ここでCPU制限を表示しているでしょうか?あなたのP95と99が悪化し始めれば分かるでしょうが、RP50は7ミリ秒、RP95は50ミリ秒未満です。素晴らしいです。
ここをクリックすると、私がデプロイした時と、移行のストーリーがどのようになるかを理解するために早期にテストしていた狂気的なものを見ることができます。
私たちが本番環境に移行した瞬間が分かり、それ以来一貫して大量のトラフィックを受け続けていますが、問題ありません。あまり気にしていません。
再び使用できて本当に良かったです。しかし、これは恐ろしい移行だったに違いありません。KVからSQLへの移行は。

コメント

タイトルとURLをコピーしました