インフラ・DevOps
オブジェクトストレージがあればすべて事足りる
Object storage is all you need (tigrisdata.com)
要約
この記事では、データベースエンジンの代わりにオブジェクトストレージを直接利用し、必要なデータベース機能(一意性制約、トランザクション、インデックス、履歴テーブル)を自前で実装する方法について解説しています。これにより、データベース層の管理(マイグレーション、接続、スキーマ)を排除し、プロトコルバッファとオブジェクトストレージの機能(強整合性、条件付き書き込み)を組み合わせて、データベースのような振る舞いを実現するアプローチが紹介されています。
全文翻訳
ContextTigrisは、FoundationDBという分散キーバリューストアの上に構築したデータベースエンジンを使用してオブジェクトストレージを構築しています。Ampbaseの創業者であるJP氏は、Tigris上にデータベースを必要としないコントロールプレーンを使用しており、今回は自身で構築する必要があったデータベースの振る舞いと、そのコストについて解説します。JPさん、ありがとう!前回のAmpbaseブログでは、使用していないすべてのデータベースエンジンについて語り、実際に何をしているのかを説明すると約束しました。私たちはリレーショナルデータベースを使用していません。Tigrisをストレージレイヤーとして直接使用し、必要なデータベースの振る舞いを、Tigrisが提供する2つのプリミティブ(基本機能)の上に実装しています。ええ、ええ、分かっていますよ。「データベースは必要なかった」というのは、通常、その8ヶ月後に「尻尾を巻いてPostgresに移行した」という次の投稿につながるキャッチーなタイトルです。実際には、データベースエンジンを手に取る時、あなたは4つの基本的な機能、つまり一意性制約、トランザクション、インデックス、履歴テーブルを求めています。Tigrisのグローバルオブジェクトストレージをデータベースとして使用するために、これらのプリミティブをすべて自分で実装する必要がありました。今日は、これらのプリミティブがどのように機能するのかを明らかにし、あなたが選択したデータベースエンジンに実際に何が含まれているのかを理解できるようにします。実際に保存されているものとは何でしょうか?すべては2層のバケットに格納されます。グローバルディレクトリバケットには組織のリストが保持され、各組織には独自のバケットが与えられます。これらのキーのうち4つは、データベースが通常あなたのために行うジョブを実行しており、ここではラベル付けされ、次のセクションで詳しく説明されています:FIG 01 - 2つのレイヤーと、各キーが実装するプリミティブ┌──────────────────────────────────────────────────────┐│ ディレクトリバケット1、グローバル││││ orgs/{org_id}/ ││ metadata.json ││ members/{sha256(email)}.json │ インデックス│ billing.json │ 比較・スワップターゲット│ channels/{channel_id}/metadata.json ││ api-tokens/{token_id}.json ││ events/audit/{event_ulid}.pb │ 監査ログ│ org-ops/queue.pb │└──────────────────────────────────────────────────────┘ プロバイダーの認証情報はこのバケットにのみ到達します┌──────────────────────────────────────────────────────┐│ 顧客ごとの組織バケット1││││ channel-slugs/{slug}.json │ 一意性制約│ channel-{channel_id}/ ││ config-meta/{config_id}.json ││ config-versions/{version_ulid}.json │ 履歴テーブル│ bundle-meta/{bundle_id}.pb ││ bundle-versions/{bundle_id}/{version_ulid}.pb ││ active-config.pb │ ポインタ、上書きされる│ events/{event_ulid}.json │└──────────────────────────────────────────────────────┘ その顧客のスコープされたキーはこのバケットにのみ到達します私たちは当初、すべてを顧客のバケットにJSONオブジェクトとして書き込んでいました。しばらくして、APIにプロトコルバッファオプションをますます採用するようになり、スキーマ定義と並行して検証を定義できるようになりました。JSONのマーシャリングとアンマーシャリングは、予想以上にコストがかかったため、直接プロトコルバッファを使用するように切り替えました。私たちのデータベースは両方の形式を処理するため、プロトコルバッファへの移行前にレコードが存在する場合、すべて期待どおりにロードされます。プロトコルバッファを使用することで、データベースレイヤーの管理(マイグレーション、接続、スキーマ)という問題全体を排除できます。唯一の欠点は、プロトコルバッファのフィールド名は永続的であることですが、正直に言うと、Postgres、MySQL、またはSQLiteでカラム名を変更するのと同程度に苦痛です。顧客ごとにバケットを作成する単純な方法は、すべて同じ$bigcloudアカウント内に顧客ごとにバケットを作成し、クォータ制限に達するたびに新しいアカウントを作成することです。または、アカウントごとのバケット制限を回避するためにプレフィックスを持つ1つのバケットを持ち、IAMポリシーの複雑さに依存して分離を強制することです。これらすべては非常に退屈に聞こえ、Tigrisにはまさにこの形状のためのパートナー統合プログラムがあります。それを1回呼び出すだけで、その顧客のためのTigris組織、そのバケット、およびそれにスコープされたアクセスキーのセットが作成されます。私たちはプロバイダーのIDを保持します。各顧客は、その下に強力な分離を持つ組織です。分離はインフラストラクチャレイヤーに組み込まれています。アプリコードで忘れがちなWHERE org_id = ?は不要です。なぜなら、1つの顧客のデータに到達する認証情報は、他の誰かのデータにアクセスできないからです。過去にデータベースを管理するプラットフォームをいくつか構築した経験からすると、これはほとんどの人が間違える部分です。分離を超えて、オブジェクトストレージが本当にデータベースを置き換えるためには、データベースのような振る舞いが必要です。しかし、オブジェクトストレージのシンプルさで、データベースのような振る舞いをどのように得られるのでしょうか?強力な書き込み後読み取り整合性、条件付き書き込み、その他のプリミティブをすべての中核として活用します。オブジェクトストレージのシンプルさでデータベースのような振る舞いを得るデータベースから(そしてオブジェクトストレージから)必要なのは、以下のすべてです:強力な書き込み後読み取り整合性条件付き書き込み一意性制約トランザクションインデックス履歴テーブル私たちは、強力な書き込み後読み取り整合性と条件付き書き込みについてはTigrisにオフザシェルフで依存しましたが、他の4つは自分で実装しました。強力な書き込み後読み取り整合性誰もがデータベースで強力な書き込み後読み取り整合性を期待しています。オブジェクトストレージは、約2020年12月まで強力な整合性モデルを持っていませんでした。オブジェクトストレージに強力な整合性がどのように追加されたかについて詳しく知りたい場合は、Werner Vogelsの記事が詳細に説明しています。Tigrisが私たちに提供する主な利点は、バケットデータがグローバルであるという事実であり、単にバケット名がグローバルなのではありません。これは、両方のクライアントが同じリージョンにある場合、データは強く整合しますが、リージョンを越えると複雑になることを意味します。グローバルレプリケーションは結果整合性を意味します。つまり、システムが変更を同期している間、時々物事が不整合になる可能性があります。私たちにとっての中心的な問題は、どこで本当に強力な整合性が必要で、どこで結果整合性で十分なのかを理解することでした。条件付き書き込み条件付き書き込みは、基本的に比較・スワップです。Tigrisは書き込みに対してHTTP事前条件をサポートしています:If-None-Match: *はキーが存在しない場合にのみ書き込み、If-Match: {etag}はオブジェクトを読み取ってから変更されていない場合にのみ書き込みます。どちらもオブジェクトの最新の状態に対して評価されます。バケットの場所タイプが提供する整合性モデル内で。整合性モデルの選択は後でより重要になります。しかし、ここで重要なのは、比較・スワップがあれば、基本的にすべてのデータベースの基盤となるコアプリミティブを持っているということです。UNIQUEなしの一意性データベースインデックスを、データをより効率的に見つけるための2つの特性(ルックアップの事前計算と、同じデータが二重に保存されないことの保証)を持つものと考えてください。これはCREATE INDEXとCREATE UNIQUE INDEXの違いです。ほとんどの場合、ルックアップをより効率的にするためにプライマリキーやUUIDにインデックスを作成することはありません。ユーザーのメールアドレスや一意の識別子を二重に保存できないようにするために作成します。データベースの一意性プロパティをインデックスで取得するために、コンテンツを認識するストレージと条件付き書き込みを組み合わせて利用します。これにより、物事が二重に保存されないことを確認できます。私たちは、PutObject呼び出しでIf-None-Match: *ヘッダーを渡すことによって、チャネルやユーザーを作成しようとします。これは、何かがすでにそのキーに保存されている場合、Tigrisにデータを拒否するように指示します。2つのアプリインスタンスが同じ場所に異なるデータを書き込もうとすると、Tigrisがどちらが勝つかを決定し、敗者にはエラーを返し、それを処理してユーザーに報告します:switch {case err == nil: return nilcase isPreconditionFailed(err): // 別のライターが同時にスラグを作成しました。 // これが冪等(同じchannelID)か競合するマッピングか // を判断するために再読み込みします。 return s.handlePutConflict(ctx, slug, channelID, err)} 迷惑なことに、この