En este documento, se comparan los conceptos y las prácticas de Apache Cassandra y Spanner. Se da por sentado que conoces Cassandra y deseas migrar aplicaciones existentes o diseñar aplicaciones nuevas mientras usas Spanner como base de datos.
Cassandra y Spanner son bases de datos distribuidas a gran escala que se compilan para aplicaciones que requieren alta escalabilidad y baja latencia. Si bien ambas bases de datos pueden admitir cargas de trabajo NoSQL exigentes, Spanner proporciona funciones avanzadas para el modelado de datos, las consultas y las operaciones transaccionales. Para obtener más información sobre cómo Spanner cumple con los criterios de las bases de datos NoSQL, consulta Spanner para cargas de trabajo no relacionales.
Migra de Cassandra a Spanner
Para migrar de Cassandra a Spanner, puedes usar el adaptador de proxy de Cassandra a Spanner. Esta herramienta de código abierto te permite migrar cargas de trabajo de Cassandra a Spanner sin realizar cambios en la lógica de tu aplicación.
Conceptos básicos
En esta sección, se comparan los conceptos clave de Cassandra y Spanner.
Terminología
Cassandra | Spanner |
---|---|
Clúster |
Instancia Un clúster de Cassandra equivale a una instancia de Spanner, que es una colección de servidores y recursos de almacenamiento. Debido a que Spanner es un servicio administrado, no tienes que configurar el hardware o software subyacente. Solo debes especificar la cantidad de nodos que deseas reservar para tu instancia o elegir el ajuste de escala automático para escalar la instancia automáticamente. Una instancia actúa como un contenedor para las bases de datos, y la topología de replicación de datos (regional, birregional o multirregional) se elige a nivel de la instancia. |
Espacio de claves |
Base de datos Un espacio de claves de Cassandra equivale a una base de datos de Spanner, que es una colección de tablas y otros elementos de esquema (por ejemplo, índices y roles). A diferencia de un espacio de claves, no necesitas configurar el factor de replicación. Spanner replica automáticamente tus datos en la región designada en tu instancia. |
Tabla |
Tabla En Cassandra y Spanner, las tablas son una colección de filas identificadas por una clave primaria especificada en el esquema de la tabla. |
Partición |
División Tanto Cassandra como Spanner se escalan mediante el particionamiento de datos. En Cassandra, cada fragmento se denomina partición, mientras que en Spanner, cada fragmento se denomina división. Cassandra usa el particionamiento de hash, lo que significa que cada fila se asigna de forma independiente a un nodo de almacenamiento según un hash de la clave primaria. Spanner se divide en intervalos, lo que significa que las filas que son contiguas en el espacio de la clave primaria también lo son en el almacenamiento (excepto en los límites de división). Spanner se encarga de dividir y combinar los datos según la carga y el almacenamiento, y esto es transparente para la aplicación. La implicación clave es que, a diferencia de Cassandra, el análisis de rangos sobre un prefijo de la clave primaria es una operación eficiente en Spanner. |
Fila |
Fila En Cassandra y Spanner, una fila es un conjunto de columnas identificadas de forma única por una clave primaria. Al igual que Cassandra, Spanner admite claves primarias compuestas. A diferencia de Cassandra, Spanner no hace una distinción entre la clave de partición y la clave de orden, ya que los datos se fragmentan por intervalo. Se puede pensar en Spanner como si solo tuviera claves de orden, con la partición administrada en segundo plano. |
Columna |
Columna En Cassandra y Spanner, una columna es un conjunto de valores de datos que tienen el mismo tipo. Hay un valor para cada fila de una tabla. Para obtener más información sobre cómo comparar los tipos de columnas de Cassandra con los de Spanner, consulta Tipos de datos. |
Arquitectura
Un clúster de Cassandra consta de un conjunto de servidores y almacenamiento ubicados junto con esos servidores. Una función hash asigna filas de un espacio de claves de partición a un nodo virtual (vnode). Luego, se asigna de forma aleatoria un conjunto de vnodes a cada servidor para entregar una parte del espacio de claves del clúster. El almacenamiento de los vnodes se conecta de forma local al nodo de entrega. Los controladores de cliente se conectan directamente a los nodos de entrega y controlan el balanceo de cargas y el enrutamiento de consultas.
Una instancia de Spanner consta de un conjunto de servidores en una topología de replicación. Spanner particiona cada tabla de forma dinámica en rangos de filas según el uso de la CPU y el disco. Los fragmentos se asignan a los nodos de procesamiento para la publicación. Los datos se almacenan físicamente en Colossus, el sistema de archivos distribuidos de Google, por separado de los nodos de procesamiento. Los controladores de cliente se conectan a los servidores de frontend de Spanner, que realizan el enrutamiento de solicitudes y el balanceo de cargas. Para obtener más información, consulta el informe técnico Ciclo de vida de las operaciones de lectura y escritura de Spanner.
En un nivel alto, ambas arquitecturas se escalan a medida que se agregan recursos al clúster subyacente. La separación de procesamiento y almacenamiento de Spanner permite un reequilibrio más rápido de la carga entre los nodos de procesamiento en respuesta a los cambios en la carga de trabajo. A diferencia de Cassandra, los traslados de fragmentos no implican traslados de datos, ya que estos permanecen en Colosus. Además, la partición basada en rangos de Spanner podría ser más natural para las aplicaciones que esperan que los datos se ordenen por clave de partición. El inconveniente de la partición basada en el rango es que las cargas de trabajo que escriben en un extremo del espacio de claves (por ejemplo, tablas con claves por marca de tiempo actual) pueden tener problemas de hotspots sin consideración adicional del diseño del esquema. Para obtener más información sobre las técnicas para superar los hotspots, consulta Prácticas recomendadas para el diseño de esquemas.
Coherencia
Con Cassandra, debes especificar un nivel de coherencia para cada operación. Si usas el nivel de coherencia de quórum, la mayoría de los nodos de réplica deben responder al nodo coordinador para que la operación se considere correcta. Si usas un nivel de coherencia de uno, Cassandra necesita un nodo de réplica único para responder para que la operación se considere correcta.
Spanner proporciona coherencia sólida. La API de Spanner no expone réplicas al cliente. Los clientes de Spanner interactúan con él como si fuera una base de datos de una sola máquina. Una operación de escritura siempre se escribe en la mayoría de las réplicas antes de confirmarse al usuario. Las lecturas posteriores reflejan los datos que se escribieron recientemente. Las aplicaciones pueden optar por leer una instantánea de la base de datos en un momento anterior, lo que podría tener beneficios de rendimiento en comparación con las lecturas sólidas. Para obtener más información sobre las propiedades de coherencia de Spanner, consulta la Descripción general de las transacciones.
Spanner se creó para admitir la coherencia y la disponibilidad necesarias en aplicaciones a gran escala. Spanner proporciona una coherencia sólida a gran escala y con alto rendimiento. Para los casos de uso que lo requieran, Spanner admite lecturas de instantáneas que relajan los requisitos de actualización.
Modelado de datos
En esta sección, se comparan los modelos de datos de Cassandra y Spanner.
Declaración de tabla
La sintaxis de declaración de tablas es bastante similar en Cassandra y Spanner. Especificas el nombre de la tabla, los nombres y tipos de las columnas, y la clave primaria que identifica de forma única una fila. La diferencia clave es que Cassandra está particionada por hash y hace una distinción entre la clave de partición y la clave de orden, mientras que Spanner está particionada por rango. Se puede considerar que Spanner solo tiene claves de ordenamiento, con particiones que se mantienen automáticamente en segundo plano. Al igual que Cassandra, Spanner admite claves primarias compuestas.
Parte única de clave primaria
La diferencia entre Cassandra y Spanner está en los nombres de los tipos y la ubicación de la cláusula de clave principal.
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) |
Varias partes de la clave primaria
En Cassandra, la primera parte de la clave primaria es la "clave de partición" y las partes posteriores de la clave primaria son "claves de orden". En Spanner, no hay una clave de partición independiente. Los datos se almacenan ordenados por toda la clave primaria compuesta.
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) |
Clave de partición compuesta
En Cassandra, las claves de partición pueden ser un compuesto. No hay una clave de partición independiente en Spanner. Los datos se almacenan ordenados por toda la clave primaria compuesta.
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) |
Tipos de datos
En esta sección, se comparan los tipos de datos de Cassandra y Spanner. Para obtener más información sobre los tipos de Spanner, consulta Tipos de datos en GoogleSQL.
Cassandra | Spanner | |
---|---|---|
Tipos numéricos |
Números enteros estándar:bigint (número entero de 64 bits con firma)int (número entero de 32 bits con firma)smallint (número entero de 16 bits con firma)tinyint (número entero de 8 bits con firma)
|
int64 (número entero de 64 bits con firma)Spanner admite un solo tipo de datos de 64 bits para números enteros con firma. |
Número de punto flotante estándar:double (número de punto flotante IEEE-754 de 64 bits)float (número de punto flotante IEEE-754 de 32 bits) |
float64 (número de punto flotante IEEE 754 de 64 bits)float32 (número de punto flotante IEEE 754 de 32 bits)
|
|
Números de precisión variable:varint (número entero de precisión variable)decimal (número decimal de precisión variable)
|
Para números decimales de precisión fija, usa numeric (precisión 38, escala 9).
De lo contrario, usa string junto con una biblioteca de números enteros de precisión variable de la capa de aplicación.
|
|
Tipos de cadenas |
text varchar
|
string(max) Tanto text como varchar almacenan y validan cadenas UTF-8. En Spanner, las columnas string deben especificar su longitud máxima (no hay impacto en el almacenamiento; esto es para fines de validación).
|
blob |
bytes(max) Para almacenar datos binarios, usa el tipo de datos bytes .
|
|
Tipos de fecha y hora | date |
date |
duration |
int64 Spanner no admite un tipo de datos de duración dedicado. Usa int64 para almacenar la duración en nanosegundos.
|
|
time |
int64 Spanner no admite un tipo de datos de hora del día dedicada. Usa int64 para almacenar el desplazamiento en nanosegundos dentro de un día.
|
|
timestamp |
timestamp |
|
Tipos de contenedores | Tipos de usuarios definidos | json o proto |
list |
array Usa array para almacenar una lista de objetos escritos.
|
|
map |
json o proto Spanner no admite un tipo de mapa dedicado. Usa columnas json o proto para representar mapas. Para obtener más información, consulta Cómo almacenar mapas grandes como tablas intercaladas.
|
|
set |
array Spanner no admite un tipo de conjunto dedicado. Usa columnas array para representar un set , con la aplicación que administra la unicidad establecida. Para obtener más información, consulta Almacena mapas grandes como tablas intercaladas, que también se pueden usar para almacenar conjuntos grandes.
|
Patrones de uso básicos
En los siguientes ejemplos de código, se muestra la diferencia entre el código cliente de Cassandra y Spanner en Go. Para obtener más información, consulta Bibliotecas cliente de Spanner.
Inicialización del cliente
En los clientes de Cassandra, creas un objeto de clúster que representa el clúster de Cassandra subyacente, creas una instancia de un objeto de sesión que abstrae una conexión al clúster y emites consultas en la sesión. En Spanner, creas un objeto cliente vinculado a una base de datos específica y emites solicitudes de base de datos en el objeto cliente.
Ejemplo de 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(...)
Ejemplo de 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(...)
Lee datos
Las operaciones de lectura en Spanner se pueden realizar a través de una API de estilo par clave-valor y una API de consulta. Como usuario de Cassandra, es posible que la API de consulta te resulte más familiar. Una diferencia clave en la API de consulta es que Spanner requiere argumentos nombrados (a diferencia de los argumentos posicionales ?
en Cassandra). El nombre de un argumento en una consulta de Spanner debe tener el prefijo @
.
Ejemplo de 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)
Ejemplo de 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) })
Inserta datos
Un INSERT
de Cassandra equivale a un INSERT OR UPDATE
de Spanner.
Debes especificar la clave primaria completa para una inserción. Spanner
admite la DML y una API de mutación de estilo clave-valor. Se recomienda la API de mutación de estilo de par clave-valor para las operaciones de escritura triviales debido a la menor latencia.
La API de DML de Spanner tiene más funciones, ya que admite la plataforma de SQL completa (incluido el uso de expresiones en la sentencia DML).
Ejemplo de Cassandra
Go
stmt := `INSERT INTO users (user_id, first_name, last_name) VALUES (?, ?, ?)` err := session.Query(stmt, 1, "John", "Doe").Exec()
Ejemplo de Spanner
Go
_, err := client.Apply(ctx, []*spanner.Mutation{ spanner.InsertOrUpdateMap( "users", map[string]any{ "user_id": 1, "first_name": "John", "last_name": "Doe", } )})
Cómo insertar datos por lotes
En Cassandra, puedes insertar varias filas con una sentencia por lotes. En Spanner, una operación de confirmación puede contener varias mutaciones. Spanner inserta estas mutaciones en la base de datos de forma atómica.
Ejemplo de 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)
Ejemplo de 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", }, ), })
Borra datos
Las operaciones de eliminación de Cassandra requieren especificar la clave primaria de las filas que se borrarán.
Esto es similar a la mutación DELETE
en Spanner.
Ejemplo de Cassandra
Go
stmt := `DELETE FROM users WHERE user_id = ?` err := session.Query(stmt, 1).Exec()
Ejemplo de Spanner
Go
_, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Delete("users", spanner.Key{1}), })
Temas avanzados
En esta sección, se incluye información para usar funciones más avanzadas de Cassandra en Spanner.
Marca de tiempo de escritura
Cassandra permite que las mutaciones especifiquen de forma explícita una marca de tiempo de escritura para una celda en particular con la cláusula USING TIMESTAMP
. Por lo general, esta función se usa para manipular la semántica de último escritor gana de Cassandra.
Spanner no permite que los clientes especifiquen la marca de tiempo de cada
escritura. Cada celda se marca internamente con la marca de tiempo TrueTime en el momento en que se confirmó el valor de la celda. Debido a que Spanner proporciona una interfaz con coherencia sólida y estrictamente serializable, la mayoría de las aplicaciones no necesitan la funcionalidad de USING TIMESTAMP
.
Si dependes de USING TIMESTAMP
de Cassandra para la lógica específica de la aplicación, puedes agregar una
columna TIMESTAMP
adicional a tu esquema de Spanner, que puede hacer un seguimiento del tiempo de modificación a nivel de la
aplicación. Luego, las actualizaciones de una fila se pueden unir en una transacción de lectura-escritura. Por ejemplo:
Ejemplo de Cassandra
Go
stmt := `INSERT INTO users (user_id, first_name, last_name) VALUES (?, ?, ?) USING TIMESTAMP ?` err := session.Query(stmt, 1, "John", "Doe", ts).Exec()
Ejemplo de Spanner
Crea un esquema con una columna de marca de tiempo de actualización explícita.
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)
Personaliza la lógica para actualizar la fila y, luego, incluye una marca de tiempo.
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 }
Verifica la condición personalizada antes de actualizar la fila.
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 })
Mutaciones condicionales
La sentencia INSERT ... IF EXISTS
en Cassandra equivale a la sentencia INSERT
en Spanner. En ambos casos, la inserción falla si la fila ya existe.
En Cassandra, también puedes crear instrucciones DML que especifiquen una condición, y la instrucción fallará si la condición se evalúa como falsa. En
Spanner, puedes usar mutaciones UPDATE
condicionales en transacciones de lectura y escritura. Por ejemplo, para actualizar una fila solo si existe una condición particular, haz lo siguiente:
Ejemplo de Cassandra
Go
stmt := `UPDATE users SET last_name = ? WHERE user_id = ? IF first_name = ?` err := session.Query(stmt, 1, "Smith", "John").Exec()
Ejemplo de Spanner
Personaliza la lógica para actualizar la fila y, luego, incluye una condición.
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 }
Verifica la condición personalizada antes de actualizar la fila.
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 admite la configuración de un valor de tiempo de actividad (TTL) a nivel de la fila o la columna. En Spanner, el TTL se configura a nivel de la fila, y designas una columna con nombre como la hora de vencimiento de la fila. Para obtener más información, consulta la descripción general del tiempo de actividad (TTL).
Ejemplo de 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()
Ejemplo de Spanner
Crea un esquema con una columna de marca de tiempo de actualización explícita
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));
Inserta filas con una marca de tiempo de confirmación.
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}), })
Almacena mapas grandes como tablas intercaladas.
Cassandra admite el tipo map
para almacenar pares clave-valor ordenados. Para almacenar tipos map
que contengan una pequeña cantidad de datos en Spanner, puedes usar los tipos JSON
o PROTO
, que te permiten almacenar datos semiestructurados y estructurados, respectivamente.
Las actualizaciones de esas columnas requieren que se vuelva a escribir todo el valor de la columna. Si tienes un caso de uso en el que se almacena una gran cantidad de datos en un map
de Cassandra y solo se debe actualizar una pequeña parte del map
, usar tablas INTERLEAVED
podría ser una buena opción. Por ejemplo, para asociar una gran cantidad de datos de par clave-valor con un usuario en particular, haz lo siguiente:
Ejemplo de Cassandra
CREATE TABLE users (
user_id bigint,
attachments map<string, string>,
PRIMARY KEY (user_id)
)
Ejemplo de 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);
En este caso, una fila de archivos adjuntos del usuario se almacena junto con la fila correspondiente del usuario y se puede recuperar y actualizar de manera eficiente junto con la fila del usuario. Puedes usar las APIs de lectura y escritura en Spanner para interactuar con tablas intercaladas. Para obtener más información sobre la intercalación, consulta Cómo crear tablas principales y secundarias.
Experiencia de los desarrolladores
En esta sección, se comparan las herramientas para desarrolladores de Spanner y Cassandra.
Desarrollo local
Puedes ejecutar Cassandra de forma local para el desarrollo y las pruebas de unidades. Spanner proporciona un entorno similar para el desarrollo local a través del emulador de Spanner. El emulador proporciona un entorno de alta fidelidad para el desarrollo interactivo y las pruebas de unidades. Para obtener más información, consulta Cómo emular Spanner de forma local.
Línea de comandos
El equivalente de Spanner a nodetool
de Cassandra es Google Cloud CLI. Puedes realizar operaciones del plano de control y del plano de datos con gcloud spanner
. Para obtener más información, consulta la guía de referencia de Spanner de Google Cloud CLI.
Si necesitas una interfaz de REPL para emitir consultas a Spanner similar a cqlsh
, puedes usar la herramienta spanner-cli
. Para instalar y ejecutar spanner-cli
en Go, haz lo siguiente:
go install github.com/cloudspannerecosystem/spanner-cli@latest
$(go env GOPATH)/bin/spanner-cli
Para obtener más información, consulta el repositorio de GitHub de spanner-cli.