Recomendaciones sobre el diseño del esquema

La arquitectura distribuida de Spanner te permite diseñar tu esquema para evitar hotspots, situaciones en las que se envían demasiadas solicitudes al mismo servidor que satura los recursos del servidor y puede causar latencias altas.

En esta página, se describen las prácticas recomendadas para diseñar esquemas que eviten y crear hotspots. Una forma de evitar los hotspots es ajustar el diseño del esquema para permiten que Spanner divida y distribuya los datos en varios servidores. La distribución de datos entre servidores ayuda a que tu base de datos de Spanner funcione de forma eficiente, en especial, cuando se realizan inserciones de datos.

Elige una clave primaria para evitar los hotspots

Como se mencionó en Esquema y modelo de datos, debes tener cuidado a la hora de elegir una clave primaria en el diseño del esquema para no crear hotspots por accidente en tu en la base de datos. Una de las causas de los hotspots es tener una columna cuyo valor cambia como la primera parte clave, ya que esto da como resultado que todas las inserciones tengan lugar al final del espacio de claves. Este patrón no es deseable porque Spanner usa rangos de claves para dividir los datos entre servidores, lo que significa que todas tus inserciones se se dirige a un único servidor que termina haciendo todo el trabajo.

Por ejemplo, supongamos que quieres mantener una columna de marca de tiempo de último acceso en las filas de la tabla UserAccessLog. En la siguiente definición de tabla, se usa un clave primaria basada en marca de tiempo como la primera parte de la clave. No la recomendamos si la tabla presenta una alta tasa de inserción:

GoogleSQL


CREATE TABLE UserAccessLog (
LastAccess TIMESTAMP NOT NULL,
UserId     INT64 NOT NULL,
...
) PRIMARY KEY (LastAccess, UserId);

PostgreSQL


CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (LastAccess, UserId)
);

El problema aquí es que las filas se escriben en esta tabla en orden de última y como las marcas de tiempo del último acceso siempre están en aumento estas siempre se escriben al final de la tabla. El hotspot se crea porque un único servidor de Spanner recibe todas las escrituras, lo que sobrecarga ese en un solo servidor.

En el siguiente diagrama, se ilustra este error:

Tabla UserAccessLog ordenada por marca de tiempo con el hotspot correspondiente

En la tabla UserAccessLog anterior, se incluyen cinco filas de datos de ejemplo, que representan cinco usuarios diferentes que realizan algún tipo de acción del usuario a un milisegundo de diferencia. El diagrama también anota el orden en que Spanner inserta las filas (las flechas etiquetadas indican el orden escrituras para cada fila). Debido a que las inserciones se ordenan por marca de tiempo, y el que el valor de la marca de tiempo aumenta constantemente, Spanner siempre agrega al final de la tabla y las dirige a la misma división. (Como se explicó en Esquema y modelo de datos, una split es un conjunto de filas de una o más tablas relacionadas que Spanner almacena el orden de clave de fila).

Esto es problemático porque Spanner asigna trabajo a diferentes servidores en unidades de divisiones, por lo que el servidor asignado a esta división en particular termina y controlar todas las solicitudes de inserción. A medida que aumenta la frecuencia de los eventos de acceso de los usuarios, también aumenta la frecuencia de las solicitudes de inserción en el servidor correspondiente. El servidor pasa a ser un hotspot y se ve así el borde rojo y el fondo de arriba. Observa que, en esta ilustración simplificada, cada servidor maneja como máximo una división, pero, en realidad, Spanner puede asignar a cada servidor más de una división.

Cuando Spanner agrega más filas a la tabla, la división crece y cuándo alcanza aproximadamente 8 GB, Spanner crea otro división, como se describe en División basada en la carga. Spanner agrega las filas nuevas posteriores a esta nueva división, y el servidor asignado a la división se convierte en el nuevo hotspot potencial.

Cuando se producen hotspots, es posible que adviertas que las operaciones de inserción tarden demasiado y que otros trabajos en el mismo servidor se ralenticen. Cambiar el orden de la columna LastAccess a ascendente no resuelve este problema porque todas las escrituras se insertan en la parte superior de la tabla, lo que aún envía todas las inserciones a un solo servidor.

Práctica recomendada de diseño de esquemas n.º 1: no elijas una columna cuyo valor aumente o disminuya monótonamente como la primera parte clave de una tabla con una tasa de escritura alta.

Usa un identificador único universal (UUID)

