Cassandra ユーザー向け Spanner

このドキュメントでは、Apache Cassandra と Spanner のコンセプトと方法を比較します。Cassandra に精通しており、Spanner をデータベースとして使用しながら既存のアプリケーションを移行または新しいアプリケーションを設計することを前提としています。

Cassandra と Spanner はどちらも、高いスケーラビリティと低レイテンシを必要とするアプリケーション向けに構築された大規模な分散データベースです。どちらのデータベースも要求の厳しい NoSQL ワークロードをサポートできますが、Spanner にはデータ モデリング、クエリ、トランザクション オペレーション用の高度な機能が用意されています。Spanner が NoSQL データベースの基準を満たす仕組みの詳細については、非リレーショナル ワークロード用の Spanner をご覧ください。

Cassandra から Spanner に移行する

Cassandra から Spanner に移行するには、Cassandra から Spanner へのプロキシ アダプタを使用します。このオープンソース ツールを使用すると、アプリケーション ロジックを変更することなく、Cassandra または DataStax Enterprise(DSE)から Spanner にワークロードを移行できます。

基本コンセプト

このセクションでは、Cassandra と Spanner の主なコンセプトを比較します。

用語

Cassandra Spanner
クラスタ インスタンス

Cassandra クラスタは、Spanner のインスタンス(サーバーおよびストレージ リソースのコレクション)と同等です。Spanner はマネージド サービスであるため、基盤となるハードウェアやソフトウェアを構成する必要はありません。インスタンスに予約するノードの数を指定するだけです。または、自動スケーリングを選択してインスタンスを自動的にスケーリングします。インスタンスはデータベースのコンテナとして機能し、データ レプリケーション トポロジ(リージョン、デュアルリージョン、マルチリージョン)はインスタンス レベルで選択されます。
キースペース データベース

Cassandra キースペースは、テーブルやその他のスキーマ要素(インデックスやロールなど)のコレクションである Spanner データベースと同等です。キースペースとは異なり、レプリケーション係数を構成する必要はありません。Spanner は、インスタンスで指定されたリージョンにデータを自動的に複製します。
テーブル テーブル

Cassandra と Spanner の両方で、テーブルはテーブル スキーマで指定された主キーによって識別される行のコレクションです。
パーティション 分割

Cassandra と Spanner はどちらも、データをシャーディングすることでスケーリングします。Cassandra では各シャードはパーティションと呼ばれ、Spanner では各シャードはスプリットと呼ばれます。Cassandra ではハッシュ パーティショニングが使用されます。つまり、各行は主キーのハッシュに基づいてストレージ ノードに個別に割り当てられます。Spanner は範囲シャーディングされています。つまり、主キー空間で連続している行は、ストレージでも連続しています(スプリット境界を除く)。Spanner は、負荷とストレージに基づいて分割と結合を行います。これはアプリケーションに対して透過的です。主な意味は、Cassandra とは異なり、Spanner では主キーの接頭辞に対する範囲スキャンが効率的なオペレーションであるということです。


Cassandra と Spanner の両方で、行は主キーによって一意に識別される列のコレクションです。Cassandra と同様に、Spanner は複合主キーをサポートしています。Cassandra とは異なり、Spanner ではデータが範囲シャーディングされるため、パーティション キーと並べ替えキーを区別しません。Spanner は、並べ替えキーのみがあり、パーティショニングがバックグラウンドで管理されていると考えることができます。


Cassandra と Spanner の両方で、列は同じ型のデータ値のセットです。テーブルの行ごとに 1 つの値があります。Cassandra 列型と Spanner の比較の詳細については、データ型をご覧ください。

アーキテクチャ

Cassandra クラスタは、一連のサーバーと、それらのサーバーと共存するストレージで構成されます。ハッシュ関数は、パーティション キー空間の行を仮想ノード(vnode)にマッピングします。その後、一連の vnode が各サーバーにランダムに割り当てられ、クラスタ キースペースの一部を処理します。vnode のストレージは、サービング ノードにローカルに接続されます。クライアント ドライバはサービング ノードに直接接続し、ロード バランシングとクエリ ルーティングを処理します。

