gRPC-микросервис на tonic

от автора

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

Сегодня мы рассмотрим, как поднять gRPC‑микросервис на tonic и обвязать его аутентификацией плюс метриками через Tower‑middleware.

Зачем gRPC в Rust-экосистеме микросервисов?

gRPC уверенно занимает свою нишу в Rust‑экосистеме микросервисов, потому что даёт то, чего не хватает REST — настоящую пропускную способность, строгую типизацию и стабильную двустороннюю коммуникацию. По бенчмаркам, при серьёзной параллельной нагрузке (500 клиентов и более) gRPC способен держать до 9× больше запросов, чем REST на JSON, а при использовании protobuf через HTTP — выигрывает около 30%.

Второй козырь — это зрелый сетевой стек: gRPC работает поверх HTTP/2 с полноценным мультиплексированием, без проблем head‑of‑line blocking и с поддержкой стриминга и bidirectional‑каналов сразу из коробки. Кодогенерация на основе .proto создаёт строгие SDK с полной типобезопасностью. А сам tonic — это стабильная обёртка на Tokio и Hyper, активно поддерживаемая и адаптированная под актуальные версии Hyper 1.0 и Prost 0.13. В общем, всё, что нужно для адекватного сервиса, тут уже есть.

Подготовка .proto и генерация кода

На первом этапе мы определяем gRPC‑контракт — для этого создаём отдельную директорию proto/, в ней описываем структуру и API нашего сервиса на языке Protocol Buffers. Получается базовая структура проекта:

my-svc/  ├─ Cargo.toml  ├─ build.rs  └─ proto/      └─ greeter.proto

Содержимое greeter.proto:

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

Объявляем сервис Greeter с одним методом SayHello, который принимает HelloRequest с полем name и возвращает HelloReply с текстом ответа.

Теперь нужно сгенерировать код на Rust, который реализует этот контракт. Это делается с помощью tonic-build — плагина, который по .proto файлам генерирует клиентские и серверные трейты и структуры. Для этого юзаем build.rs:

fn main() -> Result<(), Box<dyn std::error::Error>> {     tonic_build::configure()                  // настройка генерации         .build_server(true)                   // генерируем серверную сторону         .build_client(true)                   // и клиентскую         .compile(&["proto/greeter.proto"], &["proto"])?; // путь к .proto и include-пути     Ok(()) }

configure() позволяет настроить, что именно мы хотим сгенерировать, указываем, что нужны и сервер, и клиент. compile() принимает список файлов .proto и include‑пути (их может быть несколько, если .proto‑файлы разбиты по модулям). Всё это интегрировано с системой сборки Cargo: при первом cargo build код будет сгенерирован автоматически и попадёт в папку OUT_DIR, откуда мы его позже подключим через tonic::include_proto!.

Фрагмент Cargo.toml, нужный для этого:

[dependencies] tokio   = { version = "1.38", features = ["rt-multi-thread", "macros"] } tonic   = { version = "0.13", features = ["transport", "tls"] } prost   = "0.13"  # для метрик и интерсепторов tower       = "0.5" tower-http  = { version = "0.5", features = ["trace"] } tracing     = "0.1" tracing-subscriber = "0.3"

На выходе получаем полностью типизированный код — трейты сервиса, структуры сообщений, клиентскую обёртку, серверный интерфейс и всё это совместимо с остальным Rust‑кодом.

Асинхронный сервер на tonic

Создаём gRPC‑сервер, который реализует интерфейс Greeter и отвечает на метод SayHello. Подключаем сгенерированный код и реализуем логику:

use tonic::{transport::Server, Request, Response, Status};  pub mod greeter {     tonic::include_proto!("helloworld"); }  #[derive(Default)] pub struct MyGreeter;  #[tonic::async_trait] impl greeter::greeter_server::Greeter for MyGreeter {     async fn say_hello(         &self,         request: Request<greeter::HelloRequest>,     ) -> Result<Response<greeter::HelloReply>, Status> {         let name = request.into_inner().name;         let reply = greeter::HelloReply { message: format!("Привет, {name}!") };         Ok(Response::new(reply))     } }

Главный main.rs:

#[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {     tracing_subscriber::fmt::init();      let addr = "0.0.0.0:50051".parse()?;     let greeter = MyGreeter::default();      // Перехватчик для заголовочного токена     let auth_interceptor = tonic::service::Interceptor::new(|req: Request<()>| {         match req.metadata().get("authorization") {             Some(v) if v == "Bearer super-secret" => Ok(req),             _ => Err(Status::unauthenticated("missing or invalid token")),         }     });      Server::builder()         // Tower-слой для трассировки и метрик         .layer(             tower::ServiceBuilder::new()                 .layer(tower_http::trace::TraceLayer::new_for_grpc())                 .into_inner(),         )         .add_service(             greeter::greeter_server::GreeterServer::with_interceptor(greeter, auth_interceptor),         )         .serve(addr)         .await?;     Ok(()) }

Interceptor в tonic реализуется через замыкание FnMut(Request) -> Result. TraceLayer из tower‑http сразу умеет выводить tracing‑спаны для gRPC‑методов.

Немного про безопасность

Чтобы сервер шифровал трафик, включаем фичу tls или tls-ring и настраиваем Server::builder().tls_config(...). Если нужно mTLS (взаимная аутентификация), обязательно подключаем клиентские сертификаты и валидацию subjectAltName.

Ну и конечно не забываем про базовые защиты на уровне middleware: concurrency_limit, timeout, rate_limit, in_flight_requests из Tower — это обязательный слой при боевом деплое. Он защищает от внезапных всплесков, медленных клиентов и сетевых глюков.

Метрики и Prometheus

Если хотите интеграцию с Prometheus:

use tower_http::metrics::{InFlightRequestsLayer, PrometheusMetricLayer};

Подключаем:

.layer(InFlightRequestsLayer::new()) .layer(PrometheusMetricLayer::new())

Сами метрики можно затем отдавать через отдельный HTTP‑сервер (например, на axum, warp, hyper) — это уже зависит от общей архитектуры проекта.

Стуб-клиент

Для обращения к gRPC‑сервису используем автоматически сгенерированный клиент. Он типизирован, работает асинхронно и поддерживает те же механизмы Interceptor’ов, что и сервер.

Пример:

#[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {     // Создаём канал к серверу     let channel = tonic::transport::Channel::from_static("http://localhost:50051")         .connect()         .await?;      // Оборачиваем канал в клиент с Interceptor'ом     let mut client = greeter::greeter_client::GreeterClient::with_interceptor(         channel,         |mut req: Request<()>| {             req.metadata_mut()                 .insert("authorization", "Bearer super-secret".parse().unwrap());             Ok(req)         },     );      // Отправляем gRPC-запрос     let reply = client         .say_hello(greeter::HelloRequest { name: "Habr".into() })         .await?         .into_inner();      println!("Server answered: {}", reply.message);     Ok(()) } 

СоздаемChannel, который является абстракцией над gRPC‑транспортом. Он может быть from_static, либо динамически через DNS / load balancing.

GreeterClient::with_interceptor позволяет внедрить middleware на клиентской стороне — удобно, чтобы централизованно добавлять токены, трейсинг, идентификаторы, tenant‑id и т. п.

say_hello(...) вызывается как обычная асинхронная функция — типизация, структура запроса и ответа полностью совпадают с тем, что было описано в .proto.

Клиентский Interceptor использует точно такой же API, как и серверный — это даёт единообразие в обработке метаданных. Один раз написали обёртку — используете и на входящих, и на исходящих вызовах.

А ну и еще, если бы мы не использовали Interceptor, можно было бы добавлять метаданные вручную к каждому отдельному запросу, но это не масштабируется. Лучше один раз обернуть — и забыть.

Если вы уже обкатывали gRPC в Rust или внедряли что‑то похожее с tonic, делитесь своим опытом в комментариях — какие подходы сработали, какие мидлвары зашли и как решали вопросы с безопасностью.


Когда разбираешься с gRPC, хочется верить, что это решает всё. Но настоящая боль начинается там, где нужно продумать бизнес-логику, распределить ответственность между сервисами, не утонуть в очередях сообщений и не превратить архитектуру в клубок. Если вы тоже на этом этапе — заглядывайте на открытые уроки:

Пройдите вступительный тест и получите скидку на курс «Microservice Architecture».


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


Комментарии

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

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