Puedes usar un identificador único universal (UUID), según se define en RFC 4122 como la clave primaria. Recomendamos usar la versión 4 del UUID, ya que utiliza valores aleatorios en la secuencia de bits. No recomendamos los UUID de la versión 1 porque almacenan la marca de tiempo en bits de orden superior.

Existen varias maneras de almacenar el UUID como la clave primaria:

  • En una columna STRING(36)
  • En un par de columnas INT64
  • En una columna BYTES(16)

Para una columna STRING(36), puedes usar el GENERATE_UUID() de Spanner (GoogleSQL o PostgreSQL) como el valor predeterminado de la columna en hacer que Spanner genere automáticamente valores de UUID.

El uso de un UUID presenta algunas desventajas:

  • Son un poco grandes y usan 16 bytes o más. Las otras opciones de claves primarias no usan tanto almacenamiento.
  • No contienen información sobre el registro. Por ejemplo, una clave primaria de SingerId y AlbumId tienen un significado inherente, mientras que un UUID no lo tiene.
  • Se pierde la localidad entre los registros relacionados, por lo que se usa un UUID. elimina los hotspots.

Revierte los bits de los valores secuenciales

Debes asegurarte de que los valores numéricos (INT64 en GoogleSQL o bigint en PostgreSQL) las claves primarias no aumentan ni disminuyen de forma secuencial. Secuencial primaria claves pueden provocar hotspots a gran escala. Una forma de evitar este problema es revertir los bits de los valores secuenciales, asegurándose de distribuir de claves primarias de manera uniforme en el espacio de claves.

Spanner admite secuencias inversas de bits, lo que genera un números enteros invertidos de bits. Puedes usar una secuencia en el primer (o únicamente) de una clave primaria para evitar problemas de generación de hotspots. Para obtener más información, consulta Secuencia de bits inversa.

Intercambia el orden de las claves

Una forma de distribuir las escrituras en el espacio de claves de manera más uniforme es intercambiar el orden de las claves para que la columna que contiene el valor monótono no sea primera parte clave:

GoogleSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMP NOT NULL,
UserId     INT64 NOT NULL,
...
) PRIMARY KEY (UserId, LastAccess);

PostgreSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (UserId, LastAccess)
);

En este esquema modificado, las inserciones ahora se ordenan por UserId, en lugar de por marca de tiempo de último acceso cronológico. Este esquema distribuye las escrituras entre en diferentes divisiones porque es poco probable que un solo usuario produzca de eventos por segundo.

A continuación, se ilustran las cinco filas de la tabla UserAccessLog. que Spanner pide con UserId en lugar de una marca de tiempo de acceso:

Tabla UserAccessLog ordenada por UserId con capacidad de procesamiento de escritura equilibrado

Aquí, Spanner fragmenta los datos de UserAccessLog en tres divisiones, con cada división contiene aproximadamente mil filas de valores UserId ordenados. Esta es una estimación razonable de cómo se podrían dividir los datos del usuario, suponiendo que cada contiene alrededor de 1 MB de datos del usuario y con un tamaño máximo de división de aproximadamente 8 GB. Si bien los eventos de usuario ocurrieron acerca de un milisegundos de diferencia, un usuario diferente generó cada evento, de modo que el orden del es mucho menos probable que las inserciones creen un hotspot en comparación con el uso del y marca de tiempo para hacer el pedido.

Consulta también las prácticas recomendadas para ordenar los datos en base a marcas de tiempo claves.

Genera un hash de la clave única y distribuye las escrituras en fragmentos lógicos

Otra técnica común para distribuir la carga en varios servidores es crear una columna que contenga el hash de la clave única real y, luego, usar la columna de hash (o la columna de hash más las columnas de la clave única) como clave primaria. Con este patrón, se evitan los hotspots, ya que las filas nuevas se distribuyen de manera más uniforme en el espacio de claves.

Puedes usar el valor de hash para crear fragmentos lógicos, o particiones, en tu base de datos. En una base de datos fragmentada físicamente, las filas se distribuyen en varias servidores de bases de datos. En una base de datos fragmentada de forma lógica, los datos de la tabla definen la fragmentos. Por ejemplo, para distribuir las escrituras en la tabla UserAccessLog en N fragmentos lógicos, puedes anteponer una columna de clave ShardId a la tabla:

GoogleSQL

CREATE TABLE UserAccessLog (
ShardId     INT64 NOT NULL,
LastAccess  TIMESTAMP NOT NULL,
UserId      INT64 NOT NULL,
...
) PRIMARY KEY (ShardId, LastAccess, UserId);

PostgreSQL

CREATE TABLE UserAccessLog (
ShardId bigint NOT NULL,
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (ShardId, LastAccess, UserId)
);

