• Saltar a la navegación principal
  • Saltar al contenido principal
  • Saltar al pie de página
Bluetab

Bluetab

an IBM Company

  • Soluciones
    • DATA STRATEGY
    • DATA READINESS
    • DATA PRODUCTS AI
  • Assets
    • TRUEDAT
    • FASTCAPTURE
    • Spark Tune
  • Conócenos
  • Oficinas
    • España
    • Mexico
    • Perú
    • Colombia
  • talento
    • España
    • TALENT HUB BARCELONA
    • TALENT HUB BIZKAIA
    • TALENT HUB ALICANTE
    • TALENT HUB MÁLAGA
  • Blog
  • ES
    • EN

Practices

Empoderando a las decisiones en diversos sectores con árboles de decisión en AWS

junio 4, 2024 by Bluetab

Jeanpierre Nuñez
Senior Data Engineer

¿Sabías que los árboles de decisión pueden ayudarte a resolver problemas complejos?

En este artículo, te revelaremos cómo en Bluetab utilizamos esta poderosa herramienta de análisis de datos con el fin de impulsar a las decisiones de nuestros clientes en diversas industrias usando AWS.

Árboles de decisión: herramientas poderosas para la toma de decisiones

Los árboles de decisión son diagramas que representan las posibles opciones, resultados y consecuencias de una decisión. Se construyen a partir de nodos que contienen preguntas o condiciones, y ramas que conectan las respuestas o acciones. De esta forma, se puede visualizar el flujo lógico de la decisión y elegir la mejor alternativa.

En Bluetab, creemos en la toma de decisiones inteligentes basadas en datos. Para lograrlo, utilizamos herramientas poderosas como los árboles de decisión, que son valiosos en una variedad de sectores. A continuación, exploraremos cómo aplicamos los árboles de decisión y los beneficios que ofrecen a nuestros clientes en diferentes industrias.

Clasificación y regresión con árboles de decisión

Clasificación: los árboles de decisión de clasificación son excelentes para segmentar y categorizar datos. En el contexto empresarial, se utilizan para tomar decisiones sobre la asignación de recursos, la identificación de oportunidades de mercado y la personalización de ofertas. En el sector de la salud, los árboles de clasificación ayudan a diagnosticar enfermedades en función de síntomas y pruebas.

Regresión: los árboles de regresión se centran en predecir valores numéricos. Se utilizan para proyectar rendimientos financieros, tasas de crecimiento y otros valores cuantitativos. En el sector financiero, son esenciales para predecir el rendimiento de inversiones y carteras.

Caso práctico: Árboles de decisión en el sector de la salud

Sector de la salud: En el sector de la salud, utilizamos los árboles de decisión para diagnosticar enfermedades en función de síntomas y pruebas. Por ejemplo, si un paciente tiene fiebre, dolor de garganta y tos, el árbol de decisión nos indica que lo más probable es que tenga una infección respiratoria y nos recomienda el tratamiento adecuado.

Beneficios de los árboles de decisión en diferentes sectores

  • Toma de decisiones personalizadas: los árboles de decisión permiten decisiones precisas y personalizadas en función de los datos y las necesidades de cada sector.
  • Eficiencia operativa: al automatizar procesos repetitivos y mejorar la toma de decisiones, las empresas pueden optimizar sus operaciones.

Caso práctico: árboles de decisión en el sector bancario

Sector bancario: En el sector bancario, los árboles de decisión son vitales para la gestión de riesgos y la personalización de servicios. Los bancos utilizan estos árboles para segmentar a los clientes en grupos según su comportamiento financiero, lo que les permite ofrecer productos y servicios financieros a medida. También son esenciales en la evaluación crediticia, donde garantizan préstamos más seguros y tasas de interés adecuadas. Además, los árboles de decisión se aplican en la prevención de fraudes, donde detectan patrones sospechosos y protegen los activos de los clientes.

Beneficios en banca:

  • Gestión de riesgos: los árboles de decisión ayudan a evaluar y gestionar los riesgos crediticios, lo que es esencial en la industria bancaria.
  • Personalización de servicios: los clientes bancarios reciben ofertas personalizadas que se ajustan a sus necesidades financieras, lo que mejora su satisfacción y lealtad.
  • Prevención de fraudes: la detección temprana de transacciones fraudulentas protege a los clientes y a los bancos.

Decisiones Inteligentes en AWS: implementación de árboles de decisión y migración de datos

Introducción: 

En un mundo empresarial en constante evolución, la toma de decisiones informadas es clave para el éxito. Exploraremos ahora cómo la implementación de árboles de decisión en AWS y la migración de datos desde SQL Server pueden empoderar a las organizaciones en diferentes sectores. Analizaremos la arquitectura de soluciones en AWS que permite esta implementación y migración, detallando cada paso del proceso.

Arquitectura en AWS para Implementar árboles de decisión:

  • Ingesta de datos: comenzamos considerando la ingesta de datos desde diversas fuentes. AWS Glue se destaca como una solución versátil que puede conectarse a una amplia variedad de fuentes de datos.
  • Almacenamiento de datos: una vez que los datos se han ingestado, se almacenan de manera centralizada en Amazon S3. Esto proporciona un lugar único para la administración y acceso eficiente a los datos.
  • Procesamiento de datos y generación de árboles de decisión: utilizamos AWS Glue para transformar y preparar los datos para su análisis. En esta etapa, AWS Lambda y AWS SageMaker entran en juego para implementar algoritmos de árboles de decisión, brindando un enfoque avanzado de aprendizaje automático.
  • Análisis y consultas: una vez que se han generado los árboles de decisión, AWS Athena permite realizar consultas SQL interactivas en los datos almacenados en S3. Esto facilita la exploración de datos y la toma de decisiones basadas en los resultados de los árboles.

Migración de datos desde SQL Server a AWS:

  • Ingesta de datos: cuando se trata de la migración de datos desde SQL Server de Microsoft, AWS Database Migration Service (DMS) es una herramienta valiosa. Facilita la migración de bases de datos completas o datos específicos de SQL Server a bases de datos compatibles con AWS, como Amazon RDS o Amazon Redshift.

Arquitectura de solución en AWS utilizando AWS S3, Glue, Lambda, SageMaker, S3 y Athena:

  • Ingesta de datos: utilizamos AWS Database Migration Service (DMS) para transferir datos desde la fuente de datos SQL Server a una base de datos compatible con AWS.
  • Transformación de datos: AWS Glue se encarga de transformar y preparar los datos recién migrados para su análisis.
  • Generación de árboles de decisión: implementamos algoritmos de árboles de decisión utilizando AWS Lambda o AWS SageMaker para llevar a cabo análisis avanzados.
  • Almacenamiento de datos: los datos procesados y los resultados de los árboles de decisión se almacenan en Amazon S3.
  • Consultas y análisis: utilizamos AWS Athena para realizar consultas SQL interactivas en los datos almacenados en S3 y tomar decisiones basadas en los resultados.

Para la implementación de un arbol de clasificación vamos a utilizar los siguientes servicios: Amazon S3, Aws Glue Crawler, Aws Glue Job, IAM , Cloud Watch. 

Se seleccionan como mínimo estos servicios para poder mostrar los beneficios de este modelo.

Creacion del bucket en Amazon S3

Buscaremos ‘S3’ en la barra de busqueda y seleccionaremos el nombre del servicio para visualizar lo siguiente:

Daremos clic en ‘Crear Bucket’ y visualizaremos la siguiente pantalla:

Debemos colocar un nombre que se asocie a nuestro flujo de trabajo y seleccionar tambien la región como minimo.

Colocaran siguiente y podran observan una vista previa del bucket, luego dar en clic en crear bucket:

Para nuestro ejemplo práctico estamos seleccionando un csv publico llamado Iris. 

El conjunto de datos Iris contiene medidas de 150 flores (setosa, versicolor, virginica) con 4 características, usado comúnmente para clasificación.

Lógicamente para un modelo de ML deberiamos brindar una data ya transformada y trabajada con el propósito de tener las caracteristias y hacer las predicciones necesarias.

Daras clic en cargar y seleccionaras el csv:

Finalmente visualizaremos la carga con éxito y ya seremos capaces de obtener esta información desde otro servicios en AWS.

Creación del servicio AWS Glue Crawler

AWS Glue Crawler es una herramienta que descubre y cataloga automáticamente datos almacenados en diversos formatos y ubicaciones, facilitando la preparación y análisis de datos en AWS.

Buscaremos el servicio AWS Glue en la barra de busqueda, nos desplazaremos en la sección lateral izquierdo y daremos clic en Crawlers:

Daremos un nombre a nuestro Crawler y una descripción a elección personal, luego daremos clic en Next:

En el dropdown list de data source elegiremos el servicio de S3.

Luego colocaras Browse S3 y podras seleccionar el bucket creado previamente, deberías ser capaz de visualizar lo siguiente:

En la siguiente seccion podrás seleccionar algun rol si tienes configurado previamente, para este ejemplo crearemos un nuevol, le pondremos un nombre asociado a nuestro proceso.

Luego veremos la selección de nuestra base de datos Catalogo , sin embargo tendremos que crearlo de la siguiente manera dando clic en Add Database:

Ingresamores el nombre de la base de datos asociada a nuestro ejemplo:

Luego podremos agregar un prefijo de forma opcional cuando disponibilice los datos en el catalogo de AWS Glue, podría estar vacio también si no lo necesita. Seleccionaremos ejecución a demanda para nuestro caso.

Finalmente en la siguiente pestaña podremos visualizar creado nuestro crawler y le daremos clic en ‘Run crawler’. Deberiamos ver lo siguiente:

Creacion del Job en AWS Glue

Luego en la misma seccion lateral izquierda de AWS Glue buscaremos la seccion de ETL Job y crearemos un flujo con la interfaz grafica, haciendolo de esta manera podremos tener facilmente el código generado para tener acceso a nuestra fuente.

Luego de agregas nuestro objeto de catálogo y seleccionar lo que hemos creado previamente. Deberiamos visualizar el job de la siguiente manera:

Hasta aquí ya procederemos a colocar un nombre al job y agregar nuestro código. Haremos uso de las siguientes librerias:

`DecisionTreeClassifier` construye un modelo de árbol de decisión para clasificación.

`MulticlassClassificationEvaluator` evalúa su rendimiento multiclase. `CrossValidator` realiza validación cruzada, ajustando hiperparámetros con `ParamGridBuilder`.

Juntos, permiten la construcción, evaluación y ajuste eficaz de modelos de árboles de clasificación en PySpark MLlib.

A continuación, se muestra el código utilizado:

import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
from pyspark.ml.feature import StringIndexer, VectorAssembler
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml import Pipeline
from pyspark.sql.functions import col,udf
from pyspark.sql.window import Window
from pyspark.sql import functions as F
from awsglue.dynamicframe import DynamicFrame
from pyspark.sql.types import ArrayType, DoubleType

# Inicializar contexto de Spark y Glue
args = getResolvedOptions(sys.argv, ["JOB_NAME"])
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args["JOB_NAME"], args)
# Nombre de la base de datos y tabla en el catálogo de AWS Glue
database_name = "db-decision-tree-iris"
input_table_name = "mliris_csv"
output_table_name = "mliris_results"
# Crear DynamicFrame desde el catálogo de Glue
input_dyf = glueContext.create_dynamic_frame.from_catalog(
    database=database_name,
    table_name=input_table_name,
)
# Convertir DynamicFrame a DataFrame
df = input_dyf.toDF()
# Añadir una columna de índice
df = df.withColumn("row_index", F.monotonically_increasing_id())
# Preprocessing: StringIndexer for categorical labels
stringIndexer  = StringIndexer(inputCol="species", outputCol="label")
# Preprocessing: VectorAssembler for feature columns
assembler = VectorAssembler(inputCols=["sepal_length", "sepal_width", "petal_length", "petal_width"], outputCol="features")
#data = assembler.transform(data)
# Split data into training and testing sets
train_data, test_data = df.randomSplit([0.8, 0.2], seed=42)
# Create a Decision Tree Classifier instance
dt = DecisionTreeClassifier(labelCol='label', featuresCol='features')
# Assemble all the steps (indexing, assembling, and model building) into a pipeline.
pipeline = Pipeline(stages=[stringIndexer, assembler, dt])
paramGrid = ParamGridBuilder() \
    .addGrid(dt.maxDepth,[3, 5, 7]) \
    .addGrid(dt.minInstancesPerNode, [1,3,5]) \
    .build()
crossval = CrossValidator(estimator=pipeline, estimatorParamMaps=paramGrid,
                      evaluator=MulticlassClassificationEvaluator(
                      labelCol='label', predictionCol='prediction', metricName='accuracy'),
                      numFolds=5)

cvModel = crossval.fit(train_data)
best_model = cvModel.bestModel
predictions = best_model.transform(test_data)
predictions.show(100,truncate=False)


