Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Reduce costos de BigQuery y Looker con ClickHouse — Parte 2

By Sayle MatthewsJun 30, 202416 min read

Esta página también está disponible en English, Deutsch, Français, Italiano, 日本語 y Português.

Continuemos

En la sección anterior vimos qué hace este plan en concreto y cómo crear un servicio básico de ClickHouse con la oferta DBaaS de Aiven o de ClickHouse. En esta sección vamos a empezar a llevar los datos a ClickHouse y a configurar la replicación entre este y BigQuery.

Repositorio de GitHub

A lo largo del artículo voy a hacer referencia a una Cloud Function y a un job de BigQuery. El código fuente de ambos se encuentra aquí, en este repositorio de GitHub.

Ten en cuenta que el código es muy simple a propósito y está pensado como material de aprendizaje. Por eso, lo más probable es que tengas que personalizarlo para usarlo en un escenario real.

Cómo sacar los datos de BigQuery hacia ClickHouse

Hoy en día no existen métodos automatizados para hacer CDC o streaming de datos desde BigQuery hacia un destino arbitrario. Esto significa que los datos deben capturarse antes de insertarlos en BigQuery o extraerse después de que se insertaron. La falta de un método oficial hace que este sea un tema bastante extenso. Por eso, en este artículo voy a cubrir únicamente las cargas en batch.

Eso lo veremos en la siguiente sección, pero primero hay que sacar el dataset inicial de BigQuery y llevarlo a ClickHouse para crear nuestra línea base.

BigQuery cuenta con una función de exportación que es la forma más sencilla de mover datos hacia otro lugar. Sin embargo, tiene dos desventajas clave: solo permite exportar una tabla a la vez y solo puede apuntar a GCS para almacenarlos.

Dada esta limitación de una sola tabla a la vez, este es un buen momento para definir si todas las tablas deben replicarse en ClickHouse o cuál subconjunto es buen candidato para replicar y consumir desde Looker.

Una forma rápida de listar todas las tablas es ejecutar la siguiente consulta contra tu dataset, que te devolverá la lista completa. Además, si no tienes claro qué tablas se usan con más frecuencia, esta otra consulta mostrará el conteo de queries que han llegado a cada tabla del dataset. Ten cuidado, porque esta consulta puede salir bastante cara, así que primero revisa la estimación de costo en la UI y ajusta la cantidad de días que escanea antes de ejecutarla.

Cuando llegue el momento de hacer la exportación, lo mejor es usar el comando bq de la CLI y tomar la tabla completa. Lo recomiendo porque usar el comando SQL "EXPORT DATA…" genera cargos de procesamiento/escaneo por la cantidad de datos exportados, o cargos de slots si usas Editions, mientras que el comando de la CLI o la llamada a la API simplemente vuelcan la tabla completa sin cargos adicionales.

Si solo necesitas una parte de la tabla, que normalmente es un conjunto de particiones, hay un truco: usar el comando bq cp para copiar una partición a una nueva tabla y luego cargarla directamente en ClickHouse. Lamentablemente, este comando no admite wildcards ni más de una partición a la vez, así que tienes que ejecutarlo por cada partición. Esto se puede automatizar con un script sin mayor problema, pero igual quiero dejar el comando aquí:

bq cp –append_table=true `<source_project_id>:<source_dataset>.<source_table>$<source_partition_name>` `<target_project_id>.<target_dataset>.<target_table>`

Si la tabla no está particionada (y no es enorme), te recomiendo cargarla completa en ClickHouse y depurarla desde ahí con SQL para no incurrir en cargos de procesamiento en BigQuery.

Cuando estés listo para hacer la exportación inicial, sigamos.

Voy a volcar los datos en formato parquet, ya que es el tipo de archivo más cercano al sistema de archivos que utiliza BigQuery y se carga fácilmente en ClickHouse manteniendo intactos los tipos de columna.

El comando para exportar una tabla desde BigQuery es:

