# 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 观测。需要 go-zero 风格生产默认值时使用 `go_zero_defaults`:
```rust
use rs_zero::rpc::{LoadBalancePolicy, RpcClientConfig};
let config = RpcClientConfig::go_zero_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::go_zero_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::go_zero_defaults(&endpoint.uri);
let tonic_endpoint = endpoint_from_rpc_endpoint(&endpoint, &config)?;
```
当前 selector 保持低基数观测边界:指标和日志只记录 service/method/code,不记录实例 id、IP 或动态 key。
## RPC unary 自动韧性层
`RpcUnaryResilienceLayer` 可挂到 tonic/Tower service,在业务方法未显式调用 `run_unary` 时也能应用 timeout、concurrency、breaker、shedder 和 Redis limiter:
```rust
use rs_zero::rpc::{RpcResilienceLayer, RpcServerConfig, RpcUnaryResilienceLayer};
let config = RpcServerConfig::go_zero_defaults("hello", "127.0.0.1:50051".parse()?);
let resilience = RpcResilienceLayer::new(config.name.clone(), config.resilience.clone());
let layer = RpcUnaryResilienceLayer::new(resilience);
```
生成代码仍保留 `run_unary` helper,便于 skeleton 内直接包裹业务占位。手写 tonic service 推荐使用自动层降低漏接风险。
## streaming 观测边界
`RpcStreamingObserver` 与 `ObservedRecvStream` 是给生成代码或手写 streaming adapter 使用的组合式 wrapper:
```rust
use rs_zero::rpc::{RpcStreamingConfig, streaming::RpcStreamingObserver};
let observer = RpcStreamingObserver::new(
"chat.Chat",
"Talk",
RpcStreamingConfig::go_zero_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 后,`RpcResilienceLayer::run_unary` 会在 unary 调用完成时输出 INFO 事件 `rpc unary observed`。生成的 `rzcli rpc gen` 项目默认启用 `rpc`、`resil`、`observability`,因此生成代码中的 unary 方法默认有 RPC 观测日志。
关键字段:
- `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 使用 `request_id_interceptor()` 传播 request id。REST metrics middleware 会把当前 request id 写入请求 extensions;同一 handler 内创建的 tonic `Request` 可直接由 `request_id_interceptor()` 读取并写入 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 侧如果有 `tonic::Request<T>`,先保留 metadata,再走 `RpcResilienceLayer::run_unary_with_metadata` 或 `observe_rpc_unary_with_metadata`。`rzcli rpc gen` 会生成 `RpcRequestParts::from_request(request)` 和 `*_with_parts`,避免在观测前 `request.into_inner()` 丢掉 `x-request-id` 与 `traceparent`。
没有启用 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`