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 para evitar puntos de acceso y para cargar datos en Cloud Spanner.

Elegir una clave principal

Como se menciona en el apartado sobre esquemas y modelos de datos, se debe tener cuidado al elegir una clave principal para no crear puntos de acceso accidentalmente en su base de datos. Una forma de crear puntos de acceso accidentalmente es eligiendo una columna cuyo valor aumenta de forma monótona como la primera parte clave, ya que esto da como resultado que todas las inserciones ocurran al final de su espacio clave. No conviene obtener este resultado, ya que Cloud Spanner divide los datos entre los servidores por intervalos de claves, lo que significa que todos sus insertos se dirigirán a un solo servidor, que terminará haciendo todo el trabajo.

Por ejemplo, supongamos que deseas mantener una última columna de marca de tiempo de acceso en filas de datos de Users. La siguiente definición de tabla que utiliza una clave primaria basada en la marca de tiempo como la primera parte clave es un antipatrón:

-- ANTI-PATTERN: USING A COLUMN WHOSE VALUE MONOTONICALLY INCREASES OR
-- DECREASES AS THE FIRST KEY PART

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

El problema aquí es que las filas se escribirán en esta tabla en el orden de la última marca de tiempo de acceso y, dado que las marcas de tiempo de último acceso aumentan constantemente, siempre se escriben al final de la tabla. El punto de acceso se crea porque un solo servidor de Cloud Spanner recibirá todas las escrituras, lo que hará que ese servidor se sobrecargue.

El inconveniente viene ilustrado en el siguiente diagrama:

Tabla de usuarios ordenada por marca de tiempo con el punto de acceso correspondiente

La tabla Users anterior contiene cinco filas de datos de ejemplo, que representan a cinco usuarios diferentes que toman algún tipo de acción del usuario a un milisegundo de diferencia entre sí. El diagrama también ilustra el orden de inserción de las filas (las flechas etiquetadas indican el orden de las escrituras por cada fila). Como las inserciones se ordenan por marca de tiempo y el valor de la marca de tiempo aumenta constantemente, las inserciones siempre se agregan al final de la tabla y se dirigen a la misma división. (Como se indica en el apartado sobre esquemas y modelos de datos, una división es un conjunto de filas de una o más tablas relacionadas que se almacenan en orden de clave de registro).

Esto resulta problemático porque Cloud Spanner asigna trabajo a diferentes servidores en unidades de división, por lo que el servidor asignado a esta división en particular termina manejando todas las peticiones 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 público, como se indica arriba con el borde y el fondo en rojo. Ten en cuenta que, en esta ilustración simplificada, cada servidor maneja a lo sumo una división pero, en realidad, a cada servidor de Cloud Spanner se le puede asignar más de una división.

A medida que se agregan más insertos a la tabla, crece la división y, cuando alcanza su tamaño máximo (unos pocos GB), Cloud Spanner crea otra división, tal como se describe en el apartado sobre la división basada en la carga. Las inserciones posteriores se anexan a esta nueva división y el servidor que se le asigna se convierte en el nuevo punto de acceso potencial.

Cuando surgen puntos de acceso, puedes observar que las inserciones son lentas y que también se puede ralentizar el trabajo en el mismo servidor. Al cambiar el orden de la columna LastAccess por el orden ascendente no se resuelve este problema, ya que todas las escrituras se insertan en la parte superior de la tabla, que aún envía todas las inserciones a un único servidor.

Práctica recomendada de diseño de esquema n.º 1: No elegir una columna cuyo valor aumente o disminuya de forma monótona como la primera parte clave.

Posible solución n.º 1: Cambiar el orden de las claves

Una forma de propagar escrituras sobre el espacio de clave es intercambiar el orden de las claves para que la columna que contiene el valor de forma monótona creciente o decreciente no sea la primera parte clave:

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

En este esquema modificado, las inserciones ahora están ordenadas por UserId, en lugar de mediante la última marca de tiempo de acceso cronológico. Esto propaga escrituras entre diferentes divisiones porque es poco probable que un solo usuario genere miles de eventos por segundo.

El siguiente diagrama muestra las cinco filas de la tabla de Users, ordenadas por UserId lugar de por marca de tiempo de acceso:

Tabla de usuarios ordenada por UserId con un rendimiento de escritura equilibrado

Aquí, los datos de Users se agrupan en tres divisiones, cada una de ellas en el orden de mil filas de valores UserId ordenados. Se trata de una estimación razonable de cómo se pueden dividir los datos del usuario, suponiendo que cada fila contiene aproximadamente 1 MB de datos del usuario y se le da un tamaño máximo de división de orden de GB. Aunque los eventos del usuario se produjeron con un milisegundo de diferencia, cada evento lo llevó a cabo un usuario diferente, por lo que es mucho menos probable que el orden de los insertos cree un punto de acceso en comparación con el orden de marca de tiempo.

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

Posible solución n.° 2: Crear un hash de la clave y distribuir las escrituras entre N fragmentos