# Evaluate the model performance
evaluator = MulticlassClassificationEvaluator(labelCol="label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print(f"Test Accuracy: {accuracy:.2f}")
precision = evaluator.evaluate(predictions, {evaluator.metricName: "weightedPrecision"})
print(f"Weighted Precision: {precision:.2f}")
recall = evaluator.evaluate(predictions, {evaluator.metricName: "weightedRecall"})
print(f"Weighted Recall: {recall:.2f}")
f1_score = evaluator.evaluate(predictions, {evaluator.metricName: "f1"})
print(f"F1 Score: {f1_score:.2f}")
# Acceder al modelo de árbol de decisión dentro del pipeline
tree_model = best_model.stages[-1]  # Suponiendo que el clasificador de árbol de decisión es el último paso en tu pipeline
# Obtener el resumen del modelo
print(tree_model._java_obj.toDebugString())
output_path = "s3://bucket-training-tree-decision/output/parquet_data/"
# Función UDF para convertir VectorUDT a lista
vector_to_list_udf = udf(lambda v: v.toArray().tolist(), returnType=ArrayType(DoubleType()))
# Convertir la columna probability a lista
df = predictions.withColumn("probability", vector_to_list_udf(col("probability")))
df = df.withColumn("rawPrediction", vector_to_list_udf(col("rawPrediction")))
df = df.withColumn("features", vector_to_list_udf(col("features")))
df_subset = df.select(*["sepal_length", "sepal_width", "petal_length", "petal_width", "species", "row_index", "label", "features", "prediction", "probability"])
# Escribir el DataFrame en formato Parquet en S3
df_subset.write.parquet(output_path, mode="overwrite")
# Finalizar trabajo
job.commit()


Como se visualiza el estado del job se encuentra en ‘Succeeded’:

Configuración de políticas mediante S3 y IAM

A continuación, debemos también configurar las políticas de IAM para el acceso desde Glue hacia S3. Iremos a la sección de permisos en nuestro bucket:

Editaremos la política de la siguiente manera:

Luego en IAM a nuestro rol de machine que creamos en la sección de AWS Grawler, añadiremos el rol de s3Full Access. Seleccionamos nuestro rol:

Verificamos que tengamos nuestra relacion de confianza de la siguiente manera:

Luego añadiremos el permiso de AmazonS3FullAccess.

Finalmente al agregar el permiso deberiamos de ver nuestros permisos de la siguiente manera:

Ahora mediante CloudWatch podremos visualizar el log de nuestro job, verificar si se guardo correctamente nuestro modelo y tambien poder ver una previsualización de las reglas y predicciones.

Aquí podemos visualizar nuestros resultados e incluso la regla de clasificación:

Aquí se puede visualizar los valores de Accuracy , weighted precision y recall, y el F1 Score. A continuación, se explica el significado de cada métrica.

  1. Test Accuracy (Exactitud de Prueba):
    • Significado: proporción de predicciones correctas respecto al total de predicciones en el conjunto de prueba.
    • Utilidad: mide la precisión general del modelo y es especialmente útil cuando las clases están balanceadas.
  2. Weighted Recall (Recuperación Ponderada):
    • Significado: promedio ponderado de las tasas de verdaderos positivos para cada clase.
    • Utilidad: evalúa la capacidad del modelo para recuperar instancias de cada clase, considerando el desequilibrio en la distribución de las clases.
  3. Weighted Precision (Precisión Ponderada):
    • Significado: promedio ponderado de las precisiones para cada clase.
    • Utilidad: indica la proporción de instancias correctamente clasificadas entre las que el modelo predijo como positivas, considerando el desequilibrio de clases
  4. F1 Score:
    • Significado: media armónica de precisión y recuperación. Cuantifica la relación equilibrada entre la precisión y la capacidad de recuperación del modelo.
    • Utilidad: útil en problemas de clasificación desbalanceados, ya que considera tanto los falsos positivos como los falsos negativos, proporcionando una métrica global de rendimiento del modelo. Un F1 Score alto indica un equilibrio entre precisión y recuperación.

Finalmente se guardaron los resultados de nuestro modelo en nuestro bucket en un folder llamado output, deberia verse de la siguiente manera:

Conclusión

Esta arquitectura de solución en AWS permite a las organizaciones no solo implementar árboles de decisión eficaces sino también migrar datos desde sistemas heredados como SQL Server. Ambos procesos se combinan para empoderar a las organizaciones en la toma de decisiones informadas, mejorando la eficiencia operativa y maximizando las oportunidades de mercado. Como puedes ver, los árboles de decisión son una herramienta muy útil para tomar decisiones inteligentes basadas en datos.

En Bluetab, te ayudamos a implementarlos en tu organización para que puedas mejorar tu eficiencia operativa, personalizar tus ofertas y aprovechar las oportunidades de mercado. Si quieres saber más sobre cómo podemos ayudarte, no dudes en contactarnos.

Tabla de contenidos
  1. ¿Sabías que los árboles de decisión pueden ayudarte a resolver problemas complejos?
  2. Creacion del bucket en Amazon S3
  3. Creación del servicio AWS Glue Crawler
  4. Creacion del Job en AWS Glue
  5. Configuración de políticas mediante S3 y IAM

Publicado en: Blog, Practices, Tech

Potencia Tu Negocio con GenAI y GCP: Simple y para Todos

marzo 27, 2024 by Bluetab

Alfonso Zamora
Cloud Engineer

Introducción

El objetivo principal de este artículo es presentar una solución para el análisis y la ingeniería de datos desde el punto de vista del personal de negocio, sin requerir unos conocimientos técnicos especializados. 

Las compañías disponen de una gran cantidad de procesos de ingeniería del dato para sacarle el mayor valor a su negocio, y en ocasiones, soluciones muy complejas para el caso de uso requerido. Desde aquí, proponemos simplificar la operativa para que un usuario de negocio, que anteriormente no podía llevar a cabo el desarrollo y la implementación de la parte técnica, ahora será autosuficiente, y podrá implementar sus propias soluciones técnicas con lenguaje natural.

Para poder cumplir nuestro objetivo, vamos a hacer uso de distintos servicios de la plataforma Google Cloud para crear tanto la infraestructura necesaria como los distintos componentes tecnológicos para poder sacar todo el valor a la información empresarial.

Antes de comenzar

Antes de comenzar con el desarrollo del artículo, vamos a explicar algunos conceptos básicos sobre los servicios y sobre distintos frameworks de trabajo que vamos a utilizar para la implementación:

  1. Cloud Storage[1]: Es un servicio de almacenamiento en la nube proporcionado por Google Cloud Platform (GCP) que permite a los usuarios almacenar y recuperar datos de manera segura y escalable.
  2. BigQuery[2]: Es un servicio de análisis de datos totalmente administrado que permite realizar consultas SQL en conjuntos de datos masivos en GCP. Es especialmente eficaz para el análisis de datos a gran escala.
  3. Terraform[3]: Es una herramienta de infraestructura como código (IaC) desarrollada por HashiCorp. Permite a los usuarios describir y gestionar la infraestructura utilizando archivos de configuración en el lenguaje HashiCorp Configuration Language (HCL). Con Terraform, puedes definir recursos y proveedores de manera declarativa, facilitando la creación y gestión de infraestructuras en plataformas como AWS, Azure y Google Cloud.
  4. PySpark[4]: Es una interfaz de Python para Apache Spark, un marco de procesamiento distribuido de código abierto. PySpark facilita el desarrollo de aplicaciones de análisis de datos paralelas y distribuidas utilizando la potencia de Spark.
  5. Dataproc[5]: Es un servicio de gestión de clústeres para Apache Spark y Hadoop en GCP que permite ejecutar eficientemente tareas de análisis y procesamiento de datos a gran escala. Dataproc admite la ejecución de código PySpark, facilitando la realización de operaciones distribuidas en grandes conjuntos de datos en la infraestructura de Google Cloud.

Qué es un LLM 

Un LLM (Large Language Model) es un tipo de algoritmo de inteligencia artificial (IA) que utiliza técnicas de deep learning y enormes conjuntos de datos para comprender, resumir, generar y predecir nuevos contenidos. Un ejemplo de LLM podría ser ChatGPT que hace uso del modelo GPT desarrollado por OpenAI. 

En nuestro caso, vamos a hacer uso del modelo Codey[6] (code-bison) que es un modelo implementado por Google que está optimizado para generar código ya que ha sido entrenado para esta especialización que se encuentra dentro del stack de VertexAI[7]

Y no solo es importante el modelo que vamos a utilizar, sino también el cómo lo vamos a utilizar. Con esto, me refiero a que es necesario comprender los parámetros de entrada que afectan directamente a las respuestas que nos dará nuestro modelo, en los que podemos destacar los siguientes:

  • Temperatura (temperature): Este parámetro controla la aleatoriedad en las predicciones del modelo. Una temperatura baja, como 0.1, genera resultados más deterministas y enfocados, mientras que una temperatura alta, como 0.8, introduce más variabilidad y creatividad en las respuestas del modelo.
  • Prefix (Prompt): El prompt es el texto de entrada que se proporciona al modelo para iniciar la generación de texto. La elección del prompt es crucial, ya que guía al modelo sobre la tarea específica que se espera realizar. La formulación del prompt puede influir en la calidad y relevancia de las respuestas del modelo, aunque hay que tener en cuenta la longitud para que cumpla con el  número máximo de tokens de entrada que es 6144.
  • Tokens de salida (max_output_tokens): Este parámetro limita el número máximo de tokens que se generarán en la salida. Controlar este valor es útil para evitar respuestas excesivamente largas o para ajustar la longitud de la salida según los requisitos específicos de la aplicación.
  • Recuento de candidatos (candidate_count): Este parámetro controla el número de respuestas candidatas que el modelo genera antes de seleccionar la mejor opción. Un valor más alto puede ser útil para explorar diversas respuestas potenciales, pero también aumentará el costo computacional.

Desarrollo del prompt

Una vez que hemos definido los parámetros y sabemos bien para qué sirve cada uno de ellos y comprendemos lo que es un prompt, vamos a enfocarnos en cómo utilizarlo e implementar uno que se pueda adaptar a nuestras necesidades.

Como se ha comentado anteriormente, el objetivo es generar tanto código Pyspark como terraform para poder realizar las tareas de creación de infraestructura y tratamiento del dato. Como son tareas totalmente distintas, como primera decisión importante para nuestro prompt se ha optado por dividirlo en dos partes específicas para que cada prompt esté entrenado con ejemplos para generar un lenguaje u otro.

Para cada uno de los prompt, se hace una introducción para especificar cuál va a ser el objetivo y qué peticiones se van a realizar, y después una serie de ejemplos en los que se le da un input en lenguaje natural simulando una petición, y posteriormente también se le da el output deseado para asignar el texto al código específico. El objetivo es generar un prompt estructurado que pueda ser procesado de manera eficiente por el modelo para que en los siguientes casos, pueda asociar los ejemplos que tiene disponibles con respuestas adecuadas.

Vamos a poner en práctica estos pequeños tips para ver una de las entradas al prompt de terraform:


input: I need to read a file named `myfile.csv` inside the folder `data/input` in a bucket called `application-data-input`, and I need to create a new dataset and table in BigQuery called `tst_tf_ai`. The process has to read the file, remove the first three characters of the name, and write the result in BigQuery. The schema of the table will be ‘name’ of String type and ‘surname’ of String type. The infrastructure has to be deployed in the project `project-cloud-223` in GCP, and region `europe-southwest1`.
output: «`hcl
terraform {
 required_version = «>= 0.15»
 required_providers {
  google = «~> 5.0»
 }
}

resource «google_storage_bucket» «temporary_files» { 
 project       = «project-cloud-223» 
 name          = «temporary-files-dataproc» 
 location      = «europe-southwest1» 
 force_destroy = true 
} 

resource «google_storage_bucket» «bucket1» { 
 project       = «project-cloud-223» 
 name          = «application-data-input» 
 location      = «europe-southwest1» 
 force_destroy = true 
} 

resource «google_storage_bucket_object» «folder1» {
 name    = «data/input/»
 bucket  = google_storage_bucket.bucket1.name
 content = » «
}

resource «google_bigquery_dataset» «dataset» { 
 project       = «project-cloud-223» 
 dataset_id    = «tst_tf_ai» 
 friendly_name = «tst_tf_ai» 
 description   = «Description» 
 location      = «europe-southwest1» 
} 

resource «google_bigquery_table» «table» { 
 project              = «project-cloud-223» 
 dataset_id           = google_bigquery_dataset.dataset.dataset_id 
 deletion_protection  = false
 table_id             = «tst_tf_ai» 
 schema               = <<EOF
[ 
 { 
  «name»: «name», 
  «type»: «STRING», 
  «mode»: «NULLABLE», 
  «description»: «The name» 
 }, 
 { 
  «name»: «surname», 
  «type»: «STRING», 
  «mode»: «NULLABLE», 
  «description»: «The surname» 
 }
] 
EOF 
} 
«`
Author Name

Es importante implementar ejemplos lo más parecido posible a tu caso de uso para que las respuestas sean más precisas, y también que tenga bastantes ejemplos con variedad de peticiones para que sea más inteligente a la hora de devolver las respuestas. Una de las prácticas para que sea más interactiva la implementación del prompt, puede ser ir probando con distintas peticiones, y si no es capaz de hacer lo que se le ha pedido, se debería modificar las instrucciones.

Como hemos podido observar, el desarrollo del prompt sí necesitamos conocimientos técnicos para poder traducir las peticiones a código, por lo que esta tarea sí se debería de abordar por una persona técnica para posteriormente evadir a la persona de negocio. En otras palabras, necesitamos que una persona técnica genere la primera base de conocimiento para que luego las personas de negocio puedan hacer uso de este tipo de herramientas.

También se ha podido ver, que la generación de código en terraform es más compleja que la generación en Pyspark, por lo que se han requerido de más ejemplos de entrada en la realización del prompt de terraform para que se ajuste a nuestro caso de uso. Por ejemplo, hemos aplicado en los ejemplos que en terraform siempre cree un bucket temporal (temporary-files-dataproc) para que pueda ser utilizado por Dataproc.

Casos prácticos

Se han realizado tres ejemplos con peticiones distintas, requiriendo más o menos infraestructura y transformaciones para ver si nuestro prompt es lo suficientemente robusto. 

En el archivo ai_gen.py vemos el código necesario para hacer las peticiones y los tres ejemplos, en el que cabe destacar la configuración escogida para los parámetros del modelo:

  • Se ha decidido darle valor 1 a candidate_count para que no tenga más que una respuesta final válida para devolver. Además que como se ha comentado, aumentar este número también lleva aumento de costes.
  • El max_output_tokens se ha decidido 2048 que es el mayor número de tokens para este modelo, ya que si se necesita generar una respuesta con diversas transformaciones, no falle por esta limitación.
  • La temperatura se ha variado entre el código terraform y Pyspark, para terraform se ha optado por 0 para que siempre dé la respuesta que se considera más cercana a nuestro prompt para que no genere más de lo estrictamente necesario para nuestro objetivo. En cambio para Pyspark se ha optado por 0.2 que es una temperatura baja para que no sea muy creativo, pero para que también pueda darnos diversas respuestas con cada llamada para también poder hacer pruebas de rendimiento entre ellas.

Vamos a realizar un ejemplo de petición que está disponible en el siguiente repositorio github, en el que está detallado en el README paso por paso para poder ejecutarlo tú mismo. La petición es la siguiente:


In the realm of ‘customer_table,’ my objective is the seamless integration of pivotal fields such as ‘customer_id’, ‘name’, and ‘email’. These components promise to furnish crucial insights into the essence of our valued customer base.

Conversely, when delving into the nuances of ‘sales_table,’ the envisioned tapestry includes essential elements like ‘order_id’ ‘product’ ‘price’, ‘amount’ and ‘customer_id’. Theseattributes, meticulously curated, will play a pivotal role in the nuanced exploration and analysis of sales-related data.

The ‘bigtable_info’ table will have all the fields resulting from the union of the two tables, ‘customer_table’ and ‘sales_table.’ Here, the outcome of joining the two tables by the ‘customer_id’ numeric field will be stored.

Furthermore, as part of our meticulous data collection strategy, I plan to inaugurate a dedicated Google Cloud Storage bucket christened ‘sales-etl-bucket.’ This repository is strategically designed to serve as a robust container for collating data, particularly hailing from CSV files. This endeavor is set to enhance the efficiency and organizational prowess of our data management processes.

To enhance the structure, two distinct subfolders, ‘input/sales’ and ‘input/customers’ have been ingeniously incorporated within the ‘sales-etl-bucket,’ ensuring a refined organization of the incoming data streams.

You will need to read the CSV files within the ‘input/sales’ folder to write the data into the ‘sales_table’. Additionally, you should perform the same operation with the files from the ‘input/customers’ folder to write the data into the ‘customer_table’. Finally, you are required to perform a join between the sales and customers information based on the ‘customer_id’ number field and write the merged information into the ‘bigtable_info’.
Author Name

Ejecutamos  y vamos a analizar las salidas generadas en las carpetas generated/customers/python/etl.py y generated/terraform/main.tf.:

terraform {
 required_version = ">= 0.15"
 required_providers {
   google = "~> 5.0"
 }
}


resource "google_storage_bucket" "temporary_files" {
 project       = "my-cloud-project"
 name          = "temporary-files-dataproc"
 location      = "europe-southwest1"
 force_destroy = true
}


resource "google_storage_bucket" "sales_etl_bucket" {
 project       = "my-cloud-project"
 name          = "sales-etl-bucket"
 location      = "europe-southwest1"
 force_destroy = true
}


resource "google_storage_bucket_object" "sales_folder" {
 name    = "input/sales/"
 bucket  = google_storage_bucket.sales_etl_bucket.name
 content = " "
}


resource "google_storage_bucket_object" "customers_folder" {
 name    = "input/customers/"
 bucket  = google_storage_bucket.sales_etl_bucket.name
 content = " "
}


resource "google_bigquery_dataset" "raw_sales_data" {
 project       = "my-cloud-project"
 dataset_id    = "raw_sales_data"
 friendly_name = "Raw Sales Data"
 description   = "This dataset contains the raw sales data."
 location      = "europe-southwest1"
}


resource "google_bigquery_table" "customer_table" {
 project              = "my-cloud-project"
 dataset_id           = google_bigquery_dataset.raw_sales_data.dataset_id
 deletion_protection  = false
 table_id             = "customer_table"
 schema               = <<EOF
[
 {
   "name": "customer_id",
   "type": "INT64",
   "mode": "REQUIRED",
   "description": "The customer ID."
 },
 {
   "name": "name",
   "type": "STRING",
   "mode": "NULLABLE",
   "description": "The customer's name."
 },
 {
   "name": "email",
   "type": "STRING",
   "mode": "NULLABLE",
   "description": "The customer's email address."
 }
]
EOF
}


resource "google_bigquery_table" "sales_table" {
 project              = "my-cloud-project"
 dataset_id           = google_bigquery_dataset.raw_sales_data.dataset_id
 deletion_protection  = false
 table_id             = "sales_table"
 schema               = <<EOF
[
 {
   "name": "order_id",
   "type": "INT64",
   "mode": "REQUIRED",
   "description": "The order ID."
 },
 {
   "name": "product",
   "type": "STRING",
   "mode": "NULLABLE",
   "description": "The product name."
 },
 {
   "name": "price",
   "type": "FLOAT64",
   "mode": "NULLABLE",
   "description": "The product price."
 },
 {
   "name": "amount",
   "type": "INT64",
   "mode": "NULLABLE",
   "description": "The product amount."
 },
 {
   "name": "customer_id",
   "type": "INT64",
   "mode": "REQUIRED",
   "description": "The customer ID."
 }
]
EOF
}


resource "google_bigquery_dataset" "master_sales_data" {
 project       = "my-cloud-project"
 dataset_id    = "master_sales_data"
 friendly_name = "Master Sales Data"
 description   = "This dataset contains the master sales data."
 location      = "europe-southwest1"
}


resource "google_bigquery_table" "bigtable_info" {
 project              = "my-cloud-project"
 dataset_id           = google_bigquery_dataset.master_sales_data.dataset_id
 deletion_protection  = false
 table_id             = "bigtable_info"
 schema               = <<EOF
[
 {
   "name": "customer_id",
   "type": "INT64",
   "mode": "REQUIRED",
   "description": "The customer ID."
 },
 {
   "name": "name",
   "type": "STRING",
   "mode": "NULLABLE",
   "description": "The customer's name."
 },
 {
   "name": "email",
   "type": "STRING",
   "mode": "NULLABLE",
   "description": "The customer's email address."
 },
 {
   "name": "order_id",
   "type": "INT64",
   "mode": "REQUIRED",
   "description": "The order ID."
 },
 {
   "name": "product",
   "type": "STRING",
   "mode": "NULLABLE",
   "description": "The product name."
 },
 {
   "name": "price",
   "type": "FLOAT64",
   "mode": "NULLABLE",
   "description": "The product price."
 },
 {
   "name": "amount",
   "type": "INT64",
   "mode": "NULLABLE",
   "description": "The product amount."
 }
]
EOF
}


Vemos que va a añadir un bucket con dos carpetas para poder subir los datos a la carpeta correspondiente dependiendo si es sales o customer. También ha creado dos datasets, uno para la capa de raw para tener los datos según nos han venido de origen con las tablas de customers y sales, y el segundo dataset para la tabla final que almacenará el resultado de juntar la información de ambas tablas.

Vamos a revisar el código pyspark:

from pyspark.sql import SparkSession
from pyspark.sql import functions as F


# Configure Spark session
spark = SparkSession.builder.appName("SalesETL").getOrCreate()


# Define file paths
sales_path = "gs://sales-etl-bucket/input/sales/*.csv"
customers_path = "gs://sales-etl-bucket/input/customers/*.csv"


# Read CSV files into DataFrames
sales_df = spark.read.csv(sales_path, header=True, inferSchema=True)
customers_df = spark.read.csv(customers_path, header=True, inferSchema=True)


# Write tables to BigQuery
sales_df.write.format("bigquery") \
   .option("temporaryGcsBucket", "temporary-files-dataproc") \
   .option("table",  "raw_sales_data.sales_table") \
   .mode("overwrite") \
   .save()
customers_df.write.format("bigquery") \
   .option("temporaryGcsBucket", "temporary-files-dataproc") \
   .option("table",  "raw_sales_data.customer_table") \
   .mode("overwrite") \
   .save()


# Join sales and customers tables
bigtable_info_df = sales_df.join(customers_df, on="customer_id", how="inner")


# Write joined table to BigQuery
bigtable_info_df.write.format("bigquery") \
   .option("temporaryGcsBucket", "temporary-files-dataproc") \
   .option("table",  "master_sales_data.bigtable_info") \
   .mode("overwrite") \
   .save()


# Stop the Spark session
spark.stop()

Se puede observar que el código generado realiza la lectura por cada una de las carpetas e inserta cada dato en su tabla correspondiente. 

Para poder asegurarnos de que el ejemplo está bien realizado, podemos seguir los pasos del README en el repositorio GitHub[8] para aplicar los cambios en el código terraform, subir los ficheros de ejemplo que tenemos en la carpeta example_data y a ejecutar un Batch en Dataproc. 

Finalmente, vemos si la información que se ha almacenado en BigQuery es correcta:

  • Tabla customer:
  • Tabla sales:
  • Tabla final:

De esta forma, hemos conseguido de a través de lenguaje natural, tener un proceso funcional totalmente operativo. Hay otro ejemplo que se puede ejecutar, aunque también animo a hacer más ejemplos, o incluso mejorar el prompt, para poder meterle ejemplos más complejos, y también adaptarlo a tu caso de uso.

Conclusiones y Recomendaciones

Al ser ejemplos muy concretos sobre unas tecnologías tan concretas, cuando se hace un cambio en el prompt en cualquier ejemplo puede afectar a los resultados, o también, modificar alguna palabra de la petición de entrada. Esto se traduce en que el prompt no es lo suficientemente robusto como para poder asimilar distintas expresiones sin afectar al código generado. Para poder tener un prompt y un sistema productivo, se necesita más entrenamiento y distinta variedad tanto de soluciones, peticiones, expresiones,. … Con todo ello, finalmente podremos tener una primera versión que poder presentar a nuestro usuario de negocio para que sea autónomo.

Especificar el máximo detalle posible a un LLM es crucial para obtener resultados precisos y contextuales. Aquí hay varios consejos que debemos tener en cuenta para poder tener un resultado adecuado:

  • Claridad y Concisión:
    • Sé claro y conciso en tu prompt, evitando oraciones largas y complicadas.
    • Define claramente el problema o la tarea que deseas que el modelo aborde.
  • Especificidad:
    • Proporciona detalles específicos sobre lo que estás buscando. Cuanto más preciso seas, mejores resultados obtendrás.
  • Variabilidad y Diversidad:
    • Considera incluir diferentes tipos de ejemplos o casos para evaluar la capacidad del modelo para manejar la variabilidad.
  • Feedback Iterativo:
    • Si es posible, realiza iteraciones en tu prompt basándote en los resultados obtenidos y el feedback del modelo.
  • Prueba y Ajuste:
    • Antes de usar el prompt de manera extensa, realiza pruebas con ejemplos y ajusta según sea necesario para obtener resultados deseados.

Perspectivas Futuras

En el ámbito de LLM, las líneas futuras de desarrollo se centran en mejorar la eficiencia y la accesibilidad de la implementación de modelos de lenguaje. Aquí se detallan algunas mejoras clave que podrían potenciar significativamente la experiencia del usuario y la eficacia del sistema:

1. Uso de distintos modelos de LLM:

La inclusión de una función que permita a los usuarios comparar los resultados generados por diferentes modelos sería esencial. Esta característica proporcionaría a los usuarios información valiosa sobre el rendimiento relativo de los modelos disponibles, ayudándoles a seleccionar el modelo más adecuado para sus necesidades específicas en términos de precisión, velocidad y recursos requeridos.

2. Capacidad de retroalimentación del usuario:

Implementar un sistema de retroalimentación que permita a los usuarios calificar y proporcionar comentarios sobre las respuestas generadas podría ser útil para mejorar continuamente la calidad del modelo. Esta información podría utilizarse para ajustar y refinar el modelo a lo largo del tiempo, adaptándose a las preferencias y necesidades cambiantes de los usuarios.

3. RAG (Retrieval-augmented generation)

RAG (Retrieval-augmented generation) es un enfoque que combina la generación de texto y la recuperación de información para mejorar las respuestas de los modelos de lenguaje. Implica el uso de mecanismos de recuperación para obtener información relevante de una base de datos o corpus textual, que luego se integra en el proceso de generación de texto para mejorar la calidad y la coherencia de las respuestas generadas.

Enlaces de interés

Cloud Storage[1]: https://cloud.google.com/storage/docs

BigQuery[2]: https://cloud.google.com/bigquery/docs

Terraform[3]: https://developer.hashicorp.com/terraform/docs

PySpark[4]: https://spark.apache.org/docs/latest/api/python/index.html

Dataproc[5]: https://cloud.google.com/dataproc/docs

Codey[6]: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/code-generation

VertexAI[7]: https://cloud.google.com/vertex-ai/docs

GitHub[8]: https://github.com/alfonsozamorac/etl-genai

Tabla de contenidos
  1. Introducción
  2. Antes de comenzar
  3. Qué es un LLM 
  4. Desarrollo del prompt
  5. Casos prácticos

Publicado en: Blog, Practices, Tech

Análisis de vulnerabilidades en contenedores con trivy

marzo 22, 2024 by Bluetab

Análisis de vulnerabilidades en contenedores con trivy

Ángel Maroco

AWS Cloud Architect

Dentro del marco de la seguridad en contenedores, la fase de construcción adquiere vital importancia debido a que debemos seleccionar la imagen base sobre la que ejecutarán las aplicaciones. El no disponer de mecanismos automáticos para el análisis de vulnerabilidades puede desembocar en entornos productivos con aplicaciones inseguras con los riesgos que ello conlleva.

En este artículo cubriremos el análisis de vulnerabilidades a través de la solución Trivy de Aqua Security, pero antes de comenzar, es preciso explicar en qué se basan este tipo de soluciones para identificar vulnerabilidades en las imágenes docker.

Introducción a CVE (Common Vulnerabilities and Exposures)

CVE es una lista de información mantenida por MITRE Corporation cuyo objetivo es centralizar el registro de vulnerabilidades de seguridad conocidas, en la que cada referencia tiene un número de identificación CVE-ID, descripción de la vulnerabilidad, que versiones del software están afectadas, posible solución al fallo (si existe) o como configurar para mitigar la vulnerabilidad y referencias a publicaciones o entradas de foros o blog donde se ha hecho pública la vulnerabilidad o se demuestra su explotación.

El CVE-ID ofrece una nomenclatura estándar para identificar de forma inequívoca una vulnerabilidad. Se clasifican en 5 tipologías, las cuales veremos en la sección Interpretación del análisis. Dichas tipologías son asignadas basándose en diferentes métricas (si tenéis curiosidad, consultad CVSS v3 Calculator)

CVE se ha convertido en el estándar para el registro de vulnerabilidades, por lo que la amplia mayoría de empresas de tecnología y particulares hacen uso de la misma.

Disponemos de múltiples canales para estar informados de todas las novedades referentes a vulnerabilidades: blog oficial, twitter, repositorio cvelist en github o LinkedIn.

Adicionalmente, si queréis información más detallada sobre una vulnerabilidad, podéis consultar la web del NIST, en concreto la NVD (National Vulnerability Database)

Os invitamos a buscar alguna de las siguientes vulnerabilidades críticas, es muy posible que de forma directa o indirecta os haya podido afectar. Os adelantamos que han sido de las más sonadas 🙂

  • CVE-2017-5753
  • CVE-2017-5754

Si detectas una vulnerabilidad, te animamos a registrarla a través del siguiente formulario

Aqua Security – Trivy

Trivy es una herramienta open source enfocada en la detección de vulnerabilidades en paquetes a nivel OS y ficheros de dependencias de distintitos lenguajes:

  • OS packages; (Alpine, Red Hat Universal Base Image, Red Hat Enterprise Linux, CentOS, Oracle Linux, Debian, Ubuntu, Amazon Linux, openSUSE Leap, SUSE Enterprise Linux, Photon OS and Distroless)

  • Application dependencies: (Bundler, Composer, Pipenv, Poetry, npm, yarn and Cargo)

Aqua Security, empresa especializada en el desarrollo de soluciones de seguridad, adquirió trivy en 2019. Junto a un amplio número de colaboradores, son los encargados del desarrollo y mantenimiento de la misma.

Instalación

Trivy dispone de instaladores para la mayor parte de sistemas Linux and macOS. Para nuestras pruebas vamos a utilizar el instalador genérico:

curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/master/contrib/install.sh | sudo sh -s -- -b /usr/local/bin 

Si no queremos persistir el binario en nuestro sistema, disponemos de una imagen docker:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/trivycache:/root/.cache/ aquasec/trivy python:3.4-alpine 

Operaciones básicas

  • Imágenes locales

Trivy dispone de instaladores para la mayor parte de sistemas Linux and macOS. Para nuestras pruebas vamos a utilizar el instalador genérico:

#!/bin/bash
docker build -t cloud-practice/alpine:latest -<<EOF
FROM alpine:latest
RUN echo "hello world"
EOF

trivy image cloud-practice/alpine:latest 
  • Imágenes remotas
#!/bin/bash
trivy image python:3.4-alpine 
  • Proyectos locales:
    Permite analizar ficheros de dependencias (salidas):
    • Pipfile.lock: Python
    • package-lock_react.json: React
    • Gemfile_rails.lock: Rails
    • Gemfile.lock: Ruby
    • Dockerfile: Docker
    • composer_laravel.lock: PHP Lavarel
    • Cargo.lock: Rust
#!/bin/bash
git clone https://github.com/knqyf263/trivy-ci-test
trivy fs trivy-ci-test 
  • Repositorios públicos:
#!/bin/bash
trivy repo https://github.com/knqyf263/trivy-ci-test 
  • Repositorios de imágenes privados:
    • Amazon ECR (Elastic Container Registry)
    • Docker Hub
    • GCR (Google Container Registry)
    • Repositorios privados con BasicAuth
  • Cache database
    La base de datos de vulnerabilidades se aloja en github. Para evitar descargar dicha base de datos en cada operación de análisis, podemos utilizar el parámetro --cache-dir <dir>:
#!/bin/bash trivy –cache-dir .cache/trivy image python:3.4-alpine3.9 
  • Filtrar por criticidad
#!/bin/bash
trivy image --severity HIGH,CRITICAL ruby:2.4.0 
  • Filtrar vulnerabiliades no resueltas
#!/bin/bash
trivy image --ignore-unfixed ruby:2.4.0 
  • Especificar código de salida
    Esta opción en muy util en el proceso de integración continua, ya que podemos especificar que nuestro pipeline finalice con error cuando se encuentre vulnerabilidad de tipo critical pero las tipo medium y high finalicen correctamente.
#!/bin/bash
trivy image --exit-code 0 --severity MEDIUM,HIGH ruby:2.4.0
trivy image --exit-code 1 --severity CRITICAL ruby:2.4.0 
  • Ignorar vulnerabilidades específicas
    A través del fichero .trivyignore, podemos especificar aquellas CVEs que nos interesa descartar. Puede resultar útil si la imagen contiene una vulnerabilidad que no afecta a nuestro desarrollo.
#!/bin/bash
cat .trivyignore
# Accept the risk
CVE-2018-14618

# No impact in our settings
CVE-2019-1543 
  • Exportar salida en formato JSON:
    Esta opción es interesante si quieres automatizar un proceso antes una salida, visualizar los resultados en un front personalizado o persistir la salida con un formato estructurado.
#!/bin/bash
trivy image -f json -o results.json golang:1.12-alpine
cat results.json | jq 
  • Exportar salida en formato SARIF:
    Existe un estandar llamado SARIF (Static Analysis Results Interchange Format) que define el formato que deben tener las salidas cualquier herramienta de análisis de vulnerabilidades.
#!/bin/bash
wget https://raw.githubusercontent.com/aquasecurity/trivy/master/contrib/sarif.tpl
trivy image --format template --template "@sarif.tpl" -o report-golang.sarif  golang:1.12-alpine
cat report-golang.sarif   

VS Code dispone de la extensión sarif-viewer para la visualización de vulnerabilidades.

Procesos de integración contínua

Trivy dispone de plantillas para las principales soluciones de CI/CD:

  • GitHub Actions
  • Travis CI
  • CircleCI
  • GitLab CI
  • AWS CodePipeline
#!/bin/bash
$ cat .gitlab-ci.yml
stages:
  - test

trivy:
  stage: test
  image: docker:stable-git
  before_script:
    - docker build -t trivy-ci-test:${CI_COMMIT_REF_NAME} .
    - export VERSION=$(curl --silent "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
    - wget https://github.com/aquasecurity/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz
    - tar zxvf trivy_${VERSION}_Linux-64bit.tar.gz
  variables:
    DOCKER_DRIVER: overlay2
  allow_failure: true
  services:
    - docker:stable-dind
  script:
    - ./trivy --exit-code 0 --severity HIGH --no-progress --auto-refresh trivy-ci-test:${CI_COMMIT_REF_NAME}
    - ./trivy --exit-code 1 --severity CRITICAL --no-progress --auto-refresh trivy-ci-test:${CI_COMMIT_REF_NAME} 

Interpretación del análisis

#!/bin/bash
trivy image httpd:2.2-alpine
2020-10-24T09:46:43.186+0200    INFO    Need to update DB
2020-10-24T09:46:43.186+0200    INFO    Downloading DB...
18.63 MiB / 18.63 MiB [---------------------------------------------------------] 100.00% 8.78 MiB p/s 3s
2020-10-24T09:47:08.571+0200    INFO    Detecting Alpine vulnerabilities...
2020-10-24T09:47:08.573+0200    WARN    This OS version is no longer supported by the distribution: alpine 3.4.6
2020-10-24T09:47:08.573+0200    WARN    The vulnerability detection may be insufficient because security updates are not provided

httpd:2.2-alpine (alpine 3.4.6)
===============================
Total: 32 (UNKNOWN: 0, LOW: 0, MEDIUM: 15, HIGH: 14, CRITICAL: 3)

+-----------------------+------------------+----------+-------------------+------------------+--------------------------------+
|        LIBRARY        | VULNERABILITY ID | SEVERITY | INSTALLED VERSION |  FIXED VERSION   |             TITLE              |
+-----------------------+------------------+----------+-------------------+------------------+--------------------------------+
| libcrypto1.0          | CVE-2018-0732    | HIGH     | 1.0.2n-r0         | 1.0.2o-r1        | openssl: Malicious server can  |
|                       |                  |          |                   |                  | send large prime to client     |
|                       |                  |          |                   |                  | during DH(E) TLS...            |
+-----------------------+------------------+----------+-------------------+------------------+--------------------------------+
| postgresql-dev        | CVE-2018-1115    | CRITICAL | 9.5.10-r0         | 9.5.13-r0        | postgresql: Too-permissive     |
|                       |                  |          |                   |                  | access control list on         |
|                       |                  |          |                   |                  | function pg_logfile_rotate()   |
+-----------------------+------------------+----------+-------------------+------------------+--------------------------------+
| libssh2-1             | CVE-2019-17498   | LOW      | 1.8.0-2.1         |                  | libssh2: integer overflow in   |
|                       |                  |          |                   |                  | SSH_MSG_DISCONNECT logic in    |
|                       |                  |          |                   |                  | packet.c                       |
+-----------------------+------------------+----------+-------------------+------------------+--------------------------------+ 
  • Library: librería/paquete donde se ha identificado la vulnerabilidad.

  • Vulnerability ID: Identificador de vulnerabilidad (según estandar CVE).

  • Severity: existe una clasificación con 5 tipologías [fuente] las cuales tienen asignado una puntuación CVSS (Common Vulnerability Scoring System):

    • Critical (puntuación CVSS 9.0-10.0): fallos que podría aprovechar fácilmente un atacante no autenticado y llegar a comprometer el sistema (ejecución de código arbitrario) sin interacción por parte del usuario.

    • High (puntuación CVSS 7.0-8.9): fallos que podrían comprometer fácilmente la confidencialidad, integridad o disponibilidad de los recursos.

    • Medium (puntuación CVSS 4.0-6.9): fallos que, aún siendo más difíciles de aprovechar, pueden seguir comprometiendo la confidencialidad, integridad o disponibilidad de los recursos en determinadas circunstancias.

    • Low (puntuación CVSS 0.1-3.9): resto de problemas que producen un impacto de seguridad. Son los tipos de vulnerabilidades de los que se considera que su aprovechamiento exige unas circunstancias poco probables o que tendría consecuencias mínimas.

    • Unknow (puntuación CVSS 0.0): se otorga a vulnerabilidades que no tienen asignada puntuación.

  • Installed version: versión instalada en el sistema analizado.

  • Fixed version: versión en la que se resuelve el problema. Si no se informa la versión quiere decir que está pendiente de resolución.

  • Title: Descripción corta de la vulnerabilidad. Para más información, consultar NVD.

Ya sabemos interpretar a alto nivel la información que nos muestra el análisis. Ahora bien, ¿qué acciones debería tomar? En la sección Recomendaciones te damos alguna pista.

Recomendaciones

  • En esta sección describimos algunos aspectos más importantes dentro del ámbito de vulnerabilidades en contenedores:

    • Evitar (en la medida de lo posible) hacer uso de imágenes donde se hayan identificado vulnerabilidades critical y high

    • Incluir el análisis de imágenes en procesos de CI
      La seguridad en tu desarrollo no es opcional, automatiza tus pruebas y no dependas de procesos manuales.

    • Utilizar imágenes ligeras, menos exposiciones:
      Las imágenes tipo Alpine / BusyBox están construidas con el menor número de paquetes posible (la imagen base pesa 5MB), lo que se traduce en una reducción de vectores de ataque. Soportan múltiples arquitecturas y se actualizan con bastante frecuencia.
REPOSITORY  TAG     IMAGE ID      CREATED      SIZE
alpine      latest  961769676411  4 weeks ago  5.58MB
ubuntu      latest  2ca708c1c9cc  2 days ago   64.2MB
debian      latest  c2c03a296d23  9 days ago   114MB
centos      latest  67fa590cfc1c  4 weeks ago  202MB 
Si por algún motivo de dependencias no podéis customizar una imagen base de alpine, buscad imágenes tipo slim de proveedores de software confiables. Además del componente de seguridad, las personas que compartan red contigo lo agradecerán al no tener que bajar imágenes de 1GB
  • Obtener imágenes de repositorios oficiales: Lo recomendable es utilizar DockerHub y preferentemente imágenes de publishers oficiales. DockerHub y CVEs

  • Mantener actualizadas las imágenes En el siguiente ejemplo vemos un análisis sobre dos versiones diferentes de apache:

    Imagen publicada el 11/2018

httpd:2.2-alpine (alpine 3.4.6)
 Total: 32 (UNKNOWN: 0, LOW: 0, MEDIUM: 15, **HIGH: 14, CRITICAL: 3**) 

Imagen publicada el 01/2020

httpd:alpine (alpine 3.12.1)
 Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, **HIGH: 0, CRITICAL: 0**) 

Como podéis observar, si un desarrollo finalizó en 2018 y no se realizan tareas de mantenimiento, podría estar exponiendo un apache relativamente vulnerable. No es un problema derivado del uso de contenedores, pero debido a la versatilidad que nos proporciona docker para testar nuevas versiones de productos, ahora no tenemos excusa.

  • Especial atención a vulnerabilidades que afecten a la capa de aplicación:
    Según el estudio realizado por la compañía edgescan, el 19% de las vulnerabilidades detectadas en 2018 corresponden a capa 7 (Modelo OSI), destacando por encima de todos ataques de tipo XSS (Cross-site Scripting).

  • Seleccionar imágenes latest con especial cuidado:
    Aunque este consejo está muy relacionado con el uso de imágenes ligeras, consideramos hacer un inciso sobre las imágenes latest:

Imagen latest Apache (base alpine 3.12)

httpd:alpine (alpine 3.12.1)
 Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) 

