La arquitectura distribuida de Spanner te permite diseñar tu esquema para evitar puntos de acceso, es decir, situaciones en las que se envían demasiadas solicitudes al mismo servidor, lo que satura los recursos del servidor y puede provocar latencias altas.
En esta página se describen las prácticas recomendadas para diseñar tus esquemas y evitar la creación de puntos de acceso. Una forma de evitar los puntos de acceso es ajustar el diseño del esquema para que Spanner pueda dividir y distribuir los datos en varios servidores. Distribuir los datos entre servidores ayuda a que tu base de datos de Spanner funcione de forma eficiente, sobre todo al realizar inserciones de datos en bloque.
Elige una clave principal para evitar los puntos de acceso
Como se ha mencionado en Esquema y modelo de datos, debe tener cuidado al elegir una clave principal en el diseño del esquema para no crear puntos de acceso por error en su base de datos. Una de las causas de los puntos de acceso es tener una columna cuyo valor cambie de forma monótona como primera parte de la clave, ya que esto provoca que todas las inserciones se produzcan al final del espacio de claves. Este patrón no es recomendable porque Spanner usa intervalos de claves para dividir los datos entre los servidores, lo que significa que todas las inserciones se dirigen a un único servidor que acaba haciendo todo el trabajo.
Por ejemplo, supongamos que quiere mantener una columna de marca de tiempo de último acceso en las filas de la tabla UserAccessLogs
. La siguiente definición de tabla usa una clave principal basada en la marca de tiempo como primera parte de la clave. No recomendamos esta opción si la tabla tiene una tasa de inserción alta:
GoogleSQL
CREATE TABLE UserAccessLogs ( LastAccess TIMESTAMP NOT NULL, UserId STRING(1024), ... ) PRIMARY KEY (LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslogs ( lastaccess timestamptz NOT NULL, userid text, ... PRIMARY KEY (lastaccess, userid) );
El problema es que las filas se escriben en esta tabla en orden de la marca de tiempo del último acceso y, como estas marcas de tiempo siempre aumentan, siempre se escriben al final de la tabla. El punto de acceso se crea porque un solo servidor de Spanner recibe todas las escrituras, lo que sobrecarga ese servidor.
En el siguiente diagrama se ilustra este problema:
La tabla UserAccessLogs
anterior incluye cinco filas de datos de ejemplo, que representan a cinco usuarios diferentes que realizan algún tipo de acción de usuario con un milisegundo de diferencia entre sí. En el diagrama también se indica el orden en el que Spanner inserta las filas (las flechas etiquetadas indican el orden de las escrituras de cada fila). Como las inserciones se ordenan por marca de tiempo y el valor de la marca de tiempo siempre aumenta, Spanner siempre añade las inserciones al final de la tabla y las dirige a la misma división. (Como se explica en Esquema y modelo de datos, una división es un conjunto de filas de una o varias tablas relacionadas que Spanner almacena por orden de clave de fila).
Esto supone un problema porque Spanner asigna el trabajo a diferentes servidores en unidades de divisiones, por lo que el servidor asignado a esta división concreta termina gestionando todas las solicitudes de inserción. A medida que aumenta la frecuencia de los eventos de acceso del usuario, aumenta también la frecuencia de las peticiones de inserción en el servidor correspondiente. El servidor se vuelve propenso a convertirse en un punto de acceso y se muestra el borde y el fondo rojos que se ven en la imagen anterior. En esta ilustración simplificada, cada servidor gestiona como máximo una división, pero Spanner puede asignar más de una división a cada servidor.
Cuando Spanner añade más filas a la tabla, la división aumenta y, a continuación, crea nuevas divisiones según sea necesario. Para obtener más información sobre cómo se crean las divisiones, consulta División basada en la carga. Spanner añade las nuevas filas posteriores a esta nueva división y el servidor asignado a la división se convierte en el nuevo posible punto de acceso.
Cuando se producen puntos de acceso, es posible que observes que tus inserciones son lentas y que otras tareas en el mismo servidor se ralenticen. Cambiar el orden de la columna LastAccess
a ascendente no soluciona este problema, ya que todas las escrituras
se insertan en la parte superior de la tabla, lo que sigue enviando todas las inserciones
a un único servidor.
Práctica recomendada de diseño de esquemas 1: no elijas una columna cuyo valor aumente o disminuya de forma monótona como la primera parte de la clave para tablas de alta velocidad de escritura.
Usar un identificador único universal (UUID)
Puedes usar un identificador único universal (UUID) tal como se define en el estándar RFC 4122 como clave principal. Te recomendamos que uses la versión 4 de UUID, ya que utiliza valores aleatorios en la secuencia de bits. No recomendamos los UUIDs de la versión 1 porque almacenan la marca de tiempo en los bits de orden superior.
Hay varias formas de almacenar el UUID como clave principal:
- En una columna
STRING(36)
. - En un par de columnas
INT64
. - En una columna
BYTES(16)
.
En una columna STRING(36)
, puede usar la función GENERATE_UUID()
de Spanner (GoogleSQL o PostgreSQL) como valor predeterminado de la columna para que Spanner genere automáticamente valores UUID.
Por ejemplo, en la siguiente tabla:
GoogleSQL
CREATE TABLE UserAccessLogs (
LogEntryId STRING(36) NOT NULL,
LastAccess TIMESTAMP NOT NULL,
UserId STRING(1024),
...
) PRIMARY KEY (LogEntryId, LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslogs (
logentryid VARCHAR(36) NOT NULL,
lastaccess timestamptz NOT NULL,
userid text,
...
PRIMARY KEY (lastaccess, userid)
);
Podrías insertar GENERATE_UUID()
para generar los valores de LogEntryId
.
GENERATE_UUID()
produce un valor STRING
, por lo que la columna LogEntryId
debe usar el tipo STRING
en GoogleSQL o el tipo text
en PostgreSQL.
GoogleSQL
INSERT INTO
UserAccessLogs (LogEntryId, LastAccess, UserId)
VALUES
(GENERATE_UUID(), '2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO
useraccesslogs (logentryid, lastaccess, userid)
VALUES
(spanner.generate_uuid(),'2016-01-25 10:10:10.555555-05:00', 'TomSmith');
El uso de UUIDs tiene algunas desventajas:
- Son ligeramente grandes, ya que usan 16 bytes o más. Otras opciones de claves primarias no utilizan tanto almacenamiento.
- No contienen información sobre el registro. Por ejemplo, una clave principal de
SingerId
yAlbumId
tiene un significado inherente, mientras que un UUID no. - Se pierde la localidad entre los registros relacionados, por lo que el uso de un UUID elimina los puntos de acceso.
Invertir los bits de los valores secuenciales
Debes asegurarte de que las claves primarias numéricas (INT64
en GoogleSQL o bigint
en PostgreSQL) no aumenten ni disminuyan de forma secuencial. Las claves primarias secuenciales pueden provocar puntos de acceso a gran escala. Una forma de evitar este problema es invertir los bits de los valores secuenciales, asegurándose de distribuir los valores de clave principal de forma uniforme en el espacio de claves.
Spanner admite secuencias invertidas de bits, que genera valores únicos de enteros invertidos de bits. Puede usar una secuencia en el primer (o único) componente de una clave principal para evitar problemas de puntos de acceso. Para obtener más información, consulta Secuencia invertida por bits.
Cambiar el orden de las teclas
Una forma de distribuir las escrituras de forma más uniforme en el espacio de claves es cambiar el orden de las claves para que la columna que contiene el valor monotónico no sea la primera parte de la clave:
GoogleSQL
CREATE TABLE UserAccessLogs ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess);
PostgreSQL
CREATE TABLE useraccesslogs ( userid bigint NOT NULL, lastaccess TIMESTAMPTZ NOT NULL, ... PRIMARY KEY (UserId, LastAccess) );
En este esquema modificado, las inserciones ahora se ordenan primero por UserId
, en lugar de por la marca de tiempo cronológica del último acceso. Este esquema distribuye las escrituras entre diferentes divisiones porque es poco probable que un solo usuario genere miles de eventos por segundo.
En la siguiente imagen se muestran las cinco filas de la tabla UserAccessLogs
que Spanner ordena con UserId
en lugar de con la marca de tiempo de acceso:
En este caso, Spanner podría dividir los datos de UserAccessLogs
en tres divisiones, cada una de las cuales contendría aproximadamente mil filas de valores de UserId
ordenados. Aunque los eventos de usuario se produjeron con una diferencia de aproximadamente un milisegundo, cada evento lo activó un usuario diferente, por lo que es mucho menos probable que el orden de las inserciones cree un punto de acceso en comparación con el uso de la marca de tiempo para ordenar. Para obtener más información sobre cómo se crean las divisiones, consulta División basada en la carga.
Consulta también la práctica recomendada relacionada para ordenar claves basadas en marcas de tiempo.
Hashea la clave única y distribuye las escrituras en fragmentos lógicos.
Otra técnica habitual para distribuir la carga entre varios servidores es crear una columna que contenga el hash de la clave única real y, a continuación, usar la columna de hash (o la columna de hash y las columnas de clave única juntas) como clave principal. Este patrón ayuda a evitar los puntos de acceso, ya que las filas nuevas se distribuyen de forma más uniforme en el espacio de claves.
Puede usar el valor hash para crear fragmentos lógicos o particiones en su base de datos. En una base de datos con particiones físicas, las filas se reparten en varios servidores de bases de datos. En una base de datos con particiones lógicas, los datos de la tabla definen las particiones. Por ejemplo, para distribuir las escrituras en la tabla UserAccessLogs
en N
fragmentos lógicos, puedes añadir una columna de clave ShardId
a la tabla:
GoogleSQL
CREATE TABLE UserAccessLogs ( ShardId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, UserId INT64 NOT NULL, ... ) PRIMARY KEY (ShardId, LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslogs ( shardid bigint NOT NULL, lastaccess TIMESTAMPTZ NOT NULL, userid bigint NOT NULL, ... PRIMARY KEY (shardid, lastaccess, userid) );
Para calcular el ShardId
, comprime una combinación de las columnas de clave principal y, a continuación, calcula el módulo N del hash. Por ejemplo:
GoogleSQL
ShardId = hash(LastAccess and UserId) % N
La función hash y la combinación de columnas que elijas determinarán cómo se distribuyen las filas en el espacio de claves. A continuación, Spanner creará divisiones en las filas para optimizar el rendimiento.
En el siguiente diagrama se muestra cómo se puede usar un hash para crear tres fragmentos lógicos y distribuir el rendimiento de escritura de forma más uniforme entre los servidores:
En este caso, la tabla UserAccessLogs
se ordena por ShardId
, que se calcula como una función hash de las columnas clave. Las cinco filas UserAccessLogs
se dividen en tres fragmentos lógicos, cada uno de los cuales se encuentra en una división diferente. Las inserciones se distribuyen de forma uniforme entre las divisiones, lo que equilibra el rendimiento de escritura en los tres servidores que gestionan las divisiones.
Spanner también te permite crear una función hash en una columna generada.
Para hacerlo en GoogleSQL, usa la función FARM_FINGERPRINT durante el tiempo de escritura, como se muestra en el siguiente ejemplo:
GoogleSQL
CREATE TABLE UserAccessLogs (
ShardId INT64 NOT NULL
AS (MOD(FARM_FINGERPRINT(CAST(LastAccess AS STRING)), 2048)) STORED,
LastAccess TIMESTAMP NOT NULL,
UserId INT64 NOT NULL,
) PRIMARY KEY (ShardId, LastAccess, UserId);
La función hash que elijas determinará cómo se distribuyen las inserciones en el intervalo de claves. No necesitas un hash criptográfico, aunque puede ser una buena opción. Al elegir una función hash, debes tener en cuenta los siguientes factores:
- Evitar puntos de acceso. Una función que da como resultado más valores hash tiende a reducir los puntos de acceso.
- Leer la eficiencia. Las lecturas de todos los valores hash son más rápidas si hay menos valores hash que analizar.
- Número de nodos.
Usar el orden descendente para las claves basadas en la marca de tiempo
Si tienes una tabla para tu historial que usa la marca de tiempo como clave, considera la posibilidad de usar el orden descendente para la columna de clave si se da alguna de las siguientes circunstancias:
- Si quieres leer el historial más reciente, estás usando una tabla intercalada para el historial y estás leyendo la fila principal. En este caso, con una columna de marca de tiempo
DESC
, las entradas del historial más recientes se almacenan junto a la fila superior. De lo contrario, para leer la fila principal y su historial reciente, será necesario buscar en el medio para saltar el historial antiguo. - Si estás leyendo entradas secuenciales en orden cronológico inverso y no sabes exactamente cuánto tiempo atrás estás consultando. Por ejemplo, puedes usar una consulta SQL con un
LIMIT
para obtener los N eventos más recientes o puedes cancelar la lectura después de haber leído un número determinado de filas. En estos casos, te conviene empezar por las entradas más recientes y leerlas secuencialmente hasta que se cumpla la condición. Spanner lo hace de forma más eficiente para las claves de marca de tiempo que almacena en orden descendente.
Añade la palabra clave DESC
para que la clave de marca de tiempo sea descendente. Por ejemplo:
GoogleSQL
CREATE TABLE UserAccessLogs ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess DESC);
Práctica recomendada de diseño de esquemas n.º 2: el orden descendente o ascendente depende de las consultas de los usuarios. Por ejemplo, la parte superior puede ser la más reciente o la más antigua.
Cuándo usar un índice intercalado
Al igual que en el ejemplo anterior de clave principal que debes evitar, tampoco es recomendable crear índices no intercalados en columnas cuyos valores aumenten o disminuyan de forma monótona, aunque no sean columnas de clave principal.
Por ejemplo, supongamos que define la siguiente tabla, en la que LastAccess
es una columna que no es clave principal:
GoogleSQL
CREATE TABLE Users ( UserId INT64 NOT NULL, LastAccess TIMESTAMP, ... ) PRIMARY KEY (UserId);
PostgreSQL
CREATE TABLE Users ( userid bigint NOT NULL, lastaccess TIMESTAMPTZ, ... PRIMARY KEY (userid) );
Puede parecer conveniente definir un índice en la columna LastAccess
para consultar rápidamente la base de datos de accesos de usuarios "desde el momento X", de la siguiente manera:
GoogleSQL
CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess);
PostgreSQL
CREATE INDEX usersbylastaccess ON users(lastaccess) WHERE lastaccess IS NOT NULL;
Sin embargo, esto conlleva el mismo problema que se describe en la práctica recomendada anterior, ya que Spanner implementa los índices como tablas subyacentes y la tabla de índice resultante usa una columna cuyo valor aumenta de forma monótona como primera parte de la clave.
Puedes crear un índice intercalado en el que las filas del último acceso se intercalen en la fila del usuario correspondiente. Esto se debe a que es poco probable que una sola fila principal genere miles de eventos por segundo.
GoogleSQL
CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(UserId, LastAccess), INTERLEAVE IN Users;
PostgreSQL
CREATE INDEX usersbylastaccess ON users(userid, lastaccess) WHERE lastaccess IS NOT NULL, INTERLEAVE IN Users;
Práctica recomendada de diseño de esquemas n.º 3: no cree un índice no intercalado en una columna de alta velocidad de escritura cuyo valor aumente o disminuya de forma monótona. Usa un índice intercalado o técnicas como las que usarías para diseñar la clave principal de la tabla base al diseñar las columnas de índice. Por ejemplo, añade `shardId`.
Siguientes pasos
- Consulta ejemplos de diseños de esquemas.
- Consulta información sobre la carga de datos en bloque.