Contras de la canalización de datos: Perfilado y mejora de rendimiento

Elihu A. Cruz
Lyft Engineering en Español
11 min readMay 21, 2024

Descubre cómo Lyft identificó y solucionó problemas de rendimiento en nuestras canalizaciones de datos.

Este artículo fue publicado originalmente el 6 de Junio de 2023 en eng.lyft.com por Rakesh Kumar y fue traducido por Elihu Cruz

Contexto

Cada canalización de datos (streaming pipeline) es único. Cuando revisamos el rendimiento de una canalización, nos hacemos las siguientes preguntas: “¿existe un cuello de botella?”, “¿la canalización funciona de forma óptima?”, “¿seguirá escalando cuando la carga aumente?”.

Preguntarse estas preguntas con regularidad es vital para evitar tener que arreglar los problemas de rendimiento en el último minuto. De este modo, se pueden ajustar las canalizaciones para que funcionen de forma óptima, satisfagan sistemáticamente los acuerdos de nivel de servicio (SLA) y reduzcan el desperdicio de recursos.

Este artículo cubrirá de los siguientes temas:

  • Proceso de mejora del rendimiento
  • Estrategias para perfilar canalizaciones de datos (streaming pipelines)
  • Problemas comunes de rendimiento
  • Recomendaciones generales para mejorar el rendimiento

Proceso de mejora de rendimiento

La mejora del rendimiento de cualquier sistema informático no es una tarea independiente y aislada, sino un proceso iterativo. Este implica los siguientes pasos:

  1. Medir / Perfilar el rendimiento
  2. Identificar la causa principal
  3. Reparar
  4. Volver al paso uno

El proceso se repite hasta que se obtiene el rendimiento deseado (en la escala prevista) o se agotan todas las medidas de rendimiento.

Perfilar tu canalización

Identificar un problema de rendimiento sin ninguna herramienta de creación de perfiles es un tiro en la oscuridad. La creación de perfiles es el primer paso del proceso y requiere las herramientas adecuadas. Las herramientas ayudan a identificar más rápidamente los problemas de rendimiento más importantes. También pueden integrarse con entornos de desarrollo para proporcionar un informe completo en una fase temprana del ciclo de vida del desarrollo.

Memoria y Perfilado de CPU

Es fundamental disponer de perfiles de memoria y CPU integrados en el entorno de ejecución. Incluso el código más trivial puede ocultar graves cuellos de botella en el rendimiento. Observar el código no ayudará a identificar el problema, por lo que es importante contar con un perfilador que pueda ofrecer una imagen más precisa y ayude a encontrar la causa subyacente con rapidez.

Dado que la mayoría de nuestros pipelines se basan en Apache Beam y código en Python, utilizamos Pyflame y el perfilador asíncrono para generar un mapa de calor de la utilización del CPU. Esto ayuda a comprender las características de rendimiento y a identificar el código infractor. En el momento de escribir este artículo, Pyflame ha quedado obsoleto y ya no recibe soporte, aunque existen herramientas similares como Flameproof.

Figura 1: Ejemplo de Gráfica PyFlame

Si tu canalización está basada en JVM, puedes usar varios perfiladores de JVM para identificar cuellos de botella.

Flink Dashboard

Recientemente, la consola de Flink ha añadido herramientas visuales que muestran varios puntos de datos de rendimiento con el fin de identificar cuellos de botella en el rendimiento. Por ejemplo, puedes ver la utilización de la CPU para el operador individual que potencialmente puede causar contrapresión (back-pressure). Estos datos se representan a nivel de operador y pueden proporcionarle la información necesaria para limitar su área de búsqueda.

Figura: 2: Alto uso del CPU

Como puede verse en la Figura 2, un operador está ocupado al 100%. Esto puede dar lugar a una contrapresión en la canalización. También proporciona información valiosa sobre qué operador de canalización requiere un examen más detallado para identificar cualquier cuello de botella.

