La forma incorrecta de escalar Cassandra DB cuando los índices secundarios están en su lugar
Cassandra es mi base de datos favorita (no administrada) por muchas razones: no tiene un solo punto de falla (SPoF), es compatible con varias regiones, es buena para operaciones de lectura y escritura, flexible sobre los niveles de consistencia de lectura y escritura, se escala linealmente y no es demasiado compleja de administrar para las operaciones diarias.
Al igual que todas las bases de datos, debe usar Cassandra en función de sus patrones de acceso a datos, por lo que si necesita una base de datos flexible para consultas ad hoc o lo suficientemente adaptable para cambios constantes del modelo de base de datos, debe considerar otras opciones.
Cassandra es una base de datos orientada a columnas y es realmente potente cuando ya tiene definidas sus consultas de datos. Datastax, la compañía que admite Cassandra, recomienda que comience por diseñar sus consultas y luego su modelo de datos en Cassandra. A pesar del hecho de su estructura de columnas, Cassandra admite muchas estructuras de datos como tipo(s) de columna, como Mapas.
Cassandra es una base de datos de claves principales, lo que significa que sus datos se conservan y se organizan en torno a un clúster basado en el valor hash (la clave de partición) de la clave principal. Para tablas que tienen más de una PK, Cassandra considera solo la primera parte de la PK como su clave de partición. Vea más sobre las teclas compuestas aquí.
Para ser más claros, volvamos a una de las características más importantes de una Cassandra DB: es arquitectura y el hecho de que no tiene un SPoF.
Un clúster de Cassandra está compuesto por nodos (3 o más) y esos nodos juntos componen un anillo de nodos:
Cada nodo en un clúster de Cassandra funciona “de forma independiente”, pero diferentes nodos podrían almacenar los mismos datos de acuerdo con la configuración del factor de replicación (RF) configurada para el clúster.
Para saber dónde (qué nodo) se conservan sus datos, Cassandra utiliza el valor hash (token) calculado a través de una función hash consistente utilizando la columna PK de una tabla dada.
Cuando ejecuta una consulta, el nodo coordinador (normalmente la instancia de aplicación más cercana) buscará qué nodos de un anillo tienen sus datos, de esa manera, si un nodo está inactivo por alguna razón, otro nodo podría servir sus datos (RF ≥2). Esa es la magia de un enfoque sin maestro, donde cada nodo de un anillo es igual en términos de lectura y escritura.
Este concepto sobre PK y el factor de replicación es muy importante para comprender cómo escalar el clúster de Cassandra cuando la aplicación se encuentra en condiciones de alta carga.
Índices secundarios
Cassandra también tiene el concepto de índices secundarios. En bases de datos relacionales, puede tener muchos índices en una tabla dada, el costo de tener un índice secundario está asociado con operaciones de escritura, no con operaciones de lectura. En Cassandra esto no es cierto.
Los índices secundarios en Cassandra pueden ser útiles y tentadores cuando su modelo de datos cambia y necesita realizar consultas basadas en una nueva columna.
De esa manera, con un índice secundario, podría ejecutar una consulta como esa:
SELECCIONE * DE mi_table DONDE SECONDARY_INDEX = ‘valor’;
El problema de usar un Índice secundario
Imagine el escenario: está en un Blackfriday / CyberMonday y su clúster de Cassandra sufre de eventos pico y necesita agregar más nodos para escalar su base de datos, equilibrar mejor su tráfico y surviving sobrevivir. Bien, ¿verdad?
Normalmente, es una situación normal en una aplicación altamente escalable. Pero, ¿qué pasa si su aplicación está ejecutando consultas utilizando un índice secundario?
Sí, entendiste.
¿Recuerdas cuando dije que Cassandra distribuye datos en un anillo usando la clave de partición? Esto ya está sucediendo, pero el problema es cuando introduce un índice secundario en su consulta. Los índices secundarios NO SON PARTE de una clave de partición, y Cassandra sabe dónde viven sus datos a través de la clave de partición. Cuando ejecuta una consulta que utiliza este tipo de índice, lo que hace Cassandra es buscar cada nodo de su anillo tratando de satisfacer su consulta.
Escenario real
Durante un Blackfriday, nuestras aplicaciones eran con altas cargas. Muchos y muchos clientes que desean beneficiarse de los enormes descuentos proporcionados por un evento Blackfriday.
Echamos un vistazo a nuestra APM y todo el análisis nos llevó a nuestra persistencia, en este caso una Cassandra DB. Tenemos largos períodos de latencia,pero no para todas las solicitudes, solo para algunas.
Tratando de mantener las cosas a su estado normal de nuevo, nuestra primera maniobra fue agregar más nodos a nuestro clúster de Cassandra.
Agregamos y todavía estamos sufriendo problemas de latencia. La pregunta era: ¿por qué sigue ocurriendo esto?
estábamos equivocados. Fue una conclusión simplista y no nos preocupamos por un detalle muy importante: este comportamiento estaba ocurriendo no en todas las solicitudes, sino en algunas de ellas.
Si pensaste en el índice secundario, ¡bingo! Ese era exactamente el problema.
Agregar nodos nunca resolvería el problema, porque el problema no estaba relacionado con todas las consultas que llegaban a la base de datos, el problema estaba en algunas y esas eran las reales que degradaban el rendimiento de la base de datos. Era totalmente una cosa de Pareto.
Detallando el problema y cómo lo mitigamos
En un momento antes del evento Blackfriday, necesitábamos cambiar nuestro modelo de datos. Regionalizamos nuestra aplicación y la región del cliente comenzó a ser importante para nosotros, necesitábamos consultar datos basados en un producto O región.
Mirando hacia atrás y conectando los puntos, pudimos darnos cuenta de que éramos muy valiosos con la implementación, ya que queríamos reflejar este nuevo comportamiento no solo en la capa de API (nuevo parámetro de consulta), sino también en la forma en que accedíamos a los datos en Cassandra.
¿Y por qué éramos tan preciosos? Porque incluso teniendo en cuenta que nuestro tiempo de consulta no aumentó tanto, hicimos el cambio.
Esa implementación no solo aumentó nuestro tiempo de consulta mediante el uso de un índice secundario, sino que también generó más problemas de acuerdo con la ampliación de la infraestructura de Cassandra. A medida que agregábamos más nodos en nuestro clúster, significaba más nodos para buscar los datos, por lo que el problema aumentaba exponencialmente.
Para mitigar el problema, lo que hicimos fue recuperar el número de nodos que teníamos anteriormente y aumentar el factor de replicación para la mayoría de nuestros nodos en el clúster.
También cambiamos nuestro nivel de consistencia de lectura para ser menos consistente. Estábamos usando * QUÓRUM y en su lugar cambiamos a UNO. Esto nos ayudó a reducir la carga en los nodos.
Como congelamos nuestras aplicaciones días antes del evento, sabíamos que no teníamos datos nuevos (operaciones de escritura) y que los datos serían consistentes en su estado actual.
Los días siguientes y la solución de modelo de base de datos
Como parte de la solución final, necesitábamos (re)pensar en nuestro modelo de base de datos y revertir los cambios que hicimos como ruta de mitigación durante el evento.
Antes del evento estábamos usando el ID de producto (PID)como clave de partición, lo cual fue una buena decisión, ya que el PID tiene buenos atributos para ser un PK debido a su naturaleza de ser un número secuencial (alta cardinalidad), y de esa manera distribuir los datos uniformemente alrededor del clúster.
Sobre el nuevo campo “región”, aprovechamos el tipo de datos de colecciones Cassandra y utilizamos un mapa para cada región como columna en nuestra tabla de productos.
¿Los índices secundarios son siempre una mala idea?
La respuesta corta es no.
Explicando un poco mejor, hay dos tipos de índices en Cassandra: índices locales y globales.
Un índice local, como su nombre lo indica, es un tipo de índice que existe solo localmente, es decir, en un nodo. Al crear un índice secundario, Cassandra crea una tabla nueva (oculta) en la que la clave secundaria se convierte en una clave primaria en esta tabla. La visibilidad de esta nueva tabla es en términos de un nodo, no de un anillo (clúster). Ese es el caso de los índices secundarios.
Por otro lado, un índice global tiene visibilidad de anillo a través de su clave de partición, por lo que Cassandra sabe dónde están los datos en un anillo a través de esa clave de partición.
Los índices secundarios podrían ser una alternativa, cuando tiene en una consulta tanto índices primarios como secundarios. En ese caso, Cassandra sabe dónde residen sus datos (qué nodo) a través de la clave de partición y luego busca la tabla local en el nodo que hace referencia a los índices secundarios (locales).
También hay algunos otros matices sobre los índices secundarios que se explican muy bien aquí, pero la mejor práctica es evitarlos desnormalizando su modelo de datos.