Optimizar el diseño del esquema para Cloud Spanner

Las tecnologías de almacenamiento de Google potencian algunas de las aplicaciones más grandes del mundo. Sin embargo, la escala no siempre es un resultado automático del uso de estos sistemas. Los diseñadores deben pensar cuidadosamente sobre cómo modelar sus datos para garantizar que su aplicación pueda escalar y funcionar a medida que crece en varias dimensiones.

Cloud Spanner es una base de datos distribuida, y su uso eficaz requiere pensar de forma diferente sobre el diseño del esquema y los patrones de acceso que puedas con las bases de datos tradicionales. Los sistemas distribuidos, por su naturaleza, obligan a los diseñadores a pensar en los datos y la localización de procesamiento.

Cloud Spanner admite consultas y transacciones SQL con la capacidad de escalar horizontalmente. A menudo, es necesario un diseño cuidadoso para conseguir el beneficio completo de Cloud Spanner. Este documento analiza algunas de las ideas clave que ayudarán a garantizar que la aplicación pueda escalar a niveles arbitrarios y maximizar su rendimiento. Dos herramientas en particular tienen un gran impacto en la escalabilidad: definición de clave e intercalado.

Diseño de la tabla

Las filas en una tabla de Cloud Spanner se organizan lexicográficamente por PRIMARY KEY. Conceptualmente, las claves se ordenan por la concatenación de las columnas en el orden en que se declaran en la cláusula PRIMARY KEY. Esto exhibe todas las propiedades estándar de localización:

  • La búsqueda en la tabla en orden lexicográfico es eficiente.
  • Las filas suficientemente cercanas se almacenarán en los mismos bloques de disco, y se leerán y almacenarán en caché juntas.

Cloud Spanner replica los datos en varias zonas para ver la disponibilidad y escala, con cada zona que contiene una réplica completa de los datos. Al aprovisionar un nodo de instancia de Cloud Spanner, se obtiene esa cantidad de recursos informáticos en cada una de estas zonas. Si bien cada réplica es un conjunto completo de los datos, los datos en una réplica se particionan a través de los recursos informáticos en esa zona.

Los datos en cada réplica de Cloud Spanner se organizan en dos niveles de jerarquía física: divisiones de bases de datos y bloques. Las divisiones tienen intervalos contiguos de filas, y son la unidad mediante la cual Cloud Spanner distribuye la base de datos a través de los recursos informáticos. Con el tiempo, las divisiones se pueden dividir en partes más pequeñas, fusionarse o trasladarse a otros nodos en la instancia para aumentar el paralelismo y permitir que la aplicación se escale. Las operaciones que abarcan divisiones son más costosas que las operaciones equivalentes que no lo hacen, debido al aumento de la comunicación. Esto es cierto incluso si esas divisiones son publicadas por el mismo nodo.

Hay dos tipos de tablas en Cloud Spanner: tablas raíz (a veces llamadas tablas de nivel superior) y tablas intercaladas. Las tablas intercaladas se definen al especificar otra tabla como principal, lo que hace que las filas de la tabla intercalada se agrupen con la fila principal. Las tablas raíz no tienen elemento principal, y cada fila en una tabla raíz define una nueva fila de nivel superior o fila raíz. Las filas intercaladas con esta fila raíz se denominan filas secundarias, y la colección de una fila raíz más todos sus descendientes se denomina árbol de filas.

Cloud Spanner divide automáticamente las divisiones cuando lo considere necesario debido al tamaño o la carga, pero solo lo hará en los límites de las filas raíz. Como resultado, cualquier árbol de filas concreto siempre se mantiene en una sola división. Se caracterizan por lo siguiente:

  • Las operaciones en un árbol de filas tienden a ser más eficientes porque no requieren comunicación con otras divisiones.
  • El tamaño de un árbol de filas es un límite inferior para el tamaño de la división que lo hospeda. Se debe tener cuidado para mantener esto por debajo de aproximadamente 2 GiB para evitar un rendimiento deficiente (Si quieres obtener más información, consulta aquí).
  • El rendimiento máximo de un árbol de filas se limita al rendimiento máximo de un solo nodo, que es grande pero no infinito.

Elegir qué tablas deben ser raíces es una decisión importante al diseñar la aplicación para escalar. Las raíces suelen ser cosas como Usuarios, Cuentas, Proyectos y similares, y sus tablas secundarias contienen la mayoría de los demás datos sobre la entidad en cuestión.

Recomendaciones:

  • Si quieres mejorar la localización, usa un prefijo de clave común para filas relacionadas en la misma tabla.
  • Intercala datos relacionados en otra tabla cuando tenga sentido.

Ventajas y desventajas de localización

