¿Qué haces si tu job de Azure Stream Analytics (ASA) no rinde lo suficiente? La solución más fácil es subir el número de Streaming Units (SU), pero… ¿ayudará eso siempre a tu solución? En este artículo veremos cómo influyen en el escalado otros elementos como los Event Hubs y el diseño de la query de ASA. Si el escalado de un job de ASA no es tu objetivo, el artículo también te puede ayudar a entender mejor como funcionan las particiones de un Event Hub.

Arquitectura básica

Es muy probable que si te preocupas por el escalado de un job de ASA sepas montar en Azure una arquitectura como la siguiente en cuestión de minutos:

Arquitectura muy común en sistemas de Real-Time con Azure Stream Analytics.

Arquitectura muy común en sistemas de Real-Time con Azure Stream Analytics.

Los publishers envían los eventos a un Event Hub. Los Event Hubs son capaces de recibir una gran cantidad de eventos de diferentes publishers gracias a su sistema de particiones que veremos más en detalle durante este artículo. ASA consume los eventos que contiene el Event Hub y realiza las transformaciones necesarias utilizando una query basada en un lenguaje muy similar al SQL. Las salidas de esa query se envían a uno o varios receptores. En el diagrama se muestra Power BI y otro Event Hub, por poner solo un par de ejemplos de entre otras muchas salidas que se pueden conectar.

Hay otras opciones, pero, cuando se trata de eventos en real-time (RT), lo más común es contar con un Event Hub como entrada del Job de ASA. Dando por hecho que posees una arquitectura de este tipo, iremos revisando uno por uno los elementos que intervienen en el escalado de un job:

  1. Event Hubs y particiones
  2. Event Hubs y Throughput Units
  3. Query de ASA
  4. ASA y Streaming Units
  5. Salidas de ASA

1. Event Hubs y particiones

Los Event Hubs son la solución Software as a Service (SaaS) que ofrece Azure para la ingestión masiva de eventos. Los Event Hubs son algo muy parecido a las colas de mensajes del Service Bus, pero una de las grandes diferencias que existen entre éstas y los Event Hubs es la escalabilidad. Los Event Hubs permiten una escalabilidad totalmente horizontal utilizando un sistema de particiones.

Podemos ver las particiones como carriles dentro de una carretera. Si solo tenemos un carril y hay muchos coches, probablemente acabe formándose una cola y se produzcan retenciones. Cuantos más carriles haya más fluido irá el tráfico.

Símil entre particiones de un Event Hub y carriles de una carretera.

Siguiendo con el símil de la carretera, los conductores son los que tienen el poder de cambiarse de carril a su antojo, sin embargo, en el caso de los Event Hubs esto no es así. A cada evento se le asigna una partición cuando entra en el Event Hub. Ese evento ya no puede cambiarse de partición y tampoco puede estar en dos particiones al mismo tiempo (no existe duplicidad de eventos). Una vez los eventos están dentro del Event Hub ya no tenemos ningún control sobre ellos, es decir, no es posible vaciar las particiones (los eventos se almacenan por defecto durante 1 día y puede ampliarse hasta 7).

Entonces… ¿Cómo se le asigna a cada evento una partición? Se puede realizar de 3 formas diferentes:

  • No hacer nada y dejar que el Event Hub reparta de manera equitativa los mensajes entre particiones (siguiendo una política round-robin).
  • Indicando el ID de la partición a la hora de enviar el evento. Esta opción no se recomienda puesto que obliga al emisor a tener un conocimiento de la estructura que hay dentro del Event Hub.
  • Indicando una PartitionKey a la hora de enviar el evento. El campo PartitionKey pasa por una función hash y se mapea a una partición. Esta es la forma recomendada de particionar los eventos puesto que el publisher no requiere de un conocimiento específico del número de particiones, simplemente indica una clave de particionado y es el Event Hub el que se encarga de decidir en qué partición acabará. Si un Event Hub cuenta con 4 particiones y se le envían eventos con 4 posibles PartitionKeys, el Event Hub no tiene porqué enviar cada PartitionKey a una partición diferente (decide las particiones con una función hash y no de forma equitativa). Sin embargo, sí que nos asegura que eventos con la misma PartitionKey vayan a acabar en la misma partición.

Si le indicamos la partición, bien con el ID o con el PartitionKey, es frecuente que el número de eventos por partición crezca de forma desigual. Lo que sí se cumple, independientemente del método utilizado para distribuir los eventos, es que dentro de cada partición los eventos se almacenan de forma ordenada. También es importante saber que el número de particiones no se puede cambiar una vez desplegado el Event Hub. El número máximo de particiones que se le puede asignar a un Event Hub desde el portal de Azure es de 32, pero, si fuera necesario, se puede aumentar contactando con el equipo de Azure.

