Aplicando arquitecturas de microservicios en sistemas empotrados

En este artículo vamos a continuar expandiendo las funcionalidades del servidor OPC-UA que programamos en el anterior artículo. En este caso demostraremos, con un caso muy sencillo desarrollado sobre lo que hemos hecho anteriormente, la posibilidad de emplear arquitecturas de microservicios en sistemas empotrados dotados de sistema operativo Linux, como la Beaglebone o Raspberry Pi.

La arquitectura de microservicios es un estilo o método de desarrollo software con gran auge en los últimos años. Su objetivo es el desarrollo de una aplicación como un conjunto de servicios independientes con funcionalidades definidas que interactúan entre sí para construir una funcionalidad más completa. Dichos servicios hacen uso de interfaces de comunicación simplificadas para realizar esta interacción y pueden desplegarse de manera independiente y desarrollarse en diversos lenguajes de programación [1]. Esto se diferencia de las aplicaciones monolíticas, donde toda la lógica se encuentra contenida en un único bloque que incorpora todas las funcionalidades necesarias.

Fuente: Edvantis blog

La utilización de una arquitectura de microservicios conlleva en sí mismo una serie de ventajas frente al desarrollo de una aplicación monolítica. La más clara es quizás la separación del ciclo de desarrollo de cada una de las funcionalidades, que puede ser implementada por equipos disjuntos y sufrir actualizaciones y modificaciones en su comportamiento sin depender del resto de funcionalidades del sistema. Por otro lado, también se mejora la reutilización de servicios ya desarrollados en diversos casos de uso en base a sus necesidades (p. ej. un servicio de envío de mails, un almacenamiento en bases de datos, etc.). En cuanto a escalabilidad, se pueden escalar simplemente aquellos servicios que lo necesiten en lugar de la aplicación completa y este modelo también permite controlar mejor los errores en el sistema ya que un fallo en un servicio, si se gestiona adecuadamente, no tiene por qué implicar una caída completa de la aplicación, como seguramente ocurriría en el caso monolítico.

No obstante, también se debe tener en cuenta que la aplicación de este paradigma tiene también varias desventajas que se deberán considerar antes de desarrollar una aplicación con una arquitectura de microservicios. En primer lugar, se ha de tener en cuenta que se está trabajando con un sistema distribuido en lugar de centralizado, lo que puede introducir complejidad a la hora de desarrollar, depurar y desplegar el servicio completo. Por otro lado, la separación en servicios y el uso de interfaces de comunicación entre los mismos introduce una sobrecarga que en el caso de las aplicaciones monolíticas no existía.

En el caso de los sistemas empotrados su aplicación puede ser muy ventajosa en los sistemas tipo Raspberry por muchos de los motivos ya expuestos, aunque deberemos analizar cuidadosamente su aplicación ya que puede ser contraproducente a nivel de rendimiento y espacio ocupado por los servicios dependiendo de la aplicación. A continuación presentamos la evolución del servidor OPC-UA, que se dividirá en tres servicios desacoplados (lectura de datos de sensor, servidor OPC-UA en sí y almacenamiento en base de datos local) desplegados sobre contenedores Docker [2].

ResinOS, sistema operativo para sistemas empotrados con tecnología de contenedores

Como en el artículo anterior, escogeremos como plataforma hardware una Beaglebone Black. Aunque en este caso para el despliegue de los servicios y como sistema operativo base emplearemos ResinOS [3]. ResinOS es un sistema operativo de código libre basado en Linux (más concretamente en Yocto [4]) creado específicamente para el despliegue de aplicaciones en forma de contenedores Docker. En la figura se puede ver la arquitectura básica de este sistema operativo, el cual tiene una base muy simple que parte del kernel de Linux y las adaptaciones adecuadas para la placa hardware en la que se despliega el sistema, además de una serie de utilidades básicas. Por encima de todo se encuentra Docker, para realizar el despliegue de los contenedores con las aplicaciones creadas por el usuario, que se cargan en la placa objetivo desde una máquina de desarrollo como un PC normal. Cabe destacar que el sistema de ficheros raíz de ResinOS es de sólo lectura, lo que facilita que no tenga estado y disminuye en gran medida las posibilidades de corrupción del mismo, algo especialmente interesante para el despliegue de sistemas empotrados en producción.

Fuente: ResinOS

Desarrollo de una aplicación basada en microservicios

Estructura general de la aplicación

Como ya se comentó en la introducción del artículo, la aplicación que vamos a desplegar en la Beaglebone será una evolución de un servidor OPC-UA para dotarlo de almacenamiento con una base de datos sencilla y separar la funcionalidad de lectura de datos del sensor de la implementación del propio servidor. Para la comunicación entre los microservicios emplearemos la librería gRPC [5], un framework de código abierto empleado para conectar servicios de manera eficiente. Para no complicar demasiado el contenido del artículo vamos a utilizar una librería de OPC-UA, en este caso node-opcua [6] que, como su propio nombre indica, está basada en NodeJS y por tanto emplea Javascript como lenguaje de programación.

La siguiente figura recoge por tanto la estructura que tendría la nueva aplicación, formada por tres microservicios y las relaciones entre los mismos. El microservicio de lectura de datos del sensor actuará como cliente, siendo el servidor el microservicio OPC-UA, al que le hará un “set” de los datos cada vez que estos cambien. A su vez, el microservicio OPC-UA actuará como cliente del  microservicio de base de datos, almacenando la información en Redis. Para ilustrar la posibilidad de emplear diversos lenguajes de programación para desarrollar cada uno de estos servicios en cada uno de ellos utilizaremos uno distinto (Python, Javascript y Go).

Configuración del entorno y dispositivo

En primer lugar deberemos configurar nuestro entorno de trabajo para poder utilizar los diversos lenguajes de programación (principalmente para generar los stubs de gRPC pues el código lo desplegaremos en contenedores). Para ello seguiremos las instrucciones que se proporcionan en los siguientes enlaces suponiendo que utilizamos Debian como sistema operativo base:

  • Python
  • Go (sustituyendo la versión 1.10 por la última disponible)
  • NodeJS

Instalación de ResinOS

A continuación, cargaremos el sistema operativo en nuestra placa. En este caso emplearemos una Beaglebone, por lo que seguiremos las instrucciones de este enlace. Una vez instalado el sistema ya podremos comenzar a desplegar los servicios de nuestra aplicación.

Microservicio de lectura de GPIO

Trabajaremos para cada microservicio dentro de su propio directorio, por lo que creamos uno para este microservicio:

$ mkdir gpio-service-python
$ cd gpio-service-python

A continuación deberemos instalar las utilidades de gRPC para Python:

$ sudo pip install grpcio-tools

Con esto podremos crear el código stub para un cliente y servidor gRPC en Python a partir del fichero de definición del servicio. Para el caso que nos concierne crearemos el siguiente fichero de definición del servicio:

$ touch gpio-service.proto

Abriremos dicho fichero con nuestro editor favorito e introduciremos el siguiente contenido, donde se define el servicio que se utilizará entre el servidor OPC-UA y este mismo servicio, con sus correspondientes mensajes de petición y respuesta:

syntax = "proto3";//Version de protobuf a utilizar

//Declaracion del servicio que pasara el numero de piezas contadas por el sensor de paso
//al servidor OPC-UA. Cada vez que una pieza se detecta se incrementa este contador y se envia
//al servidor.
service CounterSensor {

	//Metodo al que se le indica el numero de piezas actual y devuelve el resultado de la actualizacion
	rpc UpdatePiecesNumber(Counter) returns (Result) {}

}

//Mensaje para el comando que indica el numero de piezas actual
message Counter {
	int32 value = 1;
}

//Mensaje para indicar al cliente el resultado de la operacion de actualizacion
message Result {
	string value = 1;
}

El siguiente paso será generar el código Python a partir de la definición del servicio, configurando como directorios de salida y de localización del fichero de servicio el directorio en el que nos encontramos en ese momento:

$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. gpio-service.proto

De esta forma se generarán dos ficheros de código Python:

  • py que contiene el código de los mensajes de petición y respuesta (Counter y Result respectivamente).
  • py que contiene el código del cliente y servidor gRPC para el servicio especificado. En el caso de este servicio únicamente emplearemos la parte de cliente como se ha comentado anteriormente. En el siguiente apartado veremos cómo generar el código del servidor para el microservicio OPC-UA a partir de esta misma definición de servicio.