Spanner インスタンスは、レプリケーション トポロジ内の一連のサーバーで構成されます。Spanner は、CPU とディスクの使用量に基づいて各テーブルを動的に行範囲にシャーディングします。シャードは、サービング用にコンピューティング ノードに割り当てられます。データは、コンピューティング ノードとは別に、Google の分散ファイル システムである Colossus に物理的に保存されます。クライアント ドライバは、リクエスト ルーティングとロード バランシングを実行する Spanner のフロントエンド サーバーに接続します。詳細については、Spanner の読み取りと書き込みのライフサイクルのホワイトペーパーをご覧ください。

大まかに言えば、どちらのアーキテクチャも、基盤となるクラスタにリソースが追加されるとスケーリングされます。Spanner のコンピューティングとストレージの分離により、ワークロードの変化に応じてコンピューティング ノード間の負荷を迅速に再分散できます。Cassandra とは異なり、シャードの移動ではデータが Colossus に残るため、データの移動は行われません。さらに、データをパーティション キーで並べ替えることを想定しているアプリケーションには、Spanner の範囲ベースのパーティショニングがより適している場合があります。範囲ベースのパーティショニングの欠点は、キースペースの一端に書き込むワークロード(現在のタイムスタンプでキーが設定されたテーブルなど)で、追加のスキーマ設計を考慮せずにホットスポットが発生する可能性があることです。ホットスポットを克服する手法の詳細については、スキーマ設計のベスト プラクティスをご覧ください。

整合性

Cassandra では、オペレーションごとに整合性レベルを指定する必要があります。クォーラム整合性レベルを使用する場合、オペレーションが成功したと見なされるには、レプリカノードの過半数がコーディネーター ノードに応答する必要があります。整合性レベルが 1 の場合、オペレーションが成功と見なされるには、Cassandra が単一のレプリカノードに応答する必要があります。

Spanner は強整合性を提供します。Spanner API はレプリカをクライアントに公開しません。Spanner のクライアントは、Spanner を単一マシン データベースとして操作します。書き込みは、ユーザーに承認される前に、常にレプリカの過半数に書き込まれます。その後の読み取りでは、新しく書き込まれたデータが反映されます。アプリケーションは、過去の時点でデータベースのスナップショットを読み取るように選択できます。これにより、強力な読み取りよりもパフォーマンスが向上する可能性があります。Spanner の一貫性プロパティの詳細については、トランザクションの概要をご覧ください。

Spanner は、大規模なアプリケーションに必要な整合性と可用性をサポートするように構築されています。Spanner は、大規模で高パフォーマンスの強整合性を提供します。必要なユースケースでは、Spanner はスナップショット読み取りをサポートし、新鮮度要件を緩和します。

データ モデリング

このセクションでは、Cassandra と Spanner のデータモデルを比較します。

テーブル宣言

テーブル宣言の構文は、Cassandra と Spanner でかなり似ています。テーブル名、列名と型、行を一意に識別する主キーを指定します。主な違いは、Cassandra はハッシュ パーティショニングであり、パーティション キーと並べ替えキーを区別するのに対し、Spanner は範囲パーティショニングである点です。Spanner は、並べ替えキーのみがあり、パーティションはバックグラウンドで自動的に維持されると考えることができます。Cassandra と同様に、Spanner は複合主キーをサポートしています。

単一の主キー部分

Cassandra と Spanner の違いは、型名と主キー句の位置にあります。

Cassandra Spanner
CREATE TABLE users (
  user_id    bigint,
  first_name text,
  last_name  text,
  PRIMARY KEY (user_id)
)
    
CREATE TABLE users (
  user_id    int64,
  first_name string(max),
  last_name  string(max),
) PRIMARY KEY (user_id)
    

複数の主キー部分

Cassandra の場合、最初の主キー部分は「パーティション キー」で、後続の主キー部分は「並べ替えキー」です。Spanner には個別のパーティション キーはありません。データは、複合主キー全体で並べ替えられて保存されます。