Imagen latest Apache (base debian 10.6)

httpd:latest (debian 10.6)
 Total: 119 (UNKNOWN: 0, LOW: 87, MEDIUM: 10, HIGH: 22, CRITICAL: 0) 

En ambos casos estamos utilizando la misma versión de apache (2.4.46), la diferencia está en el número de vulnerabilidades críticas.
¿Quiere decir que la imagen basada en debian 10 convierte en vulnerable la aplicación que ejecuta en ese sistema? Puede que si o puede que no, hay que evaluar si las vulnerabilidades pueden comprometer nuestra aplicación. La recomendación es utilizar la imagen de alpine.

  • Evaluar el uso de imágenes docker distroless
    El concepto distroless es de Google y consiste en imágenes docker basadas en debian9/debian10, sin gestores de paquetes, shells ni utilidades. Las imágenes están enfocadas a lenguajes de programación (Java, Python, Golang, Node.js, dotnet y Rust), contiene exclusivamente lo necesario para ejecutar las aplicaciones. Al no disponer de gestores de paquetes, no puedes instalar tus propias dependencias, lo que se puede traducir en una gran ventaja y en otros casos, un gran obstáculo. Realizad pruebas y si encaja con los requisitos de vuestro proyecto, adelante, siempre es beneficioso disponer de alternativas. El mantenimiento corre a cuenta de Google, así que el aspecto de seguridad estará bien acotado.

Ecosistema de analizadores de vulnerabilidades para contenedores

En nuestro caso hemos utilizado trivy ya que se trata de una herramienta open source, fiable, estable y en continua evolución, pero disponemos de multitud de herramientas para el análisis de contenedores:
  • Clair
  • Snyk
  • Anchore Cloud
  • Docker Bench
  • Docker Scan
¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB
Ángel Maroco
AWS Cloud Architect

Ángel Maroco llevo en el sector IT más de una década, iniciando mi carrera profesional con el desarrollo web, pasando una buena etapa en distintas plataformas informacionales en entornos bancarios y los últimos 5 años dedicado al diseño de soluciones en entornos AWS.

En la actualidad, compagino mi papel de arquitecto junto al de responsable de la Pŕactica Cloud /bluetab, cuya misión es impulsar la cultura Cloud dentro de la compañía.

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

LakeHouse Streaming en AWS con Apache Flink y Hudi (Parte 2)

octubre 4, 2023
LEER MÁS

¿Cómo pueden las empresas asegurarse de que sus datos estén estructurados, escalables y disponibles cuando se necesiten?

septiembre 13, 2024
LEER MÁS

Potencia Tu Negocio con GenAI y GCP: Simple y para Todos

marzo 27, 2024
LEER MÁS

La gestión del cambio: El puente entre las ideas y el éxito

febrero 5, 2025
LEER MÁS

LA BANCA Y LA ERA DEL OPEN DATA

abril 19, 2023
LEER MÁS

Del negocio físico a la explosión del On-Line

abril 7, 2021
LEER MÁS

Publicado en: Blog, Practices, Tech

Usando los Grandes Modelos de Lenguaje en información privada

marzo 11, 2024 by Bluetab

Roger Pou Lopez
Data Scientist

Un RAG, acrónimo de «Retrieval Augmented Generation», representa una estrategia innovadora dentro del procesamiento de lenguaje natural. Se integra con los Grandes Modelos de Lenguaje (LLM, por sus siglas en inglés), tales como los que usa ChatGPT internamente (GPT-3.5-turbo o GPT-4), con el objetivo de mejorar la calidad de la respuesta y reducir ciertos comportamientos no deseados, como las alucinaciones.

https://www.superannotate.com/blog/rag-explained

Estos sistemas combinan los conceptos de vectorización y búsqueda semántica, junto con los LLMs para retroalimentar su conocimiento con información externa que no se incluyó durante su fase de entrenamiento y que, por lo tanto, desconocen.

Existen ciertos puntos a favor de utilizar RAGs:

  • Permiten reducir el nivel de alucinaciones que presentan los modelos. A menudo, los LLM responden con información incorrecta (o inventada), aunque semánticamente su respuesta tenga sentido. A esto se le denomina alucinación. Uno de los objetivos principales del RAG es intentar reducir al máximo este tipo de situaciones, especialmente cuando se pregunta por cosas concretas. Esto es de alta utilidad si se quiere utilizar un LLM de forma productiva.
  • Utilizando un RAG, ya no es necesario reentrenar el LLM. Este proceso puede llegar a ser costoso económicamente, dado que necesitaría GPUs para su entrenamiento, además de la complejidad que puede conllevar ese entrenamiento.
  • Son sistemas económicos, rápidos (utilizan información indexada) y además, no dependen del modelo que se está utilizando (en cualquier momento podemos cambiarlo por de GPT-3.5 a Llama-2-70B).

En contra:

  • Se va a necesitar ayuda de código, matemáticas y no va a ser tan sencillo como lanzar un simple prompt modificado.
  • En la evaluación de los RAGs (veremos más adelante en el artículo) vamos a necesitar modelos potentes como GPT-4.

Ejemplo de caso de uso

Existen varios ejemplos donde los RAGs están siendo utilizados. El ejemplo más típico es su uso con chatbots para consultar información muy específica del negocio.

  • En call-centers, los agentes están empezando a utilizar un chatbot con información sobre tarifas para poder responder de forma rápida y eficaz a las llamadas que reciben.
  • En chatbots, como asistentes de venta donde están ganando popularidad. Aquí, los RAGs ayudan a responder a comparativas entre productos o cuando se consulta de manera específica sobre un servicio, haciendo recomendaciones de productos similares.

Componentes de un RAG

https://zilliz.com/learn/Retrieval-Augmented-Generation

Vamos a hablar en detalle sobre los distintos componentes que conforman un RAG para poder tener una idea aproximada, y luego vamos a hablar de cómo interaccionan entre sí estos elementos.

Base de conocimiento

Este elemento es un concepto un poco abierto pero también lógico: se refiere al conocimiento objetivo del cual sabemos que el LLM no es consciente y que tiene un alto riesgo de alucinación. Este conocimiento, en formato de texto, puede estar en muchos formatos: PDF, Excel, Word, etc… Los RAGs avanzados son capaces también de detectar conocimientos en imágenes y tablas.

En general, todo contenido va a ser en formato de texto y va a necesitar ser indexado. Como los textos humanos son muchas veces desestructurados, se recurre a la subdivisión de los textos con estrategias llamadas chunking.

Modelo de Embeddings

Un embedding es la representación vectorial generada por una red neuronal entrenada sobre un cuerpo de datos (texto, imágenes, sonido, etc.) que es capaz de resumir la información de un objeto de ese mismo tipo hacia un vector dentro de un espacio vectorial concreto.

Por ejemplo, en el caso de un texto que se refiere a “Me gustan los patitos de goma azules” y otro que dice “Adoro los patitos de goma amarillos”, al ser convertidos en vectores, estos estarán más próximos en distancia entre sí que un texto que se refiere a “Los automóviles del futuro son los coches eléctricos”.

Este componente es el que, posteriormente, nos permitirá indexar de forma correcta los distintos chunks de información de texto.

Base de datos vectorial

Es el lugar donde vamos a guardar y indexar la información vectorial de los chunks mediante los embeddings. Se trata de un componente muy importante y complejo donde, afortunadamente, ya existen varias soluciones open source muy válidas para poder desplegarlo de forma «fácil», como Milvus o Chroma.

LLM

Es lógico, puesto que el RAG es una solución que nos permite ayudar a responder de forma más veraz a estos LLMs. No tenemos por qué restringirnos a modelos muy grandes y eficientes (pero no económicos como GPT-4), sino que pueden ser modelos más pequeños y más «sencillos» en cuanto a la calidad de respuestas y número de parámetros.

A continuación podemos ver una imagen representativa del proceso de carga de información en la base de datos vectoriales.

https://python.langchain.com/docs/use_cases/question_answering/

Funcionamiento a Alto Nivel

Ahora que tenemos un poco más claras las piezas del rompecabezas, surgen algunas dudas:

  • ¿Cómo interactúan estos componentes entre sí?
  • ¿Por qué hace falta una base de datos vectorial?

Vamos a intentar esclarecer un poco el asunto.

https://www.hopsworks.ai/dictionary/retrieval-augmented-generation-llm

La idea intuitiva del funcionamiento de un RAG es la siguiente:

  1. El usuario hace una pregunta. Transformamos la pregunta a un vector con el mismo sistema de embedding que hemos utilizado para guardar los chunks. Esto nos va a permitir comparar nuestra pregunta con toda la información que tenemos indexada en nuestra base de datos vectorial.
  2. Calculamos las distancias entre la pregunta y todos los vectores que tenemos en la base de datos. Seleccionamos, con una estrategia, algunos de los chunks y añadimos todas esas piezas de información dentro del prompt como contexto. La estrategia más sencilla es basarse en seleccionar un número (K) de vectores más próximos a la pregunta.
  3. Se lo pasamos al LLM para que genere la respuesta en base a los contextos. Es decir, el prompt contiene instrucciones + pregunta + contexto devuelto por el sistema de Retrieval. Por este motivo, la parte de «Augmentation» en las siglas del RAG, dado que estamos haciendo prompt augmentation.
  4. El LLM nos ha generado una respuesta en base a la pregunta que hacemos y el contexto que le hemos pasado. Esta será la respuesta que el usuario va a visualizar.

Es por eso que necesitamos un embedding y la base de datos vectorial. Ahí está un poco el truco. Si eres capaz de encontrar información muy parecida a tu pregunta en tu base de datos vectorial, entonces puedes detectar contenido que puede ser de utilidad para tu pregunta. Pero para todo ello, necesitamos un elemento que nos permita poder comparar textos de forma objetiva y esa información no podemos tenerla guardada de forma desestructurada si necesitamos hacer preguntas de forma frecuente.

También, que al final todo esto termina en el prompt, que nos permite que sea un flujo independiente del modelo de LLM que vayamos a usar.

Evaluación de los RAG

De igual manera que los modelos de estadística o ciencias de datos más clásicos, tenemos una necesidad de cuantificar cómo está funcionando un modelo antes de utilizarlo de manera productiva.

La estrategia más básica (por ejemplo, para medir la efectividad de una regresión lineal) consiste en dividir el conjunto de datos en distintas partes como train y test (80 y 20% respectivamente), entrenando el modelo en train y evaluando en test con métricas como el root-mean-square error, dado que el conjunto de test son datos que no ha visto el modelo. Sin embargo, un RAG no consta de entrenamiento sino de un sistema compuesto de distintos elementos donde una de sus partes es usar un modelo de generación de texto.

Más allá de esto, aquí ya no tenemos datos cuantitativos (es decir, números) y la naturaleza del dato consiste en texto generado que puede variar en función de la pregunta que le hagamos, el contexto detectado por el sistema de Retrieval y incluso el comportamiento no determinista que tienen los modelos de redes neuronales.

Una estrategia básica que podemos pensar es en ir analizando a mano qué tan bueno está funcionando nuestro sistema, en base a hacer preguntas y ver cómo están funcionando las respuestas y los contextos devueltos. Pero este enfoque se vuelve impracticable cuando queremos evaluar todas las posibilidades de preguntas en documentos muy grandes y de forma recurrente.