2. Event Hubs y Throughput Units

Las Throughput Units (TU) son las encargadas del movimiento de datos en los Event Hub. Podríamos verlas como un guardia que controla el tráfico en 1 o más carriles. Del mismo modo que no tiene mucho sentido que varios guardias controlen el tráfico de un solo carril (uno se basta y se sobra para controlar una única hilera de coches), no se puede utilizar más de una TU por partición. Sin embargo, sí que tiene sentido que el número de TU sea igual o inferior al número de particiones.

Las TU se establecen a nivel del namespace de los Event Hubs, por lo que si se tienen varios Event Hubs y una sola TU, ésta se repartirá entre todas las particiones de los 2 Event Hubs. Cada TU soporta 1 MB/s ó 1000 eventos/s de entrada y 2 MB/s de salida. Las TU, a diferencia de las particiones, se pueden cambiar una vez creado el namespace. Desde el portal de Azure solo podemos escalar un namespace hasta 20 TU, pero, en el caso de requerirlo, podrías usar más (en bloques de 20 en 20) contactando con el equipo de soporte de Azure.

Panel desde el cuál cambiar el número de TU de un namespace de Event Hubs.

Si la cantidad de eventos que recibe tu sistema varía mucho en función de la hora del día, siempre puedes variar el número de TU automáticamente usando las APIs de Azure. De esta forma solo estarían activas las TU cuando realmente se necesiten y no constantemente activas a lo largo de todo el día.

3. Queries de ASA

La clave de un escalado masivo de ASA reside en la forma de partir los datos de entrada. Un correcto particionado en los Event Hubs permite que las diferentes SU tomen los datos de particiones diferentes, consiguiendo así un máximo nivel de paralización. Sin embargo ASA no «es consciente» de que existen esas particiones si no se lo indicas con un PARTITION BY en la query.

Vamos a analizar unas cuantas queries de ejemplo y comentar hasta qué punto podríamos escalarlas. Basaremos los ejemplos en un sistema cuyo objetivo es procesar transacciones. Cuando conectamos ASA a un Event Hub, los eventos de entrada, además de los campos que le hemos definido en el envío, llegan con un nuevo campo llamado PartitionId que contiene el valor de la partición en la que finalmente se ha almacenado.

3.1. Query no particionada

SELECT Country, SUM(Amount) AS Amount
FROM   TransactionsInputStream 
GROUP  BY TumblingWindow(second, 10), Country

 

Independientemente de si la entrada está o no particionada, esta query no contiene un PARTITION BY,  por lo que no se le puede sacar partido a las particiones. Para queries sin particionar puedes usar hasta 6 SU.

3.2. Query particionada

SELECT Country, SUM(Amount) AS Amount
FROM   TransactionsInputStream PARTITION BY PartitionId
GROUP  BY TumblingWindow(second, 10), Country, PartitionId

A diferencia de antes, esta query sí que cuenta con el PARTITION BY, por lo que se puede paralelizar con facilidad. En caso de que la query posea un GROUP BY, es obligatorio que la columna que se use para particionar se encuentre también en el GROUP BY. El número máximo de SU que se le pueden asignar a este tipo de queries es igual al número de particiones de entrada por 6. Si tuviéramos 4 particiones en nuestros Event Hubs, el job que contenga esta query puede escalarse hasta 4 * 6 = 24 SU

Observa que si la entrada no está particionada correctamente no obtendremos el resultado deseado. Por ejemplo, si las transacciones de un país están tanto en la partición 0 como en la partición 1 en lugar de estar solo en una partición, la query devolvería  2 filas para ese país. Para que el resultado sea el correcto, a la hora de enviar el evento el campo PartitionKey debe ser igual a Country.

3.3. Query con múltiples pasos

Un problema común puede aparecer cuando queremos hacer un agregado total (es decir, poner en común todos los cálculos hechos por particiones) pero sin perder la paralelización. Esta situación se puede solucionar con una estructura similar a un MapReduce; primero se realiza el acumulado por partición de forma independiente y después se hace un agregado del resultado devuelto por cada partición. Un ejemplo a continuación:

WITH TransactionsCountry AS
(
         SELECT   Country, SUM(Amount) AS Amount, System.TIMESTAMP AS Time
         FROM     TransactionsInputStream TIMESTAMP BY TransactionDate PARTITION BY PartitionId
         GROUP BY TumblingWindow(second, 1), Country, PartitionId
) 
SELECT   System.TIMESTAMP AS Time, 
         SUM(Amount)      AS TotalAmount
FROM     TransactionsCountry
GROUP BY TumblingWindow(second, 1)

 