Otra técnica común para distribuir la carga entre múltiples servidores es la clave única real, y usar el hash (o el hash + la clave única) como clave principal. De esta forma, se podrán evitar los puntos de acceso al garantizar que las filas insertadas se distribuyen de manera más uniforme en el espacio clave.

Por ejemplo, para distribuir escrituras a la tabla Users entre N fragmentos, considera agregar una columna de clave ShardId a la tabla:

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

Para calcular en qué fragmento escribir una determinada fila, calcula ShardId = hash(key parts) % N. Luego, si quieres leer todas las filas a las que se accedió desde la hora T, lee de las diferentes divisiones de la base de datos N y recopila los resultados.

El siguiente diagrama muestra cómo esta revisión distribuye el rendimiento de escritura de manera más uniforme en todos los servidores:

Tabla de usuarios ordenada por ShardId con un rendimiento de escritura equilibrado

Aquí la tabla Users está ordenada por ShardId, que se calcula como una función hash de columnas de clave. La cantidad de fragmentos que creas y el orden de las peticiones de inserción resultantes dependen de las características específicas de la función hash que definas. En este ejemplo, las cinco filas de Users están divididas en tres fragmentos, cada uno de los cuales se encuentra en una división diferente. Las inserciones se distribuyen de manera uniforme entre las divisiones, lo que equilibra el rendimiento de escritura con los tres servidores que manejan las divisiones.

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

Si tienes una tabla con marca de tiempo, haz descender la clave de marca de tiempo agregando la palabra clave DESC. De este modo, aumentará la eficacia de leer todo tu historial hasta cierto punto en el pasado, ya que Cloud Spanner no deberá escanear todos tus datos para descubrir dónde comienza dicho punto, sino que puedes comenzar a leer "en la parte superior".

Por ejemplo:

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

Mejor práctica de diseño de esquema n.° 2: Utilizar el orden descendente para las claves basadas en la marca de tiempo.

Carga de datos

Esta sección proporciona las mejores prácticas para la inserción masiva o la escritura de filas en tablas en una base de datos de Cloud Spanner.

  • Evita escribir filas en el orden de las teclas primarias. Por ejemplo, si estás cargando datos en una tabla con la clave primaria UserId, es importante asegurarse de que la carga de datos no escriba todos los usuarios en el orden UserId. Lo ideal es que cada trabajador de carga de datos esté escribiendo en una parte diferente del espacio clave, de modo que las escrituras se dirijan a muchos servidores distintos de Cloud Spanner en lugar de simplemente sobrecargar uno.

  • Particiona el espacio de claves ordenadas en rangos y, luego, haz que cada rango procesado por un trabajador agrupa los datos en ese rango en trozos de aproximadamente 1 MB o 10 MB. Es bueno tener una cantidad considerable de particiones (lo ideal: 10 veces el número de nodos) para que los trabajadores individuales no provoquen puntos de acceso móviles.

Mejores prácticas de carga de datos: Asegurarse de que las escrituras estén bien distribuidas y cargar los datos con varios trabajadores.

Creación de índices

Al igual que en el patrón anterior de la clave principal, también es una mala idea crear índices no intercalados en columnas cuyos valores aumentan o disminuyen de forma monótona, aunque no sean columnas de clave primaria.

Por ejemplo, supón que define la siguiente tabla, en la que LastAccess no es una columna de clave primaria:

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

Puede parecer conveniente definir un índice en la columna LastAccess para consultar rápidamente la base de datos para los accesos de los usuarios "desde la hora X", como en el siguiente ejemplo:

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

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess)

Sin embargo, esto da como resultado el mismo inconveniente que descrito en la práctica recomendada anterior, puesto que los índices se implementan como tablas a examen, y la tabla de índice resultante usaría una columna cuyo valor aumenta de forma monótona como su primera parte clave.

Sin embargo, está bien crear un índice entrelazado como este, ya que las filas de índices entrelazados se intercalan en las filas principales correspondientes, y es poco probable que una fila padre única produzca miles de eventos por segundo.

Práctica recomendada de diseño de esquema n.° 3: No crear un índice no entrelazado en una columna cuyo valor aumente o disminuya de forma monótona.

Si vas a cargar datos en lotes, espera a que los datos se carguen para crear índices. Si creas índices antes de cargar los datos, cada inserción dará lugar a una actualización de índice síncrona. Las transacciones insertadas con actualizaciones de índice síncronas posiblemente requieran más máquinas y sean más lentas que las transacciones sin actualizaciones de índice, por lo que la carga masiva tendrá un rendimiento inferior. Si creas los índices después de cargar de forma masiva los datos, los índices se completarán de forma asíncrona. Al igual que muchos otros sistemas de administración de bases de datos relacionales, crear un índice en datos preexistentes es más rápido que actualizar un índice de forma incremental a medida que se carga cada fila. Puedes realizar un seguimiento del estado de la creación de índices asíncronos utilizando la API de operaciones de bases de datos.

Mejor práctica de creación de índices: Crear índices después de cargar los datos de forma masiva.

¿Te sirvió esta página? Envíanos tu opinión:

Enviar comentarios sobre…

Cloud Spanner Documentation