Cassandra Spanner
CREATE TABLE user_items (
  user_id    bigint,
  item_id    bigint,
  first_name text,
  last_name  text,
  PRIMARY KEY (user_id, item_id)
)
    
CREATE TABLE user_items (
  user_id    int64,
  item_id    int64,
  first_name string(max),
  last_name  string(max),
) PRIMARY KEY (user_id, item_id)
    

複合パーティション キー

Cassandra では、パーティション キーは複合キーにできます。Spanner には個別のパーティション キーはありません。データは、複合主キー全体で並べ替えられて保存されます。

Cassandra Spanner
CREATE TABLE user_category_items (
  user_id     bigint,
  category_id bigint,
  item_id     bigint,
  first_name  text,
  last_name   text,
  PRIMARY KEY ((user_id, category_id), item_id)
)
    
CREATE TABLE user_category_items (
  user_id     int64,
  category_id int64,
  item_id     int64,
  first_name  string(max),
  last_name   string(max),
) PRIMARY KEY (user_id, category_id, item_id)
    

データ型

このセクションでは、Cassandra と Spanner のデータ型を比較します。Spanner 型の詳細については、GoogleSQL のデータ型をご覧ください。

Cassandra Spanner
数値型 標準整数:

bigint(64 ビット符号付き整数)
int(32 ビット符号付き整数)
smallint(16 ビット符号付き整数)
tinyint(8 ビット符号付き整数)
int64(64 ビット符号付き整数)

Spanner は、符号付き整数に単一の 64 ビット幅のデータ型をサポートしています。
標準浮動小数点:

double(64 ビット IEEE-754 浮動小数点)
float(32 ビット IEEE-754 浮動小数点)
float64(64 ビット IEEE-754 浮動小数点)
float32(32 ビット IEEE-754 浮動小数点)
可変精度の数値:

varint(可変精度の整数)
decimal(可変精度の小数)
固定小数点小数値の場合は、numeric(精度 38、スケール 9)を使用します。それ以外の場合は、アプリケーション レイヤの可変精度整数ライブラリと組み合わせて string を使用します。
文字列型 text
varchar
string(max)

textvarchar はどちらも UTF-8 文字列を保存して検証します。Spanner では、string 列の最大長を指定する必要があります(ストレージには影響しません。これは検証目的です)。
blob bytes(max)

バイナリデータを保存するには、bytes データ型を使用します。
日付と時刻の型 date date
duration int64

Spanner は専用の期間データ型をサポートしていません。ナノ秒の期間を保存するには、int64 を使用します。
time int64

Spanner は、専用の時刻データ型をサポートしていません。int64 を使用して、1 日以内のナノ秒オフセットを保存します。
timestamp timestamp
コンテナタイプ ユーザー定義タイプ json または proto
list array

array を使用して、型付きオブジェクトのリストを保存します。
map json または proto

Spanner は専用のマップタイプをサポートしていません。地図を表すには、json 列または proto 列を使用します。詳細については、大規模な地図をインターリーブ テーブルとして保存するをご覧ください。
set array

Spanner は専用のセット型をサポートしていません。array 列を使用して set を表し、アプリケーションがセットの一意性を管理します。詳細については、大きな地図をインターリーブ テーブルとして保存するをご覧ください。これは、大規模なセットの保存にも使用できます。

基本的な使用パターン

次のコードサンプルは、Go の Cassandra クライアント コードと Spanner クライアント コードの違いを示しています。詳細については、Spanner クライアント ライブラリをご覧ください。

クライアントの初期化

Cassandra クライアントでは、基盤となる Cassandra クラスタを表すクラスタ オブジェクトを作成し、クラスタへの接続を抽象化するセッション オブジェクトをインスタンス化して、セッションに対してクエリを実行します。Spanner では、特定のデータベースにバインドされたクライアント オブジェクトを作成し、クライアント オブジェクトに対してデータベース リクエストを発行します。

Cassandra の例

Go

import "github.com/gocql/gocql"

...

cluster := gocql.NewCluster("<address>")
cluster.Keyspace = "<keyspace>"
session, err := cluster.CreateSession()
if err != nil {
  return err
}
defer session.Close()

// session.Query(...)

