10. 网关二进制与部署
1. 背景与原理
1.1 Gateway 在 SOVD 中的角色
SOVD gateway 是聚合层 :把多个下游 SOVD server(车内多个 ECU/HPC、或下层网关)的实体汇聚成统一视图,对外暴露单一 /sovd/v1。
它的特殊性在于:
- 无状态:聚合结果来自下游,重启后由发现机制重建
- 可裁剪:车载部署可能只要 TCP + 无安全;云端部署要 TLS + JWT + Rego + CORS
- 需与平台集成:systemd socket activation、容器就绪探针、静态 Web UI 托管
因此 gateway 是"配置驱动的可执行产物"------它把库的能力通过 CLI 参数暴露出来。
1.2 为什么拆出 libcli
gateway 与 mcp 两个二进制共享:tracing 初始化、trace layer、优雅关闭。抽成 libcli 避免重复,也保证两者日志格式一致。
2. 当前实现架构
2.1 二进制与目录
opensovd-cli/
├── lib/ → crate `libcli`(tracing 初始化、trace tower layer)
├── gateway/ → bin `opensovd-gateway`(默认端口 7690)
└── mcp/ → bin `opensovd-mcp`(MCP over stdio)
2.2 CLI 参数矩阵(gateway)
| 类别 | 参数 | 环境变量 | 说明 |
|---|---|---|---|
| 网络 | --url |
SOVD_URL |
形如 http://host:port/path;host:port 用于 TCP 绑定,path 作为 base URI |
--unix-socket |
--- | Unix socket 路径;@ 前缀为 Linux abstract socket;指定时忽略 host:port |
|
| 数据 | --mock |
--- | 启用 opensovd-mocks 示例拓扑 |
--serve-dir PATH:DIR |
--- | 挂载静态目录(如 /ui:./webui/dist) |
|
| 安全 | --tls-cert / --tls-key / --tls-client-ca |
SOVD_TLS_CERT / SOVD_TLS_KEY |
TLS 与 mTLS |
--auth-jwt-key / --auth-jwt-algo / --auth-jwt-issuer |
--- | JWT(HS512/RS512,密钥 base64) | |
--auth-policy / --auth-policy-data |
--- | Rego 策略与 JSON data | |
| CORS | --cors-origin/-method/-header/-credentials/-max-age |
--- | 跨域配置 |
| 平台 | systemd socket activation(Linux) | --- | 自动检测 LISTEN_FDS |
2.3 构建期信息注入
33:38:opensovd-cli/gateway/src/main.rs
const VENDOR_INFO: OpenSovdInfo = OpenSovdInfo {
version: env!("CARGO_PKG_VERSION"),
sha1: env!("COMMIT_SHA"), // ← build.rs 注入
build_date: env!("BUILD_DATE"), // ← build.rs 注入
name: "OpenSOVD",
};
build.rs 依赖 time crate 生成构建时间戳与 commit sha,随 /version-info 下发------现场可追溯部署版本,这是车规运维的实用设计。
2.4 部署形态
| 形态 | 支持 |
|---|---|
| 容器 | docker/Dockerfile.gateway、Dockerfile.mcp;GHCR 镜像 ghcr.io/eclipse-opensovd/opensovd-gateway |
| systemd | socket activation + sd_notify READY |
| 裸机 | nightly/latest 滚动发布,4 平台二进制(linux x64/aarch64、macOS aarch64、Windows x64) |
| 开发 | cargo run -p opensovd-gateway -- --mock、devcontainer、Nix flake |
3. 核心流程与算法
3.1 启动流程
main()
├─ Cli::parse() (clap,支持环境变量)
├─ libcli::init_tracing("gw=info,srv=info,tower_http=debug,axum=trace")
├─ run(cli)
│ ├─ 有 jwt_key?
│ │ ├─ 是 → 构造 JwtAuthenticator(base64 解码密钥 → 算法 → issuer)
│ │ │ 有 policy? → RegorusAuthorizer : AllowAll
│ │ └─ 否 → NoAuth + AllowAll
│ └─ serve(cli, authenticator, authorizer)
│ ├─ 解析 --url → base_uri(path) + authority(host:port)
│ ├─ configure_listener:systemd fd > unix socket > TCP bind
│ ├─ configure_topology:--mock ? create_mock_topology() : Topology::default()
│ ├─ TLS 配置
│ ├─ CORS 层 + trace 层 + 静态目录服务
│ ├─ .base_uri().vendor_info().build()
│ ├─ notify_readiness() (sd_notify READY=1)
│ └─ server.serve().await
└─ ExitCode
3.2 监听器选择的优先级算法
186:233:opensovd-cli/gateway/src/main.rs
// 1) Linux systemd socket activation(最高优先级)
#[cfg(target_os = "linux")]
if let Some(fd) = sd_notify::listen_fds()?.next() {
let std_listener = unsafe { std::net::TcpListener::from_raw_fd(fd) }; // SAFETY: fd 由 systemd 提供且被拥有
std_listener.set_nonblocking(true)?;
return Ok(builder.listener(tokio::net::TcpListener::from_std(std_listener)?));
}
// 2) --unix-socket(支持 '@' 前缀的 abstract socket)
if let Some(ref socket_path) = cli.unix_socket { ... }
// 3) TCP bind(authority)
let listener = tokio::net::TcpListener::bind(authority).await?;
优先级:systemd fd > Unix socket > TCP。这个顺序符合"平台托管 > 本地 IPC > 网络"的惯例。
3.3 就绪通知
265:270:opensovd-cli/gateway/src/main.rs
fn notify_readiness() {
#[cfg(target_os = "linux")]
if let Err(e) = sd_notify::notify(&[sd_notify::NotifyState::Ready]) {
tracing::warn!(target: TARGET, error = %e, "Failed to notify systemd readiness");
}
}
通知发生在 serve() 之前(listener 已绑定)------语义上是"已绑定",但严格来说应在 axum 开始 accept 之后。
3.4 层叠顺序
172:177:opensovd-cli/gateway/src/main.rs
let server = builder
.layer(libcli::trace::trace_layer()) // 先加 → 最外层
.layer(tower::util::option_layer(cors))
.base_uri(base_uri)?
.vendor_info(VENDOR_INFO)
.build()?;
最终栈:trace → cors → (AuthN → AuthZ → router)。trace 在最外层可记录被 CORS/鉴权拒绝的请求。
4. 待完善与风险
4.1 部署正确性(严重)
base_uri硬编码导致反向代理下链接错误(高) :见 06 章 §4.3 缺陷 8。在 K8s Ingress / 反向代理后,/sovd前缀与http://scheme 都会不符合实际。这是容器化部署最先遇到的问题。- 就绪通知时机偏早(中) :
sd_notify(READY)在 axum 开始 accept 之前发出,K8s 可能在服务真正可用前就开始转发流量(虽然窗口极小)。建议在serve()内部或on_listening回调中通知。 - 无健康检查端点(中) :
/version-info可勉强充当 liveness,但没有 readiness/health 语义(无法表达"拓扑为空/发现失败"等降级状态)。 - 无优雅关闭超时配置(低):axum 的 graceful shutdown 没有等待上限,慢请求会无限延长关闭。
4.2 配置与运维(中)
- 密钥通过命令行传入(中) :
--auth-jwt-key会出现在ps输出与 shell history 中。建议支持从文件/环境变量/密钥管理服务读取(虽有SOVD_TLS_*的环境变量先例,但 JWT 密钥只支持命令行)。 - 无配置文件支持(中):参数众多(20+),纯 CLI 难以管理;缺少 YAML/TOML 配置与校验。
- 无配置回显/启动时自检(低):启动日志会打印启用的特性(TLS/CORS/JWT/Rego),但不打印实际生效值(如绑定的地址、base_uri),排障不便。
--serve-dir无路径穿越防护说明(中) :serve_dir基于tower_http::fs,需确认是否规范化..;文档未说明。
4.3 可观测性(中)
- 无
/metrics(中):无请求数、延迟、连接数、发现状态等指标。 - 无 request id 贯穿(中):trace 层按 HTTP 维度记录,但无跨层关联 ID。
- 日志级别由硬编码字符串决定(低) :
init_tracing("gw=info,srv=info,tower_http=debug,axum=trace")写死在代码中,tower_http=debug与axum=trace在生产会产生大量日志;应可通过RUST_LOG覆盖(需确认libcli是否支持 env 覆盖)。
4.4 发布与版本(中)
- 只有滚动 tag(中) :
latest/nightly无 semver release、无 CHANGELOG,生产环境无法锁定版本。 - 镜像标签策略未文档化(低) :GHCR 镜像与 git commit 的对应关系不清晰,故障回溯困难(虽有
sha1编入vendor_info可缓解)。
4.5 建议的改进顺序
| 优先级 | 事项 |
|---|---|
| P0 | 修复 base_uri 派生(含 X-Forwarded-* 与 TLS scheme 推断) |
| P1 | 就绪通知移到 accept 之后;增加 /healthz(含发现状态) |
| P1 | JWT 密钥支持文件/环境变量注入 |
| P2 | 配置文件支持 + 启动配置回显 |
| P2 | /metrics + request id |
| P2 | semver release + CHANGELOG |
5. 关键代码位置
| 内容 | 路径 |
|---|---|
main / run / serve |
opensovd-cli/gateway/src/main.rs:42-184 |
| VENDOR_INFO 与 build 注入 | opensovd-cli/gateway/src/main.rs:33-38、build.rs |
| 监听器选择 | opensovd-cli/gateway/src/main.rs:186-245 |
| 拓扑配置(mock) | opensovd-cli/gateway/src/main.rs:247-263 |
| 就绪通知 | opensovd-cli/gateway/src/main.rs:265-270 |
| CLI 参数定义 | opensovd-cli/gateway/src/cli.rs:43-223 |
| CORS 层构造 | opensovd-cli/gateway/src/cors.rs |
| 静态目录服务 | opensovd-cli/gateway/src/serve_dir.rs |
libcli(tracing/layer) |
opensovd-cli/lib/src/{lib.rs,trace.rs} |
| 容器 | docker/Dockerfile.gateway、docker/Dockerfile.mcp |
| systemd 示例 | examples/server/systemd/systemd.rs |