¿Entonces, cómo podemos hacer esta evaluación?

El truco: Aprovechando los propios LLMs. Con ellos podemos construir un conjunto de datos sintético con el que se haya simulado la misma acción de hacer preguntas a nuestro sistema, tal como si un humano lo hubiera hecho. Incluso le podemos añadir un nivel de fineza mayor: utilizar un modelo más inteligente que el anterior y que funcione como un crítico, que nos indique si lo que está sucediendo tiene sentido o no.

Ejemplo de conjunto de datos de evaluación

https://docs.ragas.io/en/stable/getstarted/evaluation.html

Aquí lo que tenemos son muestras de Pregunta-Respuesta de cómo hubiera funcionado nuestro sistema de RAG simulando las preguntas que le podría hacer un humano en comparativa al modelo que estamos evaluando. Para hacer esto, necesitamos dos modelos: el LLM que utilizaríamos en nuestro RAG, por ejemplo, GPT-3.5-turbo (Answer) y otro modelo con mejor funcionamiento para generar una “verdad” (Ground Truth), como GPT-4.

Es decir, en otras palabras, el ChatGPT 3.5 sería el sistema generador de preguntas y el ChatGPT 4 sería como la parte crítica.

Una vez generado nuestro conjunto de datos de evaluación, lo que nos queda es cuantificar numéricamente con algún tipo de métrica.

Métricas de Evaluación

La evaluación de las respuestas es algo nuevo pero ya existen proyectos de código abierto que logran cuantificar de forma efectiva la calidad de los RAGs. Estos sistemas de evaluación permiten medir la parte de «Retrieval» y «Generation» por separado.

https://docs.ragas.io/en/stable/concepts/metrics/index.html

Faitfulness Score

Mide la veracidad de nuestras respuestas dado un contexto. Es decir, con qué porcentaje lo que se pregunta es verdad en función del contexto conseguido a través de nuestro sistema.  Esta métrica sirve para intentar controlar las alucinaciones que pueden tener los LLMs. Un valor muy bajo en esta métrica implicaría que el modelo se está inventando cosas, aunque se le dé un contexto. Por lo tanto, es una métrica que debe estar lo más cercano a uno.

Answer Relevancy Score

Cuantifica la relevancia de la respuesta en base a la pregunta que se le hace a nuestro sistema. Si la respuesta no es relevante a lo que le preguntamos, no nos está respondiendo adecuadamente. Por lo tanto cuanto más alta sea esta métrica, mejor.

Context Precision Score

Evalua si todos los elementos de nuestros ground-truth ítems dentro de los contextos, son rankeados de forma prioritaria o no.

Context Recall Score

Cuantifica si el contexto devuelto se alinea con la respuesta anotada. En otras palabras, cómo de relevante es el contexto respecto a la pregunta que hacemos. Una valor bajo indicaría que el contexto devuelto es poco relevante y no nos ayuda a responder la pregunta.

El cómo todas estas métricas se están evaluando es un poco más complejo pero podemos encontrar ejemplos bien explicados en la documentación de RAGAS.

Ejemplo práctico utilizando LangChain, OpenAI y ChromaDB

Vamos a utilizar el framework de LangChain que nos permite construir un RAG de forma muy fácil.

El dataset que vamos a utilizar es un ensayo de Paul Graham, un dataset típico y pequeño en cuanto a tamaño.

La base de datos vectorial que vamos a utilizar va a ser Chroma, open-source y con plena integración con LangChain. El uso de esta va a ser completamente transparente, utilizando los parámetros por defecto.

NOTA: Cada llamada a un modelo asociado, tiene un coste monetario y conviene revisar el pricing de OpenAI. Nosotros vamos a trabajar con un dataset pequeño de 10 preguntas pero si se escalase, el coste podría incrementarse.

import os
from dotenv import load_dotenv  

load_dotenv() # Configurar OpenAI API Key

from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.prompts import ChatPromptTemplate

embeddings = OpenAIEmbeddings(
    model="text-embedding-ada-002"
)

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 700,
    chunk_overlap = 50
)

loader = TextLoader('paul_graham/paul_graham_essay.txt')
text = loader.load()
documents = text_splitter.split_documents(text)
print(f'Número de chunks generados gracias al documento: {len(documents)}')

vector_store = Chroma.from_documents(documents, embeddings)
retriever = vector_store.as_retriever()
Número de chunks generados gracias al documento: 158

Dado que el texto del libro está en inglés, debemos de hacer nuestro template de prompt esté en inglés.

from langchain.prompts import ChatPromptTemplate

template = """Answer the question based only on the following context. If you cannot answer the question with the context, please respond with 'I don't know':

Context:
{context}

Question:
{question}
"""

prompt = ChatPromptTemplate.from_template(template)

Ahora vamos a definir nuestro RAG mediante LCEL. El modelo a utilizar que responderá a las preguntas de nuestro RAG va a ser GPT-3.5-turbo.  Importante es que el parámetro de la temperatura esté a 0 para que el modelo no sea creativo.

from operator import itemgetter

from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough 

primary_qa_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

retrieval_augmented_qa_chain = (
    {"context": itemgetter("question") | retriever, "question": itemgetter("question")}
    | RunnablePassthrough.assign(context=itemgetter("context"))
    | {"response": prompt | primary_qa_llm, "context": itemgetter("context")}
)

.. y ahora es posible hacerle ya preguntas a nuestro sistema RAG.

question = "What was doing the author before collegue? "

result = retrieval_augmented_qa_chain.invoke({"question" : question}) 

print(f' Answer the question based: {result["response"].content}')
Answer the question based: The author was working on writing and programming before college.

También podemos investigar cuales han sido los contextos devueltos por nuestro retriever. Como hemos mencionado, la estrategia de Retrieval es la por defecto y nos devolverá los top 4 contextos para responder a una pregunta.

display(retriever.get_relevant_documents(question))
display(retriever.get_relevant_documents(question))
[Document(page_content="What I Worked On\n\nFebruary 2021\n\nBefore college the two main things I worked on, outside of school, were writing and programming. I didn't write essays. I wrote what beginning writers were supposed to write then, and probably still are: short stories. My stories were awful. They had hardly any plot, just characters with strong feelings, which I imagined made them deep.", metadata={'source': 'paul_graham/paul_graham_essay.txt'}),
 Document(page_content="Over the next several years I wrote lots of essays about all kinds of different topics. O'Reilly reprinted a collection of them as a book, called Hackers & Painters after one of the essays in it. I also worked on spam filters, and did some more painting. I used to have dinners for a group of friends every thursday night, which taught me how to cook for groups. And I bought another building in Cambridge, a former candy factory (and later, twas said, porn studio), to use as an office.", metadata={'source': 'paul_graham/paul_graham_essay.txt'}),
 Document(page_content="In the print era, the channel for publishing essays had been vanishingly small. Except for a few officially anointed thinkers who went to the right parties in New York, the only people allowed to publish essays were specialists writing about their specialties. There were so many essays that had never been written, because there had been no way to publish them. Now they could be, and I was going to write them. [12]\n\nI've worked on several different things, but to the extent there was a turning point where I figured out what to work on, it was when I started publishing essays online. From then on I knew that whatever else I did, I'd always write essays too.", metadata={'source': 'paul_graham/paul_graham_essay.txt'}),
 Document(page_content="Wow, I thought, there's an audience. If I write something and put it on the web, anyone can read it. That may seem obvious now, but it was surprising then. In the print era there was a narrow channel to readers, guarded by fierce monsters known as editors. The only way to get an audience for anything you wrote was to get it published as a book, or in a newspaper or magazine. Now anyone could publish anything.", metadata={'source': 'paul_graham/paul_graham_essay.txt'})]

Evaluando nuestro RAG

Ahora que ya tenemos nuestro RAG montado gracias a LangChain, nos falta evaluarlo. 

Parece que tanto LangChain como LlamaIndex empiezan a tener maneras de evaluar de forma fácil los RAGs sin moverse del framework. Sin embargo, por ahora, la mejor opción es utilizar RAGAS, una librería que ya habíamos mencionado y está específicamente diseñada con ese propósito. Internamente, va a utilizar GPT-4 como modelo crítico, tal y como hemos mencionado anteriormente.

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
text = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 1000,
    chunk_overlap = 200
)
documents = text_splitter.split_documents(text)

