オブジェクトストレージがあればすべて事足りる
Object storage is all you need (tigrisdata.com)
要約
この記事では、データベースエンジンの代わりにオブジェクトストレージを直接利用し、必要なデータベース機能(一意性制約、トランザクション、インデックス、履歴テーブル)を自前で実装する方法について解説しています。これにより、データベース層の管理(マイグレーション、接続、スキーマ)を排除し、プロトコルバッファとオブジェクトストレージの機能(強整合性、条件付き書き込み)を組み合わせて、データベースのような振る舞いを実現するアプローチが紹介されています。
全文翻訳
オブジェクトストレージがあればすべて事足りる
前回のAmpbaseブログでは、私たちが使用しないデータベースエンジンのすべてについて語り、実際に何を使用しているかを説明するフォローアップを約束しました。私たちはリレーショナルデータベースを使用しません。Tigrisをストレージレイヤーとして直接使用し、それが提供する2つのプリミティブの上に、実際に必要な少数のデータベースの振る舞いを実装しています。ええ、ええ、わかっていますよ。「データベースは必要なかった」というキャッチーなタイトルは、通常、8ヶ月後に「どうやって尻尾を巻いてPostgresに移行したか」という次の投稿につながるものです。
実際には、データベースエンジンに手を伸ばすとき、あなたは4つの基本的な機能、つまり一意性制約、トランザクション、インデックス、履歴テーブルを求めています。Tigrisのグローバルオブジェクトストレージをデータベースとして使用するために、私たちはこれらのプリミティブすべてを自前で実装する必要がありました。今日はそのカーテンをめくり、それらのプリミティブがどのように機能するかを説明し、あなたが選択したデータベースエンジンに実際に何が含まれているかを理解できるようにします。
実際に保存されているもの
すべては、2層のバケットに格納されます。
グローバルなdirectoryバケットは組織のリストを保持し、各組織には独自のバケットが与えられます。それらのキーのうち4つは、通常データベースが行うジョブを実行するため、ここでラベル付けされ、次のセクションで詳しく説明します。
FIG 01 2つのレイヤーと、各キーが実装するプリミティブ
┌──────────────────────────────────────────────────────┐ │ directory bucket one, global │ │ │ │ orgs/{org_id}/ │ │ metadata.json │ │ members/{sha256(email)}.json │ the index │ billing.json │ the compare-and-swap target │ channels/{channel_id}/metadata.json │ │ api-tokens/{token_id}.json │ │ events/audit/{event_ulid}.pb │ the audit log │ org-ops/queue.pb │ └──────────────────────────────────────────────────────┘provider credentials reach this one, and only this one ┌──────────────────────────────────────────────────────┐ │ org bucket one per customer │ │ │ │ channel-slugs/{slug}.json │ the unique constraint │ channel-{channel_id}/ │ │ config-meta/{config_id}.json │ │ config-versions/{version_ulid}.json │ the history table │ bundle-meta/{bundle_id}.pb │ │ bundle-versions/{bundle_id}/{version_ulid}.pb │ │ active-config.pb │ a pointer, overwritten in place │ events/{event_ulid}.json │ └──────────────────────────────────────────────────────┘that customer's scoped keys reach this one, and nothing else
私たちは当初、すべてを各顧客のバケットにJSONオブジェクトとして書き込んでいました。しばらくして、protobuf optionsを使用してAPIにますます多くの機能を取り入れるようになり、スキーマ定義とともにバリデーションを定義するprotovalidateなどの機能を利用できるようになりました。JSONのマーシャリングとアンマーシャリングは、予想以上にコストがかかることが判明したため、Protocol Buffersを直接使用するように切り替えました。私たちのデータベースは両方のフォーマットを処理するため、レコードがprotobuf移行前に作成されたものであっても、すべて期待どおりにロードされます。
Protobufを使用することで、データベースレイヤーの管理(マイグレーション、接続、スキーマ)という問題全体が解消されます。唯一の欠点は、Protobufのフィールド名は永続的であることですが、正直なところ、Postgres、MySQL、SQLiteでカラム名を変更するのと同程度に苦痛です。
顧客ごとにバケットを作成する単純な方法は、すべて同じ$bigcloudアカウント内に顧客ごとのバケットを作成し、クォータ制限に達するたびに新しいアカウントを作成することです。あるいは、プレフィックスを持つ1つのバケットを使用してアカウントあたりのバケット制限を回避し、IAMポリシーの複雑さに依存して分離を強制することです。これらすべては退屈に聞こえ、Tigrisにはまさにこの形状のためのパートナー統合プログラムがあります。それを呼び出すだけで、その顧客のためのTigris組織、そのバケット、およびそれにスコープされたアクセスキーのセットが作成されます。私たちはプロバイダーIDを保持します。各顧客は、その下に強力な分離を持つ組織となります。
分離はインフラストラクチャレイヤーに組み込まれています。アプリケーションコードでorg_id = ?を忘れることはありません。なぜなら、ある顧客のデータに到達する認証情報は、他の誰かのデータにアクセスできないからです。過去にデータベースを管理するプラットフォームをいくつか構築した経験からすると、これはほとんどの人が間違いを犯す部分です。
分離を超えて、オブジェクトストレージが本当にデータベースを置き換えるためには、データベースのような振る舞いが必要です。しかし、オブジェクトストレージのシンプルさでデータベースのような振る舞いをどのように実現できるのでしょうか?強度の読み取り後書き込み整合性、条件付き書き込み、その他のプリミティブをすべての中核として活用します。
オブジェクトストレージのシンプルさでデータベースのような振る舞いを実現する
データベースの重要な保証はすべてオブジェクトストレージから得られます。信じられないですか?あなたのPostgresを冷たくて死んだ手から奪うと言いますか?続けてお読みください。
データベース(そしてオブジェクトストレージ)から必要なのはすべて次のとおりです。
- 強度の読み取り後書き込み整合性
- 条件付き書き込み
- 一意性制約
- トランザクション
- インデックス
- 履歴テーブル
私たちは、Tigrisの強度の読み取り後書き込み整合性と条件付き書き込みをそのまま利用しましたが、他の4つは自前で実装しました。
強度の読み取り後書き込み整合性
誰もがデータベースで強度の読み取り後書き込み整合性を期待しています。オブジェクトストレージは、約2020年12月まで強整合性モデルを持っていませんでした。オブジェクトストレージに強整合性がどのように追加されたかについて詳しく知りたい場合は、Werner Vogelsの素晴らしい解説が詳細に説明しています。
Tigrisが私たちに提供する主なことは、バケットデータがグローバルであるという事実であり、単にバケット名がグローバルであるだけではありません。これは、両方のクライアントが同じリージョンにある場合、データは強整合性を持つことを意味しますが、リージョンをまたぐと複雑になります。グローバルレプリケーションは結果整合性を意味します。つまり、システムが変更を同期している間、物事が時々不均衡になることがあります。
私たちにとっての中心的な問題は、どこで強整合性が本当に必要で、どこで結果整合性で十分かを判断することでした。
条件付き書き込み
条件付き書き込みは、基本的に比較・交換(compare-and-swap)です。
Tigrisは、書き込みに対するHTTPプリコンディションをサポートしています。If-None-Match: *はキーが存在しない場合にのみ書き込み、If-Match: {etag}はオブジェクトを読み取ってから変更されていない場合にのみ書き込みます。どちらも、バケットのロケーションタイプが提供する整合性モデル内で、オブジェクトの最新の状態に対して評価されます。整合性モデルの選択は、後でより重要になります。
しかし、ここで重要なのは、比較・交換が手に入れば、基本的にすべてのデータベースの基盤となるコアプリミティブが手に入ることです。
UNIQUEなしの一意性
データベースインデックスは、データを見つけやすくする2つの特性を持っていると考えてください。ルックアップの事前計算と、同じデータを二重に保存できないことの保証です。これはCREATE INDEXとCREATE UNIQUE INDEXの違いです。ほとんどの場合、ルックアップを効率化するために主キーやUUIDにインデックスを作成するのではなく、同じユーザーメールアドレスや一意の識別子を二重に保存できないようにするために作成します。
データベースインデックスの一意性プロパティを取得するために、私たちはコンテンツを認識するストレージと条件付き書き込みの組み合わせを活用しています。これにより、物事が二重に保存されないことを確認できます。
If-None-Match: *ヘッダーをPutObject呼び出しに渡すことで、チャネルやユーザーの作成を試みます。これにより、そのキーに既に何かが保存されている場合、Tigrisはデータの拒否を指示します。2つのアプリケーションインスタンスが同じ場所に異なるデータを書き込もうとすると、Tigrisがどちらが勝つかを決定し、敗者にはエラーを返します。これを処理してユーザーに報告します。
switch {case err == nil:
return nil
case isPreconditionFailed(err):
// Another writer created the slug concurrently. Re-read to determine
// whether this is idempotent (same channelID) or a conflicting mapping.
return s.handlePutConflict(ctx, slug, channelID, err)
}
残念ながら、これはクライアントになぜレースに負けたかを教えてくれません。実際には、別のアプリケーションインスタンスが先に書き込んだ、マルチリージョンバケット書き込みの部分的失敗が少しうまくいかなかった、またはリトライが暴走して他に何も表面化しなかった、という意味になる可能性があります。何が起こっているかを理解する唯一の方法は、データベースからデータを読み取ることです。これをしないと、ユーザーが「今作成したばかりのものを作成できない」という非常に混乱したシナリオになります。これは、分散システムでしか得られない問題です。口に出して言うと意味不明で、時間的相対性の構造がないため、きれいに表現するのが難しいような問題です。コンピュータは素晴らしいですよね?
トランザクションなしのミューテーション
請求状態は組織ごとに1つのJSONオブジェクトであり、複数のものがそれに書き込みます。StripeのWebhook、保持スイーパー、ライフサイクルメールワーカーです。それらのうち2つが同時にトリガーされることは稀ですが、完全に可能です。更新の喪失は、顧客のサブスクリプション状態が静かに間違っていることを意味します。これは顧客から見つかるバグの一種です。
トランザクションがない場合、楽観的並行性が発生します。オブジェクトとそのETagを読み取り、新しい状態を計算し、ETagが一致することを確認して書き戻し、一致しない場合は新しい読み取りから再試行します。
// Nothing read, so create only while the key is still absent; a version read,
// so replace only that version.
func precondition(etag string) (ifMatch, ifNoneMatch *string) {
switch etag {
case "":
return nil, aws.String("*")
default:
return aws.String(etag), nil
}
}
switch _, err := s.client.PutObject(ctx, in); {
case isPreconditionFailed(err):
return nil, err // retry from a fresh read
}
バックオフは10msから100msに増加し、5回の試行でキャップされます。なぜなら、ここでの競合は稀であり、競合は最初の再試行で解決されるはずだからです。5回で解決しない場合は、6回目の試行では修正されない問題が発生しています。
より明白でない制約は、関数シグネチャにあります。呼び出し元はmutate関数を渡しますが、その関数は試行ごとに新しく読み取られた状態に対して再実行されるため、入力の純粋関数である必要があります。その中の副作用(メール、カウンター、Stripe呼び出し)は、更新あたり1回ではなく、試行あたり1回発生します。その要件はコンパイラによって強制されません。コメントと、次の担当者をレビューする人によって強制されます。
インデックスなしのインデックス
members/{sha256(email)}.jsonは、プライバシー上の理由からハッシュのように見えます。そうではありません。これはインデックスです。
「このユーザーはこの組織のメンバーか?」という質問は、すべての認証済みリクエストで尋ねられます。メールアドレスがキーにハッシュされているため、答えはローカルで計算できるパスへの1回のGetObjectです。リストなし、スキャンなし、同期を維持するためのセカンダリインデックスなし。キーがルックアップです。
これは設計パターン全体であり、設計の中心的な制限でもあります。これは後で説明しますが、事前に考えた質問にのみO(1)でアクセスできます。
履歴テーブルなしの履歴
設定バージョンはULIDキーの下に追記専用であり、ここでストレージモデルは回避策ではなくなり、置き換えたものよりも優れています。
ULIDは作成時間によって辞書順にソートされます。オブジェクトストレージはキーを辞書順にリストします。したがって、「このチャネルのすべての設定変更、順不同」はプレフィックスリストであり、「火曜日と木曜日の間に発生したすべてのこと」は、既にソートされているキー空間に対する範囲読み取りです。インデックスなし、ORDER BYなし、インデックスを付け忘れたcreated_atカラムなし。
バージョンオブジェクトとイベントは書き直されないため、監査証跡は誰かが実装した機能ではありません。それは、それらのキーに二度書き込むコードパスがない結果です。ケアレスなUPDATEによって履歴が失われることはありません。なぜならUPDATEが存在しないからです。
上書きされるのはポインターです。どのバージョンが現在設定を提供しているか、そして各チャネルのエージェントが読み取るデプロイ済み設定オブジェクトです。これらは設計上変更可能であり、変更不可能性の保証が、何が起こったかの記録をカバーし、現在の状態の声明をカバーしないことを正確に述べる価値があります。デプロイはポインターの移動です。ロールバックは同じ移動を逆方向に行うことです。古いバージョンはどこにも行っていません。なぜなら、それを削除するように求められたものは何もなかったからです。
実際に壊れる場所
前述したように、この種のアーキテクチャはホワイトボード上では非常にうまく機能しますが、現実世界ではいくつかの問題が発生します。ここでは、私たちが遭遇した最大のペインポイントのいくつかを、それらがどれだけ痛かったかの順に示します。
読み取り増幅が実際のコスト
コントロールプレーン、つまりコード内のメインアプリケーションは、リージョンごとにN≥2インスタンスで実行され、各インスタンスはリクエストごとに同じディレクトリバケットを読み取ります。トークン検証、組織メタデータ、メンバーRBAC、リフレッシュトークンのローテーションなどです。Nインスタンスでは、各読み取りに対してN倍のコストがかかります。さらに悪いのはファンアウトです。メンバー、招待状、APIトークン、またはWebhookのリストは、1回のListObjectsV2に続いて結果ごとに1回のGetObjectです。これは、Postgresが1回のクエリとあなたが考えもしなかった結合で請求したであろうものに対して、数十回のラウンドトリップをサービスに費やすページロードです。
答えは、データベースを下に置く場合と同じです。読み取りスルーキャッシュをその前に配置します。それがワークフローです。したがって、テクノロジーは好みです。Valkey、DragonflyDB、Memcached、Redis、キャッシュの不足はありません。私たちはまだ増幅コストを支払っています。これは、問題のライブバージョンであり、戦争の物語ではありません。
キャッシュは決して真実の源ではない
キャッシュが存在しない、到達不能、または間違っている場合でも、すべての読み取りパスは正しく機能する必要があります。キャッシュはレイテンシを短縮します。正しさをゲートすることはありません。キャッシュが負荷を担うようになった瞬間、あなたは再びデータベースを持つことになります。ただし、それはRAM上にあり、誰もバックアップしておらず、その障害モードは沈黙です。
設計した質問しかできない
結合もクエリもありません。すべてのアクセスパターンは事前に選択したキーであり、新しい質問は新しいキーを意味し、それはマイグレーションを意味します。ただし、今ではマイグレーションは手書きのバックフィルジョブであり、CREATE INDEXではありません。私たちはこのコストを何度も経験しましたし、これからも経験するでしょう。これは設計の単一の最大の継続的な税金であり、オブジェクトストレージがスキーマ作業から解放されていると主張する人は、2番目の機能を持ったことのないシステムを説明しています。
イベントログは真実の源ではなく、これをイベントソーシングと呼ぶのはお世辞だろう
私たちのバケットレイアウトは、一見するとイベントソーシングされているように見えます。追記専用のイベントログがあり、各イベントは時間順のオブジェクトです。しかし、何もそれを再生して最終状態を再構築しません。現在の状態は、その変更を引き起こした最新のイベントへのポインターとともに、データベースに直接格納されます。管理者やユーザーが、現在の状態がそのようになった理由を見つける必要がある場合、それを見つけるのはGetObjectの距離です。これにより、イベントログはイベントソーシングパイプラインよりも監査ログになります。
これの主な欠点は、オブジェクトストレージにはトランザクションがないため、イベントの追記と世界の状態の更新の両方が1つのアトミックユニットで発生することを保証できないことです。私たちは状態を先に書き込み、イベントを後に書き込むため、その間にプロセスがクラッシュすると、状態は正しくなり、履歴にはエントリが欠落します。それを検出するものは何もありません。なぜなら、それを検出するには、ログを読み取って状態と比較する必要があり、その全体的なポイントは、何もそれをしないということです。したがって、ログの各エントリは実際に発生しました。あなたが保証されないのは、発生したすべてがログに含まれていることです。
データの保存場所を気にする必要がある
Tigrisの最も良い点の1つは、データがグローバルにレプリケートされているため、インターネット接続があればどこでもインフラストラクチャを配置でき、データの実際の保存場所を気にする必要がないことです。この最も悪い点の1つは、インターネットを越えてデータをコピーするには時間がかかるため、データの保存場所を気にする必要があるということです。複数のリージョンに複数のワークロードがある場合、結果整合性を管理する方法を見つける必要があります。
追記専用の監査ログは、この中で最も簡単な部分です。追記専用ということは、誰でもほとんど何も影響を与えずに追記できるということです。すべての変更は独自の小さな世界にあり、これが一般的に人々が分散システムからイベントソーシングパイプラインを作成する理由です。これが顕著になるのは、エージェント設定やその他の世界のオブジェクトが更新されるときです。変更が順序通りに発生し、最終結果が競合しないようにするにはどうすればよいでしょうか?
あるレベルでは、条件付き書き込みがこのインフラストラクチャのがんの治療法であると仮定するのは快適です。オブジェクトを最新の状態と比較して評価し、Tigrisでそれが新しい場合は書き込みが拒否されます。この前提は単一のリージョン内では機能しますが、リージョンをまたぐと崩壊します。条件付き書き込みは、そのリージョンが現在の状態と考えるものに条件付けられます。これは、2つの異なるリージョンに同じ比較・交換操作を与え、両方が成功する可能性があることを意味しますが、分散システムが収束するときに現在の世界の状態として扱われるのは1つだけです。失われた更新は単に消えたかのようです。
FIG 02 同じ比較・交換が2つのリージョンで受け入れられる
writer A writer B region 1 region 2
│ │ │ │
│ PUT If-Match: etag-1 │ │
├───────────────┼────────────────▶│ │
│ │ │ │
region 1 compares etag-1 to │ │ │ │
what it holds: they match │ 200 OK │ │ │
│◀─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │
│ │ │ │
│ │ PUT If-Match: etag-1 │
│ ├─────────────────┼────────────────▶│
│ │ │ │
region 2 has not seen A's │ │ │ │
write yet, so etag-1 still │ │ │ │
matches there too │ │ 200 OK │ │
│ │◀─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ┤
│ │ │ │
both writers were told they won · · · ·
│ │ │ │
replication converges │ │ │ B's version │
│ │ │◀─ ─ ─ ─ ─ ─ ─ ─ ┤
│ │ │ │
last write wins, so region 1 │ │ │ │
adopts B's version │ │ │ │
A's write is not merged and not │ │ │ │
reported. It is simply gone.
これを読んで、あなたの最初の直感は「ああ、データが書き込まれた後に更新されるだけなら、それを読み戻して、ちょうどやったことと一致したことを確認すれば、無料の子犬と永遠の幸せな太陽が得られるはずだ」でしょう。笑、いいえ。分散システム理論は、そのクリーンで幸せな世界の主張を、結果整合性の冷たい現実に押しつぶします。
重要な半分は、それがどこで起こるかです。失われた更新は書き込み時に発生するため、読み取り側で行うことは何もそれに到達しません。これは、人々が最初に求める修正のクラス全体を排除します。機能するのは、同じ比較・交換を2つの場所で裁定することを拒否することです。それらに依存するRPCは注釈付けされ、単一のプライマリリージョンにリプレイされ、クライアント側のガードは他の場所からの条件付き書き込みをサイレントロスではなくラウドエラーに変換します。その後、読み取りは処理可能であり、列挙可能です。6つの条件付き書き込みサイトとそれらの周りのすべての読み取りです。それが明らかになったのは、それ自体の投稿です。乞うご期待!
アスタリスク:分析
前回の投稿はこれすべてにアスタリスクを残しました。そのアスタリスクは分析でした。オブジェクトストレージのみの設計が実際に限界に達した唯一の場所です。正直なバージョンは、ビルド途中で計画を変更したということです。
元の設計はテレメトリの形状でした。生のOTLPを取り込み、ClickHouseを使用してその1%をサンプリングする予定でした。高ボリューム入力、サーバーでの集計。次に2つの問題に直面しました。
- 各顧客の実際のログ本文と属性値のスライスを保持することは、テレメトリの費用を削減するのに役立つという製品の全体的なピッチにとって、望まないリスクでした。
- 1%のサンプルでさえ、良い分析ではありませんでした。
そこで削減はホストに移されました。スーパーバイザーはサンプルの代わりに各フレームを削減し、私たちに届くのはその結果です。スケッチ、マスクされたテンプレート、ロールアップ。エージェントごと、60秒ウィンドウごと。それは異なる形状です。それは小さく、既に集計されており、追記専用で、顧客ごとにパーティション化されて到着します。それのどれもカラム型ウェアハウスを必要としません。ファイルを入れる場所が必要です。
これにより、ツールに関する質問が再回答されました。より賢くなったからではなく、データが決定の基盤を覆したからです。
A tiger and a duck walk into a bucket. Somebody asks where the rest of the infrastructure went...
動物なしで示すと、次のようになります。
FIG 03 分析パス、ウェアハウスなし
┌─────────────────────────────────────────────┐ │ customer host │ every frame, not a 1% sample │ │ │ agent frames ─▶ supervisor reduces │ │ │ └──────────────────────┬──────────────────────┘ │ sketches, masked templates, rollups │ per agent, per 60 second window ▼ ┌─────────────────────────────────────────────┐ │ that customer's Tigris bucket │ append-only, and not addressable │ │ by any other customer's keys │ analytics/{agent_id}/{window}.pb │ │ │ └──────────────────────┬──────────────────────┘ │ read over httpfs, one bucket at a time ▼ ┌─────────────────────────────────────────────┐ │ DuckDB │ │ │ │ SELECT over the files, in place │ no columnar warehouse in the path │ │ └─────────────────────────────────────────────┘
そこでの各ホップは1人の顧客にスコープされており、以前は削減されたデータを保持していたClickHouseテーブルはなくなりました。リーダーは、それらのファイル上のDuckDBです。私たちはベンチマークの後で動物に気づきましたが、前ではありませんでした。これは正しい順序であり、悪い話です。
それがテナンシーに残したものは、私が予想しなかった部分です。私たちは顧客のコンテンツを保持しないようにしましたが、最終的に、ある顧客のテレメトリが別の顧客の認証情報からまったくアクセスできないレイアウトになりました。これは、この投稿の残りの部分と同じバケット境界が、例外だった唯一のプレーンに適用されたものです。これは、私たちが購入しようとしていたものよりも強力なプロパティであり、より少ないデータを望んだ副作用として到着しました。
ClickHouseは消えません。なぜなら、1つのジョブは本当にウェアハウスの形状をしているからです。フリートビューが構築されているインタラクティブエージェントとスーパーバイザーのテレメトリです。これは、縮小するのではなく、さらに構築したいものです。それは別の日の投稿です。
ここでのポイントはより狭いです。私たちは、持っていたテレメトリを保存するためのより賢い方法を見つけたわけではありません。私たちは異なるテレメトリを望むことに決め、その後ストレージの問題が自己解決しました。これが私たちの一般的なアプローチです。ワークフローを理解し、その周りの最適なテクノロジーを選択します。逆の方法で行うと、既にデータベースを持っているためにそれを擁護することになります。
後悔なし、1年後に聞いてください
「これを放棄させるものは何だろう?」それは「正しい」かどうかよりも良い質問です。
Ampbaseは、特定の形状のおかげでデータベースなしでうまくいっています。書き込みは低ボリュームでほとんど競合せず、読み取りは制御されたキーに対するポイントルックアップであり、データは組織ごとにきれいにパーティション化され、興味深い履歴は本質的に追記専用です。設定変更は、毎秒数千回ではなく、1日に数回発生します。
それらのいずれかを変更すると、Postgres移行に関するフォローアップブログを投稿することになります。2人のライターが継続的に同じキーで競合した場合、CASとリトライは、並行性プリミティブではなくライブロックジェネレーターになります。複数のオブジェクトにまたがるトランザクション(キーごとのCASではなく、実際のキー間のアトミティ)が必要な場合、プリコンディションでそれを構築する方法はなく、正直な行動は試みるのをやめることでしょう。製品がコントロールプレーンの状態に対してアドホッククエリを必要とする場合、私たちはクエリプランナーを一度のバックフィルで悪く再実装することになります。
それらのどれも、他の人の製品にとっては仮説ではありません。それらはまだこれには当てはまりません。
代替タイムライン
テナントごとのSQLiteは私が検討した代替案であり、Litestreamはそれを非常に現実的な可能性にしました。それは、データベースなし、または数百のより小さな簡単なデータベースかの選択であり、私はレプリケーションの責任を負いたくありませんでした。Tigrisは私に代わってレプリケーションを行うため、夜も安心して眠れます。
わずか2年前には、これらすべてが可能ではなかったでしょう。条件付き書き込みなしでは、2人のライターが両方とも勝ったと信じることができるストアで、一意性や安全なミューテーションを構築することはできません。S3は2024年8月に不在時の作成機能とその11月の比較・交換を取得し、有用な結果は「明らかにそれにはPostgresが必要だ」という声明が広範囲に真実でなくなっていることです。
私の実際の推奨は、おそらくタイトルほど派手ではありません。オブジェクトストレージがあればすべて事足りますが、しばらくの間はそうではありませんでした。そして、ギャップを埋めたのは、テレメトリを保存するためのより良い方法ではなく、それをもっと少なくしたいと決めたことでした。あなたのデータの形状も決定であり、それは通常、データベースを選択する際の鍵となります。基本的に、ワークフローがそれを選択し、あなたはそれに気づく時期を選ぶだけです。
私たちは興味を引くためにこれをしませんでした。データレイヤーは、小規模チームが夜を失う場所であり、これは私たちが awake でいる必要がある移動部品が最も少ないバージョンだからです。
グローバルにレプリケートされたストレージで条件付き書き込みが必要ですか?
Tigrisは、強度の読み取り後書き込み整合性と各バケットに対するHTTPプリコンディションを提供するため、比較・交換はデータベースではなくヘッダーです。