5. Comunicação interna

Esta página descreve como cada serviço na dojot se comunica.

5.1. Componentes

Os principais componentes atuais no dojot são mostrados em Fig. 5.1.

[Auth]
[DeviceManager]
[Persister]
[History]
[DataBroker]
[FlowBroker]

package "Databases" {
  [mongodb]
  [postgreSQL]
}
package "IoT agents" {
  [IoT MQTT]
  [IoT LoRa]
  [IoT sigfox]
  [IoT RabbitMQ]
}

[postgreSQL] <-- [Auth]
[postgreSQL] <-- [DeviceManager]
[postgreSQL] <- [Kong]
[mongodb] <- [Persister]
[mongodb] <-- [FlowBroker]
[mongodb] <-- [History]

Fig. 5.1 dojot components

Eles são:

  • Auth: mecanismo de autenticação
  • DeviceManager: armazenamento de dispositivo e modelo.
  • Persister: componente que armazena todos os dados gerados pelo dispositivo.
  • History: componente que expõe todos os dados gerados pelo dispositivo.
  • DataBroker: lida com assuntos e tópicos do Kafka, além de conexões socket.io.
  • Flowbroker: lida com fluxos (CRUD e execução de fluxo)
  • IoT agents: agentes para diferentes protocolos.

Cada serviço será descrito brevemente nesta página. Mais informações podem ser encontradas na documentação de cada componente.

5.2. Mensagens e autenticação

Existem dois meios pelos quais os componentes dojot podem se comunicar: via solicitações HTTP REST e via Kafka. Eles são destinados a diferentes propósitos.

As solicitações HTTP podem ser enviadas no momento da inicialização quando um componente deseja, por exemplo, informações sobre recursos específicos, como lista de dispositivos ou tenants. Para isso, eles devem saber qual componente possui qual recurso para recuperá-los corretamente. Isso significa - e isso é muito importante porque conduz as escolhas arquiteturais na dojot - que apenas um único serviço é responsável por recuperar modelos de dados para um recurso específico (observe que um serviço pode ter várias instâncias). Por exemplo, o DeviceManager é responsável por armazenar e recuperar o modelo de informações para dispositivos e modelos, FlowBroker para descrições de fluxo, Histórico para dados históricos e assim por diante.

Kafka, por outro lado, permite uma comunicação pouco acoplada entre instâncias de serviços. Isso significa que um produtor (quem envia uma mensagem) não sabe quais componentes receberão sua mensagem. Além disso, qualquer consumidor não sabe quem gerou a mensagem de que está sendo ingerido. Isso permite que os dados sejam transmitidos com base em “interesses”: um consumidor está interessado em receber mensagens com um determinado assunto (mais sobre isso mais tarde) e os produtores enviarão mensagens para todos os componentes que estiverem interessados nele. Observe que esse mecanismo permite que vários serviços emitam mensagens com o mesmo “assunto”, bem como vários serviços que ingerem mensagens com o mesmo “assunto”, sem soluções alternativas complicadas.

5.2.1. Enviando solicitações HTTP

Para enviar solicitações via HTTP, um serviço deve criar um token de acesso, descrito aqui. Não há outras considerações além de seguir a descrição da API associada a cada serviço. Isso pode ser visto na figura Fig. 5.2. Observe que todas as interações descritas aqui são abstrações das reais. Além disso, deve-se notar que essas interações são válidas apenas para componentes internos. Qualquer serviço externo deve usar o Kong como ponto de entrada.

actor Client
boundary Kong
control Auth

Client -> Kong: POST /auth \nBody={"admin", "p4ssw0rD"}
activate Kong
Kong -> Auth: POST /user \nBody={"admin", "p4ssw0rD"}
Auth --> Kong: JWT="873927dab"
Kong --> Client: JWT="873927dab"
deactivate Kong

Fig. 5.2 Initial authentication

Nesta figura, um cliente recupera um token de acesso para o administrador do usuário cuja senha é p4ssw0rd. Depois disso, um usuário pode enviar uma solicitação para as APIs HTTP usando-o. Isso é mostrado na Fig. Fig. 5.3. Nota: o mecanismo de autorização real é detalhado Auth + API gateway (Kong).

actor Client
boundary Kong
control Auth
control DeviceManager
database PostgreSQL

Client -> Kong: POST /device \nHeaders="Authorization: Bearer JWT"\nBody={ device }
activate Kong
Kong -> Auth: POST /pep \nBody={"admin", "/device"}
Auth --> Kong: OK 200
Kong -> DeviceManager: POST /device \nHeaders="Authorization: JWT" \nBody={ "device" : "XYZ" }
activate DeviceManager
DeviceManager -> PostgreSQL: INSERT INTO ....
PostgreSQL --> DeviceManager: OK
DeviceManager --> Kong: OK 200
deactivate DeviceManager
Kong --> Client: OK 200
deactivate Kong

