gRPC como um protocolo de comunicação entre serviços. Relatório Yandex

O gRPC é uma estrutura de código aberto para chamada de procedimento remoto. No Yandex.Market, o gRPC é usado como uma alternativa mais conveniente ao REST. Sergey Fedoseenkov, que executa o serviço de desenvolvimento de ferramentas para parceiros de mercado, compartilhou sua experiência de usar o gRPC como um protocolo para criar integrações entre os serviços Java e C ++. No relatório, você aprenderá como evitar problemas comuns se começar a usar o gRPC após o REST, como retornar erros, implementar rastreamento, depurar consultas e testar chamadas de clientes. No final, há um registro não oficial do relatório.

- Primeiro, gostaria de apresentar alguns fatos sobre o Yandex.Market, que serão úteis como parte do relatório. Primeiro fato: escrevemos serviços em diferentes idiomas. Isso impõe os requisitos do cliente para serviços.

E se tivermos um serviço em Java, seria bom se o cliente fosse, por exemplo, também um plus ou um pequeno.



Todos os serviços que temos são independentes, não há grandes lançamentos planejados para todo o mercado. Os microsserviços serão lançados independentemente e a compatibilidade com versões anteriores é importante para nós aqui, para que o protocolo o suporte.

O terceiro fato: temos integração síncrona e assíncrona. No relatório, falarei principalmente sobre síncrono.

O que usamos? Agora, é claro, a base de nossas integrações são serviços REST ou semelhantes a REST que trocam XML / JSON sobre HTTP 1.1. Também existe XML-RPC - nós o usamos principalmente na integração com o código Python, ou seja, o Python possui um servidor XML-RPC embutido. É conveniente implementá-lo lá, e nós o apoiamos.

Certa vez, tivemos o CORBA. Felizmente, nós a abandonamos. Agora principalmente REST e XML / JSON sobre HTTP.



As integrações síncronas têm problemas com os protocolos existentes. Encontramos esses problemas e tentamos tratá-los com o gRPC. Quais são esses problemas? Como eu disse, quero ter clientes em diferentes idiomas. É aconselhável que eles ainda não tenham que ser escritos por nós mesmos. E, em geral, seria legal se o cliente pudesse ser síncrono e assíncrono - dependendo dos objetivos do usuário do serviço.

Eu também gostaria do protocolo que usamos para suportar a compatibilidade com versões anteriores o suficiente: isso é muito importante em versões independentes paralelas. Todos os nossos lançamentos são compatíveis com versões anteriores, não interrompemos o feedback. Se você o quebrou, isso é um bug e você só precisa corrigi-lo o mais rápido possível.

Também é necessária uma abordagem coerente ao tratamento de erros: todos que criaram serviços REST sabem que você não pode apenas usar o status HTTP. Eles geralmente não permitem uma descrição detalhada do problema, é necessário inserir alguns de seus status, seus detalhes. Nos serviços REST, todos apresentam sua própria implementação desses erros, sempre que você precisa trabalhar de maneira diferente com isso. Isso nem sempre é conveniente.

Eu também gostaria de ter um gerenciamento de tempo limite no lado do cliente. Novamente, aqueles que trabalham com HTTP entendem que, se definirmos um tempo limite no lado do cliente e ele expirar, o cliente deixará de aguardar a conclusão da solicitação, mas o servidor não saberá nada sobre ela e continuará a executá-la. Além disso, no meio, existem vários proxies que definem tempos limite globais. E o cliente pode simplesmente não saber nada sobre eles e configurá-los nem sempre é trivial.

E, finalmente, o problema da documentação. Nem sempre é claro onde obter a documentação para recursos REST ou para alguns métodos, quais parâmetros eles aceitam, qual corpo pode ser transferido e como comunicar essa documentação com os consumidores do serviço. É claro que existe o Swagger, mas também não é tudo trivial.

gRPC Teoria


Eu gostaria de falar sobre a parte teórica do gRPC - o que é, quais são as idéias. E então vamos seguir praticando.