Para calcular el ShardId, genera un hash de una combinación de las columnas de clave primaria y, luego, calcular el módulo N del hash. Por ejemplo:

ShardId = hash(LastAccess and UserId) % N

La combinación de columnas y la función del hash determinan cómo se distribuyen las filas en el espacio de claves. Luego, Spanner creará divisiones en las filas para optimizar el rendimiento.

En el siguiente diagrama, se ilustra cómo usar un hash para crear tres fragmentos lógicos puede distribuir la capacidad de procesamiento de escritura de manera más uniforme en los servidores:

Tabla UserAccessLog ordenada por ShardId con capacidad de procesamiento de escritura equilibrada

Aquí, la tabla UserAccessLog está ordenada por ShardId, que se calcula como una función hash de las columnas de claves. Las cinco filas UserAccessLog se parten en tres fragmentos lógicos, cada uno de los cuales se encuentra casualmente en una división diferente. Las inserciones se distribuyen de manera uniforme entre las divisiones, lo que equilibra la capacidad de procesamiento de escritura en los tres servidores que administran las divisiones.

Spanner también te permite crear una función hash en un columna generada.

Para hacer esto en Google SQL, usa el FARM_FINGERPRINT durante el tiempo de escritura, como se muestra en el siguiente ejemplo:

GoogleSQL

CREATE TABLE UserAccessLog (
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 de hash que elijas determina la distribución de tus inserciones en el rango de claves. No necesitas un hash criptográfico, aunque hash criptográfico podría ser una buena opción. Cuando eliges una función hash, debes considerar los siguientes factores:

  • Elusión de hotspots. Una función que genera más valores de hash suele reducir los hotspots.
  • Eficiencia de lectura Las lecturas en todos los valores de hash son más rápidas si hay menos valores de hash para analizar.
  • Recuento de nodos

Usa 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 usar el orden descendente para la columna de clave si se encuentra alguna de las siguientes aplicar:

  • Si quieres leer el historial más reciente, utilizas una tabla intercalada para el historial, para leer 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, la lectura de la fila principal y su historial reciente requerirá una búsqueda por el medio para omitir el historial anterior.
  • Si lees entradas secuenciales en orden cronológico inverso y no sabes con exactitud qué tanto tiempo para atrás debes ir. Por ejemplo, puedes usar una consulta de SQL con un LIMIT para obtener los N eventos más recientes o cancelar la lectura después de leer una cierta cantidad de filas. En estas En este caso, debes empezar con las entradas más recientes y leer en secuencia entradas más antiguas hasta que se cumpla la condición, que hace Spanner de forma más eficiente para las claves de marca de tiempo que almacena Spanner orden descendente.

Agrega la palabra clave DESC para hacer que la clave de la marca de tiempo sea descendente. Por ejemplo:

GoogleSQL

CREATE TABLE UserAccessLog (
UserId     INT64 NOT NULL,
LastAccess TIMESTAMP NOT NULL,
...
) PRIMARY KEY (UserId, LastAccess DESC);

Práctica recomendada sobre el diseño de esquemas n.o 2: orden ascendente o descendente depende de las búsquedas de los usuarios, por ejemplo, si “Top” es la más reciente o “Top” es la más antigua.

Usa un índice intercalado en una columna cuyo valor aumente o disminuya de forma monotónica

Similar al ejemplo anterior de clave primaria que debes evitar, también es un no es una buena idea crear índices no intercalados en columnas cuyos valores se aumentan o disminuyen monótonamente, incluso si no son columnas de clave primaria.

Por ejemplo, supongamos que defines la siguiente tabla, en la que LastAccess es una columna que no es de clave primaria:

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 con rapidez en la base de datos los accesos de usuarios “desde la hora 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 da como resultado la misma dificultad que se describió en las recomendaciones porque Spanner implementa índices como tablas de forma interna, y la tabla de índice resultante usa una columna cuyo valor aumenta de forma monotónica como primera parte clave.

Sin embargo, se puede crear un índice intercalado como este, ya que las filas los índices intercalados se intercalan en las filas superiores correspondientes, y es es poco probable que una sola fila principal produzca miles de eventos por segundo.

Práctica recomendada sobre el diseño de esquemas n.o 3: No crees un índice no intercalado en una columna con una tasa de escritura alta cuyo valor aumenta o disminuye. En lugar de usar índices intercalados, usa técnicas como las que usarías para el diseño de clave primaria de la tabla base cuando se diseñan columnas de índice, para Por ejemplo, agrega `shardId`.

¿Qué sigue?