Skip to content

Laboratório 1 - Introdução gRPC

Nesse laboratório vamos explorar a utilização do Framework gRPC para chamada de procedimento remoto. Além disso, também vamos utilizar protobuffer para criação de menssagens e serviços que serão utilizados pelo gRPC. Ao final, vamos explorar um cenário de treinamento de modelos utilizando uma arquitetura cliente servidor

O que é gRPC?

gRPC ou Google Remote Procedure Call é uma estrutura RPC moderna de alto desempenho e código aberto que pode ser executada em qualquer ambiente. Ele pode conectar serviços de forma eficiente dentro e entre data centers com suporte plugável para balanceamento de carga, rastreamento, verificação de integridade e autenticação

RPC ou chamadas de procedimento remoto são as mensagens que o servidor envia ao sistema remoto para executar a tarefa (ou subrotinas). O gRPC foi projetado para facilitar a comunicação suave e eficiente entre os serviços. Ele pode ser utilizado de diferentes maneiras, como: * Conectando serviços poliglotas de forma eficiente em arquitetura de estilo de microsserviços * Conectando dispositivos móveis, clientes de navegador a serviços de backend * Gerando bibliotecas de cliente eficientes

Para o exemplo deste laboratório, utilizaremos Python para desenvolvimento. Dessa forma, para usar o grpc devemos instalar os seguintes pacotes

pip install grpcio grpcio-tools sklearn

Note

O módulo sklearn será utilizado no exercício descrito no final do laboratório.

Utilizando Proto Buffer

Protocol Buffer é mecanismo eficiente e automatizado para serializar dados estruturados. Eles fornecem uma maneira de definir a estrutura dos dados a serem transmitidos (i.e., mensagens). O são melhores que XML, pois:

  • Mais simples
  • Até dez vezes menor
  • Até 100 vezes mais rápido
  • gera classes de acesso a dados que facilitam seu uso

Os protobuf são definidos em aquivos .proto, os quais são bastante simples de usar. Nesse arquivos, definimos as mensagems que serão trocadas entre as RPCs e também os serviços, que definem as mensagem de entrada e também mensagens de resposta. Após definir a estrutura do arquivo .proto podemos utilizar o compilador protoc para gerar os arquivos de stubs e também métodos de codificação e decodificação que serão utilizados pelo programa de forma automática para linguagem de programação desejada.

Para o exemplo do PingPong vamos definir o arquivo ping_pong.proto com o seguinte conteúdo:

syntax = "proto3";

package pingponggprc;

message Ping {
    string mensagem = 1;
}

message Pong {
    string mensagem = 1;
    double tempo = 2;
}

service PingPong{
    rpc GetServerResponse(Ping) returns (Pong) {}
}

Compilando arquivo proto

Após definir o arquivo .proto devemos compilá-lo utilizando o compilador de gRPC protoc. Esse compilador está presente no módulo grpc_tools e pode ser gerado utilizando o seguinte comando

python -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. ping_pong.proto

Note

como resultado o compilador irá gerar os códigos para o apêndice do servidor (i.e., server stub ) e também para o apêndice do cliente (i.e., client stub ) os quais são responsáveis por enviar e receber as mensagens entre as chamadas RPC e também codificar e decodificar os dados que serão transmitidos.

Implementado Código Servidor

Agora precisamos implementar o código do servidor. Para esse código, devemos implementar uma classe que implemente o método GetServerResponse que será responsável pela chamada definida no arquivo .proto, esse método recebe uma requisição (i.e., a mensagem enviada pelo cliente) e também um contexto que oferece funcionalidade para a conexão com o cliente. Além disso, devemos implementar o método serve() que ficará responsável por disponibilizar o serviço. Portanto, implementamos o seguinte código em um arquivo servidor.py:

import grpc
from concurrent import futures
import time

import ping_pong_pb2_grpc as pb2_grpc
import ping_pong_pb2 as pb2

