¿Qué aprenderá?
¿Qué hará?
¿Cuáles son los prerrequisitos?
En este tutorial se usan dos aplicaciones IoT, funcionalmente iguales, pero diferentes en el diseño e implementación de la capa de datos. Las referenciamos como aplicación Postgre y aplicación Timescale teniendo en cuenta la tecnología de base de datos que usan. Postgre es una base relacional y Timescale una base de series de tiempo. Adicionalmente, la aplicación Postgre usa un patrón de diseño llamado esquema estrella y la aplicación Timescale un patrón llamado blob. El objetivo es comprender el efecto que tienen las decisiones de diseño e implementación en el rendimiento de las aplicaciones.
Postgre es una base de datos relacional open-source. Actualmente, es de los sistemas más populares entre bases de datos al lado de MySQL y SQL Server. Postgre es usada en grandes empresas como Spotify, Apple o la NASA.
Timescale es una base de datos de series de tiempo (open-source) basada en Postgre. Esta tecnología aprovecha lo mejor de ambos mundos: el rendimiento sobre consultas de datos basados en series de tiempo y la robustez de Postgre como base de datos relacional. Timescale es usada por Prometheus una tecnología para ingestar y consultar grandes volúmenes de datos de monitoreo.
Timescale agrega índices adicionales y compresiones de datos sobre la tabla que tiene los registros relacionados al tiempo. En el caso de este tutorial se trabajará sobre la tabla de las muestras captadas por los sensores.
En la siguiente tabla se encuentra una síntesis de las características principales de cada una de las aplicaciones:
Características | Postgre | Timescale |
Tipo | Relacional | Relacional, optimizada para series de tiempo |
Patrón de diseño | Esquema estrella | Blob |
Licenciamiento | Open-source | Open-source |
Tabla 1. Características principales de Postgre y Timescale
Encuentre más información sobre Postgre y Timescale en los siguientes enlaces:
Este tutorial ha sido construido para permitirle explorar el código de las aplicaciones con el fin de entender cómo se implementan los patrones de diseño de la capa de datos IoT en las tecnologías Postgre y Timescale. Luego, es necesario desplegar las aplicaciones en AWS y probarlas con Jmeter para comparar resultados de desempeño (latencia). La siguiente figura ilustra el despliegue que usaremos en el tutorial. En la parte derecha de la figura se ven dos máquinas EC2, en una se ejecutará una versión de la aplicación REMA implementada con Postgre y en la otra una versión de la aplicación REMA implementada con Timescale. Por su parte, en la izquierda está su máquina personal desde donde se envían peticiones al backend ya sea desde JMeter o desde el navegador.
Figura 1. Diagrama de despliegue del tutorial
El código que explicaremos se encuentra en este repositorio:
https://github.com/SELF-Software-Evolution-Lab/Realtime-Monitoring-webApp
En el repositorio existen varias ramas; entre esas "Postgre" y "Timescale", las cuales se usarán en este tutorial. Las dos ramas presentan diferencias en los siguientes aspectos, que se explicarán más adelante:
Los diagramas de los modelos de datos de ambas aplicaciones se presentan a continuación. Analícelos e identifique sus semejanzas y diferencias.
Postgre:
Figura 2. Modelo de datos: Postgre
Timescale:
Figura 3. Modelo de datos: Timescale
Con respecto a los modelos de datos, note que son casi iguales en el número de entidades, atributos y relaciones, sin embargo se diferencian en la entidad "Data". En Postgre se crea un registro en la base de datos por cada muestra y en Timescale se crean registros donde cada uno tiene compresión de varias muestras tomadas en un periodo de tiempo. En Postgre, la entidad Data es la tabla principal del patrón de diseño estrella, de allí hay foreign keys a las tablas "Station" y "Measurements". Vale la pena notar que en la tabla "Data" de Postgre se almacena un registro por muestra colectada. En cambio, en Timescale se está usando el patrón de diseño Blob, donde se almacenan en cada registro dos aspectos: 1) en la lista de valores "values" se almacenan muestras tomadas en un periodo de tiempo; y 2) en la lista "times" se almacenan los tiempos en los que se toma cada muestra. De esta manera la base de datos Timescale crecerá mucho más lento de manera vertical que la base Postgre sencilla. Esto tiene un impacto en el desempeño que descubriremos a lo largo del tutorial.
También, el modelo Timescale tiene 2 variables adicionales que hacen referencia al tiempo: "base_time" y "time":
Por su parte para la configuración de Timescale, la rama correspondiente ("Timescale") tiene un archivo adicional que se ejecutará únicamente al momento de hacer las migraciones a la base de datos. El archivo se llama to_timescale.py
y se encuentra bajo la carpeta migrations
. A continuación se muestra el script SQL que configura la base de datos Timescale.
# Crea la hipertabla de timescale con chunks de 3 días.
migrations.RunSQL(
"SELECT create_hypertable('\"realtimeGraph_data\"', 'time', chunk_time_interval=>259200000000);"
),
# Configura la compresión para estaciones y variables. Son llaves foráneas de la tabla principal.
migrations.RunSQL(
"ALTER TABLE \"realtimeGraph_data\" \
SET (timescaledb.compress, \
timescaledb.compress_segmentby = 'station_id, measurement_id, time');"
),
# Comprime los datos cada 7 días.
migrations.RunSQL(
"SELECT add_compression_policy('\"realtimeGraph_data\"', 604800000000);"
),
Primero, se le indica a Timescale que la tabla que tiene las muestras con los valores y tiempos es "realtimeGraph_data". Timescale la crea como hipertabla con la función create_hypertable
. Una hipertabla permite tener hasta cientos/miles de columnas. Adicionalmente, el llamado a la función se debe parametrizar con el nombre de la columna ("time") que contiene el tiempo del registro y también el tamaño de los chunks expresado en tiempo. Un chunk es una partición de datos, en este caso la partición se hace por la dimensión "time" lo que quiere decir que cada chunk va a tener muestras de tres días consecutivos. En el script se expresan estos días en microsegundos porque la columna "time" está en esa unidad de tiempo. Uno de los propósitos de armar chunks es comprimir los datos de cada chunk y controlar el crecimiento vertical de la base Timescale.
Luego, en la segunda sentencia ("ALTER TABLE..."), se agrega una column list donde se van a comprimir por cada muestra los foreign keys a las tablas de "Station" y "Measurement" y también "time". Esto permite saber dada una muestra cuál fue el dispositivo que la colectó y a qué variable del ambiente hace referencia (por ej., temperatura, humedad, etc.).
Por último, se configura con qué frecuencia se hace la compresión de chunks, en este caso se hace cada 7 días, recuerde que está expresado en microsegundos en el script.
Este script se ejecuta al momento de correr el comando python manage.py migrate
que está más adelante en los pasos de instalación de la app Web.
A continuación puede ver las diferencias principales en el código de inserción de las aplicaciones Postgre y Timescale.
Inserción de datos patrón estrella (Postgre):
def create_data(value: float, station: Station, measure: Measurement):
data = Data(value=value, station=station, measurement=measure)
data.save()
station.last_activity = data.time #tiempo de creación del registro
station.save()
return(data)
Inserción de datos patrón Blob (Timescale):
def create_data(
value: float,
station: Station,
measure: Measurement,
time: datetime = timezone.now(),
):
base_time = datetime(time.year, time.month, time.day,
time.hour, tzinfo=time.tzinfo)
ts = int(base_time.timestamp() * 1000000) # ts se usa para inicializar "time", se multiplica por 1 millón para que quede en microsegundos
secs = int(time.timestamp() % 3600) # el módulo se usa para hallar los segundos que han pasado desde el inicio de "base_time", esto es el desfase
data, created = Data.objects.get_or_create(
base_time=base_time, station=station, measurement=measure, defaults={
"time": ts,
}
)
if created:
values = []
times = []
else:
values = data.values
times = data.times
values.append(value)
times.append(secs)
length = len(times)
data.max_value = max(values) if length > 0 else 0
data.min_value = min(values) if length > 0 else 0
data.avg_value = sum(values) / length if length > 0 else 0
data.length = length
data.values = values
data.save()
station.last_activity = time
station.save()
return data
A primera vista, la diferencia en la inserción de datos de Postgre y Timescale se evidencia en el número de líneas de código. En el primer fragmento de código (Postgre) el programa sólo necesita crear un registro "Data" y guardarlo, mientras que con Blob se tiene que hacer un procesamiento adicional: se trata de recuperar un registro "Data" cuyo tiempo coincida con la variable "base_time" y si no existe dicho registro se crea. Luego, agrega el valor y el tiempo de la muestra a las listas "values" y "times" y se calculan unos valores agregados, a saber: mínimo, máximo y promedio de los valores. Estos valores son pre-calculados y almacenados en la base para optimizar el desempeño de algunas consultas. Al final, se guarda el registro en la base de datos.
Por último, puede ver las diferencias principales en el código de consulta de las aplicaciones Postgre y Timescale.
Consulta de datos patrón estrella (Postgre):
data = []
for location in locations:
stations = Station.objects.filter(location=location)
locationData = Data.objects.filter(
station__in=stations, measurement__name=selectedMeasure.name, time__gte=start.date(), time__lte=end.date())
if locationData.count() <= 0:
continue
minVal = locationData.aggregate(
Min('value'))['value__min']
maxVal = locationData.aggregate(
Max('value'))['value__max']
avgVal = locationData.aggregate(
Avg('value'))['value__avg']
data.append({
'name': f'{location.city.name}, {location.state.name}, {location.country.name}',
'lat': location.lat,
'lng': location.lng,
'population': stations.count(),
'min': minVal if minVal != None else 0,
'max': maxVal if maxVal != None else 0,
'avg': round(avgVal if avgVal != None else 0, 2),
})
Consulta de datos patrón Blob (Timescale):
data = []
# se filtran los registros cuyo "time" está en el rango (start_ts, end_ts), se multiplica por 1 millón para que quede en microsegundos que es la unidad de "time"
start_ts = int(start.timestamp() * 1000000)
end_ts = int(end.timestamp() * 1000000)
for location in locations:
stations = Station.objects.filter(location=location)
locationData = Data.objects.filter(
station__in=stations, measurement__name=selectedMeasure.name, time__gte=start_ts, time__lte=end_ts,
)
if locationData.count() <= 0:
continue
minVal = locationData.aggregate(Min("min_value"))["min_value__min"]
maxVal = locationData.aggregate(Max("max_value"))["max_value__max"]
avgVal = locationData.aggregate(Avg("avg_value"))["avg_value__avg"]
data.append(
{
"name": f"{location.city.name}, {location.state.name}, {location.country.name}",
"lat": location.lat,
"lng": location.lng,
"population": stations.count(),
"min": minVal if minVal != None else 0,
"max": maxVal if maxVal != None else 0,
"avg": round(avgVal if avgVal != None else 0, 2),
}
)
La consulta de los datos es muy similar en los dos escenarios. Esta consulta busca por cada locación (desde donde se están colectando datos, por ej., Bogotá), el valor mínimo, máximo y promedio de un "Measurement" (por ej., temperatura, humedad) en esa ciudad. Esta consulta se usa en la aplicación REMA para generar la visualización "datos por ciudad" en un rango de fechas ("start", "end") establecido por el usuario.
Lo que cambia usando Blob es que el filtro de fecha se hace con el timestamp en microsegundos pues es como está configurada la columna "time" en Timescale. También, cambia el cálculo de las diferentes agregaciones como el mínimo, el máximo y el promedio. En el caso de Blob, estos cálculos no se realizan sobre los valores sino sobre las agregaciones ya precalculadas en la inserción de cada registro.
Note que en la consulta de Blob no es necesario recorrer las listas "values" internas de cada registro ya que en la inserción se calculó el mínimo, máximo y promedio de los valores de cada lista. Esto tiene un impacto en el desempeño de las inserciones y consultas en Timescale.
El siguiente paso después de explorar el código es crear las bases de datos. Para esto necesitará el archivo de CloudFormation con el que AWS creará la infraestructura necesaria. En resumen, el archivo tiene instrucciones para crear dos EC2 (máquinas virtuales de AWS), en una de ellas se desplegará una base de datos relacional (Postgre) y en la otra una base de series de tiempo (Timescale).
Realice estos pasos para crear las bases de datos:
Figura 4. Interfaz de inicio del laboratorio
wget https://raw.githubusercontent.com/SELF-Software-Evolution-Lab/Realtime-Monitoring-webApp/main/tutoriales/Capa%20de%20Datos/IOT-Capa-Datos.template.json -O template --no-check-certificate
aws cloudformation create-stack --stack-name iot --template-body file://template --capabilities "CAPABILITY_IAM"
Dicho comando debe retornar en consola una salida como la siguiente:
{
"StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/broker-mqtt/330b0120-1771-11e4-af37-50ba1b98bea6"
}
Figura 5. Inicio de la consola de AWS
Figura 5. Stacks creados en CloudFormation
Figura 6. Máquina EC2 creada y corriendo
Host:
Port: 5432
Database: iot_data
User: dbadmin
Password: uniandesIOT1234*
Para más información sobre cómo conectarse a una base desde DBVisualizer consulte la documentación: https://confluence.dbvis.com/display/UG130/Create+a+New+Database+Connection
Teniendo las bases de datos listas, hay que conectarlas con las aplicaciones REMA que contienen el código previamente explicado. Estas aplicaciones son las que se someterán a las pruebas de carga después. Repita las instrucciones que damos a continuación en ambas máquinas virtuales.
Figura 7. Ingreso al detalle de una máquina EC2
Figura 8. Detalle de una máquina EC2
Figura 9. Ventana de conexión a una máquina EC2
Como resultado se abrirá la consola de la máquina EC2 con su información clave como id
, nombre
e ipmaquina
debajo de ella, como se muestra en la imagen que sigue con el recuadro rojo.
Figura 10. Consola de una máquina EC2
Para Postgre:
~$ wget https://github.com/SELF-Software-Evolution-Lab/Realtime-Monitoring-webApp/raw/main/tutoriales/Capa%20de%20Datos/postgresMonitoring.zip -O server.zip
Para Timescale:
~$ wget https://github.com/SELF-Software-Evolution-Lab/Realtime-Monitoring-webApp/raw/main/tutoriales/Capa%20de%20Datos/timescaleMonitoring.zip -O server.zip
server.zip
) en cada máquina, éste se encontrará en la carpeta de trabajo. Descomprima el archivo con el comando:~$ unzip server.zip
realtimeMonitoring/
, instale el ambiente y actívelo con los siguientes comandos~$ cd realtimeMonitoring/
~/realtimeMonitoring/$ pipenv install
~/realtimeMonitoring/$ pipenv shell
(realtimeMonitoring) ~/realtimeMonitoring/$ python3 manage.py makemigrations
(realtimeMonitoring) ~/realtimeMonitoring/$ python3 manage.py migrate
Al ejecutar los comandos, en caso de que se generen errores de tipo "No module named xxx", utilice los siguientes comandos para instalar los módulos que generan el inconveniente:
pip3 install Django
pip3 install Django-crontab
pip3 install psycopg2
pip3 install ldap3
pip3 install django_cron
pip3 install requests
Con las aplicaciones instaladas en las máquinas EC2 y conectadas a las bases de datos, el siguiente paso es generar datos de prueba.
Para observar mejor la diferencia entre los patrones de diseño implementados y las tecnologías de bases de datos se necesita de una gran cantidad de datos. En esta parte se generarán 500000 muestras aleatorias para poblar la base de datos. Este proceso puede tomar un momento por lo que sí necesita tener más tiempo las máquinas EC2 activas, recuerde que en la interfaz de AWS Academy, puede restablecer el temporizador de su sesión presionando "Start lab"nuevamente.
Figura 11. Interfaz de inicio del laboratorio
(realtimeMonitoring) ~/realtimeMonitoring/$ python manage.py generate_data
Durante la primera ejecución del comando generate_data
podrá observar que se están generando los datos. El sistema automáticamente se detendrá cuando los datos generados lleguen al número de muestras mencionado previamente. En algunos casos y dependiendo de la conectividad, la generación de los datos podría tomar un tiempo considerable (por ej., 60 minutos). Si este es el caso, se recomienda interactuar con la consola AWS de conexión a la instancia (por ej., haciendo clic sobre esta) para evitar que se desconecte de la máquina por inactividad. Si por algún motivo la máquina se desconecta y no se han generado todos los datos, debe volver a ejecutar la instrucción anterior. Pero antes, asegúrese de haber activado el ambiente virtual dentro de ~/realtimeMonitoring/
a través del comando:
pipenv shel
l
(realtimeMonitoring) ~/realtimeMonitoring/$ python manage.py runserver 0.0.0.0:8000
es la que obtuvo en el paso "Instalar la aplicación Web". Entre a la aplicación REMA, luego a la opción "Datos por ciudad" y ajuste el filtro de fecha con días de junio y julio 2021, ya que los datos que se generaron son de esas fechas. Observe el resultado en el mapa.Figura 12. Captura de pantalla de la tabla "realtimeGraph_data" de Timescale en la herramienta DBVisualizer
Como son bastantes registros este proceso puede tomar un tiempo y el tiempo es diferente para Postgre y Timescale. ¡Mientras se generan los datos puede adelantar otra actividad!
Para esta sección, asegúrese de tener instalada la herramienta Jmeter en su computador personal. Si no lo tiene, puede acceder al sitio web de Apache para descargar la herramienta, posteriormente debe descomprimir el archivo.
Además, descargue el script de pruebas JMeter. El script tiene dos pruebas una para la aplicación Postgre y otra para la aplicación Timescale. Cada prueba consulta las muestras del "Measurement" temperatura dentro de un intervalo de tiempo (01/07/2021 - 31/07/2021). Hemos parametrizado cada prueba para enviar 60 peticiones en un período de 1 segundo. Hemos escogido 60 usuarios porque es el número promedio de estudiantes que usan la aplicación REMA. El paso a paso para ejecutar las pruebas es el que sigue:
apache-Jmeter-5.4.3/bin/
:Para Windows:
Abrir el archivo Jmeter.bat
Para Linux:
Ejecutar el archivo Jmeter.
Para Mac:
Ejecutar el archivo Jmeter
con click derecho (abrir y confirmar la ventana emergente si sale).
Figura 13. Interfaz de inicio de Jmeter
ip_Postgre
e ip_Timescale
por las IPs correspondientes que obtuvo en el paso "Instalar la aplicación Web". Al abrir el archivo, los valores por defecto son localhost
, sin embargo, debe cambiarlos por las IPs de AWS. Ej: 3.227.21.145Figura 14. Configuración de prueba en Jmeter
Figura 15. Resumen de estado de las pruebas en Jmeter
Las siguientes preguntas lo invitan a reflexionar sobre lo que observó en el desarrollo de los pasos inmediatamente anteriores referidos como "Generar datos de prueba" y "Probar las aplicaciones Web":
Para responder los porqués piense en las características de los patrones de diseño y las tecnologías de bases de datos empleadas en el tutorial. ¡Traiga sus reflexiones a las sesiones sincrónicas!
Al finalizar este tutorial, se espera que haya evidenciado los beneficios y limitaciones en desempeño de usar ciertos patrones de diseño y bases de datos en un sistema IoT.
Juan Avelino, Kelly Garcés | Autores |
Rocío Héndez, Andrés Bayona | Revisores |