Fig. 5.3 Sending messages to HTTP API

Nesta figura, um cliente cria um novo dispositivo usando o token recuperado em Fig. 5.2. Essa solicitação é analisada por Kong, que chamará Auth para verificar se o usuário definido no token tem permissão para POST para o terminal /device. Somente após a aprovação dessa solicitação, Kong a encaminhará para o DeviceManager.

5.2.2. Enviando mensagens via Kafka

Kafka usa uma abordagem bem diferente. Cada mensagem deve ser associada a um assunto e um inquilino. Isso é mostrado na Fig. 5.4;

control DeviceManager
control DataBroker
database Redis
control Kafka

DeviceManager -> DataBroker: GET /topic/dojot.device-manager.devices \nHeaders="Authorization: Bearer JWT"
note left
  JWT contains the
  service associated
  to the subject
  (admin, for instance).
end note
activate DataBroker
DataBroker -> Redis: GET KEY\n"admin:dojot.device-manager.devices "
note left
  If the key does
  not exist, then
  it will be
  created.
end note
Redis --> DataBroker: 9d0352b7-d195-4852...
DataBroker -> Redis: GET KEY\n"profile-admin:dojot.device-manager.devices "
Redis --> DataBroker: { "topic-profile": { ... } }
DataBroker -> Kafka: CREATE TOPIC \n9d0352b7-d195-4852...\n{ "topic-profile": { ... } }
note left
  There's no need
  to recreate this
  topic if it is
  already created.
end note
Kafka -> DataBroker: OK
DataBroker --> DeviceManager: { "topic" : "9d0352b7-d195-4852..." }
deactivate DataBroker
DeviceManager -> Kafka: SEND MESSAGE\n topic:9d0352b7-d195-4852...\ndata: {"device": "XYZ", "event": "CREATE", ...}
Kafka --> DeviceManager: OK

Fig. 5.4 Retrieving Kafka topics

Neste exemplo, o DeviceManager precisa publicar uma mensagem sobre um novo dispositivo. Para isso, ele envia uma solicitação ao DataBroker, indicando qual tenant (dentro do token JWT) e qual assunto (dojot.device-manager.devices) deseja usar para enviar a mensagem. O DataBroker chamará o Redis para verificar se este tópico já foi criado e se o administrador dojot criou um perfil para essa tupla específica {tenant, subject}.

Os dois esquemas de perfis disponíveis são mostrados na Fig. 5.5 e na Fig. 5.6.

class IAutoScheme <<interface>> {
  + num_partitions: number;
  + replication_factor: number;
}

Fig. 5.5 Automatic scheme profile

O esquema automático define o número de partições Kafka a serem usadas para o tópico que está sendo criado, bem como o fator de replicação (quantas réplicas estarão disponíveis para cada partição de tópico). Cabe a Kafka decidir qual partição e réplica será atribuída a qual instância do broker. Você pode verificar Kafka partitions and replicas para conhecer um pouco mais sobre partição e réplicas. Claro que você pode conferir a Kafka’s official documentation.

class IAssignedScheme <<interface>> {
  + replica_assignment: Map<number, number[]>;
}

Fig. 5.6 Assigned scheme profile

O esquema atribuído indica qual partição será alocada para qual instância Kafka. Isso inclui também réplicas (partições com mais de uma instância Kafka associada).

5.2.3. Inicialização dos tenants

Todos os componentes estão interessados em um conjunto de assuntos, que serão usados para enviar ou receber mensagens de Kafka. Como a dojot agrupa tópicos do Kafka e tenants em assuntos (um assunto será composto por um ou mais tópicos Kafka, cada um transmitindo mensagens para um tenant específico), o componente deve iniciar cada tenant antes de enviar ou receber mensagens. Isso é feito em duas fases: tempo de inicialização do componente e tempo de execução do componente.

Na primeira fase, um componente solicita ao Auth para recuperar todos os tenants configurados no momento. Está interessado, digamos, em consumir mensagens dos assuntos device-data e dojot.device-manager.devices. Portanto, ele solicitará ao DataBroker um tópico para cada tenant para cada assunto. Com essa lista de tópicos, ele pode criar Produtores e Consumidores para enviar e receber mensagens através desses tópicos. Isso é mostrado na Fig. 5.7.

control Component
control Auth
control DataBroker
control Kafka