Con este código generaremos el cliente gRPC en un nuevo fichero:

$ touch gpio-client.py

Incluyendo el siguiente código inicial:

import grpc

import gpio_service_pb2_grpc
import gpio_service_pb2

counter = 4

# Abrimos un canal gRPC al servidor
channel = grpc.insecure_channel('localhost:50051')

# Creamos el stub del cliente
stub = gpio_service_pb2_grpc.CounterSensorStub(channel)

# Creamos un mensaje de actualizacion del valor del contador
request = gpio_service_pb2.Counter(value=counter)

# Realizamos la llamada al servidor
response = stub.UpdatePiecesNumber(request)

print(response.value)

El siguiente paso será actualizar este código para realizar la lectura del GPIO que nos dé el valor del sensor de paso.  El código actualizado es el siguiente, modificado para generar una petición gRPC con el valor de contador actualizado cada vez que se actualiza el valor del GPIO:

import grpc
import gpio_service_pb2_grpc
import gpio_service_pb2
import os.path
import sys
import time

counter = 0

# Configuramos el GPIO 60 como entrada y lo abrimos para leer el valor
try:

	if not os.path.exists("/sys/class/gpio/gpio60/"):
		setupGPIO = file("/sys/class/gpio/export", "w")
		setupGPIO.write("%d" % (60))
		setupGPIO.close()

		setupGPIO = file("/sys/class/gpio/gpio60/direction", "w")
		setupGPIO.write("in")
		setupGPIO.close()

except IOError:
	print("Error configurando el GPIO")
	sys.exit()

# Abrimos un canal gRPC al servidor
grpc_channel = grpc.insecure_channel('localhost:50051')

# Creamos el stub del cliente
stub = gpio_service_pb2_grpc.CounterSensorStub(grpc_channel)

isPassingSomething = False

# Bucle en el que leeremos periodicamente el valor del GPIO
while True:

	try:
		GPIO = file("/sys/class/gpio/gpio60/value", "r")
	except IOError:
		print("Error leyendo el GPIO")
	value = GPIO.read(1)

	if str(value) == '0':
		if isPassingSomething == False:
			counter+=1

			# Creamos un mensaje de actualizacion del valor del contador
			request = gpio_service_pb2.Counter(value=counter)

			# Realizamos la llamada al servidor
			response = stub.UpdatePiecesNumber(request)

			if response.value == 'OK':
				print("Valor actualizado correctamente")
			else :
				print("Error actualizando el valor")

			isPassingSomething = True

	else:
		isPassingSomething = False
	GPIO.close()
	time.sleep(0.05)

Finalmente, para poder desplegar la aplicación en ResinOS es necesario definir un Dockerfile que crearemos en el directorio del servicio:

$ touch Dockerfile

Y editaremos con el siguiente contenido, donde se copian los scripts de Python que hemos creado anteriormente y se ejecuta el cliente gRPC:

FROM resin/beaglebone-black-python

COPY gpio-client.py .
COPY gpio_service_pb2.py .
COPY gpio_service_pb2_grpc.py .

RUN pip install grpcio

CMD ["python","gpio-client.py"]

Dado que todavía no hemos desarrollado el servidor para este cliente gRPC pospondremos el lanzamiento del servicio hasta que finalicemos la programación de todos los servicios.

Microservicio servidor OPC-UA

Como con el anterior microservicio, trabajaremos en un directorio propio:

$ cd ..
$ mkdir opcua-server-node
$ cd  opcua-server-node

Este servicio, además de comunicarse con el que se encarga de leer los datos del GPIO, también lo hará con el que actuará a modo de base de datos para el almacenamiento de históricos, actuando como cliente. Por tanto, definiremos ya el fichero de definición de este servicio y sus mensajes:

$ touch storage-service.proto

Dicho fichero tendrá el siguiente contenido:

syntax = "proto3";

service DataStorage {

  rpc StoreData(ServerData) returns (Result) {}

}

message ServerData {
  int32 value = 1;
}

message Result {
  string value = 1;
}