generator = TestsetGenerator.with_openai()
testset = generator.generate_with_langchain_docs(
    documents, 
    test_size=10, 
    distributions={simple: 0.5, reasoning: 0.25, multi_context: 0.25}
)
test_df = testset.to_pandas()
display(test_df)
question contexts ground_truth evolution_type episode_done
0 What is the batch model and how does it relate… [The most distinctive thing about YC is the ba… The batch model is a method used by YC (Y Comb… simple True
1 How did the use of Scheme in the new version o… [In the summer of 2006, Robert and I started w… The use of Scheme in the new version of Arc co… simple True
2 How did learning Lisp expand the author’s conc… [There weren’t any classes in AI at Cornell th… Learning Lisp expanded the author’s concept of… simple True
3 How did Moore’s Law contribute to the downfall… [[4] You can of course paint people like still… Moore’s Law contributed to the downfall of com… simple True
4 Why did the creators of Viaweb choose to make … [There were a lot of startups making ecommerce… The creators of Viaweb chose to make their eco… simple True
5 During the author’s first year of grad school … [I applied to 3 grad schools: MIT and Yale, wh… reasoning True
6 What suggestion from a grad student led to the… [McCarthy didn’t realize this Lisp could even … reasoning True
7 What makes paintings more realistic than photos? [life interesting is that it’s been through a … By subtly emphasizing visual cues, paintings c… multi_context True
8 «What led Jessica to compile a book of intervi… [Jessica was in charge of marketing at a Bosto… Jessica’s realization of the differences betwe… multi_context True
9 Why did the founders of Viaweb set their price… [There were a lot of startups making ecommerce… The founders of Viaweb set their prices low fo… simple True
test_questions = test_df["question"].values.tolist()
test_groundtruths = test_df["ground_truth"].values.tolist()
answers = []
contexts = []
for question in test_questions:
  response = retrieval_augmented_qa_chain.invoke({"question" : question})
  answers.append(response["response"].content)
  contexts.append([context.page_content for context in response["context"]])

from datasets import Dataset # HuggingFace
response_dataset = Dataset.from_dict({
    "question" : test_questions,
    "answer" : answers,
    "contexts" : contexts,
    "ground_truth" : test_groundtruths
})
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision,
)

metrics = [
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision,
]

results = evaluate(response_dataset, metrics)
results_df = results.to_pandas().dropna()
question answer contexts ground_truth faithfulness answer_relevancy context_recall context_precision
0 What is the batch model and how does it relate… The batch model is a system where YC funds a g… [The most distinctive thing about YC is the ba… The batch model is a method used by YC (Y Comb… 0.750000 0.913156 1.0 1.000000
1 How did the use of Scheme in the new version o… The use of Scheme in the new version of Arc co… [In the summer of 2006, Robert and I started w… The use of Scheme in the new version of Arc co… 1.000000 0.910643 1.0 1.000000
2 How did learning Lisp expand the author’s conc… Learning Lisp expanded the author’s concept of… [So I looked around to see what I could salvag… Learning Lisp expanded the author’s concept of… 1.000000 0.924637 1.0 1.000000
3 How did Moore’s Law contribute to the downfall… Moore’s Law contributed to the downfall of com… [[5] Interleaf was one of many companies that … Moore’s Law contributed to the downfall of com… 1.000000 0.940682 1.0 1.000000
4 Why did the creators of Viaweb choose to make … The creators of Viaweb chose to make their eco… [There were a lot of startups making ecommerce… The creators of Viaweb chose to make their eco… 0.666667 0.960447 1.0 0.833333
5 What suggestion from a grad student led to the… The suggestion from grad student Steve Russell… [McCarthy didn’t realize this Lisp could even … The suggestion from a grad student, Steve Russ… 1.000000 0.931730 1.0 0.916667
6 What makes paintings more realistic than photos? By subtly emphasizing visual cues such as the … [copy pixel by pixel from what you’re seeing. … By subtly emphasizing visual cues, paintings c… 1.000000 0.963414 1.0 1.000000
7 «What led Jessica to compile a book of intervi… Jessica was surprised by how different reality… [Jessica was in charge of marketing at a Bosto… Jessica’s realization of the differences betwe… 1.000000 0.954422 1.0 1.000000
8 Why did the founders of Viaweb set their price… The founders of Viaweb set their prices low fo… [There were a lot of startups making ecommerce… The founders of Viaweb set their prices low fo… 1.000000 1.000000 1.0 1.000000

Visualizamos las distribuciones estadísticas que salen:

results_df.plot.hist(subplots=True,bins=20)

Podemos observar que el sistema no es perfecto aunque hemos generado solamente 10 preguntas (haría falta generar muchas más) y también se puede observar que en una de ellas, la pipeline del RAG ha fallado en crear el ground truth.

Aun así podríamos sacar algunas conclusiones:

  • algunas veces no es capaz de dar respuestas muy veraces (faithfulness)
  • la relevancia de la respuesta es variable pero consistentmente buena (answer_relevancy)
  • el context recall es perfecto pero el context precision ya no tanto

Ahora aquí nos podemos plantear probar con distintos elementos:

  • cambiar el embedding utilizado por uno que podemos encontrar en HuggingFace MTEB Leaderboard.
  • mejorar el sistema de retrieval con estrategias diferentes a la por defecto
  • evaluar con otros LLMs

Con estas posibilidades, es viable analizar cada una de esas estrategias anteriores y escoger la que mejor se ajuste a nuestros datos o criterios monetarios.

Conclusiones

En este artículos hemos visto en qué consiste un RAG y cómo podemos evaluar un workflow completo. Todo esta materia está en auge ahora mismo dado que es una de las alternativas más eficaces y económicas para evitar el fine-tuning de los LLMs. 

Es posible que se encuentren nuevas métricas, nuevos frameworks, que hagan la evaluación de estos más sencilla y eficaz; pero en los próximos artículos no solo vamos a poder ver su evolución, sino también cómo llevar a production una arquitectura basada en RAGs.

Tabla de contenidos
  1. Componentes de un RAG
  2. Funcionamiento a Alto Nivel
  3. Evaluación de los RAG
  4. Métricas de Evaluación
  5. Conclusiones

Publicado en: Blog, Practices, Tech

LakeHouse Streaming en AWS con Apache Flink y Hudi (Parte 2)

octubre 4, 2023 by Bluetab

LakeHouse Streaming en AWS con Apache Flink y Hudi (Parte 2)

Alberto Jaen

AWS Cloud Engineer

Alfonso Jerez

AWS Cloud Engineer

Adrián Jiménez

AWS Cloud Engineer

Introducción

Este artículo es el segundo en una serie de publicaciones que se centran en la creación de un LakeHouse con Hudi a partir de una ingesta en streaming procesada por una aplicación Flink. El primer artículo se centra en sentar una buena base para esta plataforma, donde se desplegaron unas aplicaciones Flink con KDA (Kinesis Data Analytics) para cada tipo de formato (MoR, CoW para Hudi y JSON) que escriben el resultado de este procesamiento en unos buckets.

El envío de datos que se utiliza como input se mandaba en el anterior artículo desde una máquina en local ejecutando una aplicación de Locust, lo que puede presentar problemas a la hora de escalar y querer procesar un volumen alto de eventos. Además, las aplicaciones de Kinesis Data Analytics con Flink presentan problemas de agilidad en su modo de autoescalado. Todos estos nuevos retos serán resueltos en este artículo.

También se catalogarán estas tablas en Glue, servicio que disponibiliza un catálogo de datos en AWS, para poder acceder a estos y así realizar queries de todo tipo. Como motor de queries que consumirá estos metadatos se utilizará Athena, que proporciona una experiencia escalable, ágil y serverless para poder ejecutar queries con SQL o Spark para nuestras tablas alojadas en S3.

Por otro lado, en este artículo también se han desplegado los componentes necesarios para poder monitorizar nuestras aplicaciones y extraer así conclusiones sobre la velocidad a la que se ingestan los datos y los posibles problemas a resolver para que el procesamiento tenga la latencia requerida según los requisitos que se impongan.

Finalmente se realizará una comparativa en cuanto a rendimiento y latencia de las diferentes aplicaciones de Flink que escriben datos en los formatos de Hudi y JSON para así poder ver las diferentes ventajas e inconvenientes de estos formatos. 

Arquitectura

A continuación se puede ver la arquitectura a alto nivel que se desplegará:

Para un mayor entendimiento vamos a explicarla de izquierda a derecha. Como se puede observar, el cambio más reseñable con respecto al primer artículo es la inclusión de un cluster de Kubernetes para poder escalar los eventos que serán mandados como input de nuestra aplicación de streaming. De esta manera se podrá testear de manera exhaustiva el rendimiento de las aplicaciones de Flink dependiendo de su aprovisionamiento y sobre todo del tipo de formato y tabla en el que escriben al LakeHouse. Además, se ha disponibilizado un ALB (Application Load Balancer) que permite acceder a la interfaz de Locust para poder definir el número de usuarios a simular y cómo deben escalar estos con el tiempo. La URL para acceder a esta aparecerá como output al desplegar la infraestructura con Terraform.

Por otro lado se han realizado cambios reseñables en las aplicaciones KDA de Flink y el stream del que leen estas. Cada aplicación lee ahora como consumidores EFO (Enhanced Fan Out), de tal manera que cada una de ellas tiene un ancho de banda dedicado. La razón de este cambio y sus detalles serán explicados más en detalle en el apartado dedicado para Kinesis.

En cuanto a la monitorización y la extracción de métricas en NRT (Near Real Time) se han desplegado unas funciones lambdas que acceden a las tablas apoyándose en Athena gracias a haber registrado los metadatos de estas en el catálogo de Glue. Es importante resaltar que los metadatos de las tablas de Hudi son registrados en Glue por Flink pero en el caso de JSON se despliega un crawler que registra estas tablas en el catálogo. Este crawler se debe ejecutar manualmente para que esta tabla quede registrada en Glue.

Escalado

Kinesis Stream

Dado que el objetivo es someter la aplicación a una carga considerable de eventos por segundo, es necesario explicar cómo cada una de las piezas de la arquitectura pueden escalar de acuerdo al volumen de datos.

Como hemos comentado previamente, se ha optado por un Kinesis Stream On-Demand para automatizar el escalado de los shards durante las pruebas de carga. Es necesario tener en cuenta que estos streams pueden acomodar una tasa de escritura de hasta el 200% de lo especificado por el número de shards en un momento dado.

Una vez que el stream se encuentra por encima del 100%, aumentará automáticamente el número de shards en un plazo de 15 minutos. La única limitación por tanto es no superar el doble del volumen de escritura admitido en menos de dicho periodo.

Por otro lado, dado que se tendrán tres aplicaciones de Flink leyendo del mismo stream, las limitaciones a nivel de lectura serán el mayor problema. Un Kinesis Stream solo admite 5 llamadas GetRecord por shard por segundo. Dado que cada aplicación tiene que leer todo el stream (y por lo tanto, todos los shards), aumentar el número de shards no ayuda a solventar este problema.

La solución pasa por registrar cada una de las aplicaciones como un consumidor Enhanced Fan-Out. Esta funcionalidad de los Kinesis Stream provee a cada uno de estos consumidores con un límite individual de 5 llamadas GetRecord y 2MB por shard por segundo de lectura.

Esta configuración se realiza en el lado del consumidor, en nuestro caso mediante el conector de Kinesis para Flink:

'scan.stream.recordpublisher' = 'EFO',
'scan.stream.efo.registration' = 'EAGER/LAZY',
'scan.stream.efo.consumername' = '{consumer_name}' 

Conviene mencionar que alternativamente, es posible aumentar la latencia de lectura de nuestras aplicaciones de Flink. Por defecto Flink realiza una lectura cada 200 ms por shard, de modo que una aplicación consume completamente la cuota de lectura de un stream. Incrementando este valor a 600ms podríamos acomodar las tres aplicaciones, a costa de una mayor latencia:

scan.shard.getrecords.intervalmillis = '600' 

También se hará uso de la opción Adaptive Reads, que modifica dinámicamente el número de eventos recogidos por llamada en función del tamaño de cada record. Esto permite aprovechar los 2 MB/s por shard disponibles para cada consumidor: 

'scan.shard.adaptivereads' = 'true' 

En lo que respecta al escalado en KPUs (Kinesis Processing Unit) de Flink, se ha optado por no hacer uso del autoescalado, ya que cada proceso de escalado incurren en downtime para la aplicación. Debido a los diferentes requerimientos de cada una de las aplicaciones, las acciones de escalado en momentos inesperados podrían interrumpir las pruebas de carga. Además es interesante medir el rendimiento de escritura de cada una de las aplicaciones en igualdad de capacidad de computación.

Hudi

Timeline

Uno de los sistemas base sobre la que se sustenta el funcionamiento y características de Hudi es la timeline. Hudi guarda un registro temporal de todas las acciones que se han realizado sobre la tabla, así como el estado de esta acción.

Las principales acciones que componen la timeline son

  • Commits – escritura atómica de un conjunto de registros en la tabla en formato columnar
  • Delta Commit – similar al commit, representa una escritura de registros en forma de logs en una tabla Merge on Read
  • Compaction – compactación de las escrituras en logs (delta commits) de una tabla MoR a formato columnar
  • Cleans – borrado de versiones antiguas de archivos
  • Rollback – eliminado de los registros escritos por un commit o delta commit fallido
  • Savepoint – marca un conjunto de archivos como “guardados” para que no sean eliminados por el proceso de limpieza. Permite restaurar la tabla a un punto anterior en la timeline

Cualquiera de estas acciones pueden encontrarse en uno de estos tres estados

  1. Requested – una acción ha sido planeada sin iniciar
  2. Inflight – la acción está en proceso
  3. Completed – denota que la acción ha sido completada


Tipos de tabla

Como se ha dejado entrever en el funcionamiento de la timeline de Hudi, existen dos tipos de escritura soportados: columnar y logs. El formato columnar (parquet) constituye la forma final de una tabla de Hudi, junto con los metadatos de la timeline. Sin embargo, es posible hacer uso de las escrituras en logs (avro) para disminuir la latencia de escritura y eventualmente compactarse a formato columnar sin entorpecer la escritura.

El uso de estos métodos de escritura dan lugar a los dos tipos de tabla que Hudi pone a nuestra disposición

  • Copy on Write – las escrituras se realizan exclusivamente en formato columnar, creando un nuevo fichero con los nuevos registros de la tabla. Los datos están disponibles inmediatamente pero incurre en mayor latencia de escritura
  • Merge on Read – hace uso de la escritura en logs. Los nuevos registros son inicialmente escritos como logs, y posteriormente serán transformados a formato columnar por el proceso de compactación. Obtenemos menor latencia de escritura a costa de latencia de lectura; los nuevos registros no estarán disponibles hasta que se realice la compactación.

Tipos de Query

Para poder aprovechar las características de cada tipo de tabla, existen tres tipos de queries que se pueden realizar sobre una tabla de Hudi

  • Snapshot – obtiene la última versión de la tabla. Para las tablas MoR esto implica incurrir en un proceso de compactación para obtener los últimos registros en formato log. 
  • Read Optimized – para tablas MoR, lee sólamente los registros ya expuestos en formato columnar sin incurrir en latencia de lectura adicional.
  • Incremental – recoge únicamente los nuevos registros desde un cierto commit o compactación, facilitando la creación de pipelines incrementales. No está soportada por Athena

Integración con Glue Catalog


El conector de Hudi permite una integración nativa con el catálogo de Glue en AWS. Basta con añadir las dependencias de Hive en nuestra aplicación de Flink:

com.amazonaws.aws-java-sdk-glue
org.apache.hive.hive-common
org.apache.hive.hive-exec 

Y especificar la configuración del catálogo en el conector de Hudi:

'hive_sync.enable' = 'true',
'hive_sync.db' = '{glue_database}',
'hive_sync.table' = '{table_name}',
'hive_sync.partition_fields' = '{partition_fields}',
'hive_sync.mode' = 'glue',
'hive_sync.use_jdbc' = 'false' 

Con esta integración, la aplicación creará automáticamente las tablas en el catálogo. Como hemos mencionado anteriormente, existen distintos tipos de query para consultar una tabla de Hudi. Se crearán por tanto en el catálogo distintas tablas para soportar las diferentes consultas.

Para una tabla CoW, la tabla se consultará mediante una query Snapshot. Para MoR en cambio se pondrán a disposición dos tablas, para soportar consultas Read Optimized o Snapshot.

La principal aplicación de Glue es de soporte a las lambdas para que al ejecutar las queries mediante Athena su ejecución pueda realizarse de una forma más eficiente, rápida y segura:

  • Glue Catalog: almacenamiento centralizado de la información acerca de la organización, diseño y formato de los datos, utilizado por Athena para realizar directamente las consultas a S3 sin necesidad de tener que apoyarse en terceros para conseguir esta información
  • Automatización del Esquema: Glue rastrea y cataloga automáticamente los datos en S3, detectando y adaptando los cambios en el esquema. Esto evita posibles errores y permite la lectura de los nuevos campos en caso de que se produzcan alteraciones en los esquemas de los eventos

Configuración de Hudi

Es importante entender las configuraciones que nos ofrece Hudi para optimizar nuestra aplicación, en particular para una aplicación en Near Real Time conviene estar al tanto de las opciones disponibles. Aunque la capacidad de configuración es inmensa [1], se intentará sintetizar las que pueden ser más relevantes para una primera toma de contacto con esta tecnología.

Particionado

Apache Hudi ofrece los tipos de particionado que pueden encontrarse en otras soluciones, se detallarán las principales y se justificara la implementada:

  • Simple: particionado basado en un único campo, en este caso el campo escogido es ‘ticker’ ya que se ha identificado que es el que tiene una cardinalidad menor.
  • Particionado Compuesto: particionamiento basado en múltiples campos, podría resultar interesante escoger un campo de baja cardinalidad (ticker) y otro de cardinalidad media (fecha)
  • Particionado Dinámico: elección de la variable en base de los valores, puede resultar interesante cuando la cardinalidad de las variables puede sufrir variaciones y se quiera una actualización del particionamiento de una forma automática y flexible.

Índices

Apache Hudi cuenta con una múltiples  tipos de indexación[2], comentaremos brevemente los más comunes:

  • Bloom Index – Hace uso de un bloom filter sobre la key de los eventos, adicionalmente se puede complementar con un filtrado por rango de de key. Funciona bien cuando tratamos con una tabla donde la mayoría de cambios ocurren en las particiones más recientes o para deduplicado de eventos.
  • Simple: indexación realizada mediante la combinación de FileID y RecordKey. Recomendado cuando las operaciones Upsert no son tan frecuentes debido a la simplicidad que este ofrece.

Ambos tipos de índices pueden ser usados en su forma global

  • Índice global – Imponen la unicidad de las keys en todas las particiones de la tabla, es decir, garantizan que existirá sólamente un registro con una cierta key.
  • Índice no global – La unicidad de la key sólo es exigida a nivel de partición. Si los datos son consistentes y una key sólo va a existir en una partición, este tipo de índices ofrecen un rendimiento mucho mayor y mejor escalado.

En este caso, se ha optado por un Bloom Index, el cual es el que se toma por defecto en caso de que no se declare expresamente:

"hoodie.index.type" = "BLOOM" 

La elección de este tipo de indexación se debe a que los casos de uso que se han planteado requieren de un procesamiento de datos considerablemente alto y eficiente.

Tipos de operación

Apache Hudi ofrece varios tipos de operaciones[3] que permiten a los usuarios administrar y modificar conjuntos de datos de gran tamaño. A continuación se detallan tanto las principales operaciones realizadas en los Stress Tests como en otros escenarios:

  • Upsert – Es la operación por defecto, y ejecutará un insert o un update dependiendo de si el registro ya existe tras una búsqueda en el índice. Con esta operación la tabla no tendrá duplicados para su clave primaria.
  • Insert – Esta operación ignora la búsqueda en el índice a la hora de insertar eventos. Es la más rápida pero la tabla puede contener duplicados. Aún así es útil si se utilizan métodos auxiliares  de deduplicado, o simplemente la existencia de estos es tolerable en el caso de uso.
  • Delete: Hudi ofrece dos métodos de borrado. Soft Delete convierte a nulos los valores del evento a excepción de la key. Hard Delete ejecuta un borrado físico del evento en la tabla.
  • Bulk Insert Operación similar al Insert pero optimizada para la inserción de un gran volumen de datos, a costa de sacrificar ciertas garantías en el control del tamaño de ficheros. Escala bien para cientos de TBs en caso de bootstrap inicial de una tabla de gran tamaño.

Compactación

En el caso de usar una tabla MoR es posible configurar el ritmo de compactación de logs en parquet para buscar el equilibrio entre latencia de escritura y lectura que más convenga al caso de uso. Se pueden especificar una estrategia de tiempo o número de delta commits (o ambos) que ejecutan un proceso de compactación:

compaction.delta_commits
compaction.delta_seconds
compaction.trigger.strategy 

Acciones asíncronas

Ciertas acciones de la timeline como la compactación, limpieza, archivado y clustering pueden ser realizadas asíncronamente por la aplicación, o incluso ser relegadas a procesos auxiliares a la aplicación de escritura. Para el caso de Flink, puede ayudar a mejorar la latencia de escritura y evitar problemas de BackPressure en la aplicación:

compaction.async.enabled
hoodie.clean.async
hoodie.archive.async
hoodie.clustering.async.enabled 

Stress Tests & Insights

Al desplegar las aplicaciones, se ha procedido a realizar distintos tests variando tanto la carga máxima de eventos como la concurrencia y el grado exponencial de crecimiento de los mismos. Esto ha sido posible  gracias a la flexibilidad ofrecida por Locust al estar levantado sobre un cluster de Kubernetes, pudiendo establecer un límite máximo de concurrencia de eventos y un incremental de los mismos. En los tests se ha establecido un límite máximo de 5 a 15K usuarios simultáneos (Peak Concurrency) escalando la frecuencia de los mismos de forma lineal, desde 5 a 20 usuarios más por segundo (Spawn Rate):

Se ha procedido a monitorizar los distintos test para así sacar conclusiones del rendimiento teniendo en cuenta las características específicas de cada uno de los formatos. Las métricas en las que se han apoyado los análisis son tanto las nativas de CloudWatch Metrics (CPU & Memory Utilization, KPUs, LastCheckpoint SIze & Duration,..), como las métricas obtenidas a partir de las Lambdas que periódicamente consultan el número de eventos disponibles en los buckets y realizan cálculos del promedio de la latencia de los mismos.


Número de Eventos

A la hora de analizar el número total de eventos procesados, los cuales son enviados de forma gradual, es decir, a medida que pasa el tiempo cada vez son más los eventos que se envían por segundo, se identifica una tendencia bastante similar aunque destacan JSON y Hudi MoR sobre Hudi CoW en cuanto a la rendimiento. Cabe destacar que JSON muestra un crecimiento más estable y constante en comparación con Hudi MoR y CoW y esto se debe a que estos últimos son capaces de manejar actualizaciones incrementales en los datos.

La similitud entre JSON y Hudi MoR hace que la elección se base completamente en las características del proyecto. En caso de que los datos no sean actualizados JSON puede resultar una solución más interesante debido principalmente a su simplicidad, mientras que si hay una alta frecuencia de actualización de datos históricos, Hudi MoR puede ser una mejor solución. Esto se debe tanto a la mayor eficiencia en las tareas de lectura como por la posibilidad de registrar las distintas versiones de los datos.

 

Latencia

Debido a la dificultad de estandarizar la lógica del cálculo de la latencia entre 3 tipos de almacenamiento distintos, se ha optado por simplificarla calculandolo como la diferencia entre la hora de creación del evento y la del procesamiento en la respectiva aplicación.

Se observa un comportamiento similar entre JSON y Hudi MoR, aunque este primero de una forma más crítica, al tener una latencia inicial muy baja pero a medida que tanto el tiempo de procesamiento como el volumen de carga aumenta, esta latencia se ve negativamente afectada.

La elección entre JSON y Hudi MoR dependerá tanto de la tolerancia de fallo que tenga la aplicación como las propias características de cada uno de los formatos, en caso de que la estructura de los datos sea estable y no cambie con frecuencia,o bien, no dependa de actualizaciones incrementales y pueda lidiar con reescrituras completas, en ese caso JSON puede que sea una mejor opción.

La elección de Hudi CoW sobre MoR puede darse cuando se necesite una alta tolerancia a errores y una alta capacidad de recuperación de eventos de escritura fallidos o corrompidos.`


Uso de CPU

Al analizar el uso de CPU, se ha identificado cierta homogeneidad entre los distintos tests aun trabajando con distintas cargas de trabajo. JSON Y Hudi MoR destacan por tener los niveles de uso de CPU más bajos, ambos por distintos motivos. JSON destaca por la simplicidad al incluir directamente los nuevos datos sin necesidad de tener que lidiar con versionado de datos, mientras que MoR no consume tanta CPU ya que por sus características, el consumo mayor de CPU se hace al realizar consultas de lectura, en las tareas de escritura únicamente identifica los cambios que serán aplicados al consultarlos.

Recordar que las métricas nativas de CloudWatch únicamente nos permiten monitorizar las aplicaciones, que corresponden a las tareas de escritura. La monitorización de las tareas de lectura corresponde a las Lambdas mencionadas anteriormente. 

En este caso MoR es más beneficioso respecto a CoW, dado que el mayor consumo de CPU en MoR se produce al consultar los datos almacenados mientras que en CoW tiene lugar al actualizar los datos.

La elección entre los formatos más eficientes se deben a las necesidades del proyecto, en caso de que se requiera una mayor tolerancia al fallo, versionado de los datos y una mayor eficiencia de lectura, se optara por MoR frente a JSON, entre los dos formatos de Hudi, de nuevo, la elección dependerá de las características del proyecto, en caso de que las consultas requieran transformaciones pesadas y/o complejas se optaría por MoR, si en cambio, el proyecto requiera de una mayor integridad de datos y/o la ingesta de datos sea en batch,  resultaría más interesante CoW debido a que al trabajar con esos volúmenes de datos, el contar con copias de seguridad, en caso de surgir errores, el impacto en término de costes y tiempo de recuperación es menor.

 

Memory Utilization

JSON de nuevo destaca por tener los valores de uso de memoria más bajos aunque para la operativa de transformaciones que se realizan son relativamente altos y más teniendo en cuenta que no tiene que lidiar con la administración de versiones o la combinación de datos. Estos valores se deben a que no tiene capacidades de compresión optimizadas ni manejo eficiente de esquemas.

Respecto a Hudi, se pueden obtener unas conclusiones similares a las del apartado de uso de CPU, MoR tiene una utilización de memoria mayor que JSON debido al procesamiento de logs delta y la administración de versiones y una menor a CoW ya que la consolidación real de los datos no ocurre durante la escritura.


Last Checkpoint Size

Destacar, nuevamente, la estabilidad de JSON frente a las aplicaciones Hudi, ya que no solo muestra en los test realizados un valor inferior a ambos, si no una estabilidad que no se consigue ni con MoR ni CoW, ya que como puede apreciarse, al monitorear el tamaño de los Checkpoints, se percibe una volatilidad considerable.

La volatilidad percibida en las aplicaciones Hudi se debe principalmente a fallos surgidos en Checkpoints lo que conlleva que el Checkpoint posterior al fallido, tenga un volumen mayor. Además de esto, la volatilidad en los tamaños de los Checkpoints puede estar relacionado con las operaciones de optimización y compactación realizadas internamente que puede conllevar la compactación del estado y que esto reduzca considerablemente el tamaño del mismo.

Desafíos en el desarrollo

Read Throughput de Kinesis y EFO

Para no sobrepasar el límite de lectura sobre el Kinesis Stream se ha optado por suscribir los consumidores como Enhanced Fan-Out. En algunas pruebas en conjunto con Autoscaling esto ha dado problemas con el conector de Kinesis de Flink siendo incapaces de cerrar conexiones a la hora de escalar el cluster.


Configuración de Hudi

La configuración de Hudi ha sido otro de los puntos de fricción durante el desarrollo. Bajo cargas elevadas los procesos de compactación y limpieza son más propensos a causar problemas de Backpressure y causar errores en la aplicación. Aunque configurar estos procesos para que ocurran de forma asíncrona puede aliviar este problema, pueden surgir conflictos y desalineación entre procesos bajo cargas elevadas. Un equilibrio entre estas configuraciones y la capacidad del cluster de la aplicación son claves para el buen funcionamiento de la aplicación.

Heterogeneidad de formato

Al hacer un análisis del rendimiento de las 3 aplicaciones, se cuenta con una dificultad adicional debido a la naturaleza de los tipos de formato, teniendo esto tanto un impacto a la hora de plantear la arquitectura como en el planteamiento de las lógicas.
El distinto comportamiento de los formatos en la ingesta, complica el desarrollo de las lógicas a la hora de calcular la latencia. MoR escribe en logs previa compactación, por lo que los datos no están disponibles inmediatamente como ocurre con CoW o JSON.  Esto implica que la métrica común medible para todos los formatos es la de disponibilidad de lectura, la cual no es el principal objetivo de una tabla MoR.  


Sincronización con el Glue Catalog

Una de las grandes ventajas que nos hemos encontrado con Hudi es su capacidad para sincronizarse con el catálogo de Glue, creando las tablas y manteniéndose actualizadas sin necesidad de un crawler. Esto permite una aplicación y arquitectura más limpia que para el caso de JSON, para el cual debe ejecutarse manualmente al desplegar las aplicaciones.

Conclusiones

Los resultados de los tests muestran diferencias considerables entre los formatos JSON, Hudi MoR y CoW en términos de eficiencia, capacidad de respuesta y utilización de recursos. Se procede a analizar cada uno de los aspectos más en detalle:

  • Eficiencia de Procesamiento: JSON y Hudi MoR destacan en la mayoría de las métricas, mostrando un desempeño óptimo en términos de Latencia, CPU & Memory Utilization. Sin embargo, el comportamiento de JSON es más estable y predecible, aunque MoR cuente con ventajas sobre JSON, como por ejemplo, en la gestión de actualizaciones incrementales.
  • Resiliencia y Tolerancia a Fallos: la tolerancia a fallos es un factor muy importante en la decisión sobre la elección entre Hudi y JSON. En el caso de  MoR y CoW, dependerá del grado de criticidad, ya que a nivel general el rendimiento en tareas de escritura para MoR es superior.
  • Uso de Recursos: JSON se muestra como el más ligero, con baja utilización de CPU y memoria, debido a su simplicidad inherente. Mientras que Hudi MoR y CoW, por la naturaleza de su diseño y gestión de datos, requieren más recursos, especialmente en operaciones que involucran el manejo de versiones y la compactación de datos.

Para finalizar, resulta interesante identificar en quéque casos de uso o proyectos puede resultar más recomendable cada uno de los formatos en función de las características de los mismos y las red flags que puedan establecerse:

  • JSON: Recomendado para aplicaciones con estructuras de datos estables que no requieren actualizaciones incrementales y donde la simplicidad y la estabilidad son clave.
  • Hudi MoR: Adecuado para proyectos que requieren una gestión eficiente de actualizaciones incrementales y donde la latencia y la eficiencia en la escritura son cruciales.
  • Hudi CoW: Ideal para contextos donde la integridad de los datos es esencial, y se necesita una robusta recuperación de errores, especialmente en escenarios de ingestas en batch. 

Referencias

[1] Configuraciones Tablas Hudi. [link]

[2] Tipos de Indexacion Hudi. [link]

[3] Tipos de Operaciones Hudi. [link]

Autores

Alberto Jaen

AWS Cloud Engineer

Empecé mi carrera laboral con el desarrollo, mantenimiento y administración de bases de datos multidimensionales y Data Lakes. A partir de ahí comencé a estar interesado en plataformas de datos y arquitecturas cloud, estando certificado 3 veces en AWS y 2 con Hashicorp.

Actualmente me encuentro trabajando como un Cloud Engineer desarrollando Data Lakes y DataWarehouses con AWS para un cliente relacionado con la organización de eventos deportivos a nivel mundial.

Alfonso Jerez

AWS Cloud Engineer

Apasionado de los datos y las nuevas tecnologías, especializado como AWS Cloud Engineer en la optimización de DataWarehouses y procesos de ingesta y transformación de Data Lakes. Motivado por la mejora continua y automatización de la integración de servicios.

Colaborando activamente con el grupo de Práctica Cloud en investigaciones y desarrollo de blogs de tecnologías punteras e innovadoras tales como esta, fomentando así el continuo aprendizaje.

Adrián Jiménez

AWS Cloud Engineer

Dedicado al aprendizaje constante de nuevas tecnologías y su aplicación, disfrutando de utilizarlas en la resolución de desafíos tecnológicos. Desarrollo mi carrera como Cloud Engineer diseñando, implementando y manteniendo infraestructura en AWS.

Colaboro activamente en la Práctica Cloud, donde investigamos y experimentamos con nuevas tecnologías, buscando soluciones para los retos que enfrentan nuestros clientes.

Navegación

¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

Snowflake, el Time Travel sin DeLorean para unos datos Fail-Safe.

febrero 23, 2023
LEER MÁS

Empoderando a las decisiones en diversos sectores con árboles de decisión en AWS

junio 4, 2024
LEER MÁS

¿Qué está pasando en el mundo de la AI?

marzo 6, 2023
LEER MÁS

Oscar Hernández, nuevo CEO de Bluetab LATAM

mayo 16, 2024
LEER MÁS

LakeHouse Streaming en AWS con Apache Flink y Hudi

abril 11, 2023
LEER MÁS

Algunas de las capacidades de Matillion ETL en Google Cloud

julio 11, 2022
LEER MÁS

Publicado en: Practices, Tech

Starburst: Construyendo un futuro basado en datos.

mayo 25, 2023 by Bluetab

Starburst: Construyendo un futuro basado en datos.

Lucas Calvo

Cloud Engineer

Introducción

En este nuevo artículo vamos a hablar de uno de nuestros partners: Starburst[1]. Starburst es la versión empresarial de Trino[2] realizando nuevas integraciones, mejoras de rendimiento, una capa de seguridad y restando complejidad a la gestión con una interfaz de usuario muy fácil de usar y que te permite realizar distintas configuraciones.

Para los que no conocéis Trino, es un motor de consulta SQL distribuido open-source creado en 2012 por Facebook bajo el nombre Presto. Está diseñado para consultar grandes conjuntos de datos distribuidos en una o más fuentes de datos heterogéneas. Esto significa que podemos consultar datos que residen en diferentes sistemas de almacenamiento como HDFS, AWS S3, Google Cloud Storage o Azure Blob Storage. Trino también tiene la capacidad de federar diferentes fuentes de datos como MySQL, PostgreSQL, Cassandra, Kafka.

Con las nuevas necesidades que van saliendo de arquitecturas orientadas al Data Mesh[3], plataformas analíticas como Starburst son cada vez más importantes y nos permiten centralizar y federar distintas fuentes de datos para así tener solo un punto de entrada a nuestra información. Con esta mentalidad, podemos hacer que nuestros usuarios accedan a la plataforma de Starburst con distintos roles y distinta granularidad de acceso para que puedan consultar los distintos dominios que poseen las empresas. Además Starburst no solo se queda en la consulta de datos, sino que nos permite conectarnos con herramientas analíticas como puedes ser DBT[4] o Jupyter Notebook[5] o herramientas de reporting como Power BI[6] para sacarle más rendimiento a todos nuestros datos. Pero Starburst no solo se queda en eso, sino que nos puede ayudar en la migraciones de datos hacia el Cloud, ya que fácilmente podemos conectarnos a las fuentes de datos y sacar toda la información para volcarlas en cualquier almacenamiento del Cloud.

Como podéis observar, Starburst es capaz de analizar todos sus datos, dentro y alrededor de tu Data Lake, y se conecta a todo un ecosistema de herramientas. Por eso vamos a realizar una serie de artículos para tratar los puntos más relevantes como son el despliegue y configuración de la plataforma, integración con otras herramientas y gobierno y administración de usuarios. En este primer artículo, nos vamos a centrar en el despliegue de Starburst en Kubernetes, así como la configuración que se tiene que realizar para conectar con los distintos componentes de GCP. Además hemos añadido una capa de monitorización con Prometheus[7] y Grafana[8], donde hemos publicado un dashboard con distintas métricas importantes por si cualquier compañía quiere centralizar las métricas en Grafana. Para todo ello, nos vamos a apoyar de un repositorio que hemos creado con el levantamiento de la infraestructura y la instalación de Starburst.

¿Qué necesitas para entender este artículo?

  • Algunos conceptos sobre Terraform[9].
  • Algunos conceptos de Kubernetes.
  • Algunos conceptos de Helm.
  • Algunos conceptos de Prometheus.
  • Algunos conceptos de Grafana.
  • Un cuenta en GCP.
  • Una licencia de Starburst

Arquitectura

Como se puede observar en el diagrama, estos son los componentes que se van a desplegar para la configuración de Starburst. Como pieza central del despliegue, utilizaremos Google Kubernetes Engine. Este es el servicio administrado de orquestación de contenedores de Google. Utilizaremos Kubernetes ya que nos facilitará la gestión de Starburst y aprovecharemos las ventajas del autoscaling de Kubernetes para ampliar el número de workers de Starburst y escalar en más nodos para poder así tener más recursos de computación si tenemos algún pico de trabajo o de usuarios.

Como configuración inicial de nuestro cluster de GKE, comenzaremos con un único nodepool para facilitar el despliegue. Un nodepool es una agrupación de nodos dentro de un cluster con la misma configuración y especificaciones de tipo de máquina. En nuestro caso, nuestro nodepool se llamará `default-node-pool` y el tipo de instancia utilizada será `e2-standard-16`, que es la recomendada por Starburst, ya que el tipo de carga de trabajo necesita nodos con bastante memoria. Además de la instalación de Starburst, también desplegaremos en el cluster tanto Prometheus como Grafana.

Como hemos explicado anteriormente, Starburst está basado en Trino, que es un motor de consulta distribuido. Los principales componentes de Trino son el Coordinator y los Workers. El Coordinator de Trino es el componente responsable de analizar las sentencias, planificar las consultas y gestionar los nodos Workers de Trino. El Coordinator realiza un seguimiento de la actividad de cada Worker y orquesta la ejecución de una consulta. Los Workers son el componente responsable de ejecutar tareas y procesar datos. Los nodos Workers obtienen datos de los conectores e intercambian datos intermedios entre sí. El Coordinator es responsable de obtener los resultados de los Workers y devolver los resultados finales al cliente.

Como componentes transversales de nuestra arquitectura, también desplegaremos una red con una subnet para realizar el despliegue de nuestro cluster de GKE, así como un bucket en Cloud Storage para realizar pruebas de escritura de datos desde Starburst.

Además, como componente fuera de la arquitectura, tendremos jmeter[10], la herramienta con la que realizaremos pruebas de performance para probar la elasticidad de Starburst y poder probar el autoescalado de nuestro cluster.

Despliegue de la infraestructura

Una vez explicada la arquitectura vamos a proceder a realizar el despliegue de todos los componentes. Para ello, nos vamos a ayudar de Terraform como herramienta de IaC. Como partes importantes de este despliegue, tendremos la parte más de infraestructura tradicional que son las VPC, el cluster de GKE y la parte de Cloud Storage como hemos hablado antes, además de los componentes que desplegamos en Kubernetes de una forma totalmente automatizada que son Grafana y Prometheus.

Vamos a empezar con la explicación de la infraestructura más clásica. Para este despliegue haremos uso de dos módulos que están subidos al github:

  • Módulo de GKE[11].
  • Módulo de VPC[12].

Estos dos módulos están invocados en el `main.tf` del repositorio y hacen uso del provider de Google para el despliegue:


```tf
provider "google" {
  project = var.project_id
  region  = var.region
}

provider "google-beta" {
  project = var.project_id
  region  = var.region
}


module "network" {
  source = "git@github.com:lucasberlang/gcp-network.git?ref=v1.0.0"

  project_id         = var.project_id
  description        = var.description
  enable_nat_gateway = true
  offset             = 1

  intra_subnets = [
    {
      subnet_name           = "private-subnet01"
      subnet_ip_cidr        = "10.0.0.0/24"
      subnet_private_access = false
      subnet_region         = var.region
    }
  ]

  secondary_ranges = {
    private-subnet01 = [
      {
        range_name    = "private-subnet01-01"
        ip_cidr_range = var.ip_range_pods
      },
      {
        range_name    = "private-subnet01-02"
        ip_cidr_range = var.ip_range_services
      },
    ]
  }

  labels = var.labels
}

resource "google_storage_bucket" "gcs_starburst" {
  name          = var.name
  location      = "EU"
  force_destroy = var.force_destroy
}

module "gke-starburst" {
  source = "git@github.com:lucasberlang/gcp-gke.git?ref=v1.1.0"

  project_id              = var.project_id
  name                    = "starburst"
  regional                = true
  region                  = var.region
  network                 = module.network.network_name
  subnetwork              = "go-euw1-bt-stb-private-subnet01-dev"
  ip_range_pods           = "private-subnet01-01"
  ip_range_services       = "private-subnet01-02"
  enable_private_endpoint = false
  enable_private_nodes    = false
  master_ipv4_cidr_block  = "172.16.0.0/28"
  workload_identity       = false
  kubernetes_version      = var.kubernetes_version
  
  gce_persistent_disk_csi_driver = true

  master_authorized_networks = [
    {
      cidr_block   = module.network.intra_subnet_ips.0
      display_name = "VPC"
    },
    {
      cidr_block   = "0.0.0.0/0"
      display_name = "shell"
    }
  ]

  cluster_autoscaling = {
    enabled             = true,
    autoscaling_profile = "BALANCED",
    max_cpu_cores       = 300,
    max_memory_gb       = 940,
    min_cpu_cores       = 24,
    min_memory_gb       = 90,
  }


  node_pools = [
    {
      name         = "default-node-pool"
      machine_type = "e2-standard-16"
      auto_repair  = false
      auto_upgrade = false
    },
  ]
  
  node_labels = {
    "starburstpool" = "default-node-pool"
  }

  istio     = var.istio
  dns_cache = var.dns_cache
  labels    = var.labels
}
```
 

Lo único importante a tener en cuenta, es que vamos a desplegar una red con una única subred y que el cluster de GKE está habilitado con el autoescalado para poder incrementar el número de nodos cuando haya una carga de trabajo. Asimismo, es importante tener en cuenta que se ha añadido una etiqueta a todos los nodos que es `»starburstpool» = «default-node-pool»` para aislar el propio despliegue de Starburst del que más tarde haremos uso. Aparte de estos componentes también desplegamos una Cloud Storage para luego configurar el conector de Hive.

Por otra parte, como hemos comentado, también haremos el despliegue de Grafana y Prometheus. Para ello haremos uso del provider de Helm y de Kubernetes de Terraform. 

El despliegue de estos componentes lo tenemos en el archivo `helm.tf`:

```tf
resource "kubernetes_namespace" "prometheus" {
  metadata {
    name = "prometheus"
  }
}

resource "kubernetes_namespace" "grafana" {
  metadata {
    name = "grafana"
  }
}

resource "helm_release" "grafana" {
  chart      = "grafana"
  name       = "grafana"
  namespace  = kubernetes_namespace.grafana.metadata.0.name
  repository = "https://grafana.github.io/helm-charts"

  values = [
    file("templates/grafana.yaml")
  ]
}

resource "kubernetes_secret" "grafana-secrets" {
  metadata {
    name      = "grafana-credentials"
    namespace = kubernetes_namespace.grafana.metadata.0.name
  }
  data = {
    adminUser     = "admin"
    adminPassword = "admin"
  }
}

resource "helm_release" "prometheus" {
  chart      = "prometheus"
  name       = "prometheus"
  namespace  = kubernetes_namespace.prometheus.metadata.0.name
  repository = "https://prometheus-community.github.io/helm-charts"

  values = [
    file("templates/prometheus.yaml")
  ]
}
```
 

Hay varias cosas que tenemos que tener en cuenta, estas son las configuraciones que hemos añadido en los values de cada chart. 

Primero vamos con los valores de Prometheus que hemos configurado. Hemos añadido una configuración extra para que recoja las métricas de Starburst una vez que se levante. Esto lo hemos hecho en la siguiente parte de la configuración:

```yaml
extraScrapeConfigs: |
  - job_name: starburst-monitor
    scrape_interval: 5s
    static_configs:
      - targets: 
        - 'prometheus-coordinator-starburst-enterprise.default.svc.cluster.local:8081'
        - 'prometheus-worker-starburst-enterprise.default.svc.cluster.local:8081'
    metrics_path: /metrics
    scheme: http
```
 

Lo único a tener en cuenta son los targets que hemos añadido, que básicamente son los servicios tanto del Coordinator como de los Workers de Starburst para que recoja todas las métricas.

En la parte de Grafana hemos añadido tanto la configuración de Prometheus, como un dashboard que hemos creado custom para Starburst. 

La configuración que hemos añadida es la siguiente:

```yaml
datasources:
 datasources.yaml:
   apiVersion: 1
   datasources:
   - name: Prometheus
     type: prometheus
     url: http://prometheus-server.prometheus.svc.cluster.local
     isDefault: true


dashboards:
  default:
    Starburst-cluster:
      gnetId: 18767
      revision: 1
      datasource: Prometheus
```
 

En la carpeta infra del repositorio de Github, podrás encontrar todo el código necesario para realizar dicho despliegue.

Instalación y configuración de Starburst

Una vez que tengamos toda la infraestructura levantada, vamos a proceder a desplegar Starburst en nuestro cluster de GKE. Para ello, vamos a desplegar estos componentes:

  • Postgres Database on Kubernetes
  • Hive Metastore Service
  • Starburst Enterprise

El servicio de Hive Mestastore es necesario para configurar el conector de Hive para así poder acceder o escribir a los datos que se guardan en Google Cloud Storage. Como backend de nuestro servicio de Metastore, vamos a desplegar un base de datos PostgreSQL, para así poder guardar toda la información de la metadata en esta base de datos. Además tendremos que configurar el servicio de Hive para pasarle las credenciales de Google Cloud y que tenga permisos para poder leer y escribir de GCS. Por lo tanto, vamos a proceder primero a declarar algunas variables de entorno que necesitaremos para descargar los charts del repositorio privado de Starburst y algunas variables de configuración más que necesitaremos para realizar el despliegue.

Esta serían las variables que vamos a necesitar en nuestro despliegue:

```bash
export admin_usr=     # Choose an admin user name you will use to login to Starburst & Ranger. Do NOT use 'admin'
export admin_pwd=     # Choose an admin password you will use to login to Starburst & Ranger. MUST be a minimum of 8 characters and contain at least one uppercase, lowercase and numeric value.

export registry_pwd= #Credentials harbor registry
export registry_usr= #Credentials harbor registry
export starburst_license=starburstdata.license #License Starburst
# Zone where the cluster will be deployed. e.g. us-east4-b
export zone="europe-west1"
# Google Cloud Project ID where the cluster is being deployed
export google_cloud_project=
# Google Service account name. The service account is used to access services like GCS and BigQuery, so you should ensure that it has the relevant permissions for these
# Give your cluster a name
export cluster_name=

# These next values are automatically set based on your input values
# We'll automatically get the domain for the zone you are selecting. Comment this out if you don't need DNS
#export google_cloud_dns_zone_name=$(gcloud dns managed-zones describe ${google_cloud_dns_zone:?Zone not set} --project ${google_cloud_project_dns:?Project ID not set} | grep dnsName | awk '{ print $2 }' | sed 's/.$//g')

# This is the public URL to access Starburst
export starburst_url=${cluster_name:?Cluster Name not set}-starburst.${google_cloud_dns_zone_name}
# This is the public URL to access Ranger
export ranger_url=${cluster_name:?Cluster Name not set}-ranger.${google_cloud_dns_zone_name}

# Insights DB details
# These are the defaults if you choose to deploy your postgresDB to the K8s cluster
# You can adjust these to connect to an external DB, but be advised that the nodes in the K8s cluster must have access to the URL
export database_connection_url=jdbc:postgresql://postgresql:5432/insights
export database_username=
export database_password=

# Data Products. Leave the password unset as below, if you are connecting directly to the coordinator on port 8080
export data_products_enabled=true
export data_products_jdbc_url=jdbc:trino://coordinator:8080
export data_products_username=${admin_usr}
export data_products_password=

# Starburst Access Control
export starburst_access_control_enabled=true
export starburst_access_control_authorized_users=${admin_usr}

# These last remaining values are static
export xtra_args_hive="--set objectStorage.gs.cloudKeyFileSecret=service-account-key"
export xtra_args_starburst="--values starburst.catalog.yaml"
export xtra_args_ranger=""
```
 

Una vez definidas nuestras variables de entorno procederemos a crearnos un secreto de Kubernetes para configurar las credenciales con las que Hive se va a conectar a GCS.

```bash
kubectl create secret generic service-account-key --from-file key.json
```
 

Para ello, como paso previo, nos hemos creado una service account con permisos en Cloud Storage y en Bigquery y nos hemos descargado las credenciales de esa service account. También como paso previo, añadiremos los repositorio de Helm con el siguiente comando:

```bash
helm repo add --username ${registry_usr} --password ${registry_pwd} starburstdata https://harbor.starburstdata.net/chartrepo/starburstdata
helm repo add bitnami https://charts.bitnami.com/bitnami
```
 

Una vez que tenemos la configuración previa hecha, vamos a proceder a desplegar el servicio de PostgreSQL primero, y posteriormente, el Hive Metastore. Para ello haremos uso de Helm. Para el despliegue de PostgreSQL usaremos el siguiente comando:

```bash
helm upgrade postgres bitnami/postgresql --install --values postgres.yaml \
    --version 12.1.6 \
    --set primary.nodeSelector.starburstpool=default-node-pool \
    --set readReplicas.nodeSelector.starburstpool=default-node-pool
```
 

Hay varios factores a tener en cuenta en el comando anterior. El primero es que el despliegue de PostgreSQL lo haremos en los nodos que tengan el tag `starburstpool=default-node-pool`, que es nuestro worker pool por defecto. Usaremos la versión 12.1.6 de PostgreSQL y la configuración que hemos añadido en postgres es la siguiente:

```yaml
fullnameOverride: postgresql

global:
  postgresql:
    auth:
      database: postgres
      username: postgres
      postgresPassword: ****
  storageClass: "standard"
primary:
  initdb:
    scripts:
      init.sql: |
        create database hive;
        create database ranger;
        create database insights;
        create database datacache;

service:
  type: ClusterIP
``` 

Esta información se encuentra en el archivo `postgres.yaml` y nos configurará el usuario y contraseña de PostgreSQL, y nos creará 4 bases de datos que usa internamente Starburst como backend. En nuestro caso, como podéis observar, hemos configurado el servicio de backend en el mismo cluster que la configuración de Starburst, pero esto se puede configurar fuera del cluster de Kubernetes para entornos productivos. Básicamente podríamos tener un servicio gestionado como es Cloud Sql para así evitar problemas en producción.

Ahora vamos a proceder con el despliegue del servicio de Hive Metastore, esto lo haremos con el siguiente comando:

```bash
helm upgrade hive starburstdata/starburst-hive --install --values hive.yaml \
    --set registryCredentials.username=${registry_usr:?Value not set} \
    --set registryCredentials.password=${registry_pwd:?Value not set} \
    --set nodeSelector.starburstpool=default-node-pool  \
    --set objectStorage.gs.cloudKeyFileSecret=service-account-key
``` 

Aquí tenemos que tener en cuenta varias cosas importantes, la primera es que como en el servicio de PostgreSQL el despliegue se va a realizar en los nodos con el tag `starburstpool=default-node-pool`. El segundo punto importante es que hemos realizado la configuración de las credenciales de Google para que funcione el conector de hive, esto lo hemos realizado con el siguiente comando:

`--set objectStorage.gs.cloudKeyFileSecret=service-account-key` 

Con esta acción,  montamos el fichero de credenciales como un archivo en el despliegue de Hive para que tenga visibilidad en las credenciales. Los valores extras que hemos añadido a la configuración de hive se encuentran en el archivo `hive.yaml` y son los siguientes:

```yaml
database:
  external:
    driver: org.postgresql.Driver
    jdbcUrl: jdbc:postgresql://postgresql:5432/hive
    user: #user postgres
    password: #password postgres
  type: external

expose:
  type: clusterIp

image:
  repository: harbor.starburstdata.net/starburstdata/hive

registryCredentials:
  enabled: true
  registry: harbor.starburstdata.net/starburstdata
```
 

Una vez que tenemos desplegado tanto el servicio de Postgres como el de Hive Metastore, podemos proceder a desplegar Starburst. Primero necesitaremos realizar una serie de pasos previos. El primero será crearnos un secreto de Kubernetes con la licencia de Starburst, el segundo será crearnos un secreto con las variables de entornos que hemos definido antes, esto lo haremos con un pequeño script para quitar complejidad y que nos coja las variables que ya hemos definido. 

Con el siguiente comando procederemos a realizar los pasos anteriores:

```bash
kubectl create secret generic starburst --from-file ${starburst_license}
chmod 755 load_secrets.sh && . ./load_secrets.sh
kubectl apply -f secrets.yaml
```
 

Una vez que tenemos las configuraciones previas vamos a proceder a desplegar Starburst con el siguiente comando:

```bash
helm upgrade starburst-enterprise starburstdata/starburst-enterprise --install --values starburst.yaml \
    --set sharedSecret="$(openssl rand 64 | base64)" \
    --set coordinator.resources.requests.memory=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.memory}' | awk -F "Ki" '{ print $1 }')*10/100 ))Ki) \
    --set coordinator.resources.requests.cpu=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.cpu}' | awk -F "m" '{ print $1 }')*10/100 ))m) \
    --set coordinator.resources.limits.memory=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.memory}' | awk -F "Ki" '{ print $1 }')*10/100 ))Ki) \
    --set coordinator.resources.limits.cpu=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.cpu}' | awk -F "m" '{ print $1 }')*10/100 ))m) \
    --set worker.resources.requests.memory=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.memory}' | awk -F "Ki" '{ print $1 }') - 10500000 ))Ki) \
    --set worker.resources.requests.cpu=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.cpu}' | awk -F "m" '{ print $1 }') - 3500 ))m) \
    --set worker.resources.limits.memory=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.memory}' | awk -F "Ki" '{ print $1 }') - 10500000 ))Ki) \
    --set worker.resources.limits.cpu=$(echo $(( $(kubectl get nodes --selector='starburstpool=default-node-pool' -o jsonpath='{.items[0].status.allocatable.cpu}' | awk -F "m" '{ print $1 }') - 3500 ))m) \
    --set coordinator.nodeSelector.starburstpool=default-node-pool 
```
 

Aquí como podéis observar, hay varias cosas a tener en cuenta. La primera es que todos los componentes de Starburst que se despliegan lo hacen en los nodos con el tag `starburstpool=default-node-pool`. Esto simplemente lo hemos hecho para quitar complejidad a la demo.  En entornos productivos, una buena práctica sería tener un nodepool para el Coordinator y otro nodepool para los Workers de Starburst.

Otra cosa a tener en cuenta es la configuración de la memoria y cpu que se hace tanto en los Workers como en el Coordinator. Como buenas prácticas, Starburst recomienda que haya un pod worker por cada nodo que se despliega en nuestro cluster de Kubernetes. Para ello lo que hemos hecho es ajustar la memoria y cpu de nuestros pods al tamaño de máquina que tenemos. Por último están los valores de configuración que hemos utilizado en el despliegue de Starburst, estos se pueden encontrar en el archivo `starburst.yaml` y son los siguientes:

```yaml
catalogs:
  hive: |
    connector.name=hive
    hive.security=starburst
    hive.metastore.uri=thrift://hive:9083
    hive.gcs.json-key-file-path=/gcs-keyfile/key.json
    hive.gcs.use-access-token=false
  postgres: |
    connector.name=postgresql
    connection-url=jdbc:postgresql://postgresql:5432/insights
    connection-user=******
    connection-password=******
  bigquery: |
      connector.name=bigquery
      bigquery.project-id=******
      bigquery.credentials-file=/gcs-keyfile/key.json
prometheus:
  enabled: true
  agent:
    version: "0.16.1"
    port: 8081
    config: "/etc/starburst/telemetry/prometheus.yaml"
  rules:
    - pattern: trino.execution<name=QueryManager><>(running_queries|queued_queries)
      name: $1
      attrNameSnakeCase: true
      type: GAUGE
    - pattern: 'trino.execution<name=QueryManager><>FailedQueries\.TotalCount'
      name: 'starburst_failed_queries'
      type: COUNTER
    - pattern: 'trino.execution<name=QueryManager><>(running_queries)'
      name: 'starburst_running_queries'
    - pattern: 'trino.execution<name=QueryManager><>StartedQueries\.FiveMinute\.Count'
      name: 'starburst_started_queries'
    - pattern: 'trino.execution<name=SqlTaskManager><>InputPositions\.FiveMinute\.Count'
      name: 'starburst_input_rows'
    - pattern: 'trino.execution<name=SqlTaskManager><>InputDataSize\.FiveMinute\.Count'
      name: 'starburst_input_data_bytes'
    - pattern: 'trino.execution<name=QueryManager><>UserErrorFailures\.FiveMinute\.Count'
      name: 'starburst_failed_queries_user'
    - pattern: 'trino.execution<name=QueryManager><>ExecutionTime\.FiveMinutes\.P50'
      name: 'starburst_latency_p50'
    - pattern: 'trino.execution<name=QueryManager><>WallInputBytesRate\.FiveMinutes\.P90'
      name: 'starburst_latency_p90'
    - pattern: 'trino.failuredetector<name=HeartbeatFailureDetector><>ActiveCount'
      name: 'starburst_active_node'
    - pattern: 'trino.memory<type=ClusterMemoryPool, name=general><>FreeDistributedBytes'
      name: 'starburst_free_memory_pool'
    - pattern: 'trino.memory<name=ClusterMemoryManager><>QueriesKilledDueToOutOfMemory'
      name: 'starburst_queries_killed_due_to_out_of_memory'
    - pattern: 'java.lang<type=Memory><HeapMemoryUsage>committed'
      name: 'starburst_heap_size_usage'
    - pattern: 'java.lang<type=Threading><>ThreadCount'
      name: 'starburst_thread_count'
coordinator:
  envFrom:
  - secretRef:
      name: environment-vars
  additionalProperties: |
    starburst.data-product.enabled=${ENV:data_products_enabled}
    data-product.starburst-jdbc-url=${ENV:data_products_jdbc_url}
    data-product.starburst-user=${ENV:data_products_username}
    data-product.starburst-password=
    query.max-memory=1PB
    starburst.access-control.enabled=${ENV:starburst_access_control_enabled}
    starburst.access-control.authorized-users=${ENV:starburst_access_control_authorized_users}
  etcFiles:
    properties:
      config.properties: |
        coordinator=true
        node-scheduler.include-coordinator=false
        http-server.http.port=8080
        discovery-server.enabled=true
        discovery.uri=http://localhost:8080
        usage-metrics.cluster-usage-resource.enabled=true
        http-server.authentication.allow-insecure-over-http=true
        web-ui.enabled=true
        http-server.process-forwarded=true
        insights.persistence-enabled=true
        insights.metrics-persistence-enabled=true
        insights.jdbc.url=${ENV:database_connection_url}
        insights.jdbc.user=${ENV:database_username}
        insights.jdbc.password=${ENV:database_password}
      password-authenticator.properties: |
        password-authenticator.name=file
  nodeSelector:
    starburstpool: default-node-pool
  resources:
    limits:
      cpu: 2
      memory: 12Gi
    requests:
      cpu: 2
      memory: 12Gi

expose:
  type: clusterIp
  ingress:
    serviceName: starburst
    servicePort: 8080
    host: 
    path: "/"
    pathType: Prefix
    tls:
      enabled: true
      secretName: tls-secret-starburst
    annotations:
      kubernetes.io/ingress.class: nginx
      cert-manager.io/cluster-issuer: letsencrypt

registryCredentials:
  enabled: true
  password: ******
  registry: harbor.starburstdata.net/starburstdata
  username: ******

starburstPlatformLicense: starburst

userDatabase:
  enabled: true
  users:
  - password: ******
    username: ******

worker:
  envFrom:
  - secretRef:
      name: environment-vars
  autoscaling:
    enabled: true
    maxReplicas: 10
    minReplicas: 3
    targetCPUUtilizationPercentage: 40
  deploymentTerminationGracePeriodSeconds: 30
  nodeSelector:
    starburstpool: default-node-pool
  resources:
    limits:
      cpu: 8
      memory: 40Gi
    requests:
      cpu: 8
      memory: 40Gi
  starburstWorkerShutdownGracePeriodSeconds: 120
  tolerations:
    - key: "kubernetes.azure.com/scalesetpriority"
      operator: "Exists"
      effect: "NoSchedule"

additionalVolumes:
  - path: /gcs-keyfile/key.json
    subPath: key.json
    volume:
      configMap:
        name: "sa-key"
```
 

En esta configuración hay varios valores a tener en cuenta, como son catalogs, prometheus, worker y additionalVolumes.

Vamos a empezar explicando la parte de catalogs. Para los que no lo sepan, un catálogo en Starburst es la configuración que permite acceder a unas fuentes de datos determinadas. Cada clúster de Starburst puede tener configurados múltiples catálogos y, por tanto, permitir el acceso a diversas fuentes de datos. En nuestro caso hemos definido el catálogo de Hive, PostgreSQL y Bigquery para poder acceder a dichas fuentes de datos:

```yaml
catalogs:
  hive: |
    connector.name=hive
    hive.security=starburst
    hive.metastore.uri=thrift://hive:9083
    hive.gcs.json-key-file-path=/gcs-keyfile/key.json
    hive.gcs.use-access-token=false
  postgres: |
    connector.name=postgresql
    connection-url=jdbc:postgresql://postgresql:5432/insights
    connection-user=******
    connection-password=******
  bigquery: |
      connector.name=bigquery
      bigquery.project-id=******
      bigquery.credentials-file=/gcs-keyfile/key.json
```
 

La segunda configuración a tener en cuenta es la de Prometheus, esto lo realizamos para exponer ciertas métricas a Prometheus y poder sacar información relevante en un dashboard de Grafana. Para ello tenemos la siguiente configuración:

```yaml
prometheus:
  enabled: true
  agent:
    version: "0.16.1"
    port: 8081
    config: "/etc/starburst/telemetry/prometheus.yaml"
  rules:
    - pattern: trino.execution<name=QueryManager><>(running_queries|queued_queries)
      name: $1
      attrNameSnakeCase: true
      type: GAUGE
    - pattern: 'trino.execution<name=QueryManager><>FailedQueries\.TotalCount'
      name: 'starburst_failed_queries'
      type: COUNTER
    - pattern: 'trino.execution<name=QueryManager><>(running_queries)'
      name: 'starburst_running_queries'
    - pattern: 'trino.execution<name=QueryManager><>StartedQueries\.FiveMinute\.Count'
      name: 'starburst_started_queries'
    - pattern: 'trino.execution<name=SqlTaskManager><>InputPositions\.FiveMinute\.Count'
      name: 'starburst_input_rows'
    - pattern: 'trino.execution<name=SqlTaskManager><>InputDataSize\.FiveMinute\.Count'
      name: 'starburst_input_data_bytes'
    - pattern: 'trino.execution<name=QueryManager><>UserErrorFailures\.FiveMinute\.Count'
      name: 'starburst_failed_queries_user'
    - pattern: 'trino.execution<name=QueryManager><>ExecutionTime\.FiveMinutes\.P50'
      name: 'starburst_latency_p50'
    - pattern: 'trino.execution<name=QueryManager><>WallInputBytesRate\.FiveMinutes\.P90'
      name: 'starburst_latency_p90'
    - pattern: 'trino.failuredetector<name=HeartbeatFailureDetector><>ActiveCount'
      name: 'starburst_active_node'
    - pattern: 'trino.memory<type=ClusterMemoryPool, name=general><>FreeDistributedBytes'
      name: 'starburst_free_memory_pool'
    - pattern: 'trino.memory<name=ClusterMemoryManager><>QueriesKilledDueToOutOfMemory'
      name: 'starburst_queries_killed_due_to_out_of_memory'
    - pattern: 'java.lang<type=Memory><HeapMemoryUsage>committed'
      name: 'starburst_heap_size_usage'
    - pattern: 'java.lang<type=Threading><>ThreadCount'
      name: 'starburst_thread_count'
```
 

En la configuración de los workers, vamos a activar el autoescalado de estos pods. Para ello vamos a realizar una configuración para que haya un mínimo de 3 pods workers que se traducirán en 3 nodos en nuestro cluster de GKE y un máximo de 10 pods. Para el autoescalado vamos a usar la métrica de consumo de CPU. 

Los valores son los siguientes:

```yaml
worker:
  envFrom:
  - secretRef:
      name: environment-vars
  autoscaling:
    enabled: true
    maxReplicas: 10
    minReplicas: 3
    targetCPUUtilizationPercentage: 40
```
 

Por último, añadiremos un volumen adicional a nuestro despliegue para poder montar las credenciales de Google cloud tanto en el coordinator como en los workers.

Esto lo haremos de la siguiente forma:

```yaml
additionalVolumes:
  - path: /gcs-keyfile/key.json
    subPath: key.json
    volume:
      configMap:
        name: "sa-key"
```
 

Con todos estos pasos, tendríamos nuestro cluster de Starburst ya operativo.

Consultas en GCP y autoescalado de Starburst

Una vez realizado el levantamiento del cluster de Starburst, vamos a realizar algunas consultas para probar su rendimiento y funcionamiento. Para ello vamos a realizar consultas de lectura en el esquema de TPCH[13] y después vamos a escribir la salida de estas consultas en el bucket de Google que hemos creado en los pasos de despliegue. 

Las consultas que vamos a ejecutar se encuentran en la carpeta de queries en los archivos `tpch.sql` y `gcs_storage.sql`.

Para lanzar las consultas será tan sencillo como irnos al apartado de consultas de la interfaz web y ejecutar las primeras consultas del archivo `tpch.sql`:

```sql
 CREATE SCHEMA hive.logistic WITH (location = 'gs://starburst-bluetab-test/logistic');

CREATE VIEW "hive"."logistic"."shipping_priority" SECURITY DEFINER AS
SELECT
  l.orderkey
, SUM((l.extendedprice * (1 - l.discount))) revenue
, o.orderdate
, o.shippriority
FROM
  tpch.tiny.customer c
, tpch.tiny.orders o
, tpch.tiny.lineitem l
WHERE ((c.mktsegment = 'BUILDING') AND (c.custkey = o.custkey) AND (l.orderkey = o.orderkey))
GROUP BY l.orderkey, o.orderdate, o.shippriority
ORDER BY revenue DESC, o.orderdate ASC;


CREATE VIEW "hive"."logistic"."minimum_cost_supplier" SECURITY DEFINER AS
SELECT
  s.acctbal
, s.name SupplierName
, n.name Nation
, p.partkey
, p.mfgr
, s.address
, s.phone
, s.comment
FROM
  tpch.tiny.part p
, tpch.tiny.supplier s
, tpch.tiny.partsupp ps
, tpch.tiny.nation n
, tpch.tiny.region r
WHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (p.size = 15) AND (p.type LIKE '%BRASS') AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE') AND (ps.supplycost = (SELECT MIN(ps.supplycost)
FROM
  tpch.tiny.partsupp ps
, tpch.tiny.supplier s
, tpch.tiny.nation n
, tpch.tiny.region r
WHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE'))
)))
ORDER BY s.acctbal DESC, n.name ASC, s.name ASC, p.partkey ASC;



select
  cst.name as CustomerName,
  cst.address,
  cst.phone,
  cst.nationkey,
  cst.acctbal as BookedOrders,
  cst.mktsegment,
  nat.name as Nation,
  reg.name as Region
from tpch.sf1.customer as cst
join tpch.sf1.nation as nat on nat.nationkey = cst.nationkey
join tpch.sf1.region as reg on reg.regionkey = nat.regionkey
where reg.regionkey = 1;

select
  nat.name as Nation,
  avg(cst.acctbal) as average_booking
from tpch.sf100.customer as cst
join tpch.sf100.nation as nat on nat.nationkey = cst.nationkey
join tpch.sf100.region as reg on reg.regionkey = nat.regionkey
where reg.regionkey = 1
group by nat.name;
```
 

En estas pruebas crearemos una serie de vistas y haremos unos selects con varios cruces sobre las tablas de customer(15000000 rows), nation(25 rows) y region(5 rows) del esquema sf100 para comprobar que todo funciona correctamente y ver que tenemos nuestra plataforma operativa. Una vez comprobado que todo es correcto, probaremos a escribir algunos resultados en el bucket que hemos creado.

Para ello lanzaremos las consultas que se encuentran en el archivo `gcs_storage.sql`:

{"type":"elementor","siteurl":"https://bluetab.net/es/wp-json/","elements":[{"id":"1a82503","elType":"widget","isInner":false,"isLocked":false,"settings":{"code_language":"python","code_block":"```sql\n CREATE SCHEMA hive.logistic WITH (location = 'gs://starburst-bluetab-test/logistic');\n\nCREATE VIEW \"hive\".\"logistic\".\"shipping_priority\" SECURITY DEFINER AS\nSELECT\n  l.orderkey\n, SUM((l.extendedprice * (1 - l.discount))) revenue\n, o.orderdate\n, o.shippriority\nFROM\n  tpch.tiny.customer c\n, tpch.tiny.orders o\n, tpch.tiny.lineitem l\nWHERE ((c.mktsegment = 'BUILDING') AND (c.custkey = o.custkey) AND (l.orderkey = o.orderkey))\nGROUP BY l.orderkey, o.orderdate, o.shippriority\nORDER BY revenue DESC, o.orderdate ASC;\n\n\nCREATE VIEW \"hive\".\"logistic\".\"minimum_cost_supplier\" SECURITY DEFINER AS\nSELECT\n  s.acctbal\n, s.name SupplierName\n, n.name Nation\n, p.partkey\n, p.mfgr\n, s.address\n, s.phone\n, s.comment\nFROM\n  tpch.tiny.part p\n, tpch.tiny.supplier s\n, tpch.tiny.partsupp ps\n, tpch.tiny.nation n\n, tpch.tiny.region r\nWHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (p.size = 15) AND (p.type LIKE '%BRASS') AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE') AND (ps.supplycost = (SELECT MIN(ps.supplycost)\nFROM\n  tpch.tiny.partsupp ps\n, tpch.tiny.supplier s\n, tpch.tiny.nation n\n, tpch.tiny.region r\nWHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE'))\n)))\nORDER BY s.acctbal DESC, n.name ASC, s.name ASC, p.partkey ASC;\n\n\n\nselect\n  cst.name as CustomerName,\n  cst.address,\n  cst.phone,\n  cst.nationkey,\n  cst.acctbal as BookedOrders,\n  cst.mktsegment,\n  nat.name as Nation,\n  reg.name as Region\nfrom tpch.sf1.customer as cst\njoin tpch.sf1.nation as nat on nat.nationkey = cst.nationkey\njoin tpch.sf1.region as reg on reg.regionkey = nat.regionkey\nwhere reg.regionkey = 1;\n\nselect\n  nat.name as Nation,\n  avg(cst.acctbal) as average_booking\nfrom tpch.sf100.customer as cst\njoin tpch.sf100.nation as nat on nat.nationkey = cst.nationkey\njoin tpch.sf100.region as reg on reg.regionkey = nat.regionkey\nwhere reg.regionkey = 1\ngroup by nat.name;\n```\n","_title":"","_margin":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_margin_tablet":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_margin_mobile":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_padding":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_padding_tablet":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_padding_mobile":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_element_width":"","_element_width_tablet":"","_element_width_mobile":"","_element_custom_width":{"unit":"%","size":"","sizes":[]},"_element_custom_width_tablet":{"unit":"px","size":"","sizes":[]},"_element_custom_width_mobile":{"unit":"px","size":"","sizes":[]},"_element_vertical_align":"","_element_vertical_align_tablet":"","_element_vertical_align_mobile":"","_position":"","_offset_orientation_h":"start","_offset_x":{"unit":"px","size":"0","sizes":[]},"_offset_x_tablet":{"unit":"px","size":"","sizes":[]},"_offset_x_mobile":{"unit":"px","size":"","sizes":[]},"_offset_x_end":{"unit":"px","size":"0","sizes":[]},"_offset_x_end_tablet":{"unit":"px","size":"","sizes":[]},"_offset_x_end_mobile":{"unit":"px","size":"","sizes":[]},"_offset_orientation_v":"start","_offset_y":{"unit":"px","size":"0","sizes":[]},"_offset_y_tablet":{"unit":"px","size":"","sizes":[]},"_offset_y_mobile":{"unit":"px","size":"","sizes":[]},"_offset_y_end":{"unit":"px","size":"0","sizes":[]},"_offset_y_end_tablet":{"unit":"px","size":"","sizes":[]},"_offset_y_end_mobile":{"unit":"px","size":"","sizes":[]},"_z_index":"","_z_index_tablet":"","_z_index_mobile":"","_element_id":"","_css_classes":"","motion_fx_motion_fx_scrolling":"","motion_fx_translateY_effect":"","motion_fx_translateY_direction":"","motion_fx_translateY_speed":{"unit":"px","size":4,"sizes":[]},"motion_fx_translateY_affectedRange":{"unit":"%","size":"","sizes":{"start":0,"end":100}},"motion_fx_translateX_effect":"","motion_fx_translateX_direction":"","motion_fx_translateX_speed":{"unit":"px","size":4,"sizes":[]},"motion_fx_translateX_affectedRange":{"unit":"%","size":"","sizes":{"start":0,"end":100}},"motion_fx_opacity_effect":"","motion_fx_opacity_direction":"out-in","motion_fx_opacity_level":{"unit":"px","size":10,"sizes":[]},"motion_fx_opacity_range":{"unit":"%","size":"","sizes":{"start":20,"end":80}},"motion_fx_blur_effect":"","motion_fx_blur_direction":"out-in","motion_fx_blur_level":{"unit":"px","size":7,"sizes":[]},"motion_fx_blur_range":{"unit":"%","size":"","sizes":{"start":20,"end":80}},"motion_fx_rotateZ_effect":"","motion_fx_rotateZ_direction":"","motion_fx_rotateZ_speed":{"unit":"px","size":1,"sizes":[]},"motion_fx_rotateZ_affectedRange":{"unit":"%","size":"","sizes":{"start":0,"end":100}},"motion_fx_scale_effect":"","motion_fx_scale_direction":"out-in","motion_fx_scale_speed":{"unit":"px","size":4,"sizes":[]},"motion_fx_scale_range":{"unit":"%","size":"","sizes":{"start":20,"end":80}},"motion_fx_transform_origin_x":"center","motion_fx_transform_origin_y":"center","motion_fx_devices":["desktop","tablet","mobile"],"motion_fx_range":"","motion_fx_motion_fx_mouse":"","motion_fx_mouseTrack_effect":"","motion_fx_mouseTrack_direction":"","motion_fx_mouseTrack_speed":{"unit":"px","size":1,"sizes":[]},"motion_fx_tilt_effect":"","motion_fx_tilt_direction":"","motion_fx_tilt_speed":{"unit":"px","size":4,"sizes":[]},"sticky":"","sticky_on":["desktop","tablet","mobile"],"sticky_offset":0,"sticky_offset_tablet":"","sticky_offset_mobile":"","sticky_effects_offset":0,"sticky_effects_offset_tablet":"","sticky_effects_offset_mobile":"","sticky_parent":"","_animation":"","_animation_tablet":"","_animation_mobile":"","animation_duration":"","_animation_delay":"","_transform_rotate_popover":"","_transform_rotateZ_effect":{"unit":"px","size":"","sizes":[]},"_transform_rotateZ_effect_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_rotateZ_effect_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_rotate_3d":"","_transform_rotateX_effect":{"unit":"px","size":"","sizes":[]},"_transform_rotateX_effect_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_rotateX_effect_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_rotateY_effect":{"unit":"px","size":"","sizes":[]},"_transform_rotateY_effect_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_rotateY_effect_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_perspective_effect":{"unit":"px","size":"","sizes":[]},"_transform_perspective_effect_tablet":{"unit":"px","size":"","sizes":[]},"_transform_perspective_effect_mobile":{"unit":"px","size":"","sizes":[]},"_transform_translate_popover":"","_transform_translateX_effect":{"unit":"px","size":"","sizes":[]},"_transform_translateX_effect_tablet":{"unit":"px","size":"","sizes":[]},"_transform_translateX_effect_mobile":{"unit":"px","size":"","sizes":[]},"_transform_translateY_effect":{"unit":"px","size":"","sizes":[]},"_transform_translateY_effect_tablet":{"unit":"px","size":"","sizes":[]},"_transform_translateY_effect_mobile":{"unit":"px","size":"","sizes":[]},"_transform_scale_popover":"","_transform_keep_proportions":"yes","_transform_scale_effect":{"unit":"px","size":"","sizes":[]},"_transform_scale_effect_tablet":{"unit":"px","size":"","sizes":[]},"_transform_scale_effect_mobile":{"unit":"px","size":"","sizes":[]},"_transform_scaleX_effect":{"unit":"px","size":"","sizes":[]},"_transform_scaleX_effect_tablet":{"unit":"px","size":"","sizes":[]},"_transform_scaleX_effect_mobile":{"unit":"px","size":"","sizes":[]},"_transform_scaleY_effect":{"unit":"px","size":"","sizes":[]},"_transform_scaleY_effect_tablet":{"unit":"px","size":"","sizes":[]},"_transform_scaleY_effect_mobile":{"unit":"px","size":"","sizes":[]},"_transform_skew_popover":"","_transform_skewX_effect":{"unit":"px","size":"","sizes":[]},"_transform_skewX_effect_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_skewX_effect_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_skewY_effect":{"unit":"px","size":"","sizes":[]},"_transform_skewY_effect_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_skewY_effect_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_flipX_effect":"","_transform_flipY_effect":"","_transform_rotate_popover_hover":"","_transform_rotateZ_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_rotateZ_effect_hover_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_rotateZ_effect_hover_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_rotate_3d_hover":"","_transform_rotateX_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_rotateX_effect_hover_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_rotateX_effect_hover_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_rotateY_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_rotateY_effect_hover_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_rotateY_effect_hover_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_perspective_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_perspective_effect_hover_tablet":{"unit":"px","size":"","sizes":[]},"_transform_perspective_effect_hover_mobile":{"unit":"px","size":"","sizes":[]},"_transform_translate_popover_hover":"","_transform_translateX_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_translateX_effect_hover_tablet":{"unit":"px","size":"","sizes":[]},"_transform_translateX_effect_hover_mobile":{"unit":"px","size":"","sizes":[]},"_transform_translateY_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_translateY_effect_hover_tablet":{"unit":"px","size":"","sizes":[]},"_transform_translateY_effect_hover_mobile":{"unit":"px","size":"","sizes":[]},"_transform_scale_popover_hover":"","_transform_keep_proportions_hover":"yes","_transform_scale_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_scale_effect_hover_tablet":{"unit":"px","size":"","sizes":[]},"_transform_scale_effect_hover_mobile":{"unit":"px","size":"","sizes":[]},"_transform_scaleX_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_scaleX_effect_hover_tablet":{"unit":"px","size":"","sizes":[]},"_transform_scaleX_effect_hover_mobile":{"unit":"px","size":"","sizes":[]},"_transform_scaleY_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_scaleY_effect_hover_tablet":{"unit":"px","size":"","sizes":[]},"_transform_scaleY_effect_hover_mobile":{"unit":"px","size":"","sizes":[]},"_transform_skew_popover_hover":"","_transform_skewX_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_skewX_effect_hover_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_skewX_effect_hover_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_skewY_effect_hover":{"unit":"px","size":"","sizes":[]},"_transform_skewY_effect_hover_tablet":{"unit":"deg","size":"","sizes":[]},"_transform_skewY_effect_hover_mobile":{"unit":"deg","size":"","sizes":[]},"_transform_flipX_effect_hover":"","_transform_flipY_effect_hover":"","_transform_transition_hover":{"unit":"px","size":"","sizes":[]},"motion_fx_transform_x_anchor_point":"","motion_fx_transform_x_anchor_point_tablet":"","motion_fx_transform_x_anchor_point_mobile":"","motion_fx_transform_y_anchor_point":"","motion_fx_transform_y_anchor_point_tablet":"","motion_fx_transform_y_anchor_point_mobile":"","_background_background":"","_background_color":"","_background_color_stop":{"unit":"%","size":0,"sizes":[]},"_background_color_b":"#f2295b","_background_color_b_stop":{"unit":"%","size":100,"sizes":[]},"_background_gradient_type":"linear","_background_gradient_angle":{"unit":"deg","size":180,"sizes":[]},"_background_gradient_position":"center center","_background_image":{"url":"","id":"","size":""},"_background_image_tablet":{"url":"","id":"","size":""},"_background_image_mobile":{"url":"","id":"","size":""},"_background_position":"","_background_position_tablet":"","_background_position_mobile":"","_background_xpos":{"unit":"px","size":0,"sizes":[]},"_background_xpos_tablet":{"unit":"px","size":0,"sizes":[]},"_background_xpos_mobile":{"unit":"px","size":0,"sizes":[]},"_background_ypos":{"unit":"px","size":0,"sizes":[]},"_background_ypos_tablet":{"unit":"px","size":0,"sizes":[]},"_background_ypos_mobile":{"unit":"px","size":0,"sizes":[]},"_background_attachment":"","_background_repeat":"","_background_repeat_tablet":"","_background_repeat_mobile":"","_background_size":"","_background_size_tablet":"","_background_size_mobile":"","_background_bg_width":{"unit":"%","size":100,"sizes":[]},"_background_bg_width_tablet":{"unit":"px","size":"","sizes":[]},"_background_bg_width_mobile":{"unit":"px","size":"","sizes":[]},"_background_video_link":"","_background_video_start":"","_background_video_end":"","_background_play_once":"","_background_play_on_mobile":"","_background_privacy_mode":"","_background_video_fallback":{"url":"","id":"","size":""},"_background_slideshow_gallery":[],"_background_slideshow_loop":"yes","_background_slideshow_slide_duration":5000,"_background_slideshow_slide_transition":"fade","_background_slideshow_transition_duration":500,"_background_slideshow_background_size":"","_background_slideshow_background_size_tablet":"","_background_slideshow_background_size_mobile":"","_background_slideshow_background_position":"","_background_slideshow_background_position_tablet":"","_background_slideshow_background_position_mobile":"","_background_slideshow_lazyload":"","_background_slideshow_ken_burns":"","_background_slideshow_ken_burns_zoom_direction":"in","_background_hover_background":"","_background_hover_color":"","_background_hover_color_stop":{"unit":"%","size":0,"sizes":[]},"_background_hover_color_b":"#f2295b","_background_hover_color_b_stop":{"unit":"%","size":100,"sizes":[]},"_background_hover_gradient_type":"linear","_background_hover_gradient_angle":{"unit":"deg","size":180,"sizes":[]},"_background_hover_gradient_position":"center center","_background_hover_image":{"url":"","id":"","size":""},"_background_hover_image_tablet":{"url":"","id":"","size":""},"_background_hover_image_mobile":{"url":"","id":"","size":""},"_background_hover_position":"","_background_hover_position_tablet":"","_background_hover_position_mobile":"","_background_hover_xpos":{"unit":"px","size":0,"sizes":[]},"_background_hover_xpos_tablet":{"unit":"px","size":0,"sizes":[]},"_background_hover_xpos_mobile":{"unit":"px","size":0,"sizes":[]},"_background_hover_ypos":{"unit":"px","size":0,"sizes":[]},"_background_hover_ypos_tablet":{"unit":"px","size":0,"sizes":[]},"_background_hover_ypos_mobile":{"unit":"px","size":0,"sizes":[]},"_background_hover_attachment":"","_background_hover_repeat":"","_background_hover_repeat_tablet":"","_background_hover_repeat_mobile":"","_background_hover_size":"","_background_hover_size_tablet":"","_background_hover_size_mobile":"","_background_hover_bg_width":{"unit":"%","size":100,"sizes":[]},"_background_hover_bg_width_tablet":{"unit":"px","size":"","sizes":[]},"_background_hover_bg_width_mobile":{"unit":"px","size":"","sizes":[]},"_background_hover_video_link":"","_background_hover_video_start":"","_background_hover_video_end":"","_background_hover_play_once":"","_background_hover_play_on_mobile":"","_background_hover_privacy_mode":"","_background_hover_video_fallback":{"url":"","id":"","size":""},"_background_hover_slideshow_gallery":[],"_background_hover_slideshow_loop":"yes","_background_hover_slideshow_slide_duration":5000,"_background_hover_slideshow_slide_transition":"fade","_background_hover_slideshow_transition_duration":500,"_background_hover_slideshow_background_size":"","_background_hover_slideshow_background_size_tablet":"","_background_hover_slideshow_background_size_mobile":"","_background_hover_slideshow_background_position":"","_background_hover_slideshow_background_position_tablet":"","_background_hover_slideshow_background_position_mobile":"","_background_hover_slideshow_lazyload":"","_background_hover_slideshow_ken_burns":"","_background_hover_slideshow_ken_burns_zoom_direction":"in","_background_hover_transition":{"unit":"px","size":"","sizes":[]},"_border_border":"","_border_width":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_width_tablet":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_width_mobile":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_color":"","_border_radius":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_radius_tablet":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_radius_mobile":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_box_shadow_box_shadow_type":"","_box_shadow_box_shadow":{"horizontal":0,"vertical":0,"blur":10,"spread":0,"color":"rgba(0,0,0,0.5)"},"_box_shadow_box_shadow_position":" ","_border_hover_border":"","_border_hover_width":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_hover_width_tablet":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_hover_width_mobile":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_hover_color":"","_border_radius_hover":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_radius_hover_tablet":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_border_radius_hover_mobile":{"unit":"px","top":"","right":"","bottom":"","left":"","isLinked":true},"_box_shadow_hover_box_shadow_type":"","_box_shadow_hover_box_shadow":{"horizontal":0,"vertical":0,"blur":10,"spread":0,"color":"rgba(0,0,0,0.5)"},"_box_shadow_hover_box_shadow_position":" ","_border_hover_transition":{"unit":"px","size":"","sizes":[]},"_mask_switch":"","_mask_shape":"circle","_mask_image":{"url":"","id":"","size":""},"_mask_notice":"","_mask_size":"contain","_mask_size_tablet":"","_mask_size_mobile":"","_mask_size_scale":{"unit":"%","size":100,"sizes":[]},"_mask_size_scale_tablet":{"unit":"px","size":"","sizes":[]},"_mask_size_scale_mobile":{"unit":"px","size":"","sizes":[]},"_mask_position":"center center","_mask_position_tablet":"","_mask_position_mobile":"","_mask_position_x":{"unit":"%","size":0,"sizes":[]},"_mask_position_x_tablet":{"unit":"px","size":"","sizes":[]},"_mask_position_x_mobile":{"unit":"px","size":"","sizes":[]},"_mask_position_y":{"unit":"%","size":0,"sizes":[]},"_mask_position_y_tablet":{"unit":"px","size":"","sizes":[]},"_mask_position_y_mobile":{"unit":"px","size":"","sizes":[]},"_mask_repeat":"no-repeat","_mask_repeat_tablet":"","_mask_repeat_mobile":"","hide_desktop":"","hide_tablet":"","hide_mobile":"","_attributes":"","custom_css":""},"defaultEditSettings":{"defaultEditRoute":"content"},"elements":[],"widgetType":"elementor-syntax-highlighter","editSettings":{"defaultEditRoute":"content","panel":{"activeTab":"content","activeSection":"content_section"}},"htmlCache":"\t\t<div class=\"elementor-widget-container\">\n\t\t\t<pre><code class='language-python'>```sql\n CREATE SCHEMA hive.logistic WITH (location = 'gs://starburst-bluetab-test/logistic');\n\nCREATE VIEW &quot;hive&quot;.&quot;logistic&quot;.&quot;shipping_priority&quot; SECURITY DEFINER AS\nSELECT\n  l.orderkey\n, SUM((l.extendedprice * (1 - l.discount))) revenue\n, o.orderdate\n, o.shippriority\nFROM\n  tpch.tiny.customer c\n, tpch.tiny.orders o\n, tpch.tiny.lineitem l\nWHERE ((c.mktsegment = 'BUILDING') AND (c.custkey = o.custkey) AND (l.orderkey = o.orderkey))\nGROUP BY l.orderkey, o.orderdate, o.shippriority\nORDER BY revenue DESC, o.orderdate ASC;\n\n\nCREATE VIEW &quot;hive&quot;.&quot;logistic&quot;.&quot;minimum_cost_supplier&quot; SECURITY DEFINER AS\nSELECT\n  s.acctbal\n, s.name SupplierName\n, n.name Nation\n, p.partkey\n, p.mfgr\n, s.address\n, s.phone\n, s.comment\nFROM\n  tpch.tiny.part p\n, tpch.tiny.supplier s\n, tpch.tiny.partsupp ps\n, tpch.tiny.nation n\n, tpch.tiny.region r\nWHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (p.size = 15) AND (p.type LIKE '%BRASS') AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE') AND (ps.supplycost = (SELECT MIN(ps.supplycost)\nFROM\n  tpch.tiny.partsupp ps\n, tpch.tiny.supplier s\n, tpch.tiny.nation n\n, tpch.tiny.region r\nWHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE'))\n)))\nORDER BY s.acctbal DESC, n.name ASC, s.name ASC, p.partkey ASC;\n\n\n\nselect\n  cst.name as CustomerName,\n  cst.address,\n  cst.phone,\n  cst.nationkey,\n  cst.acctbal as BookedOrders,\n  cst.mktsegment,\n  nat.name as Nation,\n  reg.name as Region\nfrom tpch.sf1.customer as cst\njoin tpch.sf1.nation as nat on nat.nationkey = cst.nationkey\njoin tpch.sf1.region as reg on reg.regionkey = nat.regionkey\nwhere reg.regionkey = 1;\n\nselect\n  nat.name as Nation,\n  avg(cst.acctbal) as average_booking\nfrom tpch.sf100.customer as cst\njoin tpch.sf100.nation as nat on nat.nationkey = cst.nationkey\njoin tpch.sf100.region as reg on reg.regionkey = nat.regionkey\nwhere reg.regionkey = 1\ngroup by nat.name;\n```\n </code></pre><script>\nif (!document.getElementById('syntaxed-prism')) {\n\tvar my_awesome_script = document.createElement('script');\n\tmy_awesome_script.setAttribute('src','https://bluetab.net/wp-content/plugins/syntax-highlighter-for-elementor/assets/prism2.js');\n\tmy_awesome_script.setAttribute('id','syntaxed-prism');\n\tdocument.body.appendChild(my_awesome_script);\n} else {\n\twindow.Prism && Prism.highlightAll();\n}\n</script>\t\t</div>\n\t\t"}]} 