bq extract — destination_format=PARQUET \
<project_id>:<dataset>.<table_name> \
gs://<bucket_name>/<path_and_filename>

(Una nota: si vas a poner los archivos en una subcarpeta, asegúrate de que exista en tu bucket; si no, recibirás un mensaje de error muy poco claro sobre argumentos posicionales.)

Una vez exportada la tabla a GCS, es momento de cargarla en ClickHouse y así seguimos con el recorrido.

Cargar los datos iniciales en ClickHouse

Al cargar en ClickHouse, la consulta SQL oficial a ejecutar es esta (al momento de escribir):

CREATE TABLE <table_name>
ENGINE = MergeTree
ORDER BY tuple() AS
SELECT *
FROM S3Cluster(default,
  'https://storage.googleapis.com/<bucket_name>/<path>/*.parquet');

Nota: hay un bug en algunas versiones de ClickHouse en el que la consulta anterior falla, pero igualmente crea la tabla. Para evitarlo, basta con hacer un INSERT INTO

seguido de la parte SELECT de arriba y se hará la inserción.

La forma recomendada y más segura es usar una clave HMAC. Asegúrate de agregarlas primero al bucket para la service account que estás usando, ya que es un error muy frecuente. Estas claves se utilizan también para algunos métodos de replicación que veremos más adelante en este artículo, así que conviene dejarlas listas desde ya: generadas y cargadas en los buckets correspondientes.

Cuando uses estas claves, utiliza este SQL en ClickHouse:

CREATE TABLE <table_name>
ENGINE = MergeTree
ORDER BY tuple() AS
SELECT *
FROM s3Cluster('default',
  'https://storage.googleapis.com/<bucket_name>/<path>/*.parquet',
  <hmac_access_key>,
  <hmac_secret>)

En estas consultas, el asterisco (*) en el nombre del archivo toma todos los archivos de ese directorio con extensión parquet. Cuidado con no confundir una clave de service account con una clave HMAC, son cosas completamente distintas. Las claves HMAC viven solo dentro de GCS y no en el servicio IAM de GCP. Además, si no pones la extensión en tus archivos, vas a tener que pasar 'Parquet' como argumento a la llamada de la función s3Cluster, ya que de lo contrario puede arrojar un error (es un problema común al trabajar con servicios que consultan desde GCS usando claves HMAC).

Aquí es donde la cosa se pone interesante y donde ClickHouse brilla. Esa línea con ENGINE es una de las claves del rendimiento. Se llaman Table Engines y dan para al menos toda una serie de capacitación sobre ClickHouse, así que no los voy a cubrir aquí. En su lugar, dejo el enlace al documento oficial de ClickHouse sobre el tema aquí.

Al cargar los datos debes elegir el motor correcto según tu uso. Los motores de la familia MergeTree son excelentes motores de almacenamiento de propósito general, con algunos más específicos dentro de la familia, pero recomiendo FUERTEMENTE revisar los demás disponibles y emparejar tu caso de uso con el correcto. ¡Esta es una de las claves —si no la clave— para lograr buen rendimiento en ClickHouse! Insisto, no se puede subrayar lo suficiente.

Dicho esto, queda un último paso importante al cargar los datos iniciales en ClickHouse: borrar los datos temporales. Mucha gente olvida este paso y termina pagando por el almacenamiento en GCS hasta que una auditoría se los hace ver más adelante; no caigas en esa trampa, resuélvelo desde ya y ahórrate algo de dinero. O al menos pon un recordatorio para no olvidarlo.

En general es buena idea borrar los archivos del bucket de GCS para ahorrar costos. Si no tienes claro si los vas a necesitar para una recarga posterior, puede ser útil configurar una regla de ciclo de vida que los borre después de 30 días (o más) para no olvidarte.

Patrones de replicación

Lo siguiente es poner en marcha la replicación de datos, que admite distintos métodos según cómo ingestes datos en BigQuery actualmente. En este artículo voy a cubrir cómo hacerlo con ingestiones en batch. La parte de streaming la abordaremos en un artículo futuro, ya que es un escenario bastante más complejo.

