最近Spannerに入門し始めてstaleという言葉を使うようになったので、この記事ではstaleに関しての話をします。
staleとは
stalenessの辞書的な意味はOxford Learner's Dictionariesにおいては1以下のように定義されています。
1. (of food, especially bread and cake) no longer fresh and therefore unpleasant to eat
2. (of air, smoke, etc.) no longer fresh; smelling unpleasant
日本語にすると、「鮮度や新しさがないこと」という意味で、「fresh」の対義語として使われる言葉です。
Spannerにおけるstaleness
Google CloudのSpannerにおいては、stalenessという言葉がRead typesの文脈で出てきます。
Read typesとは
Spannerにおいては、以下の二つのread typeが用意されています。
- strong read
- stale read
これらのread typeは Spannerにおいてデータを読み取る時のデータの新しさを指定することができるものです。
strong readは現在のタイムスタンプでの読み取りであり、現在までにcommitされたすべてのデータを読み取ります。大体の場合みなさんがデータベースに対して求める挙動はstrong readの挙動かと思います。「クエリ実行時点で最新のデータが読み取りたいので、すでにcommitされているデータは読み取りたいし、まだcommitされてないデータは読み取りたくない」というのが通常トランザクション機能を持つデータベースに対して期待する振る舞いだと思います。
実際、Spannerにおいては通常の読み取りはすべてstrong readの挙動となり、select文の結果としては読み取り開始前にcommitされたすべてのデータを確実に読み取ることができます。
一方でstale readはstrong readではない読み取りです。つまりは「最新のデータではないデータが読み取られる」readということになります。先に見た意味通り「新鮮ではない読み取り」です。
stale read
stale readは「少し古いデータでよければ、すぐに結果を返します」という読み取りとも言えます.実際に挙動を細かく見てみましょう。
stale readの中にも2種類あります。bounded-stalenessとexact-stalenessです。
exact-staleness
これはクエリにおいて指定されたタイムスタンプまたはdurationで指定された時間分正確に、古い時間のスナップショットに対してクエリが実行されるモードになります。
Google Cloudのドキュメントの例では以下のようなコード例が示されています。2
spanner.ExactStaleness()を用いて取得されるTimestampBoundtypeをWithTimestampBound()に渡してクエリにオプションとして付与することで指定できます。
client, err := spanner.NewClient(ctx, db)
if err != nil {
return err
}
defer client.Close()
ro := client.ReadOnlyTransaction().WithTimestampBound(spanner.ExactStaleness(15 * time.Second))
defer ro.Close()
iter := ro.Read(ctx, "Albums", spanner.AllKeys(), []string{"Id", "SingerId", "Title"})
これは、ちょうど15秒前時点のデータを取得するクエリになります.このクエリにおいては、図にすると以下のような形で、5秒前にcommitされたId:22のデータは読み取られず、Id:11のデータは読み取られます。
bounded-staleness
少なくとも指定したDurationより新しいデータの読み取りが行われることを保証する読み取りです。
spanner.MaxStaleness()を用いて取得されるTimestampBoundtypeをWithTimestampBound()に渡してクエリにオプションとして付与することで指定できます。
例えば以下のコード例では最悪の場合15秒前のデータが読み取られますし、場合によっては8秒前のデータを読み取ることもできます。
client, err := spanner.NewClient(ctx, db)
if err != nil {
return err
}
defer client.Close()
ro := client.ReadOnlyTransaction().WithTimestampBound(spanner.MaxStaleness(15 * time.Second))
defer ro.Close()
iter := ro.Read(ctx, "Albums", spanner.AllKeys(), []string{"Id", "SingerId", "Title"})
bounded-stalenessにおいては読み取りの再現性がありません。全く同じタイミングに2回のstale readを行った場合でも異なるタイムスタンプでの読み取りが行われる可能性があり、一貫性のない結果を返すことがあります。
なぜstale readが必要になるのか
そもそもどうしてstale readが必要となるのでしょうか?
これを理解するためには、Spanner自体のアーキテクチャを知る必要があります。
Spannerは分散データベースでデータが保存されるストレージレイヤーが分散されることによって、従来のRDBには難しかったデータベースのリソース最適化のためのスケールを可能にしています。
ストレージレイヤーが分散されている一方で、Spannerにおいてはトランザクションを用いた強い整合性も実現されています。この記事では詳細に踏み込みませんが、TrueTime3という厳密な時刻同期システムを使って、グローバルに分散したデータでも、各ノードで「どのトランザクションが、どの順番でコミットされたか」を正確に把握し、最新のデータをどこから読み取っても完全に一貫性が取れていること(外部整合性を担保すること)を実現しています。
しかし、この『各ノードで「どのトランザクションが、どの順番でコミットされたか」を正確に把握』のためにはどこかのノードでデータが更新された際、他のノードに伝播するまでの待ち時間が発生します。書き込み量が多く、ノードが地理的に遠い場所に分散している場合には、「最新性の確認と伝播を待つ時間」が読み取りレイテンシを増加させることにつながってしまうのです。
こうした場合に、stale readが効果を発揮します。
「最新のデータである必要はない」とユーザーが指定することで、Spannerは上記の待ち時間を省略し、指定された時間以内のデータを持っているレプリカから即座に読み取りを行い、レイテンシを大幅に短縮できるようになるのです。
まとめ
- Spannerには2つのread typeがある
- 通常はstrong readで問題ないが読み取りのレイテンシが大きい場合、ユースケースによっては、stale readを用いることでパフォーマンスの改善が図れる
- stale readは整合性を意図的に緩和することで、性能を改善する
▼ 新卒エンジニア研修のご紹介
レアゾン・ホールディングスでは、2025 年新卒エンジニア研修にて「個のスキル」と「チーム開発力」の両立を重視した育成に取り組んでいます。 実際の研修の様子や、若手エンジニアの成長ストーリーは以下の記事で詳しくご紹介していますので、ぜひご覧ください!
▼ 採用情報
レアゾン・ホールディングスは、「世界一の企業へ」というビジョンを掲げ、「新しい"当たり前"を作り続ける」というミッションを推進しています。 現在、エンジニア採用を積極的に行っておりますので、ご興味をお持ちいただけましたら、ぜひ下記リンクからご応募ください。