En esta prueba lo más relevante es que vamos a escribir los datos de la tablas customer(15000000 rows), orders(150000000 rows), supplier(1000000 rows), nation(25 rows) y region(5 rows) en nuestro bucket de GCS.

Como comentamos anteriormente, Starburst no solo es una herramienta que te permite lanzar consultas para analizar datos, sino que también te puede ayudar en las migraciones de datos de tu compañía, volcando la información de tu base de datos a tu plataforma de la nube. Una cosa muy importante a tener en cuenta es que Starburst te permite trabajar con distintos tipos de fichero, pudiendo escribir tus tablas finales en ORC, Parquet o formatos como Delta o Hudi dándote una libertad muy amplia en las migraciones al cloud.

Como última prueba para ver que todo está funcionando correctamente, vamos a lanzar una consulta para federar distintos datos de diversas fuentes. En nuestro caso, federaremos datos de la anterior tabla que hemos creado en Google Cloud Storage llamada customer con una tabla llamada nation, que nos crearemos en el PostgreSQL que hemos configurado en nuestro despliegue, y la tabla region que está en el esquema tcph. Esta consulta la podemos encontrar en el archivo `federate.sql`:

create schema postgres.logistic;
create table postgres.logistic.nation as select * from tpch.sf1.nation;

select
  cst.name as CustomerName,
  cst.address,
  cst.phone,
  cst.nationkey,
  cst.acctbal as BookedOrders,
  cst.mktsegment,
  nat.name as Nation,
  reg.name as Region