El tablero proporciona el rendimiento de los registros a nivel de operador, lo que ayuda a identificar cualquier problema de asimetría de datos (hot shard) en la canalización.

El tablero de Flink también incorpora una gráfica de flama (“flamegraph”) (Figura 3) que resulta útil para identificar cuellos de botella en las canalizaciones basadas en JVM. Esta gráfica de flama puede activarse estableciendo un indicador en la configuración:

rest.flamegraph.enabled: true
Figura 3: Gráfica flama de Flink

Sistema métrico de Flink

Flink también genera diferentes métricas de rendimiento de sistema a nivel tarea y nivel de operador:

  • Rendimiento/Salida
  • Latencia (marca de agua)
  • Métricas de JVM (heap, memoria usada directamente, GC, etc)

Estas métricas pueden ser emitidas a distintos sistemas de monitoreo de métricas con el fin de crear un tablero (figura 3) para monitorear continuamente el rendimiento.

Figura 4: Tablero de monitoreo JVM

Aparte de las métricas mencionadas anteriormente, hay alertas configuradas con las siguientes métricas:

  • Tamaño de punto de control
  • Punto de control de fallos
  • Reinicio de la canalización
  • Salida de la canalización por minuto

Estas métricas son importantes desde el punto de vista de la estabilidad y pueden sacar a la luz problemas en la canalización de forma inmediata.

El propietario de la aplicación también puede definir las métricas relacionadas con el negocio y los SLA que son importantes para la salud general de la aplicación. En algunos casos, las métricas del sistema pueden estar dentro del rango, pero las métricas de negocio cuentan una historia diferente. Por ejemplo, la latencia de extremo a extremo puede aumentar significativamente. En nuestro caso, esto se debió a dos factores: 1) asimetría de los datos y 2) problemas de generación de eventos ascendentes (upstream events).

Problemas comunes de rendimiento

Tras una amplia experiencia en operaciones con canalizaciones, escalándolos para procesar cientos de miles de eventos por segundo, nos hemos dado cuenta de que hay cuatro categorías de problemas de rendimiento que son comunes en todas las canalizaciones. Estos problemas se enumeran por orden de gravedad:

  1. Asimetría de datos (hot shard)
  2. Gran tamaño de ventana (Large window size)
  3. Interacción con servicios de baja velocidad
  4. Serialización y deserialización

Asimetría de datos (hot shard)

La asimetría de datos o hot shard es el más notorio de todos los problemas de rendimiento. En el 80% de los casos, lo hemos identificado como el principal responsable de los problemas de rendimiento. Esto se debe principalmente a una mayor concentración de datos en claves individuales. Algunos de sus efectos secundarios son el aumento de la latencia de extremo a extremo y la infrautilización de recursos.

La reacción instintiva ante este problema es proporcionar más potencia de cálculo a la canalización, pero esto no resuelve el problema y sólo conduce a una mayor infrautilización de los recursos. Esto puede identificarse con la ayuda del panel de control de Flink, para identificar cuidadosamente los operadores con una distribución no uniforme de los registros de entrada. Para obtener más información, consulta nuestra anterior entrada del blog sobre la asimetría de los datos (en inglés.)

Gran tamaño de ventana (Large window size)

Un gran tamaño de ventana provoca un enorme volumen de datos en el momento de la materialización de la ventana y aumenta el tamaño de los puntos de control. Algunos de los efectos secundarios son el aumento de la latencia de extremo a extremo y un patrón de utilización de recursos no uniforme (por ejemplo, una gráfica cerrada). Esto empeora, especialmente en el caso de las ventanas deslizantes, porque Flink almacena una copia de los datos de cada ventana, lo que aumenta el tamaño total de los datos y la utilización de la memoria y la CPU.

Esto puede detectarse mirando el panel de control del sistema e identificando de cerca el operador afectado con un gran tamaño de ventana.