Posteriormente, inicializamos node y cubrimos la información solicitada. Descargamos la librería node-opcua, la librería grpc, copiamos el fichero proto de definición del servicio al directorio actual y creamos el fichero en el que definiremos el servidor:

$ npm init
$ npm install node-opcua --save
$ npm install grpc --save
$ cp ../gpio-service-python/gpio-service.proto .
$ touch opcua-server.js

En el caso de NodeJS no es necesario realizar una generación previa de ficheros de código (aunque se podría hacer) ya que las propias funciones de la librería de gRPC permiten cargar los ficheros de definición y generar los métodos del servidor en tiempo de ejecución. El código del servidor, con la definición de los nodos OPC-UA que contiene, la funcionalidad de servidor gRPC para aceptar actualizaciones del contador de piezas y la funcionalidad de cliente gRPC para el almacenamiento de datos es el siguiente:

var opcua = require("node-opcua");
var grpc = require('grpc');

// Contador de piezas
var counter_sensor_value = 0;

// Especificamos la ruta al fichero con la definición del servicio de servidor
var PROTO_PATH_SERVER = __dirname+'/gpio-service.proto';

// Especificamos la ruta al ficher con la definición del servicio para el cliente
var PROTO_PATH_CLIENT = __dirname+'/storage-service.proto';

// Cargamos las definiciones de los servicios
var protoDescriptorServer = grpc.load(PROTO_PATH_SERVER);
var protoDescriptorClient = grpc.load(PROTO_PATH_CLIENT);

// Declaramos el servidor OPC-UA
var opcuaServer = new opcua.OPCUAServer({
    port: 4840 // puerto en el que escucha el servidor OPC-UA
});

// Callback llamado cada vez que se realiza un almacenamiento en base de datos
function storageCallback(error,result) {
  if (error) {
    console.log("Error almacenando los datos");
  } else {
    console.log("Almacenamiento en base de datos realizado");
  }
}

// Funcion del servidor gRPC a la que se llamara cada vez que el cliente quiera
// actualizar el valor del contador de piezas
function UpdatePiecesNumber(call, callback) {

  // Leemos el valor del contador
  counter_sensor_value = call.request.value;

  // Almacenamos el dato en remoto
  var data = {
    value: counter_sensor_value
  }
  client.StoreData(data,storageCallback);

  // Generamos la respuesta y la devolvemos
  var response = {
    value: 'OK'
  }

  callback(null,response);
}

// Configuramos el servidor gRPC
function getServer() {
  var server = new grpc.Server();
  server.addService(protoDescriptorServer.CounterSensor.service, {
    UpdatePiecesNumber: UpdatePiecesNumber
  });
  return server;
}

// Funcion en la que inicializamos el servidor OPC-UA y especificamos los nodos
// que tiene y sus variables
function post_initialize() {

  function construct_my_address_space(opcuaServer) {

      var addressSpace =opcuaServer.engine.addressSpace;

      // Declaramos un nuevo objeto para incluir la informacion
       var device = addressSpace.addObject({
           organizedBy: addressSpace.rootFolder.objects,
           browseName: "CounterSensor"
       });

       // Declaramos una variable dentro del objeto anterior que coge su valor del contador
       addressSpace.addVariable({
         componentOf: device,
            browseName: "Value",
            dataType: "UInt32",
            value: {
                get: function () {
                    return new opcua.Variant({dataType: opcua.DataType.UInt32, value: counter_sensor_value });
                }
            }
       });

  }
  construct_my_address_space(opcuaServer);
  opcuaServer.start(function() {
       console.log("Servidor inicializado ... ( presione CTRL+C para pararlo)");
       var endpointUrl = opcuaServer.endpoints[0].endpointDescriptions()[0].endpointUrl;
       console.log(" URL del endpoint ", endpointUrl );
   });

}

// Iniciamos los servidores gRPC y OPC-UA y nos conectamos al servidor gRPC
// de almacenamiento de históricos
var grpcServer = getServer();
var client = new protoDescriptorClient.DataStorage('localhost:50052', grpc.credentials.createInsecure());
grpcServer.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
grpcServer.start();
opcuaServer.initialize(post_initialize);

De nuevo, para poder desplegar el servicio en la Beaglebone deberemos crear un fichero Dockerfile:

$ touch Dockerfile

Y editarlo con el siguiente contenido:

FROM resin/beaglebone-black-alpine-node

RUN apk update && apk add --no-cache openssl

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY gpio-service.proto .
COPY storage-service.proto .
COPY opcua-server.js .

EXPOSE 4840
EXPOSE 50051

CMD ["node","opcua-server.js"]

Microservicio de base de datos

Como base de datos para el almacenamiento emplearemos Redis como un contenedor independiente a este microservicio. La tarea de este microservicio será por tanto proporcionar una interfaz a otros para facilitar almacenamiento. Dado que ya vamos a usar un contenedor que permite al acceso remoto a Redis, podría tener sentido que el propio servidor OPC-UA implementara en su código las instrucciones necesarias para hablar con Redis. No obstante, siguiendo el esquema que proponemos en este apartado, si en algún momento se quiere cambiar el método de almacenamiento (por ejemplo a otro tipo de base de datos como SQLite) únicamente sería necesario cambiar el código de este microservicio para adaptarlo. En caso contrario habría que modificar el código de todos los servicios que accedieran al almacenamiento.

En el caso de Go debemos ceñirnos a utilizar la estructura de directorios recomendada para desarrollar en el lenguaje, por lo que en primer lugar crearemos un directorio para alojar el código con las funciones del servidor gRPC que generaremos:

$ mkdir $GOPATH/src/storage_service_grpc

Copiamos el fichero de definición de servicio que creamos en la sección anterior a este directorio e instalamos la utilidad necesaria para realizar la compilación:

$ cp ../opcua-server-node/storage-service.proto $GOPATH/src/storage_service_grpc
$ cd $GOPATH/src/storage_service_grpc
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ sudo cp $GOPATH/bin/protoc-gen-go /usr/local/bin

Ejecutamos el siguiente comando, que generará el archivo storage-service.pb.go, para crear el código necesario en Go a partir de la definición del servicio:

$ protoc -I=. --go_out=plugins=grpc:. storage-service.proto

A continuación crearemos el fichero que incluirá el servidor gRPC y que contendrá las interacciones necesarias a realizar con Redis para el almacenamiento de datos. Será necesario también descargar una librería cliente de Redis para Go:

$ go get -u github.com/go-redis/redis
$ touch storage-service.go

Dicho fichero incluirá el siguiente código:

package storage_service

import(
  "log"
  "golang.org/x/net/context"
  "github.com/go-redis/redis"
)

// Estructura representando al servidor. En este caso no incluiremos ninguna
// variable ya que no son necesarias.
type Server struct {
}

// Funcion gRPC para el almacenamiento de los datos
func (s *Server) StoreData(ctx context.Context, in *ServerData) (*Result, error) {
  log.Printf("Receive message %s", in.Value)

  // Conexion con Redis
  client := redis.NewClient(&redis.Options{
		Addr:     "redis:6379",
	})

  // Almacenamiento del dato recibido en la lista contador
  err := client.LPush("contador",in.Value).Err()
  if err != nil {
    log.Printf("Error pushing to list")
  	panic(err)
	}

  return &Result{Value: "OK"}, nil
}

El siguiente paso será la creación de un archivo para la ejecución del programa principal. Para ello crearemos un nuevo directorio y un fichero main.go, además de instalar también la librería de gRPC:

$ mkdir $GOPATH/src/storage
$ cd $GOPATH/src/storage
$ go get -u google.golang.org/grpc
$ touch main.go

Y dentro del fichero main.go incluiremos el siguiente código fuente:

package main

import(
  "google.golang.org/grpc"
  "log"
  "net"
  "fmt"
  "storage_service"
)

func main() {

  // Creamos un servidor para escuchar las consultas de los clientes gRPC
  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 50052))
  if err != nil {
    log.Fatalf("Error escuchando: %v", err)
  }

  log.Printf("Servidor escuchando")

  // Instanciamos el servidor del codigo autogenerado
  s := storage_service.Server{}

  grpcServer := grpc.NewServer()

  log.Printf("Servidor gRPC creado")

  //Registramos el servicio del codigo autogenerado en el servidor gRPC creado
  storage_service.RegisterDataStorageServer(grpcServer, &s)

  log.Printf("Servicio de almacenamiento registrado")

  // Iniciamos el servidor
  if err := grpcServer.Serve(lis); err != nil {
    log.Fatalf("Error del servidor: %s", err)
  }
}