Em geral, o gRPC é uma especificação abstrata. Ele descreve uma RPC abstrata (chamada de procedimento remoto), ou seja, uma chamada de procedimento remoto que possui determinadas propriedades. Agora vamos listá-los. A primeira propriedade é o suporte para chamadas únicas e streaming. Ou seja, todos os serviços que implementam essa especificação suportam ambas as opções. O próximo item é a disponibilidade de metadados, ou seja, para que, juntamente com a carga útil, você possa passar algum tipo de metadado - condicionalmente, cabeçalhos. E - suporte para cancelar uma solicitação e tempos limite fora da caixa.

Ele também pressupõe que a descrição das mensagens e dos próprios serviços seja realizada por meio de uma determinada linguagem de definição de interface ou IDL. A especificação também descreve o protocolo de conexão por HTTP / 2, ou seja, o gRPC assume que ele funciona apenas por HTTP / 2.



Existe uma implementação típica de gRPC usada na maioria dos casos. Também o usamos e agora vamos vê-lo. O formato proto é usado como IDL. O plug-in gRPC para o compilador proto permite que você obtenha as fontes dos serviços gerados a partir da descrição do proto. E existem bibliotecas de tempo de execução em diferentes linguagens - Java, C ++, Python. Em geral, quase todos os idiomas populares são suportados, existem bibliotecas de tempo de execução para eles. E como as mensagens trocadas entre os serviços, uma mensagem proto é usada, mensagens estilizadas de acordo com o esquema protobuf.



Quero mergulhar um pouco em alguns recursos específicos. Aqui estão eles. Digitação forte, ou seja, uma mensagem proto, é uma mensagem fortemente digitada. Aqueles que trabalharam com o protobuf sabem que lá você pode descrever os campos em sua mensagem com tipos. Existem tipos primitivos e string, matrizes de bytes. Eles podem ser escalares, podem ser vetoriais. E, de fato, as mensagens podem, como um campo, conter outras mensagens, o que é bastante conveniente, em geral, qualquer modelo pode ser representado.



Sobre compatibilidade com versões anteriores. Gostaria de observar que o proto IDL é um formato no qual a compatibilidade com versões anteriores é disponibilizada, ou seja, foi concebida com uma reserva de compatibilidade com versões anteriores, e o Google lançou uma versão do proto3 que, em comparação com o proto2, melhora ainda mais a compatibilidade com versões anteriores. Além disso, existem todos os tipos de especificações, como e o que pode ser alterado para que a compatibilidade com versões anteriores seja preservada em alguns casos não triviais.

Existe a possibilidade de valores padrão, você pode adicionar novos campos e o consumidor não precisa alterar nada. Todos os campos no proto3 são opcionais e, por exemplo, podem ser excluídos e o acesso ao campo remoto não causa erros no cliente.



Outro recurso do gRPC é que o cliente e o servidor são gerados usando o compilador proto e o plug-in gRPC com base na descrição do proto. Há uma possibilidade no momento em que o código está sendo gravado para escolher qual cliente será usado. Ou seja, escolha um cliente assíncrono ou síncrono, dependendo do tipo de código que você escreve. Por exemplo, um cliente assíncrono é muito adequado para código reativo. E essa oportunidade é para qualquer idioma. Ou seja, depois de escrever uma proto-descrição, você poderá gerar um cliente para qualquer idioma e não precisará desenvolvê-los separadamente de alguma forma. Você pode distribuir a interface para o seu serviço simplesmente como uma proto-descrição. Qualquer consumidor pode gerar um cliente para si mesmo.



Sobre o cancelamento da solicitação e os prazos, gostaria de observar que a solicitação pode ser cancelada no servidor e no cliente. Se entendermos tudo, não precisamos atender mais à solicitação, então podemos cancelá-la. É possível definir um tempo limite a pedido. No gRPC, a maioria das bibliotecas de tempo de execução usa um prazo como conceito de tempo limite. Mas, de fato, é o mesmo. Ou seja, é o momento em que a solicitação deve ser concluída.

E o mais interessante é que o servidor pode descobrir o cancelamento da solicitação e a expiração do tempo limite e parar de executar a solicitação de lado. Isso é muito legal, parece-me que não há muito mais.

