# RPC 手册
`rs-zero` 的 `rpc` feature 提供 tonic 相关 helper,用于减少项目重复配置。
## 核心能力
- client endpoint 配置。
- request-id interceptor。
- `grpc-timeout` deadline metadata helper。
- 客户端 retry/backoff helper,默认只重试 `Unavailable`、`DeadlineExceeded`、`ResourceExhausted`。
- memory/static discovery 到 RPC endpoint 的解析适配。
- weighted round-robin endpoint selector。
- streaming wrapper,记录 send/recv、完成状态、timeout 和 tonic code。
- health server helper。
- graceful shutdown 配合 `rs_zero::core::shutdown_signal`。
## 示例
```rust
use rs_zero::core::shutdown_signal;
use rs_zero::rpc::serve_health_with_shutdown;
let addr = "127.0.0.1:50051".parse().unwrap();
serve_health_with_shutdown(addr, shutdown_signal()).await.unwrap();
```
完整示例见 `examples/rpc-hello/src/main.rs`。
## 客户端生产配置
默认 `RpcClientConfig::new` 保持保守行为:不启用 retry、deadline 传播、发现负载均衡或 streaming 观测。需要生产默认值时使用 `production_defaults`:
```rust
use rs_zero::rpc::{LoadBalancePolicy, RpcClientConfig};
let config = RpcClientConfig::production_defaults("http://127.0.0.1:50051");
assert!(config.retry.enabled);
assert!(config.deadline.propagate);
assert!(config.streaming.observe);
assert_eq!(config.load_balance.policy, LoadBalancePolicy::WeightedRoundRobin);
```
retry helper 会限制最大尝试次数,并按指数退避执行。需要把重试裁剪到调用总预算时,使用 `DeadlineBudget`:
```rust
use std::time::Duration;
use rs_zero::rpc::{
RpcRetryConfig,
deadline::DeadlineBudget,
retry::run_with_retry_budget,
};
let retry = RpcRetryConfig::production_defaults();
let budget = DeadlineBudget::new(Duration::from_secs(2));
let result = run_with_retry_budget(&retry, &budget, |remaining| async move {
// 将 remaining 传给下游 request timeout 或 grpc-timeout metadata。
Ok::<_, tonic::Status>(())
}).await;
```
## deadline 与 metadata
`deadline_interceptor` 可给 tonic request 注入 `grpc-timeout`,已存在 metadata 时不会覆盖:
```rust
use std::time::Duration;
use rs_zero::rpc::deadline_interceptor;
let interceptor = deadline_interceptor(Duration::from_millis(500));
```
手写 adapter 可直接使用 `insert_grpc_timeout` 和 `remaining_timeout_from_metadata`。
## discovery 与负载均衡
启用 `discovery` feature 后,可把 `Discovery` 实例解析为 tonic endpoint:
```rust
use rs_zero::{
discovery::{InstanceEndpoint, MemoryRegistry, Registry, ServiceInstance},
rpc::{
RpcClientConfig,
balancer::WeightedRoundRobinBalancer,
discovery::resolve_service_endpoint,
endpoint_from_rpc_endpoint,
},
};
let registry = MemoryRegistry::new();
registry.register(
ServiceInstance::new(
"greeter",
"a",
InstanceEndpoint::new("127.0.0.1", 50051)?,
)
.with_weight(2),
).await?;
let selector = WeightedRoundRobinBalancer::new();
let endpoint = resolve_service_endpoint(®istry, &selector, "greeter").await?;
let config = RpcClientConfig::production_defaults(&endpoint.uri);
let tonic_endpoint = endpoint_from_rpc_endpoint(&endpoint, &config)?;
```
当前 selector 保持低基数观测边界:指标和日志只记录 service/method/code,不记录实例 id、IP 或动态 key。
## RPC unary Tower-first 自动层
`RpcServerLayerStack` 是推荐的 server-side 接入方式。它可挂到 `tonic::transport::Server::builder().layer(...)`,统一应用 timeout、concurrency、breaker、shedder、Redis limiter、request id / traceparent 读取和 RPC INFO 日志:
```rust
use rs_zero::rpc::{RpcServerConfig, RpcServerLayerStack};
let config = RpcServerConfig::production_defaults("hello", "127.0.0.1:50051".parse()?);
let layer = RpcServerLayerStack::new(config).into_layer();
tonic::transport::Server::builder()
.layer(layer)
.add_service(hello_service)
.serve(addr)
.await?;
```
生成代码中的业务 method 可以直接处理 `request.into_inner()` 后的 message;入站 metadata 已由外层 Tower layer 读取。`RpcResilienceLayer::run_unary*` 和 `RpcUnaryResilienceLayer` 仍保留给手写高级场景和旧生成项目。
## streaming 观测边界
`RpcStreamingObserver` 与 `ObservedRecvStream` 是给生成代码或手写 streaming adapter 使用的组合式 wrapper:
```rust
use rs_zero::rpc::{RpcStreamingConfig, streaming::RpcStreamingObserver};
let observer = RpcStreamingObserver::new(
"chat.Chat",
"Talk",
RpcStreamingConfig::production_defaults(),
);
observer.record_send().await;
observer.record_recv().await;
observer.finish::<()>(Ok(())).await;
let snapshot = observer.snapshot().await;
```
`ObservedRecvStream` 可包装 `Stream<Item = Result<T, tonic::Status>>`,`record_stream_send` 可包裹发送动作,`run_observed_stream` 可应用 per-stream timeout。它们不会替代 tonic 生成的 stream 类型;业务代码仍负责处理消息流、取消和 backpressure。
## Service group
需要和 REST 或 worker 同进程运行时,可用 `TonicHealthService` 或 `TonicService` 接入 [`ServiceGroup`](service-group.md):
```rust
use rs_zero::{core::ServiceGroup, rpc::TonicHealthService};
let rpc = TonicHealthService::new("health-rpc", "127.0.0.1:50051".parse()?);
let mut group = ServiceGroup::new();
group.add(rpc);
group.start().await?;
# Ok::<(), Box<dyn std::error::Error>>(())
```
## RPC INFO 日志与调用链字段
启用 `observability` feature 后,`RpcServerLayerStack` 和 `RpcResilienceLayer::run_unary` 都会在 unary 调用完成时输出 INFO 事件 `rpc unary observed`。生成的 `rzcli rpc gen` 项目默认启用 `rpc`、`resil`、`observability`,并推荐在 tonic server 外层挂载 `RpcServerLayerStack`。
关键字段:
- `rpc.service` / `service`:逻辑 RPC 服务名。
- `rpc.method` / `method`:RPC 方法名,例如 `SayHello` 或 `say_hello`。
- `route`:同 `method`,用于和 HTTP route 统一检索;不再写固定值 `rpc`。
- `request_id`:`x-request-id` metadata。
- `traceparent`:入站或显式传入的 W3C TraceContext。
- `trace_id` / `span_id`:来自合法 `traceparent` 或 OTLP 当前 span。
- `code`:gRPC 状态码,例如 `Ok`、`Unavailable`。
如果只看到 `h2::*` DEBUG 日志,说明当前日志过滤器打开了底层 HTTP/2 debug,但 RPC 业务层没有启用 `observability` 或没有走 `RpcResilienceLayer` / `observe_rpc_unary`。生产建议用类似过滤器减少底层噪音:
```bash
RUST_LOG=info,h2=warn,hyper=warn,tower=warn
```
## API 调 RPC 的调用链追踪
仅靠默认文本日志,最稳定的串联字段是 `request_id`。如果要跨 HTTP -> RPC 关联完整 trace,需要:
1. API 入口启用 REST metrics middleware。
2. API 调 RPC 的 client 优先使用 `RpcClientBuilder` 建立 channel,并保留 `request_id_interceptor()` 传播 request id。REST metrics middleware 会把当前 request id 写入请求 extensions,并在 handler 执行期间设置 task-local request id;同一 handler 内使用 `request_id_interceptor()` 的 RPC client 会自动写入 metadata。若是异步任务、队列消费或手写边界,也可用 `with_rpc_request_id(request_id, async { ... })` 包住 RPC 调用,或向 `tonic::Request` extensions 插入 `RpcRequestId`。
3. 需要 trace id / span id 时,启用 `otlp`,并在 RPC client 加 `trace_context_interceptor()`。
4. RPC server 侧优先挂载 `RpcServerLayerStack`。这样 trait method 内可以直接 `request.into_inner()`,不会丢失用于日志和 metrics 的 `x-request-id` 与 `traceparent`。只有手写兼容 helper 路径时,才需要 `run_unary_with_metadata` 或 `observe_rpc_unary_with_metadata`。
没有启用 OTLP 或没有把 `traceparent` 注入 RPC metadata 时,日志中不会自动出现可串联的跨服务 `trace_id`。这种情况下只能按 `request_id` 或外部网关日志关联。
## RPC codegen
`rzcli rpc gen` 提供 proto 子集解析和 tonic skeleton 生成。使用方式见 [RPC 服务教程](../tutorials/rpc-service.md)。
## 限制
- 不替代 `protoc`。
- 生成 skeleton 默认不包含业务实现。
- 复杂 proto 特性需在应用层接入 prost/tonic build。
- 当前负载均衡先提供 weighted round-robin;P2C、channel pool 和连接预热仍需应用层或后续版本扩展。
- streaming wrapper 提供组合式 send/recv/finish/timeout 观察边界,不强行替代所有 tonic stream 类型。
## 相关测试
- `cargo test --test rpc_integration`
- `cargo test --test rpc_production_integration`
- `cargo test -p rs-zero-cli --test goctl_rpc_compat`