grpcurl 与 Evans gRPC 调试工具手册
从 UNIMPLEMENTED 开始,而不是先改服务代码
本地客户端刚生成成功,调用测试环境却返回:
ERROR:
Code: Unimplemented
Message: unknown service account.v1.AccountService浏览器 Network 面板很难解释这个现场。gRPC 以 HTTP/2 传输二进制 Protobuf 帧,方法身份不是普通 URL,而是由包名、服务名和方法名组成的全限定名。调用前,调试工具还必须拿到描述符,才能把 JSON 输入转换成 Protobuf 消息。错误可能来自四个不同层次:描述符与服务部署版本不一致、请求没有穿过支持 gRPC 的入口、metadata 未满足拦截器要求,或者 TLS 握手根本没有进入 RPC 层。
先用 grpcurl 做无状态诊断,再用 Evans 在 REPL 中探索。grpcurl 的命令、退出码和标准输出适合脚本与 CI;Evans 的补全、show、desc 和交互式流输入更适合人在现场逐步确认。两者都不是业务 SDK 的替代品,它们验证的是“描述符、传输、认证和服务实现能否组成一次合法调用”。
示例采用当前稳定版 grpcurl 1.9.3 与 Evans 0.10.11 的命令口径。执行前分别运行 grpcurl --version、evans --version;团队脚本应固定补丁版本或镜像摘要,避免 latest 在 CI 中无审查漂移。二进制与变更记录分别从 grpcurl Releases 和 Evans Releases 获取。
把工具装到可重复的位置
grpcurl 官方提供 Releases 二进制、Homebrew、Docker、Snap 和 Go 安装入口。已有 Go 工具链时可以安装明确版本:
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@v1.9.3
grpcurl --versionmacOS 可由 Homebrew 管理:
brew install grpcurl
grpcurl --version临时容器适合不污染开发机的诊断,但容器内的 localhost 指向容器自身。macOS 与 Windows 访问宿主服务要改成 host.docker.internal,proto 或 protoset 还要只读挂载:
docker run --rm \
-v "$(pwd)/proto:/protos:ro" \
fullstorydev/grpcurl:v1.9.3 \
-plaintext -import-path /protos -proto account/v1/account.proto \
host.docker.internal:50051 listEvans 官方推荐 Releases 或 Homebrew;官方 README 将 go install 标为不推荐入口,并要求 Go 1.20 或更高版本。开发机可这样安装:
brew tap ktr0731/evans
brew install evans
evans --versionWindows 与 Linux 直接使用 Evans Releases 的对应二进制。容器入口适合携带仓库内 proto:
docker run --rm -it \
-v "$(pwd):/mount:ro" \
ghcr.io/ktr0731/evans:v0.10.11 \
--path ./proto --proto account/v1/account.proto \
--host host.docker.internal --port 50051 repl安装完成的最低证据是版本命令退出码为 0,并且可在不连接服务时解析本地描述符。若命令不存在,先检查 Go 的 bin 目录或包管理器安装目录是否进入 PATH;若容器无法读取 proto,检查挂载路径和容器内 --path,不要把宿主绝对路径原样传给容器。
描述符决定 JSON 怎样变成线上字节
grpcurl 支持三种描述符源。它们最终都提供 FileDescriptorProto 信息,包括 package、service、method、message、field number、类型和依赖关系。
反射适合快速发现,但不是契约仓库
服务端启用 gRPC Server Reflection 后,grpcurl 默认向反射服务查询描述符:
grpcurl -plaintext api.example.test:50051 list
grpcurl -plaintext api.example.test:50051 list account.v1.AccountService
grpcurl -plaintext api.example.test:50051 \
describe account.v1.AccountService.GetAccount预期结果依次是服务全限定名、方法列表和等价 proto 描述。list 成功只能证明反射链路可访问;它不证明业务方法有权限,也不证明当前反射结果与准备发布的仓库契约一致。
若得到 server does not support the reflection API,先不要要求生产环境打开反射。该错误说明描述符发现失败,而非业务服务一定不可用。测试环境可以在鉴权或内网边界后启用反射;生产环境更稳妥的做法通常是向调用方交付受版本控制的 proto 或 protoset。
.proto 最透明,import 根目录最容易出错
关闭反射后,显式给出 proto 和 import 根目录:
grpcurl -plaintext \
-import-path proto \
-proto account/v1/account.proto \
api.example.test:50051 \
describe account.v1.AccountService.GetAccount-import-path 对应 proto 中 import "common/v1/page.proto"; 的查找根,而不是当前文件所在目录。典型失败是:
Failed to process proto source files: could not parse given files:
account/v1/account.proto: File not found正向实验把命令放在仓库根目录执行,并让 proto/common/v1/page.proto 与 import 字符串对应;反向实验把 -import-path proto 攰成 -import-path proto/account,错误会稳定出现在解析阶段,RPC 尚未发出。这个证据可以把“服务不可用”排除掉,定位到契约打包或工作目录。
protoset 更适合 CI 和跨仓库交付
protoset 是二进制编码的 google.protobuf.FileDescriptorSet。生成时必须包含依赖:
protoc \
--proto_path=proto \
--descriptor_set_out=build/account.protoset \
--include_imports \
account/v1/account.proto
grpcurl -protoset build/account.protoset list
grpcurl -protoset build/account.protoset \
describe account.v1.AccountService.GetAccount省略 --include_imports 时,根文件可能存在,但被引用消息的描述符缺失,消费端会报找不到依赖或符号。protoset 减少每次调用解析 proto 的开销,也避免 CI 依赖整棵源码目录;代价是人不能直接审阅二进制内容,因此要同时记录生成命令、源契约版本与制品校验值。
先跑一对正反调用
假设服务暴露 account.v1.AccountService/GetAccount,请求消息包含字符串字段 id。反射可用时,正向调用是:
grpcurl -plaintext -expand-headers \
-H 'authorization: Bearer ${GRPC_TOKEN}' \
-H "x-tenant-id: demo-tenant" \
-H "x-request-id: trace-demo" \
-d '{"id":"demo-account"}' \
api.example.test:50051 \
account.v1.AccountService/GetAccount预期证据不是固定业务值,而是:进程退出码为 0,响应可解析为 JSON,服务端或网关日志能按 trace-demo 找到同一次调用,最终 gRPC status 为 OK。-v 可展示响应 header、trailer 与 status,排查时比只保留 body 更有价值:
grpcurl -v -plaintext \
-H "x-request-id: trace-demo" \
-d '{"id":"demo-account"}' \
api.example.test:50051 \
account.v1.AccountService/GetAccount反向实验移除 authorization,预期得到 UNAUTHENTICATED;保留 token 但换成无权租户,预期通常是 PERMISSION_DENIED。若两次都返回 UNKNOWN 或代理生成的 HTML,错误没有被正确映射为 gRPC status,应继续检查网关和服务端异常处理。不要在问题单中粘贴真实 token,只记录 status、脱敏后的 metadata 键名、request id 和服务端证据位置。
metadata 在 HTTP/2 上承载为 header/trailer,但它有 gRPC 语义。键通常使用小写 ASCII;二进制 metadata 使用 -bin 后缀。-expand-headers 会在进程内展开单引号参数里的 ${GRPC_TOKEN},避免真实 token 直接出现在命令行参数中;环境变量仍可能被同身份进程或调试日志读取。grpcurl 的 -H 会同时用于反射与业务 RPC;反射需要独立凭证时,使用 -reflect-header 与 -rpc-header 分离 metadata,避免把高权限业务 token 无意发送给反射服务。
TLS 与 mTLS 要逐层证明
grpcurl 默认使用 TLS,-plaintext 明确关闭 TLS。不要用 -insecure 当常规修复,它跳过服务端证书校验,会掩盖 CA、SAN 或 SNI 配置错误。
使用受信任公网证书时:
grpcurl api.example.test:443 list使用私有 CA 时:
grpcurl \
-cacert certs/dev-ca.pem \
api.example.test:443 list地址必须与证书 SAN 匹配。直连负载均衡 IP、但证书签给域名时,可用正确域名访问;确有诊断需要时使用 -authority api.example.test 对齐 HTTP/2 :authority 与 TLS server name:
grpcurl \
-cacert certs/dev-ca.pem \
-authority api.example.test \
192.0.2.10:443 listgrpcurl 使用的 gRPC-Go 默认拨号器支持由 HTTPS_PROXY 指定的 HTTP CONNECT 代理,并用 NO_PROXY 绕过指定目标;代理变量名不代表代理自身必须是 HTTPS。当前 gRPC-Go 不支持先以 TLS 连接 HTTPS CONNECT 代理,因此常见写法是:
HTTPS_PROXY=http://proxy.example.com:8080 \
grpcurl -max-time 5 api.example.test:443 list代理是否真正生效要用反向证据确认:把 HTTPS_PROXY 临时指向 http://127.0.0.1:9,调用应在时限内非零退出;再设置 NO_PROXY=api.example.test,同一目标应恢复直连。若两次结果相同,先检查环境变量继承和固定版本行为,不要把“配置了代理变量”等同于流量已经过代理。Evans 没有专用代理参数;受限网络中应对实际固定二进制做同样的不可达代理对照,无法证明代理生效时改用获批隧道、旁路工具或 grpcurl,而不是猜测其网络路径。
mTLS 在验证服务端证书之外,还要求客户端发送证书并证明持有私钥:
grpcurl \
-cacert certs/dev-ca.pem \
-cert "${GRPC_CLIENT_CERT}" \
-key "${GRPC_CLIENT_KEY}" \
api.example.test:443 list-cert 与 -key 必须成对出现。certificate signed by unknown authority 指向客户端信任链;certificate is valid for ... 指向 SAN/SNI;握手阶段的 bad certificate 通常指向服务端拒绝客户端证书、证书过期或用途不符;进入 RPC 后的 UNAUTHENTICATED 则说明 TLS 已建立,应用层身份仍未满足。
Evans 对应配置为 --tls、--cacert、--cert 和 --certkey。探索时可以这样启动:
evans --tls \
--cacert certs/dev-ca.pem \
--cert "${GRPC_CLIENT_CERT}" \
--certkey "${GRPC_CLIENT_KEY}" \
--host api.example.test --port 443 \
-r repl私钥文件权限应仅允许当前用户或 CI 身份读取。证书可进入受控制品,真实私钥不得进入仓库、命令历史、终端录屏或工单附件;执行结束后撤销临时凭证,并清理 shell 环境变量和工作目录中的临时文件。
用 Evans 观察交互状态
反射可用时启动 REPL:
evans --host api.example.test --port 50051 -r repl关闭反射时从 proto 启动:
evans \
--path proto \
--proto account/v1/account.proto \
--host api.example.test --port 50051 \
repl进入后逐步选择对象:
> show package
> package account.v1
> show service
> service AccountService
> show method
> desc GetAccountRequest
> header authorization=Bearer <access-token>
> header x-tenant-id=demo-tenant
> call GetAccountEvans 会按字段类型提示输入,并以 JSON 输出响应。show header 用来确认当前会话实际携带哪些 metadata;header authorization 可移除该键。REPL 的状态便利也带来风险:切换 host 或租户后,旧 header 可能继续存在。每次改环境先清 metadata,再执行一个无副作用方法确认目标服务与身份。
需要脚本化时,Evans CLI 可从文件读取请求:
evans --path proto --proto account/v1/account.proto \
--host api.example.test --port 50051 \
cli call --file api-collections/grpc/get-account.example.json \
account.v1.AccountService.GetAccount对稳定的 smoke,grpcurl 通常更直接;对枚举、oneof、repeated、bytes 或不熟悉的消息,Evans 的交互提示更省试错。GUI/REPL 发现出来的最终请求应回写成脱敏 JSON 与命令,避免关键复现步骤只存在个人历史中。
流式 RPC 需要观察生命周期
一元 RPC 是“一条请求、一条响应”;服务端流是一条请求对应多条响应;客户端流是多条请求汇成一条响应;双向流允许两侧独立发送。grpcurl 从交互终端读取 -d @ 时,可以逐条输入 JSON,空行分隔消息,结束 stdin 表示客户端半关闭:
grpcurl -plaintext -d @ \
api.example.test:50051 \
account.v1.AccountService/WatchAccounts服务端流的预期证据是连续出现多个 JSON 响应,直到服务端正常结束、客户端取消或超时。客户端流与双向流中,Ctrl+D 表示 stdin EOF;Ctrl+C 是取消调用,两者在服务端看到的状态不同。CI 不应等待无限流自然结束,应设置总时限并限制样本:
printf '%s\n' '{"id":"demo-a"}' '{"id":"demo-b"}' | \
grpcurl -max-time 10 -plaintext -d @ \
api.example.test:50051 \
account.v1.AccountService/BatchGet若直连服务能持续收流,经过网关却固定在某个时长断开,优先比较入口的 HTTP/2、空闲超时、最大连接时长和缓冲策略。若客户端持续写入后服务端内存上升、响应延迟扩大,则要检查消费速度与 backpressure。若取消后服务端任务仍运行,证据应包括客户端 status、网关断开原因、服务端 context 取消日志和存活任务数,而不是笼统描述“流卡住”。
把可重复证据接进项目与 CI
仓库中保存契约、脱敏请求和脚本,不保存凭证:
proto/
account/v1/account.proto
api-collections/grpc/
get-account.example.json
build/contracts/
account.protoset
scripts/grpc-smoke/
check-account.shCI 先验证 protoset 能解析,再调用同一契约中的无副作用读取方法。下面的脚本依赖 CI Secret 注入 token,并对总调用时间设限:
set -euo pipefail
: "${GRPC_TARGET:?missing GRPC_TARGET}"
: "${GRPC_TOKEN:?missing GRPC_TOKEN}"
grpcurl -protoset build/contracts/account.protoset \
describe account.v1.AccountService.GetAccount >/dev/null
grpcurl -max-time 10 \
-protoset build/contracts/account.protoset \
-expand-headers \
-H 'authorization: Bearer ${GRPC_TOKEN}' \
-H "x-request-id: ci-grpc-smoke" \
-d '{"id":"demo-account"}' \
"${GRPC_TARGET}" account.v1.AccountService/GetAccount脚本只把退出 0 当作 gRPC OK,把非零视为失败,再从 stderr 中提取脱敏后的 gRPC status;不要假设 UNAUTHENTICATED、PERMISSION_DENIED 等状态会映射成各自固定的数字退出码。反向 smoke 删除 token 或换成低权限身份,必须同时看到非零退出、预期 status 和对应 request id,随后撤销临时身份并清除环境变量。CI 应保留工具版本、目标环境代号、gRPC status、耗时和脱敏 request id;stdout/stderr 进入日志前要过滤 metadata 与业务 body。这个 smoke 证明通路、身份和部署契约能完成一次读取,但不能替代覆盖业务规则的契约测试。
升级时先在临时分支重新生成 protoset,比较服务/方法/字段变化,再升级工具镜像。回滚顺序是恢复上一个已验证工具版本和 protoset,撤销临时凭证,删除临时导出的 proto 与响应文件;工具升级不应顺手改变服务契约。
用第一份证据决定下一步
| 现场 | 第一份有效证据 | 下一步 |
|---|---|---|
| 连接立即失败 | DNS、TCP 或 TLS 错误 | 校对目标、代理、CA、SAN 与客户端证书 |
| 返回 HTML 或 HTTP/1.1 错误 | 入口响应头与网关日志 | 检查入口是否支持端到端 gRPC/HTTP/2 |
server does not support reflection | reflection status | 改用 proto/protoset,或在受控环境启用反射 |
| proto import 失败 | 缺失文件与 import 字符串 | 修正 import 根目录或重建含依赖的 protoset |
UNIMPLEMENTED | list/describe 的全限定名 | 核对部署版本、package、service、method 和路由 |
UNAUTHENTICATED | metadata 键与 token 状态 | 补齐身份凭证,不记录真实值 |
PERMISSION_DENIED | 已认证主体、租户与 scope | 调整最小授权或测试账号 |
DEADLINE_EXCEEDED | 客户端时限、网关时限、服务耗时 | 分层对比直连与入口,避免盲目加大超时 |
RESOURCE_EXHAUSTED | message size、并发或限流 trailer | 缩小样本,核对服务与入口容量策略 |
| 流固定时长中断 | header/trailer、代理断开原因 | 检查 idle timeout、max connection age 和取消传播 |
UNKNOWN 往往是服务端异常映射不完整;UNAVAILABLE 更接近瞬时链路或实例不可用,但也可能由 TLS/代理关闭连接产生。重试前先判断方法是否幂等,并保留原 request id。对 mutation 类 RPC 盲目重试可能制造重复写入。
长期治理的是调试能力,不是个人命令历史
reflection 暴露服务名与消息结构,proto 可能含内部命名与注释,protoset 也不是不可读的安全容器。团队应把三者都视为契约资产:按服务授权访问,发布时生成,弃用时回收,生产入口是否开放 reflection 由服务 owner 与安全 owner 共同决定。
metadata、请求和响应可能包含 token、租户、用户标识与业务数据;mTLS 还引入客户端私钥。共享材料保留键名、类型、status、脱敏 request id 和最小样本。终端历史、Evans 会话配置、CI 日志与导出的 proto 都要纳入秘密扫描和保留周期。
容量与成本主要落在三处:高频反射和 describe 增加控制面访问;大响应和无限流占用长连接、网关并发与日志空间;CI smoke 数量随服务和环境相乘。治理指标可以记录 smoke 成功率、P95 调用耗时、各 status 计数、超时/取消后的存活调用数、反射访问量和日志脱敏命中。阈值应来自服务 SLO 与容量基线,不用脱离负载的固定数字冒充生产标准。
工具选型可以落成简单规则:交互发现选 Evans,稳定复现和流水线选 grpcurl,业务回归选正式 SDK 与契约测试框架,浏览器 gRPC-Web 问题再引入支持 gRPC-Web 的客户端和浏览器网络证据。团队要指定工具版本 owner、契约 owner、凭证 owner 和 CI owner,并定期演练“关闭 reflection 后仍可用 protoset 排障”“证书轮换后 smoke 可恢复”“流取消后服务端资源回落”。
完成一次调试后,确认这些结果:工具版本已记录;描述符来源可追溯;正向调用与一个可预期的反向调用都有证据;TLS/mTLS 没有依赖跳过校验;流式调用有时限和取消语义;CI 使用脱敏样本与临时凭证;临时证书、响应、导出描述符和 shell 变量已经清理。