Sobre a documentação, gostaria de observar que, como o formato proto é usado no IDL para gRPC, esse é um código comum. Lá você pode escrever comentários, incluindo os muito detalhados. E você precisa entender que, para integrar-se ao seu serviço, seus usuários precisam ter esse proto-formato em sua casa, e eles os receberão juntamente com os comentários, eles não estarão em outro lugar. É muito conveniente E você pode expandir essa descrição, ou seja, é um recurso tão conveniente que a documentação vem ao lado do código, assim como pode estar ao lado de métodos na forma de javadoc ou qualquer outro comentário.

chamada unária de gRPC. Prática


Vamos seguir em frente, veja um pouco de prática. E o exemplo mais básico de uso do gRPC é a chamada chamada unária ou chamada única. Este é um esquema clássico - enviamos uma solicitação ao servidor e obtemos uma resposta do servidor. Parece que isso funciona em HTTP.



Considere o exemplo do serviço de eco que prestamos. O servidor será gravado em vantagens, o cliente em Java. O circuito de balanceamento clássico foi usado aqui. Ou seja, o cliente endereça o balanceador e, em seguida, o balanceador já seleciona um back-end específico para processar a solicitação.

Eu queria prestar atenção - como o gRPC funciona em HTTP / 2, uma conexão TCP é usada. Além disso, vários fluxos passam por ele. Aqui você pode ver que a conexão entre o cliente e o balanceador é estabelecida uma vez e permanece persistente e, em seguida, o balanceador equilibra a carga em back-end diferentes para cada chamada. Se você olhar, acontece assim e assim se as mensagens forem distribuídas.



Aqui está um código de amostra para o nosso arquivo proto. Você pode perceber que primeiro descrevemos a mensagem, ou seja, temos o EchoRequest e o EchoResponse. Ele possui apenas um campo de sequência que armazena a mensagem.

A segunda etapa, descrevemos nosso procedimento. O procedimento de entrada aceita EchoRequest, retorna EchoResponse como resultado, tudo é bastante trivial. Esta é a descrição do serviço gRPC e das mensagens que serão perseguidas.




Vamos ver como isso está indo no caso de vantagens, por exemplo. É montado em três etapas. No primeiro estágio, nossa tarefa é gerar fontes de mensagens. Com essa equipe, estamos fazendo isso. Chamamos o proto-compilador, passamos o arquivo-proto para a entrada, indicamos onde colocar os arquivos de saída.

A segunda equipe. Também geramos serviços da mesma maneira. A única diferença com o comando anterior é que passamos o plug-in e, com base na descrição, que está no formato proto, gera serviços.

A terceira etapa - coletamos tudo isso em um binário para que nosso servidor possa ser iniciado.

Um sinalizador adicional é passado para o vinculador, chamado grpc ++ _ reflection. Quero observar - o servidor gRPC tem esse recurso, reflexão do servidor. Permite explorar que tipo de serviços, chamadas RPC e mensagens que o serviço possui. Por padrão, está desativado e você pode acessar o serviço apenas se tiver um proto-formato em mãos. Mas, por exemplo, para depuração, é muito conveniente, sem o proto-formato disponível, basta ligar o servidor com o recurso de reflexão e receber informações imediatamente.




Agora vamos ver a implementação. A implementação também é minimalista. Ou seja, nossa principal tarefa é implementar o serviço de eco gerado. Ele tem um método getEcho. Ele apenas gera mensagens e as envia de volta. Status OK - status de sucesso.

Em seguida, criamos o ServerBuilder, registramos nosso serviço nele, que construímos um pouco mais alto.




Agora, apenas começamos e aguardamos as solicitações recebidas.





Agora vamos ver o cliente em Java. Eu coleciono gradle. Nossa tarefa é conectar o plug-in protobuf primeiro.

Há um conjunto básico de dependências que precisamos arrastar para o nosso serviço; elas são necessárias no estágio de compilação.

Também quero observar que existe uma biblioteca de tempo de execução. Para Java, ele usa netty como servidor e cliente, suporta HTTP / 2, é bastante conveniente e de alto desempenho.

Em seguida, configuramos o compilador proto. O próprio compilador não precisa ser instalado localmente para Java; ele pode ser retirado de artefatos.

A mesma coisa com os plugins. Localmente para Java, não é necessário. Você pode arrastar um artefato. E é importante simplesmente configurá-lo para que todos os shuffles também sejam chamados, para que stubs sejam gerados.





Vamos para o código Java. Aqui somos os primeiros a criar o esboço do nosso serviço. Essa é a nossa tarefa para o Java fornecer Canal. Há um ChannelBuilder na biblioteca de tempo de execução com o qual podemos construir este canal. Aqui, ativamos manualmente o texto sem formatação para simplificar, mas o HTTP2 e o gRPC criptografam tudo por padrão e usam o TLS.

Temos um esboço do nosso cliente, um cliente síncrono é gerado aqui. Da mesma forma, você pode gerar um cliente assíncrono, existem outras opções.

Em seguida, criamos nossa solicitação de protobuff, ou seja, construímos uma mensagem protobuff.





Isso é tudo, envie-o. Em nosso cliente, chamamos getEcho e imprimimos o resultado. Tudo é simples. Como você pode ver, é necessário um pouco de código e a integração é criada.

streaming de gRPC. Prática


Agora, vamos olhar para uma coisa mais avançada, isso é streaming. Vou lhe dizer como funciona e, mais tarde, como usá-lo.



O cliente-servidor de streaming parece arquiteturalmente o mesmo. Ou seja, temos uma conexão persistente entre o cliente e o balanceador. Então as diferenças começam. A essência do streaming é que o cliente está conectado a algum back-end final e a conexão é salva. Ou seja, continua assim. E assim Aqui, gostaria de observar separadamente que o uso de um balanceador não é típico para streaming, ou seja, você precisa entender que as solicitações de streaming podem durar bastante. Ou seja, você pode abri-los e trocar mensagens por um longo tempo. E essas mensagens passarão pelo balanceador, mas, na verdade, sempre vão para o mesmo back-end. E não está muito claro por que é necessário.

Uma prática comum é quando um serviço, por exemplo, é puramente streaming, ou principalmente streaming, e a descoberta de serviço é usada. O GRPC possui um ponto de extensão em que a descoberta de serviço pode ser incorporada.



O que precisamos para implementar serviços de streaming? Temos o mesmo formato proto. Estamos adicionando outro RPC e aqui você pode perceber que adicionamos duas palavras-chave antes da solicitação e antes da resposta. Assim, declaramos os fluxos EchoRequest e EchoResponse.




O mais interessante começa. Nossa compilação não muda de forma alguma para os serviços de streaming. Nossa próxima tarefa é substituir nosso novo método em nosso serviço Echo, que funcionará com fluxos. No caso do servidor, tudo isso é um pouco mais fácil. Ou seja, podemos ler constantemente do fluxo e responder a alguma coisa. Nós podemos responder de forma assíncrona. Ou seja, eles são independentes, transmitem para escrever e transmitem para ler, e aqui tudo é simples para um cenário simples.



Aqui está a leitura agora, aqui está a gravação.




Nos clientes Java, as coisas são um pouco mais complicadas. Lá você não pode usar nenhuma API síncrona, ou seja, ela simplesmente não funciona com fluxos. E a API assíncrona é usada. Ou seja, nossa tarefa é implementar o modelo Observer. Existe uma interface StreamObserver lá. Ele contém três métodos: onNext, onCompleted e onError. Aqui, por simplicidade, eu implementei apenas o Next. Ele se contrai apenas quando a resposta chega do servidor.




Aqui, coloquei uma fila para enviar mensagens entre threads.



Qual a diferença? Em vez de blockStStub, apenas criamos newStub. Esta é uma implementação assíncrona que pode funcionar apenas com o Observer. De fato, você pode fazer chamadas unárias ao Observer, apenas não tão conveniente. Nós, pelo menos, não o usamos de maneira tão ativa.

Em seguida, construímos nosso Observador.

E fazemos nossa ligação RPC. Passamos o ResponseObserver para a entrada e, na saída, emite RequestObserver para nós. Além disso, podemos fazer chamadas no RequestObserver, transmitindo mensagens para o servidor. E o nosso ResponseObserver irá se contrair e processar as mensagens.

Aqui está um exemplo. Estamos apenas fazendo uma ligação. Ligue para a próxima, passe Solicitação lá.

Além da fila, esperamos que o servidor responda e imprima.