Si los datos se escriben o se leen juntos con frecuencia, puede beneficiar tanto a la latencia como al rendimiento para agruparlos al seleccionar cuidadosamente las claves principales y utilizando el intercalado. Esto se debe a que hay un costo fijo para comunicarse con cualquier servidor o bloque de disco, así que ¿por qué no obtener tanto como sea posible mientras esté allí? Además, cuantos más servidores te comuniques, mayores serán las probabilidades de encontrarse con un servidor temporalmente ocupado, lo que aumentará las latencias de cola. Finalmente, las transacciones que abarcan divisiones, aunque son automáticas y transparentes en Cloud Spanner, tienen un costo de CPU y una latencia ligeramente mayores debido a la naturaleza distribuida de la confirmación en dos fases.

Por otro lado, si los datos se relacionan, pero no se accede a ellos con frecuencia, de forma conjunta, plantéate la posibilidad de separarlos. Se obtiene mayor beneficio de esto, cuando los datos a los que se accede con poca frecuencia son grandes. Por ejemplo, muchas bases de datos almacenan datos binarios grandes fuera de banda de los datos de fila principales, solo con referencias a los datos grandes intercalados.

Ten en cuenta que algún nivel de confirmación de dos fases y operaciones de datos no locales son inevitables en una base de datos distribuida. No te preocupes demasiado por obtener una historia de localización perfecta para cada operación. Concéntrate en obtener la localización deseada para las entidades raíz más importantes y los patrones de acceso más comunes, y permite que las operaciones distribuidas menos frecuentes o menos sensibles al rendimiento ocurran cuando lo necesiten. La confirmación de dos fases y las lecturas distribuidas están ahí para ayudar a simplificar los esquemas y facilitar el trabajo del programador: en todos los casos prácticos que no sean los de mayor rendimiento crítico, es mejor dejarlos.

Recomendaciones:

  • Organiza los datos en jerarquías de manera que los datos leídos o escritos juntos estén cerca.
  • Considera almacenar columnas grandes en tablas no intercaladas si se accede a ellas con menos frecuencia.
  • Diseña el esquema para satisfacer las necesidades de la aplicación, y permite que la transacción distribuida y la capacidad de lectura remota de Cloud Spanner hagan su trabajo.

Opciones de índice

Los índices secundarios permiten encontrar filas rápidamente por valores distintos de la clave principal. Cloud Spanner admite índices no intercalados e intercalados. Los índices no intercalados son los predeterminados y es el tipo más análogo a lo que se admite en un RDBMS tradicional. No colocan ninguna restricción sobre las columnas que se indexan y, aunque son potentes, no siempre son la mejor opción. Los índices intercalados se deben definir en columnas que comparten un prefijo con la tabla principal y permiten un mayor control de la localización.

Cloud Spanner almacena datos de índice de la misma manera que las tablas, con una fila por entrada de índice. Muchas de las consideraciones de diseño para las tablas también se aplican a los índices. Los índices no intercalados almacenan datos en tablas raíz. Debido a que las tablas raíz se pueden dividir entre cualquier fila raíz, esto garantiza que los índices no intercalados puedan escalar a un tamaño arbitrario e, ignorando los puntos calientes, a casi cualquier carga de trabajo. También significa que las entradas de índice generalmente no están en las mismas divisiones que los datos principales. Esto crea trabajo adicional y latencia para cualquier proceso de escritura, y agrega divisiones adicionales para consultar en tiempo de lectura.

Los índices intercalados, por el contrario, almacenan datos en tablas intercaladas. Son adecuados al buscar dentro del dominio de una sola entidad. Los índices intercalados obligan a los datos y las entradas de índice a permanecer en el mismo árbol de filas, haciendo que las uniones entre ellos sean mucho más eficientes. Ejemplos de usos para un índice intercalado:

  • Acceder a fotos por varios tipos de órdenes como fecha de toma, fecha de última modificación, título, álbum, etc.
  • Encontrar todas las publicaciones que tienen un conjunto particular de etiquetas.
  • Encontrar pedidos de compras anteriores que contenían un artículo específico.

Recomendaciones:

  • Usa índices no intercalados cuando necesites encontrar filas desde cualquier lugar de la base de datos.
  • Prefiere los índices intercalados siempre que las búsquedas tengan un alcance en una sola entidad.

Cláusula de índice STORING

Los índices secundarios permiten buscar filas por atributos que no sean la clave principal. Si todos los datos solicitados se encuentran en el índice en sí, se pueden consultar por sí solos sin leer el registro principal. Esto puede ahorrar recursos significativos ya que no se requiere unirse.

Las claves de índice están limitadas a 16 en número y 8 KiB en tamaño agregado, lo que restringe lo que se puede incluir en ellas. Para compensar estas limitaciones, Cloud Spanner tiene la capacidad de almacenar datos adicionales en cualquier índice, a través de la cláusula de STORING. El STORING de una columna en un índice genera como resultado la duplicación de sus valores, con una copia almacenada en el índice. Puede pensar en un índice con STORING como una simple vista materializada de una sola tabla (las vistas no son compatibles de forma nativa en Cloud Spanner en este momento).

