CockroachDB in detail

El primer post de 2022 no podía sino estar centrado en una de esas bases de datos distribuidas que tanto gustan por aquí, y en esta ocasión le ha llegado el turno a CockroachDB, un producto claramente inspirado en el archiconocido Google Cloud Spanner. Ahora bien, ¿hasta dónde llega dicha inspiración? ¿Está a la altura del servicio del gigante de Mountain View? Para conocer estas respuestas y mucho mas, toma una buena taza de café, acomódate y disfruta.

Como ya es costumbre en la casa, en el presente artículo se pretende describir la arquitectura y características de la citada base de datos, no sin antes invitaros a leer la primera y segunda parte acerca de los conceptos básicos de las bases de datos distribuidas.

Introduction

CockroachDB se describe oficialmente cómo una base de datos SQL distribuida construida sobre un sistema de almacenamiento clave-valor transaccional y consistente, que escala horizontalmente; sobrevive a fallos de disco, máquina, rack e incluso centros de datos sin necesidad de intervención manual; admite transacciones ACID con consistencia total; y proporciona una API SQL para interactuar con los datos.

Si bien se trata de una definición algo larga y farragosa cuando se traduce al castellano, no se puede negar que tienen muy claro lo que quieren transmitir. Es decir, se trata de una base de datos relacional distribuida, multiregión, horizontalmente escalable, altamente disponible y lo mas importante, que garantiza la consistencia de las operaciones de forma global entre todos los nodos que conforman el cluster. Y sí, internamente utiliza un sistema de almacenamiento clave-valor para dar vida al producto, de igual manera que Cloud Spanner. Oh no, ya empieza con las comparaciones y solo está en la introducción…

Comencemos con un poco de historia. CockroachDB nace en febrero de 2014 como proyecto open-source en GitHub de la mano de Spencer Kimball, un ex-ingeniero de Google y miembro clave en el desarrollo de Google File System, para ofrecer una alternativa open-source a Cloud Spanner. En 2015 fundaría Cockroach Labs junto a Peter Mattis y Ben Darnell, también ex-empleados de Google, con los que compartía su interés por Bigtable, y cómo no, por Spanner.

Así, el proyecto atrajo rápidamente el interés de la comunidad y con ello una gran cantidad de contribuidores, lo que hizo que ganara el premio Open Source Rookie of the Year otorgado por Black Duck Software.

En Junio de 2019 la compañía neoyorquina anunciaba un cambio de licenciamiento, pasando de una licencia Apache License 2.0 a una licencia BSL (Business Source License), con la que evitar que empresas terceras puedan ofrecer una versión comercial de CockroachDB como servicio sin necesidad de comprar una licencia. Seguro que mñas de uno recordáis como Elastic llevo acabo este mismo movimiento a principios de 2021 para evitar que AWS pudiera ofrecer un servicio gestionado de Elasticearch y Kibana sin aportar beneficio alguno al proyecto.

Architecture

CockroachDB se basa una arquitectura descentralizada peer to peer (P2P) de nodos simétricos que conforman un cluster, en el que cada uno de ellos tiene exactamente el mismo conjunto de responsabilidades que el resto. Es decir, no hay distintas tipologías de nodos que ejerzan funciones dedicadas, si bien algunos de ellos pueden desempeñar adicionalmente ligeras tareas de coordinación.

Cada nodo tiene toda la información de enrutamiento necesaria para redirigir cada petición directamente al nodo apropiado, sin necesidad de acceder a un registro centralizado en el que almacenar, tanto la ubicación de los datos, como el estado de salud del resto de los nodos que conforman el cluster.

Para ello, internamente implementa el protocolo gossip, también conocido como epidemic protocol, en el que cuando un nodo habla con otro, no solo proporciona información sobre su estado, sino también sobre los nodos con los que se había comunicado previamente.

Este diseño de arquitectura simplifica el proceso de aprovisionamiento y mantenimiento del sistema, al mismo tiempo que trata de garantizar un teórico escalado lineal del rendimiento. Es decir, si con un nodo la base de datos es capaz de atender 200 peticiones, al añadir un segundo nodo podría llegar hasta las 400, si bien esto depende de mas factores, y todo ello sin procesos de actualización que afecte todos los nodos ni tiempos de caída.