Interacción con servicios de baja velocidad

Algunos de los casos de uso requieren que una canalización interactúe con servicios externos, ya sea para almacenar la salida o para alimentar el registro entrante con datos adicionales.

La interacción con servicios externos tiene un coste de latencia no controlada o de estado indeterminado, cuando el servicio externo no puede seguir el ritmo. Los efectos de este problema son la contrapresión (back-pressure) en los operadores ascendentes (upstream) y una mayor latencia de extremo a extremo. Lo ideal sería evitar este tipo de operaciones en las canalizaciones. En el caso de que no puedan evitarse, el efecto puede minimizarse estableciendo el paralelismo adecuado para los operadores y realizando las operaciones por lotes.

Serialización y deserialización

En la mayoría de los casos, los problemas de rendimiento en la serialización y deserialización existen a plena vista. El problema puede identificarse observando el gráfico de flamas. En algunos casos extremos, hemos observado que ~20% de la utilización de recursos puede atribuirse a los costes de serialización, que se agravan cuando se consume un gran número de eventos. Lo ideal sería minimizar este tipo de operaciones y llevarlas al límite de la canalización.

Referencias generales

Cada canalización es diferente. No hay mejoras de rendimiento que sirvan para todos los casos, pero existen referencias generales que pueden aportar algunos beneficios rápidos.

Evitar operadores duplicados

En la mayoría de los casos, las operaciones duplicadas se escapan dentro del código de las canalizaciones. Compruebe regularmente si hay operaciones duplicadas. Por ejemplo, una canalización puede tener diferentes subsecciones paralelas que podrían estar deserializando los mismos datos una y otra vez. Esta operación puede transferirse al operador anterior para que se realice una sola vez.

Evitar mezclado innecesario

El mezclado (shuffling) es la causa más común de degradación del rendimiento. Tener un buen conocimiento de los operadores y de cómo causan el shuffling ayuda a diseñar un mejor canalización. La pauta general es minimizar el uso de estos operadores. Esto puede aumentar significativamente el rendimiento y reducir la latencia de extremo a extremo.

Es posible identificar el shuffling utilizando el cuadro de mandos de Flink. Siempre que una arista entre dos operadores aparezca denotada con Hash o Rebalance (Figura 5), significa que los datos están siendo mezclados de nuevo por el operador ascendente.

Generalmente, Flink proporciona los nombres de los métodos en los que se produce el shuffling. Si el shuffling es innecesario, debe eliminarse del canal.

Uno de los objetivos de alto nivel del diseño de la canalización es minimizar el número de shufflings en la canalización.

Figura 5: Mezclado o rebalanceo

Habilitar Cython

Esto se aplica específicamente a las canalizaciones escritas en Python (Apache Beam). En nuestro caso, Cython supone un aumento del rendimiento del 5%. Habilitar Cython en Beam es fácil: instala Cython en el entorno de construcción y ejecución (asegurándote de que la versión de Cython es ≥ 0.28.1).

Además, puedes utilizar Pyflame profiling para identificar las funciones lentas y “cythonizarlas”.

Remover los datos innecesarios de forma temprana

Generalmente, las canalizaciones están pensadas para procesar grandes cantidades de datos, pero en cualquier caso procesar datos innecesarios afectará al rendimiento en cuanto a utilización de CPU, memoria y red. Siempre sigue el principio del menor procesamiento de datos. Nosotros removemos de forma agresiva los datos o atributos innecesarios al principio de la canalización. Esto ha aumentado nuestro rendimiento en un 7% en promedio.

Usar Protobuf

Como podrás saber, se produce un aumento de la latencia cuando los operadores envían datos a través de la red. Intentamos reducir el tamaño de los datos convirtiéndolos en objetos protobuf. Protobuf es un formato binario con un esquema bien conocido, lo que resulta en datos mucho más pequeños a través de la red. Usamos un mensaje protobuf para pasar los datos dentro de los operadores en una canalización.

