ラベル Firestore の投稿を表示しています。 すべての投稿を表示
ラベル Firestore の投稿を表示しています。 すべての投稿を表示

2026/01/17

RDB脳に注意

RDB脳 って何

 俗に「RDB脳」と呼ばれる現象の発生が、ソフトウェア開発者の中で知られています。

これは、リレーショナルDBを利用してきた技術者が、非リレーショナルなDBを使おうとした際に、以下の現象がみられるものです。

  • 非リレーショナルなDBの仕様の理解に苦しむ。
  • 非リレーショナルなDBを使いこなせず、十分な性能が出ない。


非リレーショナルなDBの登場について

歴史的にはリレーショナルDBの欠点を解決するために登場したのが、非リレーショナルなDBと考えていいと思います。リレーショナルDBの欠点は、具体的には以下ではないかと思います。

  • プライマリキー
    レコードを唯一に識別する仕組みとして使用されているがプライマリキーです。単純にユニークな値を持つ単独のIDとかで十分なのに、わざわざ複数のデータを使うのは不自然です。
    リレーショナルDBが普及し始めた当時は、今と違ってまだストレージが高価だった時代ですので、ストレージの節約は重要でした。少しでもストレージを節約するために、データの一部をIDとして利用することで、記録に必要な容量を減らす仕組みがプライマリキーであると考えられます。IDだとユニークにするためには短くないはずですので、それさえも削減したいということでしょう。
    (CassandraやDynamoDBのように非リレーショナルでありながらプライマリキーの概念があるDBも存在します)
  • Write on Schema
    レコードを記録する前に、レコードのデータ構造であるスキーマを決定しておく必要があります。当然スキーマと異なるデータ構造は記録できませんので、データ構造の変更は容易にはできません。
    またレコード間でスキーマを揃えなくてはならないので、実際にデータが保存されているのは一部のフィールドのみという無駄が発生する状況になることもあります。
  • スケーリングへの対応能力
    クラウドで処理能力を拡大/縮小する仕組みであるスケーリングへの対応が難しいようです。そのためクラウドベンダーの用意したクラウドネイティブなリレーショナルDBでない場合は、スケーリングへの対応はレプリカの設定程度に限定されます。


非リレーショナルなDBのプロダクト

Google Cloudだと以下が非リレーショナルなDBです。

  • ドキュメント指向DB
    • Firestore
  • キー・バリュー型DB
    • Bigtable
    • Memorystore(Redis/Valkey/memcached どれもキー・バリュー型)
    • Datastore(Firestoreの前身のいにしえのDBであり、すでに提供終了)


RDB脳を生み出す原因

RDB脳を生み出す原因となっているのは、リレーショナルDBと非リレーショナルDBの仕様や使い方の違いと思われます。具体的には以下が該当しそうです。

  • SQL   vs.   NoSQL
    リレーショナルDBは一般的にはSQLで利用されます。
    それに対して非リレーショナルDBは、「Not only SQL」を略して「NoSQL」と表現されるように、SQLを使用しません。SQLライクな言語が用意されることもありますが(例:CassandraのCQL)、そもそも言語で操作が不可能なDBもあります。
  • Write on Schema   vs.  Read on Schema
    非リレーショナルDBがNoSQLである理由の1つです。
    「Write on Schema」は、データの書き込み前にスキーマを決定しておく必要があることを表しています。リレーショナルDBで採用されています。
    「Read on Schema」は、データの書き込み時にはスキーマを固定しておく必要はありません。かと言ってスキーマが存在しないのではなく、データを読み出すとスキーマが判明します。そのためデータごとにスキーマが異なる可能性があります。
  • プライマリキー   vs.   ID/キー
    リレーショナルDBで一般的に使われるSQLでは、レコードを一意に識別するために、レコード内の複数のデータを組み合わせてプライマリキーとして利用します。
    非リレーショナルDBで用いられるIDまたはキーは、それだけでデータの記録場所を特定できます。
  • 正規化   vs.   非正規化
    リレーショナルDBの場合は通常、1つのデータは1つだけ保存されるようにデータ構造を最適化する正規化を行います。これによりデータの一貫性を保証します。
    非リレーショナルDBでは、読み出し時のレスポンスを確保するためにあえて非正規化する場合があります。
  • CURDの概念
    非リレーショナルDBでは一般的に、CreateとUpdateの違いがありません。どちらも書き込みであり、新規追加か既存更新かは意識しません。
  • 取得の概念
    リレーショナルDBで一般的に使われるSQLには、検索と取得の区別がありません。データを取得するためには、検索します。レコードの識別がID/キーではなく、プライマリキーという複数のデータであることに起因しています。
    非リレーショナルDBでの取得は、ID/キーを使用して一発で取得できます。そのため検索は別の機能です。またキー・バリュー型DBの場合は、検索が用意されていないこともあります。
  • インデックスの扱い
    リレーショナルDBでは検索の高速化のために任意で使用しますが、非リレーショナルDBでは単に検索するために予めインデックスを作っておく必要があることもあります。


事例

実際RDB脳でやらかしてしまったと思われる事例は、ネットでも見つかります。


Firestore → Cloud SpannerでDBコスト93%削減!無停止でやり切った 1 年間の全記録

どのような処理を行っているのか説明されていないので推測ですが、状況からRDB脳案件の可能性があります。DBマイグレーションしてますが、移行先がRDBのSpannerなので、RDB脳なら問題なく使えるのは当たり前です。
FirestoreはサーバレスなのでI/O数によって課金される仕様です。そのため無駄にI/Oを行う実装になっていると、この記事のような運用コストの問題が発生します。まずは無駄なI/Oを削減する必要があります。ロジックを見直すのが本来の解決方法ですが、MemorystoreやFirestore Data Bundlesでキャッシュするのも有効な解決手段となります。
あるいは本来Firestoreが向かない用途に使ってしまったケースかもしれません。

Spanner は本当に高い?思ったよりも低い Firestore との損益分岐点
上記の記事と同じ状況と思われます。無駄にI/O行う実装になっていないか疑問があり、結論の信憑性に大きな疑問があります。

DynamoDB設計で痛い目にあった話 – RDB脳から抜け出すための実践ガイド
非リレーショナルDB初心者にありがちな失敗談が語られていて、共感できます。
プライマリキーやインデックスがテーブル作成時にしか指定できないのは不便そうです。


以下は参考になりそうな記事。

非リレーショナルDBでテーブルを実装するケースで参考になりそうです。正規化/非正規化という点に着目している点がおもしろいと感じました。
しかし非リレーショナルDBではテーブルにする必要がない点で、非リレーショナルDBでの実装が必ずこのようになるというわけではありません。結果整合性についての記述もそれを採用したDBではそのとおりですが、強整合性のDBではあてはまらないことに注意が必要です。

ある夏のFirestoreのこわい話
うっかりやらかしていたことに気付いて改善した例です。この例はSQLでも同じ問題が起きます。やらかしていることに気づきにくく、思わず同情してしまいました。


「DB = リレーショナルDB」は過去の話

現在では非リレーショナルなDBも当たり前に使われるようになっていますが、いまだに非リレーショナルなDBの存在を知らない人に出会ったことがあります。リレーショナルDBと非リレーショナルなDBは、使い分けが重要です。


RDB脳を回避する方法

リレーショナルDBに慣れた方が、新たに非リレーショナルDBを学ぼうとする場合、RDB脳に陥る可能性があります。まず非リレーショナルDBを学ぶ前に、リレーショナルDBの知識・経験をきれいに忘れてください。

...と言っても、現実はそんな都合よく忘れられるわけがないのが困ったところです。


私の場合はドキュメント指向DBから入ったという経緯もあり、私だけかもしれませんが、後から学んだリレーショナルDBが「ヘンテコな仕様」に見えました。具体的には以下が理解しがたかったと記憶しています。

  • SQL → なんで言語にする? やるならGUIだろ。
  • プライマリキー → なんでIDじゃないの?
  • 取得がない → なんで検索(query)しかないの?
  • Write on Schema → 実装は難しかしそうなので、仕方ないか。
  • JOIN/MERGE → 意味不明。


RDB脳を回避するには、まずは非リレーショナルなDBについてそれが生まれた背景や、目指すところの違いを意識するのが早いのではないかと思います。そうすれば自ずと特徴や使いどころも理解できるのではないでしょうか。


2024/08/26

Firestore/Datastoreで複数の範囲フィルタを使ってクエリが可能に

Firestoreが登場する前の昔のDatastoreから、Firestore/Datastoreのクエリには「不等式はクエリ内で1つだけ」という制限があったのですが、2024/7/29にやっと解除されました。

上記リンクはFirestoreですが、Datastoreも同様のページがあります。Datastoreの日本語ページにはプレビューマークがついていますが、リリースノートにはGAと説明されています。

この制限のためにこれまで「DBに範囲を記録しておいて、指定の値がその範囲内にあるデータを検索」ができませんでした。具体的には以下のようなケースです。
  • 開始・終了日時を保存しておき、指定の日時に対応するデータを検索。
  • 上限・下限の金額を保存しておき、指定の金額に対応するデータを検索。
  • etc.