El otro gran tema es que no existe un enfoque único que sirva para todo: depende mucho de cómo funcione tu pipeline de datos hoy. Lo que voy a hacer es proponer los más habituales que vemos en DoiT y dejar que tú, como implementador, decidas cuál es la mejor estrategia para tu organización.

Prerrequisitos de replicación

Hay algunos prerrequisitos comunes a todos estos métodos de replicación de datos. Por suerte, la mayoría de los datasets ya los tienen incorporados, así que normalmente no es algo de qué preocuparse, pero por transparencia conviene mencionarlos.

Prerrequisito 1: Tiempo de ingestión o "marcadores incrementales"

Poder determinar dónde comenzó o se detuvo la replicación es clave para evitar insertar datos duplicados, lo que provocaría escenarios con datos incorrectos. Puede ser un tiempo de ingestión o una "clave primaria" única de naturaleza incremental. En BigQuery, muchas veces esto se alinea con un límite de partición.

Prerrequisito 2: Determina la fuente de tus datos

En resumen: averigua de dónde vienen tus datos antes de cargarse en BigQuery. Muchas veces están en GCS o S3 cuando se trata de datos batch, o pueden enviarse por streaming directamente a BigQuery mediante una suscripción en Pub/Sub, o quizás vienen de Dataflow/Beam transformando los datos. Saber de dónde vienen es clave para elegir el mejor método para replicarlos.

Prerrequisito 3: Define qué tan rápido necesitas los datos

Más que nada es preguntarte si tu herramienta de BI necesita datos "en tiempo real" o "eventuales" al consultar ClickHouse. Si los necesitas en pocos minutos, llamémoslo "tiempo real"; y si te basta con que se actualicen cada 30 minutos o más, llamémoslo "eventual" para elegir una estrategia.

Configurar replicación en batch

La replicación en batch hacia ClickHouse ocurre en intervalos para llevar los datos desde la fuente o desde BigQuery hacia ClickHouse. Estas estrategias son las más fáciles de implementar y se utilizan cuando ya tienes los datos en archivos u otro mecanismo de batch antes de cargarlos en BigQuery.

La otra alternativa es extraer los datos desde BigQuery hacia GCS y luego cargarlos en ClickHouse desde ahí.

El primer método consiste en cargar datos almacenados en GCS directamente a ClickHouse usando una Cloud Function. Solo funciona si ya tienes datos almacenados en GCS y en un formato compatible que ClickHouse soporte para carga.

Es muy fácil y se trata de un job de carga directo, muy parecido al que la mayoría de los clientes hacen hacia BigQuery.

El segundo método aplica si los datos ya están en BigQuery y necesitan replicarse hacia ClickHouse para su uso. No es tan sencillo: tiene más piezas en movimiento y costos, por lo que no es el método preferido.

Ten en cuenta que si tienes datos en otro datastore, como una base MySQL o PostgreSQL, que actúa como paso intermedio en el camino a BigQuery o que se replica hacia BigQuery, entonces el segundo método será el adecuado. Más abajo detallo el proceso para escenarios con datastores que no son BigQuery.

Configurar replicación en batch con datos en GCS antes de cargar en BigQuery

Este método es por mucho el más fácil: apenas unos pocos pasos y configuración mínima.

En resumen, lo que hace es tomar tus archivos de datos de GCS y ejecutar un LOAD de esos archivos en ClickHouse. Como siempre, no todo es tan sencillo, pero está bastante cerca y ya estoy entregando la mayor parte del código.

El método más fácil que he encontrado hasta ahora es configurar un trigger en GCS para los archivos que se finalizan en tu bucket y llamar a una Cloud Function que cargue el archivo en ClickHouse. El proceso para hacerlo está aquí; usa el código fuente provisto en tu deployment, pero asegúrate de leer la documentación primero para cumplir con los prerrequisitos (en concreto, crear el secret que contendrá toda la información de conexión).

Hay una salvedad: probablemente quieras cargar solo archivos de cierto tipo y, quizás, hacerlo en una tabla distinta según la ruta o el nombre del archivo. Esa lógica deberás agregarla tú para tu caso de uso específico, y dejé documentado en el archivo dónde se deben hacer estas modificaciones.

Configurar replicación en batch con datos que ya están en BigQuery

Lo siguiente en estrategias de replicación es la replicación en batch desde BigQuery a ClickHouse en intervalos. Estos métodos funcionan bien si estás cargando datos directamente desde otra fuente hacia BigQuery y no puedes capturarlos antes de la carga. Es habitual cuando haces inserciones por streaming, cargas con la BigQuery Storage API o tienes datos cargándose desde fuentes externas como Stitch o FiveTran a BigQuery.

Nota sobre costos: Este método probablemente tendrá que consultar los datos en intervalos, así que generará algunos cargos. Tenlo presente y asegúrate de particionar y/o agrupar los datos en la tabla subyacente de manera adecuada para minimizarlos. También puedes optar por facturación on-demand en lugar de Editions, según cuántos datos se ingesten. Muchas veces, lo recomendable es crear un proyecto separado que haga estas exportaciones de los datos almacenados en el proyecto principal, para mantener una separación de los workloads. Esto también permite usar facturación on-demand o una reserva separada si usas Editions, de modo que puedas aprovechar Standard Edition con un costo más bajo.

El esquema general de este método es ejecutar una consulta programada en intervalos dentro de BigQuery, que exporta los datos nuevos a un bucket de GCS. Una vez allí, una Cloud Function se dispara y carga los datos en ClickHouse cuando el archivo termina de escribirse.

Este método puede salir caro tanto en consultas como en almacenamiento y volumen de llamadas a la Cloud Function, pero permite cargar los datos en intervalos muy predecibles. Si no puedes cargar los datos antes de BigQuery, esta podría ser la mejor opción y, aun así, lo más probable es que ahorres dinero en costos de consulta.

Para reducir el costo, en general lo mejor es alinear las exportaciones con una partición, de modo que la partición se exporte directamente a GCS (ver el comando de arriba). Por ejemplo, si particionas por hora, programa el proceso para que ejecute justo cuando ya pueda tomar el valor completo de esa hora y cargarlo de una sola vez.

¡Vamos al grano y veamos cómo funciona!

El ejemplo funcional

Para este ejemplo creé una tabla muy básica con el siguiente esquema dentro de BigQuery:

La tengo particionada por transaction_time para acelerar las consultas en este caso.

Los datos se cargan en esta tabla cada hora con todos los datos de la última hora, lo que significa que no se perderá ninguno que vaya a incluirse en una carga posterior. Es por simplicidad y muy raramente ocurriría así en el mundo real.

Por simplicidad, y como muy buena práctica, cuando la carga se completa se publica un mensaje en un topic de Pub/Sub. Una suscripción se asocia a ese topic, lo que dispara una Cloud Function que hace el trabajo por ti, en lugar de tener que montar un pequeño pipeline. Como alternativa, cuando el archivo termina de escribirse se puede disparar una Cloud Function que lo cargue automáticamente, lo que se acercaría más a tiempo real que cargar en intervalos.

Aquí está el enlace a la Cloud Function que hace esta carga, junto con un readme que explica su uso.

Ten en cuenta que coloqué las credenciales de ClickHouse en un secret de Secret Manager y estoy exponiendo esos valores como variables de entorno a la Cloud Function. Esta es la forma más fácil y una de las más seguras de acceder a tus credenciales en una Cloud Function. Está todo documentado en el archivo README.md del código Python asociado.

El primer paso es crear una Cloud Function. Ten en cuenta que se dispara con el evento finalize en un bucket de GCS. Tu configuración debería verse algo así (presta atención también a la advertencia sobre la service account, asegúrate de otorgar esos permisos):