Otra aplicación útil de STORING es como parte de un índice NULL_FILTERED. Esto permite definir, de forma efectiva, lo que es una vista materializada de un subconjunto disperso de una tabla que se puede buscar de manera eficiente. Por ejemplo, se puede crear dicho índice en la columna is_unread de un buzón de correo para poder publicar la vista de mensajes no leídos en una búsqueda de tabla única, pero sin pagar una copia completa de cada buzón de correo.

Recomendaciones:

  • Haga un uso prudente de STORING para compensar el rendimiento del tiempo de lectura con el tamaño de almacenamiento y el rendimiento del tiempo de escritura.
  • Use NULL_FILTERED para controlar los costos de almacenamiento de los índices dispersos.

Antipatrones

Antipatrón: orden de marca de tiempo

Muchos diseñadores de esquemas tienden a definir una tabla raíz que se ordena por marca de tiempo y se actualiza en cada escritura. Esta es una de las cosas menos escalables que se puede hacer. La razón es que este diseño genera como resultado un gran punto caliente al final de la tabla que no se puede mitigar fácilmente. A medida que aumentan las velocidades de escritura, también lo hacen las RPC en una sola división, al igual que los eventos de contención de bloqueo y otros problemas. A menudo, este tipo de problemas no aparecen en pequeñas pruebas de carga y, en su lugar, aparecen después de que la aplicación ha estado en producción por cierto tiempo. Para entonces, ¡es demasiado tarde!

Si la aplicación debe incluir un registro ordenado por marca de tiempo, considera si puedes convertir el registro local entrelazándolo en una de las otras tablas raíz. El beneficio de esto es el de distribuir el punto caliente en muchas raíces. Pero aún se debe tener prestar atención de que cada raíz distinta tenga una velocidad de escritura suficientemente baja.

Si se necesita una tabla ordenada por marca de tiempo global (raíz cruzada), y se necesita admitir velocidades de escritura más altas en esa tabla que las de un solo nodo es capaz de admitir, usa fragmentación de nivel de aplicación. Fragmentar una tabla significa dividirla en un número N de divisiones aproximadamente iguales llamadas fragmentos. Esto normalmente se hace prefijando la clave principal original con otra columna ShardId que incluye valores enteros entre [0, N). El ShardId para una escritura determinada se selecciona normalmente al azar o al realizar el cifrado de hash en una parte de la clave base. Es preferible usar cifrado de hash porque puede usarse para asegurar que todos los registros de un tipo determinado entren en el mismo fragmento para mejorar el rendimiento de la recuperación. De cualquier manera, el objetivo es garantizar que, con el tiempo, las escrituras se distribuyan por todos los fragmentos por igual. Este enfoque a veces significa que las lecturas necesitan buscar todos los fragmentos para reconstruir el orden total original de las escrituras.

Ilustración de fragmentos para paralelismo y filas en orden cronológico por fragmento

Recomendaciones:

  • Evite tablas e índices ordenados por marca de tiempo de alta velocidad de escritura a toda costa.
  • Use alguna técnica para propagar puntos calientes, ya sea intercalando en otra tabla o por fragmentación.

Antipatrón: secuencias

Los desarrolladores de aplicaciones adoran usar secuencias (o autoincrementos) de bases de datos para generar claves principales. Este hábito de los días de RDBMS (llamados "claves sustitutivas") es casi tan dañino como el antipatrón para ordenar las marcas de tiempo descrito anteriormente. La razón es que las secuencias de la base de datos tienden a emitir valores de una manera casi monotónica, a lo largo del tiempo, para producir valores que se agrupan uno cerca del otro. Esto normalmente produce puntos calientes cuando se usan como claves principales, especialmente para las filas de la raíz.

Contrario a la sabiduría convencional de RDBMS, recomendamos usar atributos del mundo real para claves primarias cuando tenga sentido. Este es particularmente el caso si el atributo nunca va a cambiar.

Si deseas generar claves principales únicas numéricas, intenta hacer que los bits de orden superior de los números subsiguientes se distribuyan aproximadamente igual en todo el espacio numérico. Un truco es generar números secuenciales por medios convencionales, y luego invertir los bits para obtener un valor final. De forma alternativa, se podría examinar un generador de UUID, pero presta atención: no todas las funciones de UUID se crean por igual, y algunas almacenan la marca de tiempo en los bits de orden superior, lo que impide de forma efectiva el beneficio. Asegúrate de que el generador UUID elija bits de orden superior de forma pseudoaleatoria.

Recomendaciones:

  • Evita usar valores de secuencia incrementales como claves principales. En cambio, invierte los bits de un valor de secuencia, o usa un UUID cuidadosamente elegido.
  • Usa valores del mundo real para claves principales en lugar de claves sustitutivas.
¿Te sirvió esta página? Envíanos tu opinión:

Enviar comentarios sobre…

Cloud Spanner Documentation