Este servicio también lo desplegaremos en la Beaglebone dentro de un contenedor Docker, pero procederemos de una manera un poco distinta a los casos anteriores, en los que creábamos un Dockerfile para compilar completamente la aplicación al cargarla en la placa. En este caso, para minimizar el tamaño del contenedor vamos a compilarlo de manera estática para incluir en un único binario todas las librerías necesarias, haciéndolo ya para arquitectura ARM (este proceso podríamos hacerlo también dentro de un contenedor que genere el binario utilizando por ejemplo la funcionalidad de multi-stage builds de Docker):

$ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o main .

Una vez que tenemos este binario deberemos retornar al directorio del proyecto inicial, donde estábamos trabajando y crear una nueva carpeta en la que copiaremos el binario y crearemos un fichero Dockerfile:

$ cd directorio_previo_del_proyecto
$ mkdir storage-service-go
$ cd storage-service-go
$ cp $GOPATH/src/storage/main .
$ touch Dockerfile

Finalmente, el Dockerfile que generaremos será muy sencillo, utilizando únicamente una imagen Alpine scratch para contener el servicio:

FROM hypriot/rpi-alpine-scratch

ADD main /

EXPOSE 50052

CMD ["/main"]

Una vez creados todos los servicios el único paso restante es desplegarlos en la Beaglebone.

Despliegue de los servicios

Para el despliegue de los servicios en la Beaglebone haremos uso de Docker Compose [7], una herramienta que permite definir aplicaciones multi-contenedor para facilitar su despliegue. Su uso está soportado por ResinOS desde hace pocos meses, con su introducción en la versión 2.12.0 [8]. Procederemos creando en primer lugar un fichero yaml en el raíz de nuestro proyecto en el que se incluye la definición de los distintos servicios y sus relaciones:

$ cd ..
$ touch docker-compose.yml

Su contenido será el que se expone a continuación:

version: '2'
services:

  redis:
    image: "arm32v7/redis"
    restart: always
    command: redis-server --appendonly yes
    container_name: redis
    ports:
      - 6379:6379 # Esta redireccion de puertos se hace unicamente para poder conectarse en remoto a Redis 
		  # y verificar que todo funciona. En un caso real NUNCA se deberia dejar abierto el puerto de Redis.
  storage:
    build:
      context: storage-service-go
    restart: always
    container_name: storage
    depends_on:
      - redis
  opcua-server:
    build:
      context: opcua-server-node
    restart: always
    container_name: opcua-server
    ports:
      - 4840:4840
    depends_on:
      - storage
  gpio-client:
    build:
      context: gpio-service-python
    restart: always
    container_name: gpio-client
    depends_on:
      - opcua-server

Para realizar el despliegue simplemente deberemos ejecutar el siguiente comando, donde “nombre_host” es el que hayamos configurado al crear la imagen de ResinOS, y hacer una pausa para tomar un café, pues el despliegue de algunos contenedores (en especial el del servidor OPC-UA) llevará su tiempo: host

$ DOCKER_API_VERSION=1.22 DOCKER_HOST=tcp://nombre_host:2375 docker-compose up -d

Cuando el despliegue finalice podemos probar que todo funciona correctamente conectándonos a Redis en remoto desde nuestro PC y verificando que la lista de valores se actualiza cada vez que el sensor de paso detecta un objeto. Lo haremos de la siguiente manera:

$ sudo apt-get install redis-tools
$ redis-cli -h nombre_host -p 6379

Dentro del cliente con el siguiente comando veremos la lista de valores del contador:

$ lrange contador 0 -1

Conclusiones

A lo largo de este artículo hemos demostrado cómo ya es posible llevar los modelos de arquitectura de microservicios basados en contenedores, muy utilizados en el mundo de la nube, a los sistemas empotrados. Para ello hemos desarrollado una demostración sobre placas de prototipado tipo Raspberry Pi o Beaglebone. Como hemos comentado anteriormente, la utilización de este tipo de arquitectura, aunque tiene múltiples ventajas, también tiene una serie de desventajas. Analicemos algunos puntos sobre la aplicación que acabamos de desarrollar.