Spanner の例

Go

import "cloud.google.com/go/spanner"

...

client, err := spanner.NewClient(ctx,
    fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, database))
defer client.Close()

// client.Apply(...)

データを読み取る

Spanner での読み取りは、Key-Value スタイルの APIクエリ API の両方を使用して実行できます。Cassandra ユーザーは、クエリ API の方が使い慣れているかもしれません。クエリ API の主な違いは、Spanner では名前付き引数が必要になることです(Cassandra の位置引数 ? とは異なります)。Spanner クエリ内の引数の名前には、接頭辞 @ を付ける必要があります。

Cassandra の例

Go

stmt := `SELECT
           user_id, first_name, last_name
         FROM
           users
         WHERE
           user_id = ?`

var (
  userID    int
  firstName string
  lastName  string
)

err := session.Query(stmt, 1).Scan(&userID, &firstName, &lastName)

Spanner の例

Go

stmt := spanner.Statement{
  SQL: `SELECT
          user_id, first_name, last_name
        FROM
          users
        WHERE
          user_id = @user_id`,
  Params: map[string]any{"user_id": 1},
}

var (
  userID    int64
  firstName string
  lastName  string
)

err := client.Single().Query(ctx, stmt).Do(func(row *spanner.Row) error {
  return row.Columns(&userID, &firstName, &lastName)
})

データの挿入

Cassandra の INSERT は、Spanner の INSERT OR UPDATE と同じです。挿入には、完全な主キーを指定する必要があります。Spanner は、DML とキーバリュー スタイルのミューテーション API の両方をサポートしています。レイテンシが低いため、単純な書き込みには Key-Value スタイルのミューテーション API をおすすめします。Spanner DML API は、SQL サーフェス全体(DML ステートメントでの式の使用など)をサポートしているため、より多くの機能を備えています。

Cassandra の例

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)`
err := session.Query(stmt, 1, "John", "Doe").Exec()

Spanner の例

Go

_, err := client.Apply(ctx, []*spanner.Mutation{
  spanner.InsertOrUpdateMap(
    "users", map[string]any{
      "user_id":    1,
      "first_name": "John",
      "last_name":  "Doe",
    }
  )})

データをバッチで挿入する

Cassandra では、バッチ ステートメントを使用して複数の行を挿入できます。Spanner では、commit オペレーションに複数のミューテーションを含めることができます。Spanner は、これらのミューテーションをデータベースにアトミックに挿入します。

Cassandra の例

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)`
b := session.NewBatch(gocql.UnloggedBatch)
b.Entries = []gocql.BatchEntry{
  {Stmt: stmt, Args: []any{1, "John", "Doe"}},
  {Stmt: stmt, Args: []any{2, "Mary", "Poppins"}},
}
err = session.ExecuteBatch(b)

Spanner の例

Go

_, err := client.Apply(ctx, []*spanner.Mutation{
  spanner.InsertOrUpdateMap(
    "users", map[string]any{
       "user_id":    1,
       "first_name": "John",
       "last_name":  "Doe"
    },
  ),
  spanner.InsertOrUpdateMap(
    "users", map[string]any{
       "user_id":    2,
       "first_name": "Mary",
       "last_name":  "Poppins",
    },
  ),
})

データの削除

Cassandra の削除では、削除する行の主キーを指定する必要があります。これは、Spanner の DELETE ミューテーションに似ています。

Cassandra の例

Go

stmt := `DELETE FROM
           users
         WHERE
           user_id = ?`
err := session.Query(stmt, 1).Exec()

Spanner の例

Go

_, err := client.Apply(ctx, []*spanner.Mutation{
  spanner.Delete("users", spanner.Key{1}),
})

高度なトピック

このセクションでは、Spanner で高度な Cassandra 機能を使用する方法について説明します。

書き込みタイムスタンプ

Cassandra では、USING TIMESTAMP 句を使用して、特定のセルの書き込みタイムスタンプをミューテーションで明示的に指定できます。通常、この機能は Cassandra の「最後に書き込んだユーザーが勝ち」のセマンティクスを操作するために使用されます。