class ExemploServer(pb2_grpc.PingPongServicer):

    def GetServerResponse(self, request, context):

        mensagem = request.mensagem
        resposta = f"Pong!"

        mensagem_resposta = {
            'mensagem' : resposta,
            'tempo'    : time.time()
        }

        return pb2.Pong(**mensagem_resposta)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    pb2_grpc.add_PingPongServicer_to_server(ExemploServer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    print("Server started at 50051")
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

Implementando Código Cliente

No cliente devemos implementar uma classe que se conecta no servidor e também invoca o método descrito no serviço passando a mensagem de entrada e recebendo uma mensagem de resposta. Dessa forma, para implementação do ping pong implementamos o seguinte código para o cliente em um arquivo cliente.py:

import grpc
import ping_pong_pb2 as pb2
import ping_pong_pb2_grpc as pb2_grpc
import time

class ExemploGRPC(object):

    def __init__(self):
        self.host = 'localhost'
        self.server_port = 50051
        self.channel     = grpc.insecure_channel(f"{self.host}:{self.server_port}")
        self.stub        = pb2_grpc.PingPongStub(self.channel)

    def get_ping_response(self, message):
        message = pb2.Ping(mensagem=message)

        return self.stub.GetServerResponse(message)

if __name__ == '__main__':
    client   = ExemploGRPC()
    mensagem = 'Ping!'
    while True:
        tempo = time.time()
        print(f'Cliente -> {mensagem} {tempo}')
        resposta = client.get_ping_response(mensagem)
        print(f'Servidor -> {resposta.mensagem} {resposta.tempo}')
        print(f'Duração Ping -> Pong: {time.time() - tempo}')
        print('--------------------------------')
        time.sleep(1)

Ping Pong

Por fim, para realizar o ping pong entre cliente servidor basta executar os respectivos códigos

Example

python servidor.py
python cliente.py

Exercício - Treinamento Modelos baseado em Cliente Servidor

Agora está na hora de colocar em prática o que foi abordado nesse laboratório. A idéia é realizar o treinamento de um modelo de aprendizado de máquina supervisionado para classificação (i.e, KNN, SVM, Decision Tree, Random Forest, etc). Os dados utilizados serão do dataset iris presente no sklearn, que pode ser importado utilizando

sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

iris      = load_iris()
atributos = iris.data
rotulos   = iris.target

x_treino, x_teste, y_treino, y_test = train_test_split(atributos, rotulos, test_size=0.2)
Para separar os dados de treino e teste (x_treino, x_teste, y_treino, y_teste), onde x corresponde aos atributos e y corresponde aos rótulos utilizaremos o train_test_split do sklearn. Mais detalhes sobre preparação dos dados para treinamento e avaliação serão apresentados nas próximas aulas.

Nesse exercício os dados estão disponíveis no cliente e o modelo está implementado no servidor, assim como apresentado na figura. Nesse cenário, o cliente deve encaminhar os uma mensagem com os dados para o servidor que deve treinar o modelo e retornar a acurácia de treinamento para os dados enviados via RPC.

Info

O código para treinamento do modelo no servidor será explicado detalhadamente na aula de revisão sobre aprendizado supervisionado e também na aula de treinamento de modelos.

Dessa forma, as seguintes tarefas devem ser realizadas:

  • Gerar arquivo .proto com as mensagens que serão utilizadas pelo serviço para treinamento e avaliação de modelo. Por exemplo, FitRequest, FitResponse, PredictRequest, PredictResponse
  • Compilar o arquivo .proto para gerar os arquivos de stubs do cliente e servidor
  • Implementar o código do cliente que deve solicitar o treinamento do modelo e também a predição para outros dados após o treinamento
  • Implementar o código do servidor que terá dois serviços, um para treinamento e outro para predição. O serviço de treinamento deve treinar o modelo e retornar a acurácia de treinamento, enquanto o serviço de predição deve receber uma amostra de dados e retornar a classificação para tal amostra
Tip

Para definir as mensagens que irá conter o dataset que será enviado para o servidor (i.e., array), você pode criar uma mensagem que represente uma amosta com dois campos, um utilizando repeated float para representar os atributos do dataset e outra int32 para representar o rótulo. Em seguida, você pode criar uma outra mensagem que é um conjunto de amostras usando repeated nome_da_mensagem Você pode utilizar qualquer classificador disponível no sklearn, como por exemplo

from sklearn.neighbors import KNeighborsClassifier
model = KNeighborsClassifier()
Para criar a mensagem para envio do dataset, você deve criar uma instância de uma amostra para cada linha do seu dataset e adiciona-lá na mensagem que representa o dataset todo como uma lista .append()

Para o treinamento do modelo no servidor você pode utilizar o seguinte código

#Treina o modelo para os dados entradas e labels recebidos
model.fit(input_data, input_labels)
#calcula a acurácia do modelo para os dados de treino.
accuracy = model.score(input_data, input_labels)
Para predição você pode usar o método model.predict(entrada) o qual retorna a predição (i.e., classe) para a entrada fornecida