Tamaño de las imágenes

Si nos conectamos a la Beaglebone por SSH (deberemos estar utilizando la imagen de desarrollo de ResinOS) y ejecutamos el siguiente comando podremos ver el tamaño de las imágenes Docker generadas:

$ balena images

Que se recoge en la siguiente tabla:

Imagen

Lenguaje

Tamaño

gpio-clientPython498MB
opcua-serverJavascript595MB
storageGo16.3MB
redisN/A83.3MB

Como se puede observar, el tamaño final de la aplicación desarrollada es de unos nada despreciables 1,16GB, algo completamente desorbitado para una aplicación a desplegar en un sistema empotrado, en el que está ocupando más de una cuarta parte de su memoria disponible en el caso de una Beaglebone. No obstante, debemos destacar la diferencia entre el tamaño de las imágenes en las que empleamos Python y Javascript con aquella en la que empleamos Go. Aunque seguramente podríamos tomar varias decisiones para minimizar su tamaño, el de esta última es mucho más acorde con lo esperado para un sistema empotrado de este tipo. Por tanto, debemos ser muy cuidadosos a la hora de escoger los lenguajes y las imágenes base para utilizar este tipo de arquitecturas en sistemas empotrados. Habría sido más recomendable por ejemplo emplear la librería open62541 [9], en C, para desarrollar el servidor OPC-UA y código C o Go también para la lectura de los GPIO.

Complejidad del desarrollo

En el caso del ejemplo desarrollado en este artículo hemos empleado gRPC como mecanismo para posibilitar la comunicación entre los microservicios. Esto incluye una complejidad adicional en el ciclo de desarrollo pues es necesario realizar una definición concreta de todos los servicios y mensajes y generar el código adicional para la serialización e interacción con los servicios. Este proceso podría simplificarse a través de la utilización de otros mecanismos para la comunicación entre los servicios como por ejemplo MQTT o el propio Redis. No obstante, hay que tener en cuenta que a través de gRPC estamos formalizando unos servicios concretos con un formato de mensaje específico, mientras que MQTT o Redis son más laxos en ese sentido.

Escalabilidad

La división del código en microservicios permite escalar las funcionalidades desarrolladas con mayor facilidad. Por ejemplo, imaginemos que el almacenamiento asignado a Redis en una Beaglebone se agota, con esta arquitectura sería posible redirigir las peticiones de almacenamiento a otra Beaglebone con el mismo servicio situada dentro de la misma red. Sin embargo debemos de ser de nuevo cuidadosos con las funcionalidades que desarrollamos y cómo lo hacemos. En el ejemplo hemos creado un microservicio para la lectura de un GPIO pero, ¿tendría sentido replicar este microservicio cada vez que leamos un GPIO? La respuesta es no. Hacer esto causaría una alta ineficiencia, no sólo por el tamaño de las imágenes generadas sino también por la sobrecarga en las comunicaciones. En este caso, la solución más viable sería extender nuestro servicio para que permitiera suscribirse a las actualizaciones de varios GPIOs por ejemplo, sin replicarlo para cada uno de ellos.

Dentro de la línea de Internet de las Cosas de Gradiant, estamos trabajando con este tipo de arquitecturas para facilitar el desarrollo de nuevos servicios basados en tecnologías para sistemas empotrados, sobre todo para industria y sector primario, con el objetivo de facilitar su desarrollo y despliegue, minimizar los tiempos de instalación y configuración y ayudar en la orquestación de dichos servicios según las necesidades inherentes a cada caso de uso.

Referencias

[1] https://martinfowler.com/articles/microservices.html

[2] https://www.docker.com/

[3] https://resinos.io/

[4] https://www.yoctoproject.org/

[5] https://grpc.io/

[6] http://node-opcua.github.io/

[7] https://docs.docker.com/compose/

[8] https://resin.io/blog/multicontainer-on-resin-io-is-here/

[9] https://open62541.org/

 

 

 


Autor: Daniel García Coego, responsable técnico IoT y CPS en el área de Sistemas Inteligentes en Red (INetS) de Gradiant