Spanner では、クライアントが各書き込みのタイムスタンプを指定することはできません。各セルには、セル値が commit された時点での TrueTime タイムスタンプが内部的にマークされます。Spanner は強整合性と厳密なシリアル化可能なインターフェースを提供するため、ほとんどのアプリケーションでは USING TIMESTAMP の機能は必要ありません。

アプリケーション固有のロジックに Cassandra の USING TIMESTAMP を使用する場合は、Spanner スキーマに追加の TIMESTAMP 列を追加できます。これにより、アプリケーション レベルで変更時間を追跡できます。行の更新は、読み取り / 書き込みトランザクションでラップできます。次に例を示します。

Cassandra の例

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)
         USING TIMESTAMP
           ?`
err := session.Query(stmt, 1, "John", "Doe", ts).Exec()

Spanner の例

  1. 明示的な更新タイムスタンプ列を含むスキーマを作成します。

    GoogleSQL

    CREATE TABLE users (
      user_id    INT64,
      first_name STRING(MAX),
      last_name  STRING(MAX),
      update_ts  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
    ) PRIMARY KEY (user_id)
  2. ロジックをカスタマイズして行を更新し、タイムスタンプを追加します。

    Go

    func ShouldUpdateRow(ctx context.Context, txn *spanner.ReadWriteTransaction, updateTs time.Time) (bool, error) {
      // Read the existing commit timestamp.
      row, err := txn.ReadRow(ctx, "users", spanner.Key{1}, []string{"update_ts"})
    
      // Treat non-existent row as NULL timestamp - the row should be updated.
      if spanner.ErrCode(err) == codes.NotFound {
        return true, nil
      }
    
      // Propagate unexpected errors.
      if err != nil {
        return false, err
      }
    
      // Check if the committed timestamp is newer than the update timestamp.
      var committedTs *time.Time
      err = row.Columns(&committedTs)
      if err != nil {
        return false, err
      }
      if committedTs != nil && committedTs.Before(updateTs) {
        return false, nil
      }
    
      // Committed timestamp is older than update timestamp - the row should be updated.
      return true, nil
    }
  3. 行を更新する前にカスタム条件を確認します。

    Go

    _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
      // Check if the row should be updated.
      ok, err := ShouldUpdateRow(ctx, txn, time.Now())
      if err != nil {
        return err
      }
      if !ok {
        return nil
      }
    
      // Update the row.
      txn.BufferWrite([]*spanner.Mutation{
        spanner.InsertOrUpdateMap("users", map[string]any{
          "user_id":    1,
          "first_name": "John",
          "last_name":  "Doe",
          "update_ts":  spanner.CommitTimestamp,
        })})
    
      return nil
    })

条件付きミューテーション

Cassandra の INSERT ... IF EXISTS ステートメントは、Spanner の INSERT ステートメントと同等です。どちらの場合も、行がすでに存在する場合、挿入は失敗します。

Cassandra では、条件を指定する DML ステートメントを作成することもできます。条件が false と評価された場合、ステートメントは失敗します。Spanner では、読み取り / 書き込みトランザクションで条件付きの UPDATE ミューテーションを使用できます。たとえば、特定の条件が存在する場合にのみ行を更新するには、次のようにします。

Cassandra の例

Go

stmt := `UPDATE
           users
         SET
           last_name = ?
         WHERE
           user_id = ?
         IF
           first_name = ?`
err := session.Query(stmt, 1, "Smith", "John").Exec()

Spanner の例

  1. ロジックをカスタマイズして行を更新し、条件を含めます。

    Go

    func ShouldUpdateRow(ctx context.Context, txn *spanner.ReadWriteTransaction) (bool, error) {
      row, err := txn.ReadRow(ctx, "users", spanner.Key{1}, []string{"first_name"})
      if err != nil {
        return false, err
      }
    
      var firstName *string
      err = row.Columns(&firstName)
      if err != nil {
        return false, err
      }
      if firstName != nil && firstName == "John" {
        return false, nil
      }
      return true, nil
    }
  2. 行を更新する前にカスタム条件を確認します。

    Go

    _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
      ok, err := ShouldUpdateRow(ctx, txn, time.Now())
      if err != nil {
        return err
      }
      if !ok {
        return nil
      }
    
      txn.BufferWrite([]*spanner.Mutation{
        spanner.InsertOrUpdateMap("users", map[string]any{
          "user_id":    1,
          "last_name":  "Smith",
          "update_ts":  spanner.CommitTimestamp,
        })})
    
      return nil
    })

TTL

Cassandra は、行レベルまたは列レベルで有効期間(TTL)値の設定をサポートしています。Spanner では、TTL は行レベルで構成され、名前付き列を行の有効期限として指定します。詳細については、有効期間(TTL)の概要をご覧ください。

Cassandra の例

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)
         USING TTL 86400
           ?`
