Qué Cambia el Almacenamiento Desacoplado Cuando Construyes sobre ClickHouse® DB
En el último post de esta serie te dejé un cabo suelto. La pieza sobre diccionarios terminaba con una línea sobre cómo ObsessionDB guarda los datos en el almacenamiento de objetos, salvo los diccionarios, que siguen viviendo en la RAM de cada nodo.
Este es ese siguiente post. Y va más allá de los diccionarios.
Aquí tienes una tabla de uno de nuestros clústeres de pruebas:
CREATE TABLE hits_10b
(
WatchID Int64,
EventTime DateTime,
EventDate Date,
CounterID Int32,
UserID Int64,
URL String,
-- ...105 columnas en total
)
ENGINE = SharedMergeTree
ORDER BY (CounterID, EventDate, UserID, EventTime, WatchID)
SETTINGS storage_policy = 's3'
Diez mil millones de filas, una sola tabla, y cincuenta y cuatro gigabytes guardados una única vez. Si has corrido ClickHouse® a escala en tu propio hardware, esa definición debería incomodarte un poco. ¿Dónde está la tabla _local? ¿Dónde está la tabla Distributed por encima? ¿Dónde está el ON CLUSTER? ¿Por qué hicimos sharding?
Nada de eso es necesario en nuestra infra. Esa maquinaria que falta es de lo que va este post.
Hemos pasado esta serie yendo desde los fundamentos de ClickHouse hasta sus motores de tabla, sus vistas materializadas y sus patrones de query. Este es el cierre, y hace una sola cosa: vuelve a leer todo lo que ya aprendiste a través de un único cambio. El almacenamiento y el cómputo ya no están en la misma máquina. Voy a recorrer qué cambia para ti, la persona que escribe los esquemas.
Almacenamiento y cómputo desacoplados: el cambio del que cuelga todo
Los despliegues clásicos de ClickHouse acoplan almacenamiento y cómputo. Cada nodo es dueño de sus datos en disco local, y las réplicas guardan sus propias copias completas. La coordinación pasa por ClickHouse Keeper (o el más antiguo ZooKeeper). Si quieres que la tabla sobreviva a la caída de un nodo, la replicas, y ahora tienes dos copias de los bytes y un protocolo manteniéndolas en sincronía.
Desacoplar almacenamiento y cómputo rompe eso en dos. Una única copia de los datos vive en el almacenamiento de objetos, y nodos de cómputo sin estado leen de ahí. Separar almacenamiento de cómputo permite que cada uno escale de forma independiente. En ClickHouse Cloud el motor que hace esto es SharedMergeTree. Nosotros construimos nuestra propia versión de la misma idea, por razones a las que llegaré al final.
No voy a volver a explicar cómo funciona por dentro. Ya escribimos esos posts. Si quieres el porqué, lee La cruda verdad sobre autoalojar ClickHouse a escala, que recorre las matemáticas de la replicación que nos echaron de nuestros propios clústeres. Si quieres saber cómo las queries siguen siendo rápidas con los datos en S3, lee cómo construimos una caché distribuida sin estado. Este post da la arquitectura por supuesta y hace una pregunta más concreta: ¿qué es distinto cuando construyes sobre ella?
Dejas de escribir la replicación en tu esquema
Así es como se ve una tabla con sharding y replicación en ClickHouse autoalojado. Escribes la tabla de almacenamiento:
CREATE TABLE hits_local ON CLUSTER my_cluster
(/* columnas */)
ENGINE = ReplicatedMergeTree('/clickhouse/{shard}/hits', '{replica}')
ORDER BY (CounterID, EventDate);
Luego escribes la tabla que realmente consultas, la que reparte las peticiones entre los shards:
CREATE TABLE hits ON CLUSTER my_cluster AS hits_local
ENGINE = Distributed(my_cluster, default, hits_local, rand());
Dos objetos para una sola tabla lógica. Una expresión de sharding que tuviste que elegir (rand() aquí, a menudo un hash de alguna columna, y la elección equivocada te deja un sesgo que estarás corrigiendo durante meses). Cada cambio de esquema sale con ON CLUSTER y se propaga por Keeper, y cuando un shard aplica el cambio y otro no, te toca depurarlo a una hora poco agradable.
La versión desacoplada es el CREATE TABLE con el que abrí. ENGINE = SharedMergeTree. Sin segunda tabla. Sin ON CLUSTER. Sin clave de sharding, porque no hay shards entre los que repartir. Una copia de los datos, una definición, un ALTER cuando necesitas cambiarla.
Lo que quiero que notes es que esto no es una mejora de operaciones que lees en unas notas de versión. Cambia lo que tecleas, las migraciones dejan de ser problemas de sistemas distribuidos, y escribes la tabla como lo harías en un portátil, y corre sobre un pool de nodos sin estado.
Separación cómputo-cómputo: un pool por carga de trabajo
En un clúster acoplado, añadir capacidad significa mover datos. ¿Un shard nuevo? Copia un trozo de tu dataset en él y reequilibra. Con veinticinco terabytes eso se mide en días, y planificas a su alrededor.
Desacoplado, añadir un nodo es una operación de metadatos. El nodo nuevo lee el mismo almacenamiento de objetos que todos los demás, así que aparece, se registra y empieza a servir. No hay datos que copiar porque los datos nunca estuvieron atados al nodo. Los nodos nuevos arrancan en segundos. Sigue pareciéndome ligeramente irrazonable, en el buen sentido, después de años tratando los cambios de capacidad como proyectos.
Pero esa velocidad no es la parte interesante. La parte interesante es lo que te deja construir.
Esta es la parte alrededor de la que yo diseñaría de verdad, y tiene nombre: separación cómputo-cómputo. Divides el cómputo en pools separados que leen todos la misma única copia de los datos, y dimensionas cada pool para el trabajo que hace. La capa de serving de API en tiempo real tiene sus propios nodos, pequeños y siempre calientes, afinados para lookups de baja latencia. Las queries analíticas pesadas y ad-hoc tienen un pool distinto que puede crecer mucho y luego encogerse. La ingesta tiene el suyo. Un endpoint caro nuevo, o un backfill nocturno que antes te quitaba el sueño, puede tener cómputo dedicado que nadie más nota.
Todos ven los mismos datos. Sin copias, sin un segundo sistema, sin un pipeline moviendo filas entre un clúster de "analítica" y uno de "serving". Y el fallo con el que ha tropezado todo el que construye sobre ClickHouse, donde un GROUP BY pesado sobre diez mil millones de filas ahoga la ruta que responde a tus usuarios, deja de ser un ejercicio de tuning. Le das a esa query su propio cómputo y dejas el pool de serving en paz.
Y el almacenamiento sigue siendo único. Nuestra tabla de diez mil millones de filas son cincuenta y cuatro gigabytes en el almacenamiento de objetos, y punto. En un montaje ReplicatedMergeTree de tres réplicas eso son unos ciento sesenta gigabytes de los mismos datos, porque cada réplica guarda su propia copia. El almacenamiento escala añadiendo bytes. El cómputo escala añadiendo nodos. Dejaron de ser la misma palanca.
Todo lo que aprendiste sigue funcionando
Cuando la gente oye "el motor de almacenamiento es distinto", asume que su conocimiento de ClickHouse, ganado a pulso, ahora es sospechoso. No lo es. Desacoplar es un cambio en dónde viven los bytes y en cómo el cómputo se conecta a ellos, pero no es un cambio en el motor de queries.
ORDER BY y el índice primario disperso funcionan exactamente igual que antes. Los códecs de compresión, igual. La semántica de merge de MergeTree, igual. Y cada motor especializado que aprendiste en esta serie se comporta de forma idéntica: ReplacingMergeTree sigue deduplicando en el merge, AggregatingMergeTree sigue plegando filas en estados de agregación, CollapsingMergeTree sigue cancelando pares, las vistas materializadas siguen disparándose en el insert, las proyecciones siguen dándote un orden alternativo. Si interiorizaste el post de optimización de queries, todo se transfiere. A ninguno de esos mecanismos le importa que las partes sobre las que operan vivan en S3.
Lo que cambia es operativo, y cambia a tu favor. Las partes existen una sola vez, en el almacenamiento compartido, en lugar de N veces entre réplicas. Los merges de fondo corren contra esa copia compartida. Nuestra tabla de diez mil millones de filas está ahora mismo en una única parte activa: los merges ocurrieron, colapsaron miles de inserts en una parte, y nunca programamos ni vigilamos nada de eso.
Desacoplar movió tus datos, las partes de columnas, al almacenamiento de objetos. No movió la capa en memoria que mantiene rápido a ClickHouse, y nunca fue la intención. El índice primario disperso construido a partir de tu sorting key sigue cargándose en la RAM del nodo que corre la query, exactamente como en autoalojado, y lo mismo las cachés de marks y de datos sin comprimir que viven en la ruta de lectura. Los diccionarios son parte de esa misma capa, solo que la más visible: cada nodo de cómputo guarda una copia completa en memoria de cada diccionario, refrescada como lo haría un único servidor autoalojado. La razón es la latencia. dictGet existe para enriquecer en submilisegundos, y un viaje de ida y vuelta a S3 en cada lookup arruinaría el propósito, así que el diccionario se queda en RAM y pagamos el coste en cada nodo. Esa es la respuesta al cabo suelto que te dejé: desacoplar cambió dónde viven tus datos, no dónde viven tus estructuras calientes en memoria.
Una objeción antes de que alguien la plantee. El ClickHouse open source ya puede poner datos de MergeTree en un disco s3, y sí, eso es real. Pero eso son datos sobre almacenamiento de objetos con cada réplica todavía dueña de su propio estado y coordinación. No es cómputo sin estado sobre almacenamiento compartido. Guardar partes en S3 y desacoplar cómputo de almacenamiento son cosas distintas, y la diferencia es todo el post.
El coste nuevo que tienes que tener en cuenta
Te mentiría si te dijera que desacoplar es gratis. Te quita trabajo de encima y te pone una cosa nueva.
Las buenas noticias primero. Guardar una vez y comprimir bien suma. Esa tabla de diez mil millones de filas son 5,71 terabytes sin comprimir y 54 gigabytes en disco, lo que da en torno a cien a uno en este dataset (los datos hits comprimen inusualmente bien, así que no te tomes el ratio exacto al pie de la letra). Pagas por almacenamiento de objetos, que es barato y duradero, y lo pagas una sola vez.
Aquí está lo nuevo. Cuando los datos viven en el almacenamiento de objetos, la primera lectura de una parte en frío paga un viaje por la red, y la red es ahora tu cuello de botella en lugar del disco. Nuestra caché absorbe la mayor parte de eso, y está distribuida en cada nodo.
Ese hueco es lo que ahora razonas y antes no. Qué datos necesitan mantenerse calientes. Cuánto cuesta una query en frío la primera vez que alguien la corre. Cómo se comporta la caché bajo tu patrón de acceso. El mecanismo que mantiene calientes los datos calientes, nuestra caché distribuida, tiene su propio post y no lo voy a repetir aquí. Como builder, el hábito que adquieres es pensar en el "calor" como una propiedad de tu carga de trabajo.
Y el límite honesto: este trato no le compensa a todo el mundo. Si tu dataset es pequeño, estático y cabe cómodo en un solo nodo, desacoplar añade una penalización de lectura en frío (piensa en cinco a quince milisegundos en una query fría) a cambio de una elasticidad que no vas a usar. El modelo se gana el sueldo a escala, con cargas mixtas en tiempo real y analíticas, con datos que crecen y carga que tiene picos. Por debajo de eso, un MergeTree de un solo nodo es la respuesta correcta y te lo diré sin problema.
Lo que dejas de pagar es la parte que a la gente se le olvida contar. Sin un conjunto de ZooKeeper que mantener vivo. Sin reequilibrar shards a las 3 de la mañana. Sin almacenamiento de réplicas multiplicando tu factura por tu factor de replicación. Eso es dinero de verdad y fines de semana de verdad.
A dónde lleva esto
Probablemente ya ves a dónde apuntaba la serie. La versión cloud-native de MergeTree, hacia la que ha estado caminando todo este embudo, es lo que construimos en ObsessionDB: almacenamiento y cómputo desacoplados, las ventajas del modelo SharedMergeTree, hecho por gente que corrió ClickHouse a escala casi de petabytes para Numia y se cansó del impuesto de la replicación.
Quiero ser claro sobre el panorama, porque exagerar aquí es fácil y está mal. Desacoplar no es invención nuestra y no voy a fingir que lo sea. Lo que defendemos es más concreto: te damos la misma arquitectura sin el modelo de precios ni el lock-in de ClickHouse Cloud, alojada en la UE, con los controles que los operadores de verdad quieren, y a mejor precio por rendimiento. En un benchmark de diez mil millones de filas medimos un 35% más de velocidad en queries y hasta 9x mejor rendimiento por dólar que ClickHouse Cloud. Esos números tienen su propio post; los cito, no los vuelvo a correr aquí.
Si corres ClickHouse a escala y las matemáticas de la replicación de esta serie te sonaron familiares, hacemos evaluaciones de carga de trabajo en ObsessionDB. Trae tus queries reales y te enseñamos la comparación lado a lado.
Empezamos esta serie en los fundamentos de ClickHouse. La terminamos en cómo construir sobre el motor una vez que el almacenamiento y el cómputo se separan. La versión corta: desacoplar elimina una categoría de trabajo que solías hacer (sharding, replicación, coordinación), añade exactamente un hábito (pensar en qué se mantiene caliente), y deja intacto todo lo que aprendiste sobre modelado y queries. Es un buen trato. La mayor parte del tiempo.
Seguir Leyendo
Publicado originalmente en obsessionDB. Lee el artículo original aquí.
ClickHouse is a registered trademark of ClickHouse, Inc. https://clickhouse.com