ただし無制限になったわけではなく、少ないながらも制限事項もあるようです。検索時間などパフォーマンス面も未検証なので、遅くないのかなど気になります。

これまではこの制限を回避するためだけにSearch APIを利用したりElasticsearchをインストールしたりといった対応が必要だったのですが、だいぶFirestore/Datastoreのみで検索ができるようになりそうです。

2023/01/13

データストアの選択

世の中には データを保存しておく製品にも、いろいろと選択肢が用意されています。GCPでも目的に応じて特徴の異なる複数のサービスが用意されています。その中でもBigQueryって特徴的で判りやすいと(個人的には)思っていたのですが、最近になってGCPにかかわりながらもBigQueryがどういうものなのかを未だに知らない人に遭遇しました。

ここでGCPのデータの保存目的に使えるサービスを比較しておきたいと思います。


一覧

GCPに用意されたサービスの一覧です。
名称略称概要スケーラブルマネージド
Firestore(特になし)NoSQL、ドキュメント指向データベース。
Cloud SQL(特になし)SQLのリレーショナルデータベース。
Cloud SpannerSpannerSQLのリレーショナルデータベース。
Cloud BigtableBigtableNoSQL・低レイテンシのデータベース。
MemorystoreMemstoreオンメモリで高速動作。
BigQuery(特になし)検索に特化したデータウェアハウス。
Cloud StorageGCS非構造化データ用のストレージ。

「略称」はGoogleが決めた正式なものではないですが、開発者の間でよく使われている呼称です。
「マネージド」の〇はフルマネージド、△はフルではないマネージドを表します。

これら以外にもFirebaseのサービスもあるのですが、それらに関しては割愛します。

各サービスの特徴

サービスを比較・選択するうえで参考としていただけるように、ざっくりと説明します。詳細はGoogleの各サービスをご覧ください。

Firestore

スケーラブルなので負荷が増えても重くなる心配がありません。むしろアクセスが少ないと優先順位が下げられて待ちが長くなる感じさえあります。フルマネージドなのでメンテナンスフリーです。
スキーマレスでドキュメント指向です。自分で各ドキュメントの構造を同じにして使えば、テーブルとして使えます。検索用のインデックスが付いた連想配列の様な感じですが、インデックスによる検索機能は貧弱です。柔軟な検索が必要なら、ElasticSearchやGAEのSearch APIと組み合わせる必要があります。
強整合性でトランザクションにも対応しています。
費用は主に保存容量に対して発生し、無料枠もあるので、ランニングコストは低めです。
SQLしか経験のない方には理解しにくいかもしれませんが、「Not only SQL」なデータベースです。
過去には似たようなDatastoreというサービスがありましたが、それが廃止されてFirestoreのDatastore互換モードとなりました。Firestoreにはネイティブモードもあり、こちらはドキュメントを階層構造にすることもできます。

Cloud SQL

MySQL/PostgreSQL/SQL Serverのいずれかから選んで使用します。これらを自分でGCEに乗せなくても、予めGoogleが用意してくれていると理解すればいいと思います。
なので利点も欠点もMySQL/PostgreSQL/SQL Serverを引き継ぎます。スケーリングに関しては設定した範囲内に限定され、無限ではありません。幸いフルマネージドでメンテフリーで使えます。
費用は主に保存容量とCPUに対して発生します。CPUはGCEと同等なので、ランニングコストはそれなりに発生します。

Cloud Spanner

スケーラブルなので負荷が増えても重くなる心配なし、フルマネージドでメンテナンスフリー、強整合性と、ここまではFirestoreと同じです。こちらはドキュメント指向でありません。SQLでリレーショナルデータベースを使いたい人にとっては、Cloud SQLとFirestore/Bigtableの「良いとこ取り」の様な感じになります。GCPではコスト以外はほぼ最強ではないでしょうか。
費用は主に保存容量とコンピューティングインスタンスに対して発生します。「Cloud Spanner vs Cloud SQL」も参考になると思います。

Cloud Bigtable

スケーラブル、フルマネージド、NoSQLと、ここまではFirestoreと同じです。低レイテンシが最大の売りでしょうか。
GCPに昔からあるサービスで、他のデータベース/ストレージの基盤となるサービスだと聞いたことがあります。(今GoogleのWeb上のドキュメントを探してもそのような説明は見当たらないのですが、当時詳しい人だったかGoogle日本法人の人から聞いた話なので、嘘ではないはず)
費用は主に保存容量とコンピューティングに使われるノードに対して発生します。ノードは事実上GCEと同じなので、たぶんそこそこかかるはず。