En este sentido, se desmarca completamente de la aproximación de Cloud Spanner e implementa los mismos patrones de diseño ya vistos en otras base de datos distribuidas como DynamoDB o Kafka, si bien esta ultima sigue haciendo uso de Zookeeper para llevar a cabo tareas de coordinación, o al menos, de momento.

Entrando más en profundidad, CockroachDB se compone de un conjunto de capas secuenciales que interactúan directamente entre sí como servicios opacos.

Fuente Original

La capa SQL se encarga de exponer una API SQL compatible con PostgreSQL con la que interactúan las aplicaciones, convirtiendo dichas sentencias en operaciones clave-valor, que son las que a la postre utiliza internamente la base de datos. Recordar que, tal y como se comentó en la introducción, CockroachDB emplea un sistema de almacenamiento distribuido clave-valor basado basado en RocksDB, una variante de LevelDB.

Esto tiene su lado positivo y negativo. Por una parte, el hecho de soportar el protocolo de conexión de PostgreSQL implica que las aplicaciones no deben modificar su diseño actual para poder trabajar con CockroachDB, por lo que herramientas como Dbeaver, Intellij o pgdump o los drivers de Java, C, Go o Python, entre otros, son compatibles desde el día 1 (Listado completo disponible en el siguiente enlace).

Ahora bien, dado que se trata de una API que realiza una conversión de SQL a operaciones clave-valor, algunas de las características de PostgreSQL no funcionan exactamente igual o directamente no están disponibles. Es especialmente dolorosa la ausencia de procedimientos y funciones almacenadas, los eventos o los triggers. Por lo tanto, antes de lanzaros a utilizar este producto, es mas que recomendable analizar en detalle los requisitos técnicos a corto-medio plazo y si es CockroachDB es capaz de cumplirlos.

Dicho esto y dado que todos los nodos son simétricos, cada uno de ellos contiene su propia capa de SQL y por lo tanto, puede actuar como gateway de cara el cliente, de ahí a que sea imprescindible en un entorno productivo desplegar un balanceado por delante. Así, el nodo gateway recibe la petición y la distribuye a los distintos nodos del cluster según sea necesario, devolviendo finalmente los resultados al cliente.

Para llevar a cabo esta operativa, la capa SQL se apoya en un almacén de clave-valor que contiene la ubicación de los almacenes clave-valor residentes en los nodos físicos de CockroachDB, que son los que realmente contiene los datos finales. Es decir, gestiona los detalles relativos al direccionamiento de los datos en base a la estrategia de sharding, para proporcionar la abstracción de un único almacén de valor clave monolítico. Señalar que un nodo puede contener varios almacenes clave-valor.

Data modeling

Aunque CockroachDB se auto define como una base de datos relacional, lo cierto es que internamente trabaja con un modelo de datos basado en tablas semi-relacionales esquematizadas, almacenadas en múltiples almacenes clave-valor monolíticos, que a su vez están distribuidos por los distintos nodos que conforman el cluster.

Es decir, no es un modelo puramente relacional, sino que para cada tabla se generan un conjunto de entradas clave-valor con todos los datos de la misma, lo que a la postre epermite realizar sharding a nivel de fila y garantizar así una escalabilidad horizontal teóricamente infinita al producto.

El siguiente ejemplo lo ilustra a la perfección. Dada una base de datos my-company, que alberga una tabla products, compuesta por las columnas name (clave primaria), price y stock, se generarían las siguientes entradas correspondientes al schema.

KeyValue
/system/databases/my-company/id4
/system/tables/products/id8
/system/desc/4/8/price16
/system/desc/4/8/stock23
Modelo simplificado

Por otro lado, para cada fila de la tabla se generarían las siguientes entradas:

KeyValue
/4/8/Ipad/161309
/4/8/Ipad/23123
Modelo simplificado

Así, cada clave contendría tanto el identificador de la base de datos como de la tabla, seguido del prefijo de clave primaria (iPad), seguido del sufijo de columna (16, 23).

Gracias al prefijo de la clave primaria, todas las claves para una fila de una tabla en particular se almacenan de forma contigua en el mismo almacén clave-valor, lo que permite a CockroachDB recuperar todos sus datos de forma optimiza mediante un escaneo en el prefijo.

