Создаём быстрые gRPC-сервисы с Tonic и Rust

от автора

Привет, Хабр!

Сегодня посмотрим, как с помощью фреймворка Tonic и языка Rust создавать gRPC-сервисы для задач машинного обучения. Если в вашем проекте нужно максимально эффективно строить распределённые системы, а производительность и асинхронное программирование — это то, что вы цените, то Rust в связке с Tonic станет отличным инструментом

Установка Tonic

Первое, что нужно сделать, это создать новый проект на Rust. Для этого открываем терминал и вводим:

cargo new my_grpc_service cd my_grpc_service

Теперь открываем файл Cargo.toml. Добавим Tonic и некоторые другие необходимые библиотеки.

[dependencies] tonic = "0.12.2" prost = "0.11" tokio = { version = "1", features = ["full"] }
  • Tonic — основной фреймворк для gRPC.

  • Prost — библиотека для кодирования и декодирования Protocol Buffers.

  • Tokio — асинхронный runtime для Rust, который нам нужен для работы с Tonic.

Теперь добавим необходимые флаги функций для Tonic. Это нужно для включения всех необходимых возможностей. В Cargo.toml должно быть что-то вроде этого:

[dependencies] tonic = { version = "0.12.2", features = ["transport", "codegen", "tls"] } prost = "0.11" tokio = { version = "1", features = ["full"] }  [build-dependencies] tonic-build = "0.12.2"
  • transport — включает поддержку HTTP/2.

  • codegen — включает кодогенерацию из .proto файлов.

  • tls — для работы с безопасными соединениями.

Теперь, когда есть зависимости, создадим базовую структуру нашего проекта.

mkdir proto

Создадим файл hello.proto в этой папке и добавим следующий код:

syntax = "proto3";  package hello;  service Greeter {     rpc SayHello (HelloRequest) returns (HelloResponse); }  message HelloRequest {     string name = 1; }  message HelloResponse {     string message = 1; }

Этот простой протокол описывает сервис Greeter, который принимает HelloRequest и возвращает HelloResponse.

Теперь нужно сгенерировать код из .proto файла. Для этого создаем файл build.rs в корне проекта с содержимым:

fn main() {     tonic_build::compile_protos("proto/hello.proto").unwrap(); }

Этот код заставит tonic-build скомпилировать наш протокол при сборке проекта.

Теперь, когда у нас есть всё готово, пора собрать проект. Выполняем команду:

cargo build

Если всё сделано правильно, то проект скомпилируется без ошибок.

Создание gRPC-сервиса

Начнем с того, что нужно создать .proto файл, в котором определим наши сообщения и сервисы. Возьмем тот же hello.proto, который мы создали ранее, и немного расширим его. Допустим, нужно добавить функционал для приветствия пользователей и получения списка пользователей:

syntax = "proto3";  package hello;  service Greeter {     rpc SayHello (HelloRequest) returns (HelloResponse);     rpc ListUsers (ListUsersRequest) returns (ListUsersResponse); }  message HelloRequest {     string name = 1; }  message HelloResponse {     string message = 1; }  message ListUsersRequest {}  message ListUsersResponse {     repeated string users = 1; }

Здесь добавили новый метод ListUsers, который возвращает список пользователей. Теперь сгенерируем код.

Выполняем команду сборки:

cargo build

Это сгенерирует Rust-модули на основе .proto файла.

Теперь создаем файл src/main.rs и добавляем следующий код:

use tonic::{transport::Server, Request, Response, Status}; use hello::greeter_server::{Greeter, GreeterServer}; use hello::{HelloRequest, HelloResponse, ListUsersRequest, ListUsersResponse};  pub mod hello {     include!("generated.hello.rs"); }  #[derive(Default)] pub struct MyGreeter {}  #[tonic::async_trait] impl Greeter for MyGreeter {     async fn say_hello(         &self,         request: Request,     ) -> Result, Status> {         let name = request.into_inner().name;         let reply = HelloResponse {             message: format!("Hello, {}!", name),         };         Ok(Response::new(reply))     }      async fn list_users(         &self,         _request: Request,     ) -> Result, Status> {         let users = vec!["Alice".to_string(), "Bob".to_string(), "Charlie".to_string()];         let reply = ListUsersResponse { users };         Ok(Response::new(reply))     } }  #[tokio::main] async fn main() -> Result<(), Box> {     let addr = "[::1]:50051".parse()?;     let greeter = MyGreeter::default();      Server::builder()         .add_service(GreeterServer::new(greeter))         .serve(addr)         .await?;      Ok(()) }