En esta query, el segundo SELECT no se puede paralelizar debido a que no cuenta con un PARTITION BY. En este típo de situaciones (en las que se requiere un acumulado total) no hay problema al no paralelizar el segundo paso de la query debido a que se realiza el agrupado entre el número de particiones, que nunca son tantas como para ralentizar el job.

Suponiendo que hay 4 particiones, esta query podría usar hasta 6 * 4 + 6 = 30 SU. 6 por cada partición del WITH y 6 más por el último SELECT.

4. ASA y Streaming Units

Subir el número de SU es la manera de asignarle más recursos a nuestro job de ASA. Cada SU corresponde aproximadamente a 1 MB/s de throughput. Una vez seguros que las SU podrán ser utilizadas correctamente debido al sistema de particionado y al diseño de la query, es el momento de aumentar el número de SU. Las unidades que se pueden elegir son: 1, 3, 6, y a partir de aquí múltiplos de 6: 12, 18, 24…

Escalado de SU de un job de ASAEscalado de SU de un job de ASA

Escalado de SU de un job de ASA

Al igual que las TU, las SU pueden cambiarse usando la API, por lo que es posible aumentar el número de SU solo en los momentos necesarios con el objetivo de abaratar costes.

5. Salidas de ASA

Para sacarle un mayor partido a las particiones de entrada, las salidas deberían de estar también particionadas. Para conseguir una paralelización máxima, en número de particiones de entrada debe de ser igual al número de particiones de salida. Salidas a Event Hubs y a Blob Storage soportan particionado, pero otras como Power BI aún no lo hacen.

Para que funcione el particionado de una salida Event Hub hay que indicarle como partition key el campo PartitionId a la hora de crear la salida en ASA.

Creando output a un Event Hub

Es necesario indicar que el PartitionKey es PartitionId a la hora de añadir una salida a nuestro job de ASA.

Para los Blob Storages no hace falta configurar nada 🙂

5.1. Power BI

Por desgracia, actualmente Power BI no soporta particionamiento. Pero además de esa limitación a la hora de paralelizar un job deberías tener en cuenta otras limitaciones que posee este servicio.

Hay que diferenciar entre una falta de capacidad de la combinación Event Hub – ASA con la capacidad de la API de Power BI en tiempo real. Power BI tiene un límite de 1 millón de filas por hora y descubriás que has sobrepasado ese límite cuando encuentres en el log de ASA un error similar a:

There was a problem pushing data due to throttling by Power BI. The following information may be helpful diagnosing the Power BI issue. ReferenceId:xxxxxxxxxx. Stream Analytics will retry this operation after xxxx seconds, PowerBI service response: «You are over your rows per hour limit for this dataset. Please retry your request later.»

Otro limitación de Power BI cuando trata con datos en tiempo real es el número de filas que puede almacenar. A día de hoy no se pueden almacenar más de 200.000 filas en un dataset de streaming.

La solución tanto al problema del límite de filas por hora como el máximo de las 200.000 filas es agregar más los datos para poder así reducir la frecuencia de envíos por hora y poder almacenar más filas. Esto se puede conseguir ampliando las ventanas de tiempo o eliminando campos de los GROUP BY.

Monitorización

La mejor forma de medir la carga de trabajo de nuestro job es usando las métricas que nos proporciona el panel de Azure. Puedes consultar el porcentaje de utilización de las SU de tu job de ASA desde el menú «Metrics» del apartado «Monitoring» del job. Si el porcentaje de utilización de las SU pasa por encima del 80% es posible que se den retrasos en las salidas.

Gráfico con porcentaje de utilización de las SU

En el apartado de métricas podemos observar el porcentaje de utilización de las SU.

Además del porcentaje de utilización es conveniente que compruebes que el throughput del job (en eventos por segundo o bytes por segundo) es realmente el esperado. En caso de no serlo debes replantearte las particiones de entrada/salida, las TU, la query y las SU.

Conclusiones

A lo largo de este artículo hemos ido enumerando los elementos que intervienen en la escalabilidad de un job de ASA. No solo importa el número de SU utilizadas, lo primordial es partir el dato de una forma correcta para poder así conseguir un escalado horizontal.

Es el momento de que revises todos los elementos de tu arquitectura RT de Azure. Comprueba cada uno de los elementos comentados en este artículo y ajústalos a las necesidades de tu sistema 🙂

Si quieres saber más sobre ASA no dudes en preguntarnos por un curso de Analytics que lanzaremos dentro de poco. En ese curso de 3 días, además de dedicarle casi por completo una jornada a ASA y al procesamiento de datos en tiempo real, hablaremos de otras tecnologías relacionadas con Big Data y Machine Learning que ofrece Azure.

Referencias