from hive.datalake.customer as cst
join postgres.logistic.nation as nat on nat.nationkey = cst.nationkey
join tpch.sf1.region as reg on reg.regionkey = nat.regionkey
where reg.regionkey = 1;
 

Este tipo de consultas es uno de los puntos fuertes que tiene Starburst, poder federar consultas que se encuentren en distintos silos de información sin la necesidad de migrar los datos y pudiendo atacar a distintos Cloud o a información que se tenga en el onpremise. 

Una vez que hemos probado que tanto las consultas como la escritura en GCS funcionan correctamente, vamos a realizar unos test de performance para simular usuarios en paralelo y ver como autoescala nuestra plataforma. Vamos a configurar JMeter para estas pruebas. Para ello hemos tenido que configurar el conector jdbc de trino para que mande consultas a nuestro cluster.

Vamos a simular 20 usuarios en paralelo, y cada uno lanzará una secuencia de 5 consultas. Esto significa que habrá 20 consultas en paralelo al mismo tiempo, simulando un escenario real, ya que generalmente no se lanzarán consultas de todos los usuarios en el mismo momento. Las consultas que vamos a ejecutar son las siguiente:

```sql
select
  cst.name as CustomerName,
  cst.address,
  cst.phone,
  cst.nationkey,
  cst.acctbal as BookedOrders,
  cst.mktsegment,
  nat.name as Nation,
  reg.name as Region
from tpch.sf1.customer as cst
join tpch.sf1.nation as nat on nat.nationkey = cst.nationkey
join tpch.sf1.region as reg on reg.regionkey = nat.regionkey
where reg.regionkey = 1;

SELECT
  s.acctbal
, s.name SupplierName
, n.name Nation
, p.partkey
, p.mfgr
, s.address
, s.phone
, s.comment
FROM
  tpch.tiny.part p
, tpch.tiny.supplier s
, tpch.tiny.partsupp ps
, tpch.tiny.nation n
, tpch.tiny.region r
WHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (p.size = 15) AND (p.type LIKE '%BRASS') AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE') AND (ps.supplycost = (SELECT MIN(ps.supplycost)
FROM
  tpch.tiny.partsupp ps
, tpch.tiny.supplier s
, tpch.tiny.nation n
, tpch.tiny.region r
WHERE ((p.partkey = ps.partkey) AND (s.suppkey = ps.suppkey) AND (s.nationkey = n.nationkey) AND (n.regionkey = r.regionkey) AND (r.name = 'EUROPE'))
)))
ORDER BY s.acctbal DESC, n.name ASC, s.name ASC, p.partkey ASC;

SELECT
count(*)
FROM
  tpch.sf1.customer c
, tpch.sf1.orders o
, tpch.sf1.lineitem l
WHERE ((c.mktsegment = 'BUILDING') AND (c.custkey = o.custkey) AND (l.orderkey = o.orderkey))
GROUP BY l.orderkey, o.orderdate, o.shippriority
ORDER BY o.orderdate ASC;
```
 