Figura 6: Comparación del tamaño final (Crédito a OAuth [artículo en inglés])

Evitar la asimetría de datos

Como se mencionó en el post anterior (Gotchas of Streaming pipeline: Data Skewness, en inglés), evita en la medida de lo posible la asimetría de los datos. Esta es la causa más común de obstaculizar el rendimiento y reduce el consumo de recursos óptimo. Consulta el post anterior para obtener ideas sobre cómo identificar la asimetría de datos y cómo solucionarla.

La esencia de la estrategia es identificar una clave que distribuya los datos de la forma más uniforme posible. Por ejemplo, la fragmentación basada en una región o ciudad podría conducir a la asimetría de datos (algunas ciudades son más grandes que otras), mientras que la fragmentación basada en el ID de usuario da una mayor probabilidad de distribuir uniformemente los datos y evitar la asimetría de datos.

Punto de control

El punto de control (checkpoint) también puede ser una fuente de degradación del rendimiento si la alineación del punto de control tarda más de varios segundos. Los puntos de control más largos son cancelados automáticamente por Flink. Los puntos de control frecuentes reducen el rendimiento, por lo que es imprescindible encontrar el intervalo óptimo entre puntos de control sucesivos. De lo contrario, la canalización acabará dedicando la mayor parte del tiempo a realizar puntos de control y no a realizar el trabajo real. Puedes probar varias configuraciones, medir el rendimiento y elegir la que más se ajuste a tus requisitos acorde al acuerdo del nivel de sistema (SLA). También puedes probar puntos de control no alineados, que no han mostrado efectos negativos en nuestras pruebas.

Además, monitorea el tamaño de los puntos de control (figura 7). Evita almacenar datos innecesarios en el estado para reducir el tamaño total de los puntos de control.

Figura 7: Tablero Flink para tamaño de referencia

Estado de menor tamaño

A veces es necesario utilizar el procesamiento con estados, pero siempre es recomendable mantener el tamaño del estado ligero. Esto ayuda a mantener el punto de control más ligero y minimizar la transferencia de datos.

Para mantener el tamaño del estado ligero, necesitas asegurarte de que el estado se purgue regularmente si está asociado a una ventana global. Una ventana global es ilimitada y nunca se materializa, por lo que el estado se mantiene en memoria para siempre. Esto podría llevar a fallos de memoria (OOM). Es mejor tener alguna lógica que purgue los datos de estado regularmente.

Además, una ventana deslizante prolongada tiende a tener un tamaño de estado mayor porque cada ventana tiene una copia de los datos. Esto aumenta el volumen total de datos y, por tanto, el tamaño del estado.

Red

La mayoría de los servicios se despliegan en infraestructura básica que utiliza multi-regiones y zonas de disponibilidad (AZ) para mejorar la disponibilidad. Sin embargo, una canalización de datos en streaming es diferente. No puede estar parcialmente inactiva. Además, la velocidad de la red es una parte vital del rendimiento del canal y, por lo tanto, un canal debe desplegarse por zona (por ejemplo, en una única AZ) para mantener todas las instancias/gestores de tareas cerca y minimizar la latencia de la red.

Supervisamos las canalizaciones en tres ejes: latencia, disponibilidad y coste, y optimizamos cada uno de ellos en función de los requisitos de la empresa.

Conclusiones

Existen varias estrategias disponibles para mejorar el rendimiento, aunque la complejidad puede variar en función de los datos y el diseño de la canalización. Cuando crees una canalización, no la optimices de antemano. Más bien, considéralo como un proceso iterativo que continúa a lo largo del ciclo de vida de la canalización, y mejora gradualmente el rendimiento.

Si estás interesado en leer entradas anteriores de esta serie, puedes encontrarlas aquí:

Un agradecimiento especial a Jim Chiang, Ravi Kiran Magham y Seth Saperstein por contribuir a este artículo.

--

--