err := session.Query(stmt, 1, "John", "Doe", ts).Exec()

Spanner の例

  1. 明示的な更新タイムスタンプ列を含むスキーマを作成する

    GoogleSQL

    CREATE TABLE users (
      user_id    INT64,
      first_name STRING(MAX),
      last_name  STRING(MAX),
      update_ts  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
    ) PRIMARY KEY (user_id),
      ROW DELETION POLICY (OLDER_THAN(update_ts, INTERVAL 1 DAY));
  2. commit タイムスタンプを含む行を挿入します。

    Go

    _, err := client.Apply(ctx, []*spanner.Mutation{
      spanner.InsertOrUpdateMap("users", map[string]any{
                  "user_id":    1,
                  "first_name": "John",
                  "last_name":  "Doe",
                  "update_ts":  spanner.CommitTimestamp}),
    })

大きな地図をインターリーブ テーブルとして保存します。

Cassandra は、順序付き Key-Value ペアを格納するための map 型をサポートしています。少量のデータを含む map 型を Spanner に保存するには、JSON 型または PROTO 型を使用します。これらの型を使用すると、それぞれ半構造化データと構造化データを保存できます。このような列を更新するには、列の値全体を書き換える必要があります。Cassandra map に大量のデータが保存され、map のごく一部のみを更新する必要があるユースケースでは、INTERLEAVED テーブルの使用が適しています。たとえば、大量の Key-Value データを特定のユーザーに関連付けるには:

Cassandra の例

CREATE TABLE users (
  user_id     bigint,
  attachments map<string, string>,
  PRIMARY KEY (user_id)
)

Spanner の例

CREATE TABLE users (
  user_id  INT64,
) PRIMARY KEY (user_id);

CREATE TABLE user_attachments (
  user_id        INT64,
  attachment_key STRING(MAX),
  attachment_val STRING(MAX),
) PRIMARY KEY (user_id, attachment_key);

この場合、ユーザー アタッチメント行は対応するユーザー行と同じ場所に保存され、ユーザー行とともに効率的に取得および更新できます。Spanner の読み取り / 書き込み API を使用して、インターリーブ テーブルを操作できます。インターリーブの詳細については、親テーブルと子テーブルを作成するをご覧ください。

デベロッパー エクスペリエンス

このセクションでは、Spanner と Cassandra のデベロッパー ツールを比較します。

ローカルでの開発

Cassandra をローカルで実行して、開発と単体テストを行うことができます。Spanner には、Spanner エミュレータを使用してローカル開発に類似した環境が用意されています。エミュレータは、インタラクティブな開発と単体テスト用の高忠実度環境を提供します。詳細については、Spanner をローカルでエミュレートするをご覧ください。

コマンドライン

Cassandra の nodetool に相当する Spanner は Google Cloud CLI です。gcloud spanner を使用して、コントロール プレーンとデータプレーンのオペレーションを実行できます。詳細については、Google Cloud CLI Spanner リファレンス ガイドをご覧ください。

cqlsh と同様に Spanner にクエリを実行する REPL インターフェースが必要な場合は、spanner-cli ツールを使用できます。Go で spanner-cli をインストールして実行するには:

go install github.com/cloudspannerecosystem/spanner-cli@latest

$(go env GOPATH)/bin/spanner-cli

詳細については、spanner-cli GitHub リポジトリをご覧ください。