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 o DataStax Enterprise (DSE) a Spanner, cualquier cambio 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 es equivalente a un clúster de Spanner instancia: un conjunto de servidores y de almacenamiento de Google. Debido a que Spanner es un servicio administrado, no tienes que configurar el hardware o software subyacente. Tú solo debes especificar la cantidad de nodos que quieres reservar para tu instancia o elige ajuste de escala automático 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 teclas |
Base de datos Un espacio de claves de Cassandra es equivalente a un espacio de Spanner database, que es una colección de tablas y otros elementos del esquema (por ejemplo, índices y roles). A diferencia de un espacio de teclas, no necesitas configurar el factor de replicación. Spanner replica automáticamente tus datos en la región designada en tu instancia. |
Tabla |
Tabla Tanto en Cassandra como en 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 escalan fragmentando datos. En Cassandra, cada fragmento se denomina partición, mientras que, en Spanner, cada fragmento se llama 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 está fragmentado por rangos, lo que significa que las filas contiguas en el espacio de clave primaria también son contiguas en el almacenamiento (excepto en 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 una colección 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 clave de partición y clave de orden, ya que los datos fragmentados por rango. Se puede pensar en Spanner como si solo tuviera claves de ordenamiento, con la partición administrada en segundo plano. |
Columna |
Columna Tanto en Cassandra como en Spanner, una columna es un conjunto de datos con valores del mismo tipo. Hay un valor para cada fila de una tabla. Para obtener más información sobre cómo comparar tipos de columnas de Cassandra con 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 (vnode). Luego, se asigna de forma aleatoria un conjunto de vnodos a cada servidor para que entregue 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 cliente se conectan directamente a los nodos de entrega se encargan del balanceo de cargas y el enrutamiento de consultas.
Una instancia de Spanner consta de un conjunto de servidores en un topología de replicación. Spanner particiona cada tabla de forma dinámica en rangos de filas según el uso de CPU y 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 del cliente se conectan al 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 una mayor velocidad El rebalanceo de la carga entre los nodos de procesamiento en respuesta a los cambios de 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 generación de hotspots, consulta Prácticas recomendadas para el diseño del esquema.
Coherencia
Con Cassandra, debes especificar un nivel de coherencia para cada operación. Si usas nivel de coherencia de quórum, una mayoría de nodos de réplica debe responder al coordinador para que la operación se considere exitosa. 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 Spanner como si fuera una base de datos de una sola máquina. Una escritura es siempre se escribe en la mayoría de las réplicas antes de confirmarla al usuario. Todas las lecturas posteriores reflejan los datos recién escritos. Las aplicaciones pueden elige leer una instantánea de la base de datos en un momento del pasado, lo que podría tener en comparación con lecturas sólidas. Para obtener más información sobre la 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 protección coherente 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 de 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 inequívoca una fila. La diferencia clave es que Cassandra está particionada con hash y distingue entre clave de partición y clave de ordenación, mientras que Spanner está particionado 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 primaria.
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
Para Cassandra, la primera parte de la clave primaria es la “clave de partición” y las las partes de la clave primaria subsiguientes son “claves de ordenación”. En el caso de Spanner, no es una clave de partición independiente. Los datos se almacenan ordenados por todo el compuesto clave primaria.
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
Para Cassandra, las claves de partición pueden ser compuestas. No hay una partición independiente en Spanner. Los datos se almacenan ordenados por todo el compuesto clave primaria.
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 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 firmado de 64 bits)int (número entero firmado de 32 bits)smallint (número entero firmado de 16 bits)tinyint (número entero firmado de 8 bits)
|
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 con 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,
string columnas deben especificar su longitud máxima (no se produce ningún impacto en
storage; esto se hace con 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 dedicado de tiempo dentro del día. 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 exclusividad del conjunto Para obtener más información, consulta Cómo almacenar mapas grandes como tablas intercaladas.
que también se puede usar para almacenar grandes conjuntos.
|
Patrones de uso básicos
En los siguientes ejemplos de código, se muestra la diferencia entre el código de cliente de Cassandra y de 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, puedes crear un objeto de cliente vinculado a una base de datos específica y emitir solicitudes de base de datos en el objeto del 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). Es el nombre de un
en una consulta de Spanner debe tener un 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 es equivalente 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 Spanner DML tiene más funciones, ya que admite la superficie completa de SQL (incluidas las
el uso de expresiones en la declaración 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", } )})
Inserta 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 eliminaciones de Cassandra requieren que se especifique 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.
Escribir marca de tiempo
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 es
para manipular la semántica
de la última escritura de Cassandra.
Spanner no permite que los clientes especifiquen la marca de tiempo de cada
escribir. Cada celda se marca internamente con la marca de tiempo TrueTime en el
la hora en que se confirmó el valor de la celda. Dado que Spanner proporciona un
coherente y estrictamente serializable, la mayoría de las aplicaciones no necesitan
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. Las actualizaciones de una fila se pueden unir en una regla de lectura y escritura
transacción. 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 e incluir 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 es equivalente a INSERT
en Spanner. En ambos casos, la inserción falla si la fila ya existe.
En Cassandra, también puedes crear declaraciones DML que especifiquen una condición.
la instrucción falla 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 fila y tú designas un como la fecha y 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 explícita de marca de tiempo de actualización
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
map
que contienen una pequeña cantidad de datos en Spanner, puedes
puedes usar JSON
o PROTO
que te permiten almacenar datos semiestructurados y estructurados, respectivamente.
Las actualizaciones a esas columnas requieren que se vuelva a escribir el valor de la columna completa. Si
tienes un caso de uso en el que se almacena una gran cantidad de datos en un map
de Cassandra.
solo se necesita actualizar una pequeña parte de map
, con
INTERLEAVED
tablas pueden ser una buena opción. Por ejemplo, para asociar una gran cantidad de
datos de pares clave-valor con un usuario en particular:
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 adjuntos de usuario se almacena junto con el nombre de usuario de la fila de usuario y se pueden recuperar y actualizar de manera eficiente junto con la fila de usuario. Puedes usar la función de lectura y escritura APIs en Spanner para interactuar con tablas intercaladas. Para ver más para obtener más información sobre la intercalación, consulta Crear tablas superiores y secundarias
Experiencia del desarrollador
En esta sección, se comparan Spanner y Cassandra para desarrolladores.
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 una alta 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 Spanner equivalente a nodetool
de Cassandra es el
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 REPL para emitir consultas a Spanner
De manera similar a cqlsh
, puedes usar la herramienta spanner-cli
. Para instalar y ejecutar
spanner-cli
en Go:
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.