Component -> DataBroker: GET /topic/dojot.tenancy \nHeaders="Authorization: JWT"
DataBroker --> Component: {"topic" : "eca098e7f..."}
Component-> Auth: GET /tenants
Auth --> Component: {"tenants" : ["admin", "tenant1"]}
loop each tenant
  Component -> DataBroker: GET /topic/device-data \nHeaders="Authorization: JWT[tenant]"
  DataBroker --> Component: {"topic" : "890874987ef..."}
  Component -> Kafka: SUBSCRIBE\ntopic: 890874987ef...
  Kafka --> Component: OK
  Component -> DataBroker: GET /topic/dojot.device-manager.devices \nHeaders="Authorization: JWT[tenant]"
  DataBroker --> Component: {"topic" : "890874987ef..."}
  Component -> Kafka: SUBSCRIBE\ntopic: 890874987ef...
  Kafka --> Component: OK
end

Fig. 5.7 Tenant bootstrapping at startup

A segunda fase inicia após a inicialização e seu objetivo é processar todas as mensagens recebidas pelo Kafka. Isso incluirá qualquer tenant criado após todos os serviços estarem em funcionamento. Fig. 5.8 mostra como lidar com essas mensagens.

control Kafka
control Component
control DataBroker

Kafka -> Component: MESSAGE\ntopic:98797ce98af...\nmessage: {"tenant" : "new-tenant"}
Component -> DataBroker: GET /topic/device-data\nHeaders: "Authorization: Bearer JWT"
note left
  JWT contains
  new tenant
end note
DataBroker --> Component: OK {"topic" : "876ca876g7..."}
Component -> Kafka: SUBSCRIBE\ntopic: 876ca876g7...
Kafka --> Component: OK
Component -> DataBroker: GET /topic/dojot.device-manager.devices\nHeaders: "Authorization: Bearer JWT"
note left
  JWT contains
  new tenant
end note
DataBroker --> Component: OK {"topic" : "22432c4a..."}
Component -> Kafka: SUBSCRIBE\ntopic: 22432c4a...
Kafka --> Component: OK

Fig. 5.8 Tenant bootstrapping

Todos os serviços que estão de alguma forma interessados em usar assuntos devem executar este procedimento para receber corretamente todas as mensagens.

5.3. Auth + API gateway (Kong)

Auth é um serviço profundamente conectado ao Kong. É responsável pelo gerenciamento, autenticação e autorização do usuário. Como tal, é invocado por Kong sempre que uma solicitação é recebida por um de seus pontos de um dos endpoints. Esta seção detalha como isso é realizado e como eles funcionam juntos.

5.3.1. Configuração do Kong

Existem dois procedimentos de configuração ao iniciar o Kong na dojot:

  1. Migrando Dados Existentes
  2. Registrando endpoints e plug-ins de API.

A primeira tarefa é realizada simplesmente invocando Kong com uma bandeira especial.

O segundo é executado executando um script de configuração kong.config.sh. Seu único objetivo é registrar endpoints no Kong, como:

(curl -o /dev/null ${kong}/apis -sS -X POST \
    --header "Content-Type: application/json" \
    -d @- ) <<PAYLOAD
{
    "name": "data-broker",
    "uris": ["/device/(.*)/latest", "/subscription"],
    "strip_uri": false,
    "upstream_url": "http://data-broker:80"
}
PAYLOAD