Si nos fijamos, en nuestro cluster de Kubernetes podemos ver que se están levantando más workers de Starburst por el momento de alta demanda en nuestra simulación:

Esto es una de las características más cómodas e importantes que nos da Starburst, ya que hace que nuestra plataforma de analítica de datos sea 100% elástica y podamos ir adaptándonos a los picos de demanda que tengamos.

Métricas

Por último, Starburst nos proporciona una interfaz donde visualizar ciertas métricas del consumo de nuestro cluster, como puede ser la memoria, la cpu o las consultas realizadas en tiempo real en nuestro cluster.

Además de estas métricas, hemos añadido también a nuestra configuración el despliegue de Prometheus y Grafana para integrarnos con las herramientas más comunes dentro de cualquier organización. Las métricas que hemos añadido a Grafana son consumo de memoria de nuestro cluster de Starburst, consultas realizadas por los usuarios, consultas con errores, memoria total de nuestro cluster de Kubernetes y Workers activos. Una vez integradas dichas métricas, el dashboard que tendríamos sería el siguiente:

Una vez integrado con Grafana, podríamos crearnos alertas de envío de mensajes por si hay algún problema en nuestro cluster de Starburst, y así tener todo el flujo de operaciones cubierto para evitarnos dolores de cabeza si hubiera algún tipo de incidencia o indisponibilidad.

El dashboard está publicado en Grafana[14] para que cualquier persona pueda hacer uso de él.

Conclusiones

Desde hace ya unos años, las grandes corporaciones se enfrentan a un desafío común cuando intentan compartir y analizar información entre departamentos ya que cada departamento almacena y gestiona sus datos de manera aislada. Estos silos dificultan el acceso y la integración de datos, lo que impide una visión completa y unificada de la información empresarial. La falta de interoperabilidad entre los silos de datos obstaculiza la toma de decisiones informada, ralentiza los procesos analíticos y limita la capacidad de las organizaciones para obtener una ventaja competitiva. Si tu organización se encuentra en una situación similar, Starburst es tu herramienta.

Starburst te facilita el acceso y análisis a todos estos silos de información y da la capacidad de federar datos de diversas fuentes y ubicaciones, ya sea datos en el Cloud o en tu datacenter onpremise. Permite realizar consultas en tiempo real sin necesidad de mover o transformar los datos previamente. Esto agiliza el proceso analítico y brinda a las organizaciones una visión 360 de sus datos. Además, no solo te ayuda a la hora de consultar datos de distintas fuentes, sino que también te puede ayudar en tus migraciones al Cloud, ya que te permite consultar cualquier origen y volcar dicha información en un almacenamiento como S3 o GCS en formato de ficheros abierto, como puede ser Parquet.

Una de las principales ventajas de Starburst, es que te permite desplegar la infraestructura en Kubernetes para aprovechar así todo su potencial. Kubernetes te da la capacidad de adaptarse dinámicamente a la carga de trabajo. Con esta función, los clústeres de Starburst pueden aumentar o disminuir automáticamente el número de Workers según la demanda. Esto permite optimizar el uso de recursos y garantizar un rendimiento óptimo, ya que los pods adicionales se crean cuando la carga aumenta y se eliminan cuando disminuye. Esto dentro de cualquier organización es un punto muy importante, ya que mejora la eficiencia operativa al minimizar el tiempo de inactividad y los costos innecesarios, al tiempo que asegura una disponibilidad constante y una respuesta ágil a los picos de trabajo. Además, una cosa a tener en cuenta es que puedes realizar la instalación de Starburst tanto en cualquiera de los Cloud, como en onpremise.

Además, también te permite tener un roleado y gobierno de los usuarios dentro de tu plataforma, dando una granularidad a nivel de acceso a los datos a cada usuario, permitiéndote crear roles para ciertos esquemas, tablas o hasta columnas y filas dentro de una tabla.

Los que trabajamos con datos sabemos de la dificultad de trabajar con multitud de fuentes de datos, entornos diversos, herramientas de todo tipo, etc. Uno de los puntos más diferenciales de Starburst es tener la capacidad de consultar los datos desde su almacenamiento, eliminando duplicidad de información, pudiendo así tener una mejor eficiencia en cuanto al storage, y facilitando también el gobierno de estos datos.

En conclusión, Starburst es una herramienta a tener en cuenta si quieres llevar a tu organización al siguiente nivel en el mundo de los datos, o si te estás planteando una estrategia de datos con una visión y una filosofía más orientada al data mesh.

Referencias

[1] Qué es Starburst.[link] 

[2] Qué es Trino. [link]

[3] Principios del Data Mesh. [link]

[4] Introducción  a DBT. [link]

[5] Introducción a Jupyter Notebook. [link]

[6] Introducción a Power BI. [link]

[7] Qué es Prometheus.. [link]

[8] Qué es Grafana. [link]

[9] Qué es Terraform. [link]

[10] Qué es Jmeter.[link]

[11] Módulo de GKE.[link]

[12] Módulo de VPC.[link]

[13] Qué es TPCH.[link]

[14] Dashboard Grafana.[link]

[15] Repositorio de Github con el despliegue.[link]

Navegación

Lucas Calvo

Cloud Engineer

¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

Los Incentivos y el Desarrollo de Negocio en las Telecomunicaciones

octubre 9, 2020
LEER MÁS

Mi experiencia en el mundo de Big Data – Parte II

febrero 4, 2022
LEER MÁS

MICROSOFT FABRIC: Una nueva solución de análisis de datos, todo en uno

octubre 16, 2023
LEER MÁS

Cómo preparar la certificación AWS Data Analytics – Specialty

noviembre 17, 2021
LEER MÁS

MDM como ventaja competitiva en las organizaciones

junio 18, 2024
LEER MÁS

Azure Data Studio y Copilot

octubre 11, 2023
LEER MÁS

Publicado en: Blog, Practices, Tech

  • Página 1
  • Página 2
  • Página 3
  • Página 4
  • Ir a la página siguiente »

Footer

LegalPrivacidadPolítica de cookies

Patrono

Patrocinador

© 2025 Bluetab Solutions Group, SL. All rights reserved.