Es decir, que la sentencia SELECT * FROM PRODUCTS WHERE NAME = ‘IPAD’ datos se traduce en Scan(/products/Ipad/, /products/Ipad/Ω), donde Ω representa el último sufijo de clave posible. Posteriormente la base de datos se encarga de formatear dicha información al formato de respuesta SQL esperado por el cliente.

Todo esto no impide que funcione de una forma similar (que no igual) a las bases de datos tradicionales conocidas por todos y a diferencia de Cloud Spanner, no existe una limitación física en cuanto al número de bases de datos, tablas, o columnas, si bien cada nodo almacena en memoria una copia completa del esquema y los metadatos de todas las tablas del cluster, por lo que tener una gran cantidad de ellas podría repercutir en una degradación en el rendimiento del sistema.

Ahora bien, esta forma de modelar internamente los datos implica que cada tabla debe contener una única clave primaria, compuesta por una o múltiples columnas, que identifique a cada fila de forma única, combinando la propiedades UNIQUE y NOT_NULLT. Ambas restricciones son imprescindibles, ya que, tal y como se ilustraba previamente, la clave primaria se emplea para generar el indice primario que posteriormente CockroachDB utiliza de forma predeterminada para acceder a los datos de la tabla.

Lo interesante es que, de nuevo, a diferencia de Cloud Spanner, sí que es posible modificar la clave primaria de una tabla, sin necesidad de recrearla. Así, si se emplea la sentencia ALTER PRIMARY KEY, el antiguo índice de clave primaria se convierte en un índice secundario, lo que permite garantizar un rendimiento optimo en aquellas consultas ejecutadas en base a la clave primaria anterior, a costa claro está, de ocupar un mayor espacio en disco. También es posible optar por el comando ALTER TABLE … DROP CONSTRAINT … PRIMARY KEY para recrear la clave primaria sin necesidad de generar un indice secundario.

Esto son solo algunos de los puntos a tener en cuenta, pero es altamente recomendable sino imprescindible leer en profundidad la documentación de CockroachDB y tener bien claros los requisitos de la aplicación a construir, antes de embarcarse en esta trepidante aventura.

Partitioning

En CockroachDB los datos se distribuyen por defecto a lo largo de los distintos nodos que forman el cluster mediante una estrategia de sharding basada en rangos de la clave primaria.

Así, inicialmente toda la información relativa a una tabla y sus índices secundarios se sitúa en un solo range (shard), si bien al alcanzar los 512 MiB de tamaño es dividido en dos de forma automática y totalmente transparente para el desarrollador, velando así por la escalabilidad del sistema.

Desde CockroachDB consideran que este tamaño es lo suficientemente pequeño como para replicarse rápidamente entre nodos, pero a su vez lo suficientemente grande como para almacenar un conjunto de datos significativamente contiguo cuyas claves es más probable que se accedan a juntas.

Fuente Original

Esta estrategia permite realizar búsquedas filtradas y ordenadas por la primary key de forma eficiente, pero en cambio no garantiza una distribución uniforme de los datos y por lo tanto, tampoco de la carga de trabajo.

Como en todo sistema distribuido, es altamente recomendable leer el documento de buenas practicas de CockroachDB para definir la clave primaria, entre otras cosas, porque podría acabar generando puntos calientes o cuellos de botella en la plataforma, que derivan en un rendimiento nefasto del sistema.

Es relativamente conocido el benchmark que publicó la gente de Yugabyte para demostrar como su producto era superior a CockroachDB, y la replica en forma de análisis, en la que probaban como se había optado intencionadamente por una clave primaria incremental, que funciona a las mil maravillas con una estrategia de sharding basada en hashing (curiosamente la que emplea por defecto YugabyteDB), pero que penaliza el rendimiento cuando se emplea con una estrategia de sharding basada en rangos. Si tenéis la ocasión, leeros ambos artículos, así como la contrarréplica de Yugabyte.

Finalmente, comentar qué desde la version 20.1 CockroachDB ofrece una estrategia de sharding basada en hashing para evitar precisamente esta problemática. No deja de ser curioso que hayan preferido tragarse su orgullo en una decisión tan importante como esta, en lugar de incidir en que son los desarrolladores quienes deben adquirir un conocimiento mínimo cuando van a trabajar con su producto.

Replication

Llega el momento de hablar de la replicación, uno de los factores clave que permite a CockroachDB proporcionar una consistencia fuerte con semántica ACID para las transacciones de lectura y escritura.