Antes de presionar guardar, despliega el menú "Runtime, build, connections and security settings" y desplázate hasta Security and Image Repo. Arriba aparece una sección "Secrets" donde tendrás que agregar un secret por cada uno de los valores requeridos, como se ve abajo. Asegúrate de seleccionar "Exposed as environment variable" en el dropdown de cada uno para que la Cloud Function funcione.

Vas a tener que hacer esto para cada uno de los secrets que necesita la Cloud Function (host, port, secure, username y password). Port y secure tienen valores por defecto de 9019 y True respectivamente, así que esos secrets no son obligatorios si te basta con los valores por defecto. Toma nota también de la advertencia sobre la service account que dejé: confirma que la service account tenga acceso de lectura a Secret Manager antes de continuar.

El siguiente paso es usar esta query como Scheduled Query (aquí está la documentación oficial para quienes nunca lo han hecho) en BigQuery, programada para ejecutarse 5 minutos después de cada hora.

Hecho esto, espera hasta tu hora designada y revisa tu bucket de GCS para verificar que se haya creado un nuevo directorio con los archivos. Si está, tu Cloud Function funcionó.

Cómo usar este ejemplo en el mundo real

Este ejemplo es MUY simple e idealizado por motivos de claridad. En el mundo real sabemos que los datos nunca se cargan perfectamente dentro de los límites de hora ni a tiempo, sin que lleguen datos tardíos. Omití a propósito estas condiciones porque ClickHouse tiene una buena respuesta para ellas: ReplacingMergeTree y CollapsingMergeTree.

El equipo de ClickHouse escribió un excelente artículo sobre esto aquí, donde explica cómo aprovecharlos para updates y deletes. Según los patrones de uso de los datos, conviene elegir uno u otro.

Además, es muy probable que quieras agregar funcionalidad adicional, como notificaciones, disparar jobs de ETL/ELT, etc., así que recomendaría agregar más código al final de la ejecución de la Cloud Function para enviar un mensaje a Pub/Sub que dispare cualquier funcionalidad downstream.

Conectar Looker a ClickHouse

Lo último por hacer es convertir tus conexiones de Looker para que utilicen ClickHouse en lugar de BigQuery.

A veces cuesta encontrarlo, pero entrando al modo Admin de Looker, está en Database->Connections, en el panel izquierdo del modo Admin.

Admin->Database->Connections

Luego presiona agregar y completa la información de tu host en la página de conexiones para tu instancia de ClickHouse.

Al hacer clic en connections y seleccionar un Dialect de ClickHouse, aparecerá lo siguiente:

Vista de nueva conexión a ClickHouse en Looker

Una vez cargada, copia y pega la información de tu instancia, como por ejemplo la siguiente desde la consola de Aiven:

Información de conexión de la instancia desde la consola de ClickHouse de Aiven

La información que vas a buscar es, por lo general, la del conector JDBC o la del acceso HTTPS de ClickHouse.

Una vez completado, usa el botón de prueba en Looker para confirmar que funciona.

Por último, esto es muy importante para la seguridad: la función de IP whitelisting en tu instancia de ClickHouse. Esta lista define las únicas IPs autorizadas a conectarse a tu instancia. Si sigues las instrucciones aquí de Google, encontrarás un conjunto de direcciones IP que puedes agregar a la whitelist. Así te aseguras de que solo las conexiones desde las IPs de Looker podrán conectarse a tu instancia de ClickHouse.

Vista de direcciones IP permitidas desde la consola de Aiven

Este patrón debería servirte como punto de partida para tener una instancia de ClickHouse a la que puedas apuntar desde Looker en lugar de BigQuery, lo que potencialmente te ahorra mucho dinero en consultas.

Como es un ejemplo elemental, seguramente no será una solución lista para usar para todas tus necesidades de carga de datos, pero conviene verlo como la primera piedra del río en el camino para ahorrar dinero "cacheando" los datos en ClickHouse en lugar de pagar por consulta en BigQuery.