blob: 8656fb40cc8b9442d846dd02bd56540be71c1956 [file] [view]
# Proxy Admin 管理接口
> 英文版见 `docs/proxy-admin.md`。
## 1. 动机
RocketMQ 5.0 把客户端访问放到了无状态的 Proxy 后面,但运维侧仍然通过 broker 侧的结构(broker 上的 `ConsumerManager`、Remoting 时代的 admin 命令)来观测客户端。连到 Proxy 的 gRPC 客户端对这些工具不可见:控制面无法回答"哪些 SDK 客户端在线、它们订阅了什么、是否健康",只能依赖间接的指标推断。
控制面 dashboard 需要一个标准的服务端接口来读取完整的 gRPC 客户端数据。本次改动在 Proxy 上实现这个接口。
## 2. 契约选择:上游 `Admin` service
gRPC 契约是**上游 `Admin` service**,定义在 `apache/rocketmq-apis` 仓库(main 分支,作为 git submodule 引入,见 §6)的 `apache/rocketmq/v2/admin.proto` 中。本实现刻意**不**在协议层引入自定义的 `ProxyAdminService`:控制面 RPC 接口位于 dashboard 和多语言 SDK 已经在消费的、经过社区评审的版本化 proto 中,并与上游演进保持增量兼容。
proxy 实现 `AdminGrpc.AdminImplBase`(全部 16 个 RPC),并把它绑定在一个独立的 admin gRPC server 上。
## 3. 目标
1. 在 Proxy 上提供一个独立、专用的 gRPC Admin server,与数据面 `MessagingService` 隔离。
2. 来自 rocketmq-apis main 的稳定、向后兼容契约(`Admin` service,`apache/rocketmq/v2/admin.proto`)。
3. 通过专用的 `proxy.admin.*` ACL 2.0 资源实现一等公民的鉴权,区分只读与高权限动作。
4. 集群级语义但不把拓扑泄漏进协议:每个 proxy 都暴露连接/订阅数据的集群可见视图,并把客户端定向 RPC 转发到持有该客户端的 proxy。
## 4. 设计决策
### D1 — 服务放置
上游 `Admin` service 绑定在自己的 gRPC server/端口(`grpcAdminServerPort`,默认 8088)上,有自己的拦截器链,与数据面(8081)分离。整个接口由全局 kill switch `grpcAdminServerEnable` 门控,**默认为 false**:admin server 是 opt-in 的,除非显式启用否则不会启动。
理由:控制面流量绝不能与数据面竞争,且 admin 端口可以只对运维网络开放防火墙。admin server 刻意不暴露 channelz 或 proto reflection。它复用数据面的 `GrpcChannelManager` / `GrpcClientSettingsManager`,使在线客户端对 admin 查询可见。本特性引入的运行时线程池均归实例所有并接入 Proxy 生命周期:admin timeout scheduler 随 `ProxyAdminGrpcService` 关闭,blocking gateway executor 随 `AdminService` 关闭。
### D2 — 鉴权:专用 `proxy.admin.*` 资源
凭据通过标准 gRPC `Authorization` metadata 传入(与数据面相同的方案,ACL 2.0 签名)。认证以拦截器方式执行,顺序排在标准链路的 `HeaderInterceptor` **之后**(因 gRPC 按注册的逆序调用拦截器,故注册在其之前)。这个顺序是必需的:`HeaderInterceptor` 会把入站的 `x-mq-channel-id` 替换为从传输层派生的 channel id,而认证结果按 channel id 缓存,若在客户端自带的值上做认证,一条连接的请求就能复用另一条连接的缓存结果、跳过签名校验。
每个 RPC 映射到一个资源 + 一个动作:
| 资源 | RPC | 动作 |
|---|---|---|
| `proxy.admin.client` | ListSubscription, ListConsumerConnection (List); DescribeSubscription, DescribeGroupAccumulation, GetConsumerRunningInfo, QueryTimeSpan (Get) | List / Get |
| `proxy.admin.config` | ChangeLogLevel | Update |
| `proxy.admin.connection` | PrintThreadStackTrace, VerifyMessage | Update(高权限)|
| `proxy.admin.route` | GetTopicRoute | Get |
| `proxy.admin.ops` | GetProxyRuntimeStats, DescribeTopicStatus, QueryMessage (Get); ResetGroupOffset (Update); DeleteSubscription (Delete); AdminSendMessage (Pub) | Get / Update / Delete / Pub |
建模说明:ACL 2.0 的资源类型是 cluster/namespace/topic/group。admin 资源建模为 CLUSTER 类型的字面量,使用保留名称(资源 key `cluster:proxy.admin.<module>`),从而在不与真实集群名冲突、不改动鉴权核心的前提下实现精确的最小权限匹配。
模式:
- 集群鉴权关闭、`grpcAdminServerAuthEnable=false` → 开放接口(与数据面语义相同);
- 集群鉴权开启 → 针对每方法 `proxy.admin.*` 资源的标准 authenticate + authorize 流程;
- `grpcAdminServerAuthEnable=true` → fail-closed。admin 接口必须既能认证调用方**又能**强制每方法 ACL,因此该模式要求集群级的认证**和**授权开关都开启。任一关闭时所有请求都被拒绝(认证关 → `UNAUTHENTICATED`; 授权关 → `FAILED_PRECONDITION`),而不是在没有真正权限校验的情况下放行。这是刻意为之:ACL 2.0 评估器本身受集群授权开关门控,若在授权关闭时服务请求,会静默放行每个调用,包括破坏性的那些。
审计:每个被服务的 RPC 都会向鉴权审计 logger 写入 `[PROXY-ADMIN-AUDIT] subject/method/resource/action/sourceIp`(与 ACL 2.0 引擎自身的审计日志一起,满足 Console 用户 + AK + 资源 + 操作的审计四元组要求)。
### D3 — 多 proxy 语义:集群可见,协议不含拓扑
一个 gRPC 客户端同一时刻只连到一个 proxy,但 dashboard 仍需从任意单个 admin 端点看到整个集群。两个机制在不给协议加 scope/peering 字段的前提下做到这点:
- **连接 / 订阅数据是集群可见的。** 列表类 RPC(`ListConsumerConnection`、`ListSubscription`、`DescribeSubscription`)把 broker 侧的 `ConsumerConnection` 视图(覆盖 remoting 客户端,以及被 `HeartbeatSyncer` 在 proxy 间同步的 gRPC 客户端)与本 proxy 自己的 `ConsumerManager`(覆盖本地接入的 gRPC v2 客户端)合并。结果按 group + topic 去重,因此应答不随在线消费者数量膨胀,也不取决于查询的是哪个 proxy。
- **客户端定向 RPC 转发到持有该客户端的 proxy。** `PrintThreadStackTrace`、`VerifyMessage`、`GetConsumerRunningInfo` 需要只有持有方 proxy 才有的实时 telemetry 流。`ProxyAdminForwarder` 识别出属于 peer 的客户端(其 `ConsumerManager` 条目是 `RemoteChannel`,`getRemoteProxyIp()` 为 peer 的 `localServeAddr`),把整个 RPC——连同原始 metadata——转发到 peer 的 admin 端口,并用 `x-mq-admin-forwarded` 头防止转发成环。peer 的 admin 端口按集群统一假设(心跳同步载荷不携带它); 异构 admin 端口的集群需要先把它加入心跳记录。
`GetProxyRuntimeStats` 刻意报告**本进程**的状态(自身的连接/生产者/消费者计数),因此想要按节点统计的 dashboard 直接查询每个 proxy。
### D4 — 每个 RPC 的数据源
| 数据源 | RPC | 机制 |
|---|---|---|
| 集群可见连接 | ListConsumerConnection, ListSubscription, DescribeSubscription | broker 侧 `ConsumerConnection`(remoting + proxy 间同步的客户端)与本 proxy 的 `ConsumerManager`(本地 gRPC v2 客户端)合并 |
| 客户端定向 telemetry relay | PrintThreadStackTrace, VerifyMessage, GetConsumerRunningInfo | relay 到持有方 `GrpcClientChannel` 的 telemetry command; 客户端在 peer 上时转发到 peer proxy(D3)|
| Broker 网关(异步、多 broker fan-out)| DescribeTopicStatus, DescribeGroupAccumulation, ResetGroupOffset, QueryMessage, QueryTimeSpan, GetTopicRoute, DeleteSubscription | proxy 自己的异步 `AdminService` remoting 网关; 每个 broker hop 有独立 deadline,慢/无响应的 broker 不会挂起整个 RPC |
| Proxy producer 路径 | AdminSendMessage | `MessagingProcessor.sendMessage`(定时 / FIFO 消息通过 system properties 生效)|
| Proxy 运行时 | ChangeLogLevel | root logger 上重定位的 logback API |
所有 broker 侧调用都走异步 `AdminService` 网关并并发扇出到所有相关 broker,因此 gRPC 执行线程不会被阻塞。
### D5 — 协议覆盖
连接/订阅列表是集群可见的,同时包含 remoting 客户端(通过 broker 侧 `ConsumerConnection`)和 gRPC v2 客户端(通过 proxy 的 `ConsumerManager`)。客户端定向 telemetry RPC(`PrintThreadStackTrace`、`VerifyMessage`、`GetConsumerRunningInfo`)作用于持有 proxy telemetry 流的 gRPC v2 客户端,并支持跨 proxy 转发; 它们的完整 running-info 载荷受 telemetry 协议能承载的内容限制(见 §8 诚实边界)。
## 5. 能力映射
| 需求 | 交付为 | 说明 |
|---|---|---|
| ListClients(按 group/topic/clientId 前缀过滤,分页)| `ListConsumerConnection(group[, topic])` | clientId 前缀过滤和分页在上游契约里无法表达; 这是本次部分交付的已记录边界 |
| DescribeClient(SDK 版本、订阅、心跳、鉴权、Pop 进度)| `DescribeSubscription`(每客户端订阅)+ `GetConsumerRunningInfo`(订阅 + 运行信息)| 心跳/鉴权状态/Pop 进度字段在上游契约里不存在 |
| ListClientsByGroup / ListClientsByTopic | `ListConsumerConnection(group[, topic])` | 完全覆盖 |
| 多 proxy 集群聚合 | 集群可见连接数据 + 客户端定向 RPC 转发 | 见 D3 |
M2+ 项(配置热更新、配额、连接控制、Pop/batch 诊断、路由观测流)不属于上游 `Admin` 契约,不在本次交付范围内。
## 6. 构建 proto 源码
`apache/rocketmq-apis` 仓库(main 分支,包含 `apache/rocketmq/v2/admin.proto`)作为 **git submodule** 引入(`rocketmq-apis/`,见 `.gitmodules`); CI workflow 用 `submodules: true` 检出。
- **Maven**:`rocketmq-proto` 模块在构建时通过 `protobuf-maven-plugin` 为 `apache/rocketmq/v2/{definition,service,admin}.proto` 生成 Java + gRPC stub; 它的版本跟随 reactor(`${revision}`),因此构建本仓库不需要已发布的 proto 制品。
- **Bazel**:`//rocketmq-proto:rocketmq-proto` 通过对 submodule 的 `genrule`(作为 `@rocketmq_apis` 外部仓库呈现)构建相同的类,使用 pin 的 `protoc` / `protoc-gen-grpc-java` 二进制和 well-known-type proto。
- 其他消费者(dashboard、SDK)继续使用 rocketmq-apis 发布的制品; 只有本仓库从源码构建 proto。
## 7. 配置参考
| Key | 默认值 | 含义 |
|---|---|---|
| `grpcAdminServerEnable` | false | kill switch; admin 接口是 opt-in 的。设为 `true` 启动 admin gRPC server; `false`(默认)= 不启动 |
| `grpcAdminServerPort` | 8088 | 专用 admin gRPC 端口(<=0 禁用)|
| `grpcAdminServerAuthEnable` | false | fail-closed 模式; 要求集群认证**和**授权都已开启(见 D2)|
| `grpcAdminServerRequestTimeoutMillis` | 3000 | admin RPC 扇出的每个 broker hop 的 deadline |
## 8. 诚实边界
admin 接口绝不填写自己无法如实提供的字段; 下列情况以明确 status 回应或留空,而不是伪造:
- **DeleteSubscription 在开源里事实上是 no-op。** 开源代码没有任何地方写入 `SubscriptionGroupConfig.subscriptionDataSet`(broker 只读它),因此没有持久化的 per-topic 订阅可删。该 RPC 返回 `NOT_FOUND` 并说明原因,**消费组及其位点保持不动**。需要该 RPC 的发行版用外部订阅存储填充同一字段。
- **QueryMessage** 支持按 `message_id`(unique-key 索引)和 `message_key` 查询。`subscription` / `lite_topic` / 纯时间范围扫描三种变体需要开源 broker 不提供的分页游标,返回 `BAD_REQUEST`。
- **GetProxyRuntimeStats** 的 `in_tps` / `out_tps` 留空:开源 proxy 没有进程级吞吐计数器,填 0 会与空闲 proxy 无法区分。
- **GetConsumerRunningInfo** 对 gRPC v2 客户端只返回 `subscriptions`:v2 telemetry 协议没有承载 `properties`、`message_queue_table`、`consume_status_table` 的回包。该 RPC 返回 `OK` 并附说明性 status message,而不是空壳。
- **DescribeTopicStatus** 的 `create_timestamp` / `tags` 留空,因为 broker 不记录 topic 创建时间。
## 9. 里程碑
- M1(本次交付):基于上游 `Admin` service 的在线客户端查询 — ListConsumerConnection / DescribeSubscription / GetConsumerRunningInfo / DescribeGroupAccumulation,以集群可见方式并支持跨 proxy 转发提供,外加完整的 16-RPC `Admin` 接口用于 dashboard 集成。
- 未来(需要上游 proto 演进):客户端列表的 clientId 前缀过滤 / 分页、心跳和鉴权状态字段,以及 M2+ 接口(配置/配额/连接/路由观测)。
## 10. 验收标准映射
| 标准 | 状态 |
|---|---|
| 设计文档 + 稳定向后兼容的 proto 契约 | 本文档 + 上游 `admin.proto`(rocketmq-apis main)|
| 客户端查询 RPC 合入服务端仓库 | `ProxyAdminGrpcService` 在 proxy 上实现全部 16 个 `Admin` RPC |
| 独立 ACL 控制、只读/高危分离、最小权限文档 | D2 资源/动作 + 下方 §11(最小权限配置指南)|
| 与 dashboard 的 E2E | 契约 = 上游 `Admin` service,为 dashboard 集成冻结(跨仓库)|
## 11. 最小权限配置指南
admin 接口对每个 RPC 按专用的 `proxy.admin.*` ACL 2.0 资源鉴权(见上方 §4 决策 D2)。本节给出每个运维角色的最小权限策略。
### 11.1 资源与动作模型
资源(ACL 2.0 key; 建模为带保留名称的 cluster 类型字面量):
| 资源 key | 保护 |
|---|---|
| `cluster:proxy.admin.client` | 在线客户端查询与客户端诊断 |
| `cluster:proxy.admin.config` | proxy 运行时日志级别变更 |
| `cluster:proxy.admin.connection` | 客户端定向 telemetry 命令:线程栈、verify message(高危)|
| `cluster:proxy.admin.route` | topic 路由视图 |
| `cluster:proxy.admin.ops` | broker 侧运维:stats/topic status/message query(读)和 reset offset / delete subscription / admin send(高危)|
动作分类:
- 只读:`Get`, `List`
- 高权限(变更 / 破坏性):`Update`, `Delete`, `Pub`
服务端把每个 RPC 映射到恰好一个(资源, 动作)对; 授予只读动作绝不会授权高权限 RPC。
### 11.2 角色模板
所有命令针对集群的任意 broker/namesrv 运行(ACL 2.0 存储)。先创建用户:
```bash
sh mqadmin createUser -n <namesrv-addr> -u <username> -p <password>
```
#### 角色 A — 只读观察者(dashboard 服务账号)
在线客户端、订阅、堆积、诊断、配置/路由视图。
```bash
sh mqadmin updateAcl -n <namesrv-addr> \
-s user:rip2-ro \
-r cluster:proxy.admin.client,cluster:proxy.admin.config,cluster:proxy.admin.route,cluster:proxy.admin.ops \
-a Get,List \
-d Allow
```
说明:`proxy.admin.ops` 上的 `Get,List` 覆盖只读的 broker 侧 RPC; 变更类 ops RPC 需要 `Update`/`Delete`/`Pub`,仍被拒绝。
#### 角色 B — on-call 运维(观察者 + 客户端诊断命令)
角色 A 加上向已连接客户端下发 telemetry 命令(线程栈 / verify message)的能力。
```bash
sh mqadmin updateAcl -n <namesrv-addr> \
-s user:rip2-oncall \
-r cluster:proxy.admin.connection \
-a Update \
-d Allow
# 外加上面的角色 A 授权
```
#### 角色 C — Admin(完全控制,break-glass)
offset 重置、订阅删除、admin send、客户端诊断命令。
```bash
sh mqadmin updateAcl -n <namesrv-addr> \
-s user:rip2-admin \
-r cluster:proxy.admin.client,cluster:proxy.admin.config,cluster:proxy.admin.connection,cluster:proxy.admin.route,cluster:proxy.admin.ops \
-a Get,List,Update,Delete,Pub \
-d Allow
```
(把角色 C 账号控制在最小范围; 每次使用都会以 `[PROXY-ADMIN-AUDIT]` 前缀记录在鉴权审计日志里。)
#### 环境限制(推荐)
通过 `-i` sourceIp 选项把 admin 访问限制到运维网络:
```bash
sh mqadmin updateAcl -n <namesrv-addr> \
-s user:rip2-ro \
-r cluster:proxy.admin.client \
-a Get,List \
-d Allow \
-i 10.0.0.0/8
```
### 11.3 Fail-Closed 模式
默认情况下 admin server 遵循集群级认证/授权开关(与数据面行为相同)。fail-closed 模式让 admin 接口拒绝服务任何它无法完全授权的请求:
```properties
# proxy.json / -D grpcAdminServerAuthEnable=true
grpcAdminServerAuthEnable: true
```
`grpcAdminServerAuthEnable=true` 时,admin 接口要求集群认证**和**授权开关都开启。认证关闭时请求以 `UNAUTHENTICATED` 拒绝; 授权关闭时以 `FAILED_PRECONDITION` 拒绝。这保证 `proxy.admin.*` ACL 被真正强制执行——当 admin 端口无法做网络隔离时使用。
### 11.4 启用 / 禁用接口
接口**默认关闭**(`grpcAdminServerEnable=false`)。要启用:
```properties
grpcAdminServerEnable: true # 在 grpcAdminServerPort 上启动 admin gRPC server
```
要保持禁用(默认),保留 `grpcAdminServerEnable: false`,或把 `grpcAdminServerPort` 设为 0 / 负数,此时 admin gRPC server 完全不启动。
### 11.5 审计
每个被服务的 admin RPC 都会把 subject(Console 登录用户 / AK)、method、resource、action 和 source IP 记录到鉴权审计 logger; 被拒绝的请求由 ACL 2.0 引擎自身记录。这满足审计四元组要求(Console 用户 + AK + 资源 + 操作)。