Este comando registrará o endpoint /dispositivo/*/latest e /subscription e todas as solicitações serão encaminhadas para http//data-broker:80. Você pode verificar a documentação sobre como adicionar endpoints na Kong’s documentation.

Para alguns dos endpoints registrados, o kong.config.sh adicionará dois plug-ins aos endpoints selecionados:

  1. Geração JWT. A documentação para este plugin está disponível na Kong JWT plugin page.
  2. Configure um plug-in que encaminhará todas as solicitações de para o Auth. invocará Auth para autenticar solicitações. Este plugin está disponível no PEP-Kong repository.

A solicitação a seguir instala esses dois plug-ins na API do data-broker:

curl -o /dev/null -sS -X POST ${kong}/apis/data-broker/plugins -d "name=jwt"
curl -o /dev/null -sS -X POST ${kong}/apis/data-broker/plugins -d "name=pepkong" -d "config.pdpUrl=http://auth:5000/pdp"

5.3.1.1. Mensagens emitidas

O Auth emitirá apenas uma mensagem via Kafka para a criação do tenant:

{
  "type" : "CREATE",
  "tenant" : "XYZ"
}

5.4. Device Manager

O DeviceManager armazena e recupera modelos de informações para dispositivos e modelos e algumas informações estáticas sobre eles também. Sempre que um dispositivo é criado, removido ou apenas editado, ele publica uma mensagem no Kafka. Depende apenas do DataBroker e Kafka pelos motivos já explicados neste documento.

Todas as mensagens publicadas pelo Device Manager no Kafka podem ser vistas no Device Manager Device Manager mensagens.

5.5. Agente IoT

Os agentes de IoT recebem mensagens de dispositivos e os convertem em uma mensagem padrão a ser publicada em outros componentes. Para fazer isso, eles podem querer saber quais dispositivos são criados para filtrar corretamente as mensagens que não são permitidas na dojot (usando, por exemplo, informações de segurança para bloquear mensagens de dispositivos não autorizados). Ele usará o assunto device-data e a inicialização de tenants, conforme descrito em Inicialização dos tenants.

Após solicitar os tópicos para todos os tenants no assunto device-data, o agente da IoT começará a receber dados dos dispositivos. Como há várias maneiras pelas quais os dispositivos podem fazer isso, esta etapa não será detalhada nesta seção (isso depende muito de como cada agente de IoT funciona). No entanto, ele deve enviar uma mensagem para Kafka para informar outros componentes de todos os novos dados que o dispositivo acabou de enviar. Isso é mostrado na Fig. 5.9

control Kafka

IoTAgent -> Kafka: SEND MESSAGE\n topic:890874987ef...\ndata: IoTAgentMessage
Kafka -> IoTAgent: OK

Fig. 5.9 IoT agent message to Kafka

Os dados enviados pelo agente de IoT têm a estrutura mostrada na Fig. 5.10.

class Metadata {
  + deviceid: string
  + tenant: string
  + timestamp: long int
 }

 class IoTAgentMessage {
   + metadata: Metadata
   + attrs: Dict<string, any>
 }

 IoTAgentMessage::metadata -> Metadata

Fig. 5.10 IoT agent message structure

Essa mensagem seria:

{
    "metadata": {
        "deviceid": "c6ea4b",
        "tenant": "admin",
        "timestamp": 1528226137452
    },
    "attrs": {
        "humidity": 60,
        "temperature" : 23
    }
}

5.6. Persister

Persister é um serviço muito simples, cujo único objetivo é receber mensagens dos dispositivos (usando o assunto device-data) e armazená-las no MongoDB. Para isso, é realizado o procedimento de inicialização (detalhado em Inicialização dos tenants) e, sempre que uma nova mensagem é recebida, ele cria um novo documento Mongo e o armazena na coleção do dispositivo. Isso é mostrado na Fig. 5.11.

control Kafka
control Persister
database MongoDB

Kafka -> Persister: MESSAGE\ntopic:98797ce98af...\nmessage: IoTAgentMessage
Persister -> MongoDB: NEW DOC { IoTAgentMessage }
MongoDB --> Persister: OK
Persister --> Kafka: COMMIT

Fig. 5.11 Persister

Este serviço é simples, pois é por design.

5.7. History

O histórico também é um serviço muito simples: sempre que um usuário ou aplicativo envia uma solicitação, ele consulta o MongoDB e cria uma mensagem adequada para enviar de volta ao usuário/aplicativo. Isso é mostrado na Fig. 5.12.

actor User
boundary Kong
control History
database MongoDB

User -> Kong: GET /device/history/efac?attr=temperature\nHeaders="Authorization: JWT"
activate Kong
Kong -> Kong: authorize
Kong -> History: GET /history/efac?attr=temperature\nHeaders="Authorization: JWT"
activate History
History -> MongoDB: db.efac.find({attr=temperature})
MongoDB --> History: doc1, doc2
History -> History: processDocs([doc1, doc2])
History --> Kong: OK\n{"efac":[\n\t{"temperature" : 10},\n\t{"temperature": 20}\n]}
deactivate History
Kong -> User: OK\n{"efac":[\n\t{"temperature" : 10},\n\t{"temperature": 20}\n]}
deactivate Kong

Fig. 5.12 History

5.8. Data Broker

O DataBroker possui algumas funcionalidades a mais do que apenas gerar tópicos para pares {tenant, subject}. Ele também servirá conexões socket.io para emitir mensagens em tempo real. Para fazer isso, ele recupera todos os tópicos para o assunto device-data, assim como em qualquer outro componente interessado nos dados recebidos dos dispositivos. Assim que receber uma mensagem, ela será encaminhada para uma ‘sala’ (usando o vocabulário socket.io) associada ao dispositivo e ao tenant associado. Portanto, todo cliente conectado a ele (como interfaces gráficas de usuário) receberá uma nova mensagem contendo todos os dados recebidos. Para obter mais informações sobre como abrir uma conexão socket.io com o DataBroker, consulte a Documentação do DataBroker.