En concreto, CockroachDB emplea una estrategia de replicación síncrona de los datos basada en Raft, un protocolo de consenso que garantiza que los datos se almacenan de forma segura a lo largo de los distintos nodos del cluster. Se entiende por seguro, que todos los nodos estén de acuerdo con el estado actual de los datos, incluso si algunas de ellos se desconecta temporalmente, para lo que cada cambio deberá de ser aprobado por un quórum de las réplicas.

Para ello, la base de datos realiza al menos 3 copias de cada range y las distribuye en 3 nodos distintos, ya que es el número más pequeño con el que se puede alcanzar el quórum (es decir, 2 de 3). La clásica regla de (N/2)+1 que tantas veces se ha comentado en esta casa.

Así, Raft agrupa todos los nodos que contienen una réplica de un range en un grupo llamado grupo Raft y selecciona uno de ellos para que actúe como líder y se encargue de ejecutar los comandos, mientras que el resto de réplicas seguidoras tan solo se encargan replicar los datos, valga la redundancia.

Cuando el líder recibe una petición de escritura, la reenvía a los seguidores para replicar dicho mensaje, si bien el cambio no es confirmado hasta que la mayoría de las nodos lo corroboran (“quórum de escritura”), momento en el que el líder da la orden de hacer commit y se devuelve el resultado al cliente. Por supuesto, si alguna de las réplicas se retrasa en la escritura, pero esta ha sido confirmada por el grupo, puede solicitar los datos que faltan a otra réplica para tener una copia completa y actualizada de los mismos.

A diferencia de otros sistemas, es el líder quien está en continua comunicación con los seguidores para que estos sepan que está vivo (heartbeats) y puedan replicar los mensajes. En caso de que este fallara, se determinaría un nuevo líder entre las replicas candidatas que contengan todos los datos confirmados hasta el momento.

Destacar que la implementación de Raft empleada en CockroachDB, la cual fue desarrollada junto al equipo de CoreOS, está especialmente optimizada para la gestión masiva de heartbeats y procesamiento de peticiones en batch, ya que un nodo puede llegar a albergar millones de grupos de Raft (uno para cada range).

¿Y qué ocurre con las peticiones de lectura? ¿Son atendidas también por el líder del grupo de Raft? No exactamente. Dado que las operaciones a través de Raft son computacionalmente costosas, CockroachDB maneja el concepto de leases, con los que optimizar las operaciones de lectura, sin renunciar por ello a la consistencia.

Así, uno de los nodos del grupo Raft actúa como leaseholder, siendo el encargado de atender todas las peticiones de lectura para ese range y de proponer escrituras al líder del grupo Raf. Tal y como estaréis imaginando, a la hora de servir las operaciones de lectura no requiere interactuar con el líder ni el resto de replicas, ya que él mismo contiene la ultima versión confirmada de los datos, lo que optimiza notoriamente el proceso.

Destacar que, aunque no sea estrictamente necesario, CockroachDB trata que el leaserholder sea también el líder del grupo Raft, con el objetivo de reducir el numero de saltos entre nodos y optimizar así el tiempo de las escrituras.

De hecho, dispone de una opción muy interesante que permite rotar periódicamente el leaseholder (cada 10 minutos por defecto), en función de la ubicación geográfica desde la que mas se accede, número de peticiones recibidas y la cantidad de leaseholders del nodo, todo ello claro está, para tratar de estar lo mas próximo posible al usuario y reducir así la latencia. Sobra decir qué, en caso de que el leaserholder se cayera, cualquiera del resto de nodos trataría de ocupar su lugar.

De cara precisamente a optimizar tanto la disponibilidad como el tiempo de respuesta en aquellas operaciones de lectura, que no requieran de consistencia, CockroachDB ofrece una funcionalidad enterprise que permite leer los datos del nodo mas cercano, independientemente de si trata del leaseholder o no, lo cual puede ser francamente útil en despliegues multiregión. Evidentemente esta petición no tiene porque devolver en el resultado el ultimo valor almacenado, por lo que es conveniente estudiar con detenimiento el caso de uso, antes que hacer uso de esta característica.

En resumen, esta estrategia de replicación síncrona basada en Raft permite que se trabaje sobre la ultima versión de los datos, ya que todos los ranges contienen la ultima version confirmada de los mismos y se intenta que todas las peticiones de lectura y escritura sean atendidas por el mismo nodo. Además, en caso de que alguna de las replicas fallara, ya sea por un error en la zona o en el nodo, otra pasaría a tomar su lugar, sin que se pierdan datos por el camino.

Consistency

Tal y como se detallaba en la anterior sección, CockroachDB garantiza una consistencia fuerte con semántica ACID para las transacciones de lectura y escritura, con un nivel de aislamiento SSI (serializable snapshot isolation) por defecto, el más estricto existente, si bien también permite configurar un nivel de aislamiento SI (snapshot isolation) menos estricto, pero mas rápido.

Así, SSI simula que todas las transacciones se ejecutan de manera equivalente a una programación en serie o de forma secuencial. Esto penaliza ligeramente el rendimiento, pero evita que se puedan dar casuísticas de dirty reads, nonrepeatable reads, phantom reads y serialization anomaly.

El factor principal que la diferencia del resto de bases de datos tradicionales monolíticas, es que es capaz de garantizar una consistencia externa para todas sus operaciones. Es decir, de cara al cliente las transacciones se procesan de forma secuencial, como si de una sola maquina se tratara, aunque internamente las ejecute en varios nodos ubicados distintas zonas o regiones. Consistente a la par que horizontalmente escalable.

Para ello, se apoya no solo en el protocolo de consenso Raft, sino también en Multi-Version Concurrency Control (MVCC), un mecanismo de control de concurrencia comúnmente utilizado por los sistemas de administración de bases de datos, para proporcionar operaciones concurrentes dentro de la base de datos, sin bloqueos.

Su principal premisa es que, en lugar de actualizar directamente los datos existentes en base de datos, cada actualización crea una nueva versión de estos, de modo que aquellas peticiones de lectura concurrentes aún puedan ver la versión anterior mientras se procesa la transacción de actualización. Es decir, que las operaciones de lectura únicamente tienen visibilidad sobre aquellos datos confirmados en el momento en que comenzó la transacción.

Mas de uno os estaréis preguntando que como resuelven la ausencia del TrueTime de Google, un reloj distribuido con alta disponibilidad, compuesto por relojes atómicos y GPS, con el que se asignan distintas marcas de tiempo a todas a las transacciones para garantizar el orden de las mismas.

Como no podía ser de otra forma, la respuesta oficial es que no lo requiere, ya que desde Cockroach Labs abogan por un producto abierto que pueda ser fácilmente instalado ya sea on-premise o en la nube, de forma independiente al proveedor. Ahora bien, recomiendan encarecidamente instalar NTP u otro software de sincronización de reloj en cada nodo, antes de poner en marcha la base de datos.

Finalmente, la pregunta de rigor obligatoria. ¿Se trata entonces de una base de datos que cumple con la 3 capacidades del teorema de CAP? No, se trata de una BD CP (Consistent and Partition tolerant), es decir, que el sistema dejará de estar disponible antes de llevar a cabo operaciones que puedan causar resultados inconsistentes.

Ahora bien, al igual que Cloud Spanner, también se denomina como altamente disponible, entendiendo por ello una disponibilidad de “five nines”, es decir, el 99.999% del tiempo.

Conclusiones

En conclusión, CockroachDB es una base de datos relacional distribuida, multiregión, horizontalmente escalable, altamente disponible y lo mas importante, que garantiza la consistencia de las operaciones de forma global entre todos los nodos que conforman el cluster. Para ello, se inspira abiertamente en el modelo propuesto por Cloud Spanner, pero toma su propio camino con la inclusión del protocolo de consenso Raft o el sistema de gestión de concurrencia MVCC, entre otros.

La propuesta es realmente interesante pero aun les queda camino por recorrer, especialmente en lo que a la ausencia de ciertas funcionalidades básicas se refiere. Y sí, la falta de procedimientos y funciones almacenadas, los eventos o los triggers puede ser determinante a la hora de acceder a grandes compañías que cargan con un doloroso pasado a sus espaldas.

Dicho esto, no haríais mal en seguirles la pista, por que en unos años pueden convertirse en un estándar.

Referencias

Se recomienda encarecidamente leer los siguientes artículos que han servido de base para el escrito:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s