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

Сегодня мы рассмотрим, как поднять 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".

Комментарии (1)


  1. sdramare
    26.07.2025 18:44

    И чем эта статья с пометкой "сложно" отличается от документации самого tonic, а именно буквано первого примера hello world https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md ?