Memorystore

Redis/Memcachedのいずれかから選んで使用します。たぶんそれらをGCPに乗せただけと思います。オンメモリなので速いはず。しかもスケーラブル。
速さを活かしてキャッシュや書き換えの集中するデータの保存に向きます。

2021/12/18

Firestoreのトランザクションのリトライがおかしい

公式ドキュメントの説明 

トランザクションとデータ競合」には以下の記載があります。

Firestore では、いずれかのオペレーションを遅延または失敗させることで、データ競合を解決します。データ競合が原因で失敗したトランザクションは、Firestore クライアント ライブラリによって自動的に再試行されます。一定回数の再試行が行われると、トランザクション オペレーションは失敗し、エラー メッセージが返されます。

つまりデータ競合によりトランザクションが失敗するのは、一定回数リトライしても競合を解決できなかった場合ということになります。


実際に起きた例外

ところが Java版のサーバクライアントライブラリを使用したアプリで、割とあっさりとこのエラーが発生しました。以下がその時のログに出ていた例外とそのメッセージ。

java.util.concurrent.ExecutionException: com.google.api.gax.rpc.AbortedException: io.grpc.StatusRuntimeException: ABORTED: Aborted due to cross-transaction contention. This occurs when multiple transactions attempt to access the same data, requiring Firestore to abort at least one in order to enforce serializability.


検証用にアプリを作って故意にデータ競合を起こしてみると、以下の例外が発生しました。別アプリなのでJavaライブラリのバージョンも違っていたかもしれません。

java.util.concurrent.ExecutionException : com.google.cloud.firestore.FirestoreException: Transaction was cancelled because of too many retries.

この例外のgetCause()メソッドで得られたThrowableは、以下。

 com.google.cloud.firestore.FirestoreException : Transaction was cancelled because of too many retries.

 

ネットで検索してみるとStack Overflowにも同様の現象に遭遇した質問「Firestore retry transaction logic」が見つかります。状況は最初の方と同じです。この記事には回答がまったく付いていなくてさみしい限りです。


考察

例外が発生するときと、例外が起きずに正常動作する時とで実行時間にほとんど差がないことから、公式ドキュメントの説明と異なり、実際には以下の実装ではないかと考えられます。

  • トランザクションはリトライしていない、あるいは
  • リトライはしているがウェイトがないのでデータ競合が解決することなく規定回数を回ってしまう

Javaクライアントライブラリのソースを探して解析すれば何かわかるかもしれませんが、面倒ですしそんな時間もありません。


回避策

ベストな解決策は、Googleさんがこの問題を解決したバージョンのライブラリを公開してくれることです。
しかしそれを待っていられないなら、自分でウェイト付きリトライをトランザクションにかましてみればいいでしょう。私はこの方法で、上記の例外がほぼ発生しなくなっています。


最後に

公式ドキュメントにはリトライはクライアントライブラリが実行しているとの記載があります。今回発見した現象はJavaライブラリによるものですが、他の言語のライブラリは使ったことがないので未確認です。


2021/12/23追記

本日、Javaライブラリをひととおり新しくしてみたら、上記の例外は発生しなくなっていました。どのあたりのバージョンからかは分かりませんが、修正されたようです。

2021/12/08

GCPのデモアプリ : vsReversi


 GCPのデモとして、vsReversiを作ってみました。ネット対戦リバーシ(オセロ)です。

  • 対戦相手を選択できる。
  • 適当にマッチングして対戦もできる。
  • 相手がいないときは対コンピュータ戦もできる。

使用しているGCPのサービスは以下のとおりです。
  • App Engine : Webサーバ
  • Firestore : データベース
  • Identity Platform (Firebase Authentication) : ユーザ認証
  • Cloud Tasks : 対コンピュータ戦の思考ルーチン
対コンピュータ戦の思考ルーチンはJavaで書いていますが、実質数秒以下で1手を打ちます。そのためCloud Tasksを使用する意味はないのですが、GCPのデモとして無理矢理使っています。でないとただのGAEのデモなりそうなので。
デモにコストをかけられるほど裕福ではないので、GAEを中心に無料枠の範囲内で運用できるように、GAEの最大インスタンス数を制限しています。そのため利用が集中すると、応答が悪かったり、エラーになる可能性があります。最大インスタンス数の制限を外せば大勢が同時にアクセスしてもレスポンス低下の起きにくいGAE/Firestoreの特徴を活かせるのですが、残念なことになっています。GAE/Firestoreのいいところを全くアピールできていない、デモとしてはダメアプリです。

それでも、見つけた方は楽しんでいただければと思います。