Mejores prácticas para el diseño de esquemas

En esta página, se describen las prácticas recomendadas para diseñar esquemas de Cloud Spanner a fin de evitar los hotspots y cargar datos en Cloud Spanner.

Elige una clave primaria para evitar los hotspots

Como se menciona en Esquema y modelo de datos, debes tener cuidado cuando eliges una clave primaria para no crear hotspots de forma accidental en tu base de datos. Una de las causas de los hotspots es tener una columna con un valor que aumenta de forma monotónica como la primera parte de la clave, ya que esto da como resultado que todas las inserciones ocurran al final del espacio de clave. Este patrón no es recomendable porque Cloud Spanner divide los datos entre servidores por rangos de clave, lo que significa que todas tus inserciones se dirigirán a un solo servidor que hará todo el trabajo.

Por ejemplo, supongamos que deseas mantener una columna de marca de tiempo del último acceso en las filas de la tabla UserAccessLog. La siguiente definición de tabla que utiliza una clave primaria basada en la marca de tiempo como primera parte de la clave es un antipatrón si la tabla verá una tasa de inserción alta:

-- ANTI-PATTERN: USING A COLUMN WHOSE VALUE MONOTONICALLY INCREASES OR
-- DECREASES AS THE FIRST KEY PART OF A HIGH WRITE RATE TABLE

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

El problema es que las filas se escribirán en esta tabla en el orden de la marca de tiempo de último acceso y, debido a que las marcas de tiempo de último acceso siempre están en aumento, siempre se escriben al final de la tabla. El hotspot se crea porque un único servidor de Cloud Spanner recibirá todas las escrituras, lo que sobrecarga a ese servidor único.

En el siguiente diagrama, se ilustra este inconveniente:

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. En el diagrama, también se indica el orden en el que se insertarán las filas (las flechas etiquetadas indican el orden de las escrituras para cada fila). Debido a que las inserciones se ordenan por marca de tiempo, y el valor de la marca de tiempo siempre aumenta, las inserciones siempre se agregan al final de la tabla y se dirigen 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 más tablas relacionadas que se almacenan por orden de clave de fila).

Esto es problemático porque Cloud Spanner asigna trabajo a diferentes servidores en unidades de divisiones, por lo que el servidor asignado a esta división en particular termina administrando 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 se vuelve propenso a convertirse en un hotspot, como lo indican el fondo y el borde en color rojo anterior. Ten en cuenta que, en esta ilustración simplificada, cada servidor controla como máximo una división, pero en la realidad, cada servidor de Cloud Spanner puede tener asignada más de una división.

Cuando se agregan más filas a la tabla, la división crece; cuando esta alcanza su tamaño máximo (aproximadamente 4 GB), Cloud Spanner crea otra división, como se describe en División basada en la carga. Las filas nuevas subsiguientes se agregan a esta división nueva y el servidor que se le asigna se convierte en el hotspot potencial nuevo.

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.

Intercambia el orden de las claves

Una manera de distribuir escrituras en el espacio de clave es cambiar el orden de las claves para que la columna que contiene el valor creciente o decreciente monotónico no sea la primera parte de la clave:

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

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

En el siguiente diagrama, se muestran las cinco filas de la tabla UserAccessLog ordenadas por UserId en lugar de la marca de tiempo de acceso:

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

Aquí, los datos de UserAccessLog se parten en tres divisiones y cada división contiene el orden de mil filas de valores UserId ordenados. Esta es una estimación razonable de cómo se podrían dividir los datos del usuario, si se da por sentado que cada fila contiene alrededor de 1 MB de datos del usuario y un tamaño de aproximadamente 4 GB. Aunque los eventos del usuario ocurrieron aproximadamente a un milisegundo de diferencia, cada evento fue generado por un usuario diferente, por lo que es mucho menos probable que el orden de las inserciones cree un hotspot en comparación con el orden por marca de tiempo.

Consulta también las prácticas recomendadas para ordenar claves basadas en marca de tiempo.

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 de forma física, las filas se distribuyen en varias bases de datos. En una base de datos fragmentada de forma lógica, los fragmentos se definen según los datos de la tabla). 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:

CREATE TABLE UserAccessLog (
  ShardId     INT64 NOT NULL,
  LastAccess  TIMESTAMP NOT NULL,
  UserId      INT64 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 calcula el módulo N del hash, 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. A continuación, Cloud Spanner creará divisiones en las filas para optimizar el rendimiento. Ten en cuenta que, es posible que las divisiones no se alineen con los fragmentos lógicos.

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 balanceada

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.

La función de hash elegida determinará el nivel de distribución de las inserciones en el rango de claves. No necesitas un hash criptográfico, aunque contar con uno puede ser una buena opción. Cuando eliges una función de hash, debes tener en cuenta varios factores:

  • Evitar los hotspots. Una función que genera más valores de hash suele reducir los hotspots.
  • Eficacia de la 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 un identificador único universal (UUID)

Puedes usar un identificador único universal (UUID), según se define en RFC 4122 como la clave primaria. Se recomienda el UUID de la versión 4, porque usa valores aleatorios en la secuencia de bits. El UUID de la versión 1 almacena la marca de tiempo en los bits de orden superior y no se recomienda.

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)

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 incluyen información sobre el registro. Por ejemplo, una clave primaria de SingerId y AlbumId tiene un significado inherente, mientras que un UUID no lo tiene.
  • Se pierde la localidad entre los registros que están relacionados, por lo que el uso de un UUID elimina los hotspots.

Revierte los bits de los valores secuenciales

Cuando generas claves primarias únicas numéricas, los bits de orden superior de los números subsiguientes deben distribuirse de igual modo en todo el espacio numérico. Una forma de hacerlo es generar números secuenciales por medios convencionales y, luego, revertir los bits para obtener los valores finales.

Cuando se revierten los bits, se mantienen valores únicos en las claves primarias. Solo debes almacenar el valor revertido, ya que puede volver a calcular el valor original en el código de la aplicación.

Limita el tamaño de la fila

El tamaño de una fila debe ser inferior a 4 GB para obtener el mejor rendimiento. Este tamaño incluye la fila de nivel superior y todas las filas secundarias y con índices intercaladas. En general, Cloud Spanner crea una división nueva cuando una división existente alcanza los 4 GB y Cloud Spanner solo puede dividirse en las filas de nivel superior. Si una fila supera los 4 GB, la capacidad de procesamiento de escritura podría verse afectada.

Diseña tablas intercaladas para evitar los hotspots

Cloud Spanner solo puede crear divisiones a lo largo de las filas de nivel superior. Considera este ejemplo con tres tablas intercaladas:

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)

Cloud Spanner crea divisiones que mantienen todos los álbumes y las canciones de cada cantante juntos en la misma división. Si resulta que las canciones de un solo cantante se convierten en un hotspot para lecturas o escrituras, Cloud Spanner no puede dividir la tabla Songs entre los servidores. Si, en cambio, Songs es una tabla de nivel superior, Cloud Spanner puede crear divisiones basadas en canciones:

-- Schema hierarchy:
-- + Singers (top-level table)
--   + Albums (interleaved table, child table of Singers)
-- + Songs (top-level table)

Usa el orden descendente para las claves basadas en la marca de tiempo

Si tienes una tabla para tu historial que está señalada por marca de tiempo, considera usar el orden descendente en las columnas de claves si se cumple alguna de las siguientes condiciones:

  • Si usas una tabla intercalada para el historial y también leerás la fila superior. 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 estos casos, debes comenzar con las entradas más recientes y leer las entradas más antiguas de forma secuencial hasta que se cumpla tu condición, lo que Cloud Spanner hace de forma más eficiente para claves de marca de tiempo almacenadas en orden descendente.

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

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.º 2: usa el orden descendente para las claves basadas en la marca de tiempo.

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

Al igual que la clave primaria antipatrón anterior, también es una mala idea crear índices no intercalados en columnas con valores que aumentan o disminuyen de forma monotónica, 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:

CREATE TABLE Users (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP,
  ...
) 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:

-- ANTI-PATTERN: CREATING A NON-INTERLEAVED INDEX ON A COLUMN WHOSE VALUE
-- MONOTONICALLY INCREASES OR DECREASES ON A HIGH WRITE RATE COLUMN

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess)

Sin embargo, esto genera la misma dificultad que se describió en la práctica recomendada anterior, porque los índices se implementan como tablas internas y la tabla de índice resultante usa una columna cuyo valor aumenta de forma monotónica como la primera parte de la clave.

De todas formas, está bien crear un índice intercalado como este, porque las filas de índices intercalados están intercaladas en las filas superiores correspondientes y no es probable que una fila superior produzca miles de eventos por segundo.

Práctica recomendada sobre el diseño de esquema n.º 3: no crees un índice no intercalado en una columna de tasa de escritura alta cuyo valor aumente o disminuya de forma monotónica.

Qué sigue