MyGreeter: реализация сервиса Greeter. Реализуем методы, определенные в нашем .proto файле.

say_hello: метод обрабатывает запрос на приветствие. Он извлекает имя из HelloRequest, формирует ответ и возвращает его.

list_users: метод возвращает список пользователей. Просто создаем вектор со строками и оборачиваем его в ListUsersResponse.

В Tonic обработка запросов и ответов происходит с помощью типов Request и Response. Эти типы позволяют нам работать с данными, поступающими от клиента, и формировать ответы, которые будут отправляться обратно.

Request: Этот тип содержит данные, полученные от клиента, а также метаданные. Можно извлечь основное сообщение с помощью метода into_inner().

let name = request.into_inner().name;

Response: Этот тип используется для формирования ответов. Создаем экземпляр Response, передавая в него данные, которые хотите вернуть клиенту.

let reply = HelloResponse {     message: format!("Hello, {}!", name), }; Ok(Response::new(reply))

Чтобы запустить сервер, просто выполняем команду:

cargo run

Если всё прошло успешно, вы увидите, что сервер запущен на localhost:50051.

Немного оптимизации

Управление размером сообщений

Первый шаг к оптимизации — это управление размерами сообщений.

В Tonic есть возможность задавать максимальные размеры входящих и исходящих сообщений. По дефолту лимит составляет 4 МБ для входящих сообщений и usize::MAX для исходящих.

Добавим следующую конфигурацию в реализацию сервера:

use tonic::{transport::Server, Request, Response, Status}; use hello::greeter_server::{Greeter, GreeterServer}; use hello::{HelloRequest, HelloResponse};  #[tokio::main] async fn main() -> Result<(), Box> {     let addr = "[::1]:50051".parse()?;     let greeter = MyGreeter::default();      Server::builder()         .max_decoding_message_size(10 * 1024 * 1024) // 10 МБ         .max_encoding_message_size(10 * 1024 * 1024) // 10 МБ         .add_service(GreeterServer::new(greeter))         .serve(addr)         .await?;      Ok(()) }

Здесь установили пределы в 10 МБ для входящих и исходящих сообщений.

Сжатие — ещё один мощный инструмент оптимизации. Tonic поддерживает сжатие сообщений с использованием алгоритмов gzip и zstd.

Для начала нужно будет включить сжатие в конфигурации сервера. Вот как это можно сделать:

use tonic::{transport::Server, Request, Response, Status}; use hello::greeter_server::{Greeter, GreeterServer}; use hello::{HelloRequest, HelloResponse};  #[tokio::main] async fn main() -> Result<(), Box> {     let addr = "[::1]:50051".parse()?;     let greeter = MyGreeter::default();      let compression = tonic::Compression::Gzip;      Server::builder()         .compression(compression)         .add_service(GreeterServer::new(greeter))         .serve(addr)         .await?;      Ok(()) }

Теперь сервер будет использовать сжатие gzip для всех сообщений. Если нужно использовать zstd, просто заменяем tonic::Compression::Gzip на tonic::Compression::Zstd.

На стороне клиента можно включить сжатие:

let mut client = GreeterClient::connect("http://[::1]:50051")     .await?     .accept_compressed(tonic::Compression::Gzip)     .await?;

Таким образом, при отправке данных они будут автоматически сжиматься, а при получении — распаковываться.

Tonic построен на основе tokio, что позволяет использовать асинхронные потоки для обработки запросов.

Допустим, нужно обрабатывать запросы с задержкой, чтобы эмулировать взаимодействие с удаленной БД или API. Можно использовать tokio::time::sleep для создания задержки:

#[tonic::async_trait] impl Greeter for MyGreeter {     async fn say_hello(         &self,         request: Request,     ) -> Result, Status> {         let name = request.into_inner().name;          // Имитация задержки         tokio::time::sleep(std::time::Duration::from_secs(2)).await;          let reply = HelloResponse {             message: format!("Hello, {}!", name),         };         Ok(Response::new(reply))     } }

Так сервер может оставаться отзывчивым и обрабатывать другие запросы, пока ожидается завершение длительной операции.


Подробнее с Tonic можно ознакомиться здесь.

Изучить продвинутые ML приемы для практикующих Data Scientists, желающих повысить свой профессиональный уровень до Middle+, можно на онлайн‑курсе «Machine Learning. Advanced».


ссылка на оригинал статьи https://habr.com/ru/articles/845212/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *