Привет, Хабр!
Сегодня мы рассмотрим, как поднять 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, хочется верить, что это решает всё. Но настоящая боль начинается там, где нужно продумать бизнес-логику, распределить ответственность между сервисами, не утонуть в очередях сообщений и не превратить архитектуру в клубок. Если вы тоже на этом этапе — заглядывайте на открытые уроки:
28 июля, 20:00
Основы проектирования бизнес-логики в микросервисной архитектуре
Как не похоронить масштабируемость под слоем хаотичной логики. Разбор паттернов, ошибок и живых кейсов.11 августа, 20:00
RabbitMQ против Kafka — что выбрать для вашей структуры
Что реально стоит за этими системами, когда вы работаете не с туториалами, а с продом. Честное сравнение, нюансы и рекомендации для вашей архитектуры.
Пройдите вступительный тест и получите скидку на курс "Microservice Architecture".
sdramare
И чем эта статья с пометкой "сложно" отличается от документации самого tonic, а именно буквано первого примера hello world https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md ?