Quero chamar a atenção para o fato de que nossa tarefa aqui, como as pessoas responsáveis ​​pela implementação do streaming, é lidar corretamente com o fechamento deste RequestObserver. Ou seja, em caso de erro, devemos chamar o método onError e, em caso de conclusão bem-sucedida, quando acreditamos que o fluxo pode ser fechado, devemos chamar o método onCompleted.



Nós seguimos em frente. Quais são os aplicativos de streaming? Isso é algo mais avançado, não o fato de ser diretamente útil a todos, mas às vezes ser usado. Ou seja, o primeiro é o download e o upload de grandes quantidades de dados. O servidor ou cliente pode produzir dados em algumas partes. Essas partes já podem, de alguma forma, ser agrupadas no cliente ou no servidor. Ou seja, você já pode fazer otimizações adicionais aqui.

Além disso, o esquema de streaming é adequado para envio por servidor. Você precisa entender que eu considerei a opção mais extrema quando temos streaming bidirecional. E talvez fluindo em uma direção. Por exemplo, de cliente para servidor ou de servidor para cliente. No caso de um servidor para um cliente, podemos nos conectar a algum servidor, e ele enviará pushies para nós e, para isso, não precisaremos pesquisar regularmente.

A próxima vantagem do streaming é vinculativa a uma máquina. Como eu disse, uma conexão de ponta a ponta será estabelecida para todas as mensagens dentro do fluxo, e essa conexão será vinculada a uma máquina e, definitivamente, não será alterada em lugar algum. Portanto, é possível, em primeiro lugar, simplificar algo, algum tipo de sincronização entre servidores e, além disso, você pode fazer coisas transacionais.

E o streaming bidirecional, apenas um exemplo que mostrei, é a capacidade de criar alguns dos meus próprios protocolos. Coisa interessante o suficiente. Temos filas internas no Yandex que usam apenas o fluxo bidirecional. E se de repente alguém tiver essas tarefas, haverá uma oportunidade suficientemente boa para usá-lo.

, . . . , - , , . . gRPC .


, gRPC.



, . - . gRPC . , , , , , . , runtime- . , . , OK, runtime- .

, Java . . google.rpc.Status 3 : , . , . , . — , , .

error details, , . : , , , stack traces, . , .

— , HTTP , ? . BadRequest . , , error details, .

. , , BadRequest - ( ), - error detail. , , , - . , .



. . , , , . - - , - - , , . . , , Zipkin. , HTTP , — metadata. .

, . , - , , , .

runtime-, - , . Java ClientInterceptor ServerInterceptor. , , . , , , , , - . , - API - . , , , , - . , gRPC, , - . , , - , , , .



- . -. Java . , , - . - , .



. gRPC — . HTTP/2 . - , ? : , . . , gRPC grpc_cli, curl. , . , -, . , gRPC , .

, evans. , CLI: , , , . , . - , , , , , .

- UI — , Postman, — BloomRPC. Postman . Postman, , , . , BloomRPC , .

- , . , , grpc_cli. . , . , , . , . , - - . — .



, , gRPC. . - , - , . Swagger. , HTTP/1 . OpenAPI , . . , HTTP/2, Swagger — .

WSDL — , . . Swagger, , . . -.

, , , , JAX-RS, Java . .

Twirp. ? Go, . . , , Go , gRPC Twirp. ? , gRPC — , , , IDL . proto- , gRPC-. protoc, , .

Twirp . proto- , HTTP/1.1 , JSON. , Twirp Go. , , Java Jetty. , .



? gRPC — REST . , , , , HTTP/2 balancer. service discovery, . gRPC , . .

gRPC — , . CLI, UI. , .

, gRPC. inter-process-. , sidecar pattern. , . , . , -. - , , -. . , , , , - .

, . gRPC . , . , unary-. , .

:
C gRPC — , . , , , .
Awesome gRPC — GitHub . , , , . . — , .

Você pode encontrar muitos outros recursos na Internet, em alguns slides. Mas eu gostei mais disso. O código ligeiramente modificado da apresentação está aqui . Obrigada

Gravação informal de relatórios

Source: https://habr.com/ru/post/pt484068/


All Articles