Backfill y Reingesta Automáticos para ClickHouse® DB
Matamos una reingesta de 20 millones de filas cuando llevaba un tercio hecho. Sin miramientos: un kill en seco a mitad de faena, igual que haría un portátil al cerrarse o un runner de CI que se queda sin tiempo. Después lanzamos un solo comando para retomarla. Acabó lo que quedaba en 25 segundos y dejó exactamente 20.000.000 de filas en el destino. Ni una perdida. Ni una duplicada.
Esa recuperación es justo el quid, así que luego vuelvo a ella. Primero, el problema que resuelve.
Si alguna vez has tenido que mover una tabla grande en ClickHouse®, te suena la escena. Cambiaste una sort key, o añadiste una columna, o arreglaste una materialized view, y de repente hay que reescribir unos cuantos miles de millones de filas para que cuadre. El primer impulso es un único INSERT ... SELECT bien gordo. Con datos de verdad, ese impulso te tira el clúster.
La trampa no es el INSERT. Es el bucle.
Una sola sentencia reescribiendo miles de millones de filas le pelea la memoria a tu tráfico de lectura, entorpece los merges de fondo, toca techo de memoria y muere. Muere al 80%, sin checkpoint, así que vuelves a empezar desde cero. Reintentas y, como el insert no es idempotente, ahora tienes la mitad de las filas duplicadas. Y en todo ese rato no tienes ni idea de por dónde iba. Ahí estás, en otra terminal, lanzando SELECT ... FROM system.processes y forzando la vista.
Así que haces lo sensato: lo troceas. Partes el rango, iteras e insertas una porción cada vez.
Y aquí es donde te pilla. El bucle de chunks es el trabajo de verdad. Para hacerlo bien tienes que elegir unos límites que mantengan cada porción por debajo de un presupuesto de memoria, lo que significa mirar cómo se reparten los datos, lo que significa pelearte con el sesgo cuando un único valor se lleva media tabla. Tienes que hacer cada chunk idempotente para que un reintento no duplique. Tienes que guardar el progreso para que una caída no te cueste la ejecución entera. Tienes que ir comprobando si ha terminado, cazar los fallos y enseñar el progreso en algún sitio. Eso ya es un sistema pequeño. La mayoría de equipos lo escriben una vez, con una fecha de entrega encima, un poco mal, y lo arrastran para siempre.
Ese bucle nos lo conocemos bien. En Numia movíamos petabytes de datos on-chain sobre ClickHouse, y la reingesta era una tarea más de la semana. Un cambio de esquema, una materialized view arreglada, una cadena que tocaba volver a traer desde un bloque temprano, y otra vez unos cuantos miles de millones de filas a moverse para que todo cuadrara. Así que escribimos el bucle de chunks. Y luego lo volvimos a escribir para la siguiente tabla. Lo metimos en un script, le colgamos el resume después de que una ejecución se muriera de madrugada y se llevara el progreso por delante, y de ahí en adelante, a parchearlo. Nunca fue el trabajo interesante. Solo estaba siempre plantado delante del trabajo interesante.
Nos cansamos de escribir ese sistema. Así que el plugin de backfill de CHKit lo escribe por ti, y ese mismo plan lo puedes correr desde tu máquina o dárselo a nuestra plataforma para que lo ejecute como un job gestionado. Es open source, con licencia MIT, y funciona contra ClickHouse Cloud, ObsessionDB o un clúster de ClickHouse autoalojado.
Apúntalo a una tabla de ClickHouse y te devuelve un plan de backfill
Empiezas por un plan. Le das una tabla y un presupuesto de tamaño por chunk:
chkit backfill plan --target analytics.events --max-chunk-bytes 2G
CHKit lee las particiones y las sort keys de la tabla y calcula los límites de los chunks él solo. El primer corte son las particiones. Cualquier partición que se pase del presupuesto se vuelve a cortar por la sort key, y decide cómo según los datos: una clave numérica se parte en rangos por cuantiles, una columna de tiempo en buckets temporales, y una clave sesgada donde un valor manda se corta por la siguiente columna de la sort key, para que ese valor caliente no acabe convertido en un chunk enorme. Tú no calculas nada de esto. Recibes un plan inmutable, una lista de chunks, cada uno más o menos del tamaño que pediste.
¿Y esto aguanta con datos reales? Lo apuntamos a wikicold, una tabla de 4.230 millones de filas en nuestro clúster de benchmark, con un presupuesto de 2 GiB. Planificó 89 chunks repartidos en 28 particiones en unos 69 segundos. Salieron parejos: 1,7 GiB de media, 2,18 GiB el más grande, todos pegados al objetivo. Nada tocado a mano.
El plan es solo contexto, todavía no se ha ejecutado nada. Y es a propósito, porque el plan es lo que consumen los dos caminos de ejecución.
Se puede interrumpir sin miedo, y reintentar sin miedo
Ahora lo ejecutas:
chkit backfill run --plan-id <id>
Aquí es donde el bucle que no escribiste se gana el sueldo. Pasan tres cosas sin que las tengas que pedir.
Cada chunk es idempotente. Cada uno lleva un token de dedup determinista, construido a partir del plan y del id del chunk, así que ClickHouse tira el bloque duplicado si el mismo chunk aterriza dos veces. Reintenta un chunk, vuelve a lanzar el plan entero, ejecútalo dos veces mientras duermes: el número de filas no se mueve. El problema de los duplicados no se arregla porque tú vayas con cuidado. Es que no puede pasar.
Los chunks corren como queries asíncronas con IDs deterministas, y su progreso se lee de vuelta desde system.query_log. No hay nada que vigilar en una terminal. La ejecución limita su propia concurrencia y va sondeando cada chunk hasta que termina.
Y una ejecución se puede reanudar. El progreso va dejando checkpoints en disco, y el resume hace algo un pelín paranoico antes de seguir: primero se reconcilia con el servidor. Comprueba qué chunks terminaron de verdad (incluidos los que se completaron en el clúster pero nunca llegaron al checkpoint local) y solo ejecuta lo que queda de verdad. --replay-failed vuelve a lanzar los que dieron error.
Nada de eso es un flag que tengas que acordarte de poner. La idempotencia, los checkpoints, el paso de reconciliación: son lo que hay por defecto, porque la única promesa que una reingesta no puede romper es que puedas irte y volver a ella. Lo hicimos pensando en la versión de ti de las 4 de la mañana mirando una ejecución a medias, no en la que lee la documentación una tarde tranquila.
Volvamos al número del principio. Planificamos 20 millones de filas en 15 chunks, matamos el proceso cuando llevaba 5, y el destino se quedó con unos 10,8 millones de filas: un estado a medias. Entonces:
chkit backfill resume --plan-id <id>
25,5 segundos después, el destino tenía exactamente 20.000.000 de filas. Volvimos a lanzar el plan entero por encima, los 15 chunks otra vez, y el número se quedó clavado en 20.000.000. El resume terminó el trabajo. La idempotencia hizo que repetirlo no cambiara nada.
Un algoritmo, dos sitios donde ejecutarlo
Todo lo de hasta aquí corre desde tu portátil, contra cualquier clúster de ClickHouse al que lo apuntes. Ese es el plugin que usará casi todo el mundo, @chkit/plugin-backfill, y no tiene ni idea de que ObsessionDB existe.
Hay un segundo plugin, @chkit/plugin-obsessiondb, también open source, que importa el mismo planificador. La diferencia es un verbo:
chkit backfill submit --target analytics.events
En vez de correr los chunks desde tu máquina, submit le pasa el plan al sistema de jobs de ObsessionDB. Los chunks corren en el servidor, en nuestra infraestructura, y a ti te devuelve un enlace al job en la consola. Lo miras ahí en lugar de tener una terminal abierta. Mismo plan, mismo SQL de los chunks, mismos tokens de dedup. Lo único que cambia es dónde corre el trabajo, y quién tiene que vigilarlo, que ahora no es nadie.
Cuando lo probamos contra un servicio del clúster de benchmark, submit montó el plan, mandó cuatro tareas al backend, y las cuatro llegaron al final en el servidor, siguiéndolas por el enlace de la consola. El número de filas se mantuvo. La misma idempotencia, porque es el mismo SQL.
Aquí tienes ese job en la consola. Progreso, filas leídas y escritas, throughput, y una fila por tarea con su duración y sus bytes. Una tarea que falla se reintenta desde aquí, no desde una terminal que te dejaste abierta. Vive al lado de tus servicios, tu monitorización y tus alertas, en el mismo sitio donde ya vigilas la base de datos.
Podríamos haber escrito un backfill de cloud aparte, ajustado a nuestra propia infraestructura. No lo hicimos a propósito. El camino gestionado importa el mismo planificador que usa el open source, corre el mismo SQL y estampa los mismos tokens de dedup. En cuanto esos dos se separan, cómo se porta en tu portátil deja de predecir cómo se porta en el nuestro, y ese es justo el momento en que la gente deja de fiarse de una herramienta. Las tripas compartidas y aburridas eran justo el objetivo.
Te lo cuento sin trampa. El código es open source y gratis; la capacidad es para clientes. Enviar un job necesita un servicio de ObsessionDB al que enviarlo. Correrlo en local no necesita nada de nosotros. El camino gestionado está para los backfills que son demasiado grandes o demasiado largos como para vigilarlos desde una terminal.
Cuándo tirarías de esto
Reingestar después de un cambio de esquema es el caso obvio, y es el que llevamos rato describiendo. Hay otros cuantos con la misma forma. Recomputar una materialized view después de cambiarle la definición, donde el backfill mete el insert por el propio SELECT de la vista. Volver a traer un dataset desde cero. Rellenar una ventana de datos derivados después de arreglar el bug que los calculó mal la primera vez. Cualquier copia masiva que se repita y que, si no, te tocaría mantener a mano con ese bucle.
La reingesta es el primer job. La apuesta es la plataforma.
El camino gestionado no es una copia de la CLI subida a un servidor. Es la primera pieza de cara al cliente de un sistema de jobs que llevamos tiempo construyendo dentro de ObsessionDB: lo que mueve grandes cantidades de datos por nuestros propios clústeres.
La reingesta es el primer tipo de job que abrimos, pero la forma es general. Un recompute de materialized view es el mismo problema. Y también lo son una importación masiva desde un bucket, traer un origen desde cero, o rellenar una ventana después de un arreglo. Todos son una gran cantidad de datos que tiene que moverse, con seguridad, bajo demanda o en un horario, sin nadie vigilando una terminal.
La apuesta es sencilla. Las operaciones de datos fáciles y repetibles son lo que separa una base de datos gestionada de una simplemente más barata. El rendimiento y el precio te meten por la puerta. Pero si un equipo puede recomputar un modelo, rellenar un arreglo y reingestar un origen varias veces al día sin tocar infraestructura ni escribir a mano un bucle de chunks, eso cuesta mucho de soltar.
backfill submit es el paso uno. Tu reingesta corre en nuestra infraestructura, y la miras en la consola. Lo que viene después es el resto de tipos de job, y una forma de crearlos y seguirlos sin tocar la CLI para nada.
La regla que me apuntaría
Si un job es puntual y cabe en una sentada, córrelo en local. Si es grande, largo, tiene que sobrevivir a que cierres el portátil, o quieres que quede registrado y sea repetible, envíalo. Local para lo rápido. Submit para lo que no quieres sostener en las manos.
Pruébalo
El plugin de backfill está en GitHub y en npm:
bun add -d @chkit/plugin-backfill
Es MIT, y funciona contra ClickHouse Cloud, ObsessionDB o un clúster de ClickHouse autoalojado (el soporte de un solo nodo está al caer). Está en beta: los comandos del core son estables, puede haber pequeños cambios que rompan algo antes de la 1.0, y el camino de submit es nuevo, lo lanzamos esta semana. Corremos backfill en producción para la reingesta de clientes, que es de donde salieron casi todos estos detalles.
Para el camino gestionado, añade @chkit/plugin-obsessiondb y levanta un servicio gratis en console.obsessiondb.com.
La referencia completa de comandos está en la documentación de backfill. Y si aún no has visto el lado de esquema de CHKit, el post sobre schema-as-code cuenta cómo las propias tablas se convierten en código.
Seguir Leyendo
Publicado originalmente en obsessionDB. Lee el artículo original aquí.
ClickHouse is a registered trademark of ClickHouse, Inc. https://clickhouse.com