Dapr 内部 API(dapr/proto/internals/v1)协议深度解析:sidecar 间通信的 gRPC 契约与 Proto 客户端生成指南
【免费下载链接】daprDapr is a portable runtime for building distributed applications across cloud and edge, combining event-driven architecture with workflow orchestration.项目地址: https://gitcode.com/GitHub_Trending/da/dapr
本文以 Dapr 仓库中的 dapr/proto/internals/v1/README.md 为骨架,完整解析 Dapr 内部 API 协议包的定位、五个 .proto 文件的契约细节(服务调用、Actor 提醒、Job 调度),并逐条还原官方推荐的 Proto 客户端生成流程(make init-proto/make gen-proto)。读完本文,你将能够理解daprdsidecar 之间"零 HTTP 中间层"的直连通信模型,掌握从.proto到pkg/proto生成代码的完整工具链,并能在源码中定位这些内部 RPC 的实际调用点。
Internal APIs 在 Dapr 协议体系中的定位
Dapr 仓库将全部 Protobuf 定义按职责划分为若干包,dapr/README.md 中的总览表给出了它们的定位:
| packages | description |
|---|---|
| common | common protos that are imported by multiple packages |
| internals | internal gRPC and protobuf definitions which is used for Dapr internal |
| runtime | Dapr and App Callback services and its associated protobuf messages |
| operator | Dapr Operator gRPC service |
| placement | Dapr Placement service |
| sentry | Dapr Sentry for CA service |
| components | Dapr gRPC-based components services |
internals包正是本文的主角。dapr/proto/internals/v1/README.md 第一句话就给出了它的使命:
This folder is intended for the Internal APIs that the
daprdsidecars use to communicate with each other.
也就是说,这里的协议不面向用户应用,而是用于 Dapr sidecar(daprd)之间的"内部互通"。典型场景是:服务 A 的 sidecar 在收到用户对服务 B 的调用请求后,需要把请求转交给服务 B 的 sidecar——这条 sidecar-to-sidecar 链路走的就是internals包定义的 gRPC 服务ServiceInvocation。
该目录下共包含 5 个.proto文件,覆盖四类内部能力:
- service_invocation.proto:sidecar 间的服务调用与 Actor 调用(2021 年引入,最核心);
- apiversion.proto:内部 API 版本枚举;
- status.proto:HTTP/gRPC 应用通道的响应状态;
- reminders.proto:Actor 提醒/定时器事件(2023 年引入);
- jobs.proto:Job 调度请求与事件(2024 年引入)。
所有文件统一使用syntax = "proto3",包名为dapr.proto.internals.v1,并声明 Go 包映射github.com/dapr/dapr/pkg/proto/internals/v1;internals——这正是生成代码落盘位置的依据。
ServiceInvocation:sidecar 间服务调用的核心契约
RPC 方法总览
service_invocation.proto 定义了唯一的 gRPC 服务ServiceInvocation(第 36–60 行),包含 5 个 RPC 方法:
| RPC 方法 | 类型 | 用途 |
|---|---|---|
CallActor | Unary | 调用指定 Actor 的方法 |
CallLocal | Unary | 调用指定服务(应用)的方法 |
CallActorReminder | Unary | 触发远程内部 Actor 的 Reminder |
CallLocalStream | Bidi-streaming | 以数据流方式调用服务方法(请求分块上行、响应分块下行) |
CallActorStream | Server-streaming | 调用 Actor 方法并流式返回响应 |
.proto注释中有一个非常关键的设计说明:这些 RPC 的 gRPC 响应状态只代表内部连接状态,不代表被调用方的业务响应状态。也就是说,CallLocal/CallActor的 gRPC 层在大多数情况下都返回OK,真正的 HTTP 状态码/业务错误被封装在InternalInvokeResponse.status字段中带回调用方,由调用方 sidecar 再翻译成对用户请求的响应。这一点从消息结构上可以看得很清楚(见下文Status消息)。
核心消息结构
InternalInvokeRequest(第 73–86 行)是调用方 sidecar 向被调用方 sidecar 传输的完整请求载体:
message InternalInvokeRequest { APIVersion ver = 1; // 必填,Dapr Runtime API 版本 map<string, ListStringValue> metadata = 2; // 必填,调用方的 HTTP header 或 gRPC metadata common.v1.InvokeRequest message = 3; // 必填,调用方的调用请求 Actor actor = 4; // 仅 Actor 调用时使用 }InternalInvokeResponse(第 90–103 行)则反向携带被调用方的响应:
message InternalInvokeResponse { Status status = 1; // 必填,HTTP/gRPC 状态 map<string, ListStringValue> headers = 2; // 必填,应用回调的响应头 map<string, ListStringValue> trailers = 3; // 仅 gRPC 应用回调使用 common.v1.InvokeResponse message = 4; // 被调用方的响应消息 }两个辅助消息也很常用:
Actor(第 63–69 行):由actor_type(必填)与actor_id(必填)唯一标识一个 Actor 实例。在 pkg/proto/internals/v1/service_invocation_additional.go 中,手写辅助方法GetActorKey()用||分隔符把二者拼成 Actor 的 FQDN key(actor_type || actor_id),这个 key 会被 Reminder、定时器等模块复用。ListStringValue(第 126–129 行):repeated string values,用于承载可重复的 header/metadata 值。
流式 RPC 的分块语义
CallLocalStream是CallLocal的数据流版本。虽然它声明为双向流,但.proto注释明确要求其行为等价于"简单 RPC":调用方先发送完整请求(按块分多条消息),再读取完整响应(同样按块分条)。协议对消息编排有严格要求:
- 流中第一条消息必须携带
request(调用方)或response(被调用方),且所有必填属性齐全;该消息可以携带一个payload,也可以为空; - 后续所有消息只能携带
payload,不得再出现request/response等其他属性; - 携带
payload的每条消息必须带序号seq:从 0 开始、每个分块递增 1;不带payload的消息不得出现seq; - 发送方发完数据后必须调用
CloseSend; - 每个方向上至少要发送一条消息;若只发一条,则该消息必须同时携带
request/response(payload允许为空)。
对应的流消息定义(第 106–123 行)在结构上刻意与 Unary 版本错开:InternalInvokeRequestStream内部分别持有InternalInvokeRequest request(其message.data不携带数据)和common.v1.StreamPayload payload(数据分块),InternalInvokeResponseStream同理。
从源码可以看到该协议在流式调用时的工程约束:pkg/messaging/v1/util.go 中注释说明CallLocalStream的缓冲上限为 2KB;pkg/messaging/direct_messaging.go 发起CallLocalStream调用,并在 第 489 行附近 对"对端 sidecar 不支持CallLocalStream"(返回Unimplemented)做了降级回退处理——这是新旧 sidecar 混布(版本倾斜)场景下的兼容性设计。单测 pkg/messaging/direct_messaging_test.go 中还分别 mock 了CallLocalStream的客户端与服务端流,验证了这套流式消息编排。
三个轻量辅助协议:版本、状态与事件
APIVersion:内部 API 的版本门禁
apiversion.proto(第 21–27 行)只定义了一个枚举:
enum APIVersion { APIVERSION_UNSPECIFIED = 0; // 未指定 V1 = 1; // Dapr API v1 }InternalInvokeRequest.ver字段使用该枚举,保证调用双方 sidecar 对内部 API 版本的语义一致。目前只有V1一个正式版本。
Status:业务状态与 gRPC 状态解耦
status.proto(第 23–32 行)定义了Status:
message Status { int32 code = 1; // 必填,状态码 string message = 2; // 错误信息 repeated google.protobuf.Any details = 3; // 错误详情列表 }该消息承载的是"被调用方应用的响应状态"(HTTP 状态码或 gRPC 状态),与ServiceInvocation服务自身的 gRPC 连接状态相互独立——这正是前文"内部 RPC 大多返回 OK"设计落地的关键。
Reminder 与 TimerFiredEvent:Actor 提醒/定时器事件
reminders.proto(2023 年引入)定义了存储于 Dapr Actor 状态存储中的提醒结构:
message Reminder { string actor_id = 1; string actor_type = 2; string name = 3; google.protobuf.Any data = 4; string period = 5; google.protobuf.Timestamp registered_time = 6; string due_time = 7; google.protobuf.Timestamp expiration_time = 8; bool is_timer = 9; bool skip_lock = 10; } message Reminders { repeated Reminder reminders = 1; } message TimerFiredEvent { google.protobuf.Timestamp fire_at = 1; int32 timerId = 2; uint64 generation = 3; }Reminder统一了"提醒(reminder)"与"定时器(timer)"两种模型(is_timer区分),并支持周期(period)、到期时间(expiration_time)与锁跳过(skip_lock)。TimerFiredEvent描述了定时器触发时发送给 Actor 的事件(含触发时刻、timerId 与代际generation)。在源码中,Reminder会被直接作为远程 RPC 的入参:调用方侧 pkg/actors/router/router.go 通过CallActorReminder(ctx, &internalv1pb.Reminder{...})触发对端 sidecar 的 Actor 提醒;接收方侧 pkg/api/grpc/daprinternal.go 的CallActorReminder实现会校验actor_type、actor_id并交由 Actor 运行时投递。
JobHTTPRequest 与 JobEvent:Job 调度协议
jobs.proto(2024 年引入)定义了两个消息:
// 用于 HTTP Dapr Job API,保证 data 始终以 JSON 对象(google.protobuf.Struct)序列化 message JobHTTPRequest { string name = 1 [json_name = "name"]; optional string schedule = 2 [json_name = "schedule"]; optional uint32 repeats = 3 [json_name = "repeats"]; optional string due_time = 4 [json_name = "dueTime"]; optional string ttl = 5 [json_name = "ttl"]; google.protobuf.Value data = 6 [json_name = "data"]; optional bool overwrite = 7 [json_name = "overwrite"]; optional common.v1.JobFailurePolicy failure_policy = 8 [json_name = "failurePolicy"]; } message JobEvent { string key = 1; // Job 的 FQDN key string name = 2; // Job 名称 scheduler.v1.JobMetadata metadata = 3; google.protobuf.Any data = 4; }JobHTTPRequest的注释特意说明:其字段文档以dapr/proto/runtime/v1/dapr.proto中的对应定义为准,而之所以在内部包中单独定义一份,是为了保证 HTTP 侧data始终以 JSON 对象(google.protobuf.Struct)形态序列化。该消息被 HTTP API 层直接复用:pkg/api/http/jobs.go 将JobHTTPRequest作为 HTTP 处理器与 Universal 层之间的请求载体。JobEvent则是 Job 到期时交由 Scheduler 处理的事件,携带 FQDN key、元数据与数据负载。
Proto 客户端生成:从 .proto 到 pkg/proto
README 给出了完整的生成流程,核心是两条make命令。下面是逐步拆解,并与当前仓库 Makefile 的实际实现做了对齐。
步骤 1:安装 protoc
按 README 要求,首先需要安装指定版本的 protoc:
- Install protoc version: v4.25.4
即protobuf v4.25.4对应的 protoc 编译器。需要特别说明版本一致性:当前仓库 Makefile 中实际固定的是PROTOC_VERSION = 34.1(PROTOBUF_SUITE_VERSION = 34.1),而gen-proto依赖的check-proto-version目标(Makefile)会严格校验protoc --version输出是否为libprotoc 34.1,版本不匹配会直接报错退出。因此:如果你使用较新版本的 Dapr 源码,建议以 Makefile 中PROTOC_VERSION/PROTOC_GEN_*_VERSION固定的版本为准;README 中的 v4.25.4 是编写该文档时的基线。dapr/README.md还提供了 Windows(WSL2 + Ubuntu 24.04)下通过wget下载 protoc 压缩包、解压并放入/usr/local/bin的替代安装步骤。
步骤 2:安装生成插件
make init-protoinit-proto目标(Makefile)通过go install安装三个官方插件:
init-proto: go install google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GEN_GO_VERSION) go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v$(PROTOC_GEN_GO_GRPC_VERSION) go install connectrpc.com/connect/cmd/protoc-gen-connect-go@v$(PROTOC_GEN_CONNECT_GO_VERSION)当前仓库固定版本为:protoc-gen-go v1.32.0、protoc-gen-go-grpc v1.3.0、protoc-gen-connect-go v1.18.1(见 Makefile)。三个插件各司其职:protoc-gen-go生成消息的 Go 结构体(*.pb.go),protoc-gen-go-grpc生成 gRPC 服务代码(*_grpc.pb.go),protoc-gen-connect-go生成 Connect RPC 客户端/服务端代码(*connect/目录)。
步骤 3:生成 gRPC Proto 客户端
在仓库根目录执行:
make gen-protogen-proto目标(Makefile)先运行check-proto-version校验工具链版本,然后为dapr/proto下的每个子包(GRPC_PROTOS,即 operator、placement、sentry、common、runtime、internals、components 等)逐一执行生成。每个包的实际 protoc 命令模板(Makefile)为:
protoc -I . ./dapr/proto/$(1)/v1/*.proto \ --go_out=. --go_opt=module=github.com/dapr/dapr \ --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false,module=github.com/dapr/dapr \ --connect-go_out=. --connect-go_opt=module=github.com/dapr/dapr几个值得注意的选项:
module=github.com/dapr/dapr:让生成文件按 Go module 路径布局,直接落入仓库内的pkg/proto/...,而不是产生github.com/dapr/dapr的冗余目录层级;require_unimplemented_servers=false:生成的服务接口不强制内嵌UnimplementedXxxServer,便于测试代码用轻量 mock 直接实现接口(pkg/messaging/direct_messaging_test.go 中的mockGRPCServerUnary就是直接实现CallLocal的例子);- 生成完毕后
gen-proto还会执行modtidy(go mod tidy)保持依赖一致。
步骤 4:查看生成产物
README 指出生成结果位于pkg/proto。对 internals 包而言,生成产物在 pkg/proto/internals/v1 下:
pkg/proto/internals/v1/ ├── apiversion.pb.go ├── jobs.pb.go ├── reminders.pb.go ├── service_invocation.pb.go ├── service_invocation_grpc.pb.go ├── service_invocation_additional.go # 手写扩展(非生成) ├── service_invocation_additional_test.go # 手写扩展的单元测试 ├── status.pb.go └── internalsconnect/ └── service_invocation.connect.go # Connect RPC 生成代码其中service_invocation_additional.go是人工编写的辅助代码(文件头注释明确说明"contains additional, hand-written methods added to the generated objects"),定义了GRPCContentType、JSONContentType、ProtobufContentType、OctetStreamContentType等媒体类型常量(第 25–34 行),以及Actor.GetActorKey()、NewInternalInvokeRequest(method)等便捷构造器(第 46–60 行)。NewInternalInvokeRequest会自动填充Ver: APIVersion_V1并初始化common.v1.InvokeRequest{Method: method},是调用方 sidecar 构造内部请求的标准入口。
在源码中验证内部 RPC 的真实调用链
生成代码最终服务于 sidecar 的运行时逻辑,以下是三条可以对照阅读的核心调用链:
服务调用的主路径:pkg/messaging/direct_messaging.go 是
direct messaging的枢纽。Unary 调用在 第 385 行 通过clientV1.CallLocal(ctx, pd, opts...)把用户请求转发给目标应用的 sidecar;需要传输大请求时则切换为 第 400 行 的CallLocalStream。配合 pkg/messaging/v1/util.go 的 2KB 缓冲注释,可以看到流式调用专为突破 Unary 消息大小限制而设计。Actor 提醒的远程触发:pkg/actors/router/router.go 在确认目标 Actor 不在本地后,构造
internalv1pb.Reminder并调用client.CallActorReminder(...);对端 pkg/api/grpc/daprinternal.go 的CallActorReminder实现接收后投递给 Actor 运行时。这条链路完整复现了reminders.proto与service_invocation.proto的协作。Job 的 HTTP 入口:pkg/api/http/jobs.go 直接把
internalsv1pb.JobHTTPRequest作为 HTTP→Universal 层的中介类型,印证了jobs.proto"HTTP 侧 data 恒为 JSON 对象" 的设计初衷;而 dapr/proto/scheduler/v1/scheduler.proto 则是JobEvent.metadata中JobMetadata的来源,二者共同构成"Job API → 内部事件 → Scheduler"的链路。
小结与扩展阅读
dapr/proto/internals/v1是 Dapr sidecar 之间"不打用户 HTTP 网关"的内部通信契约:ServiceInvocation承载服务调用与 Actor 调用(含流式分块),Reminder/JobEvent承载 Actor 提醒与 Job 调度事件,APIVersion与Status则保障版本语义与"业务状态/连接状态解耦"。配合make init-proto+make gen-proto的工具链,任何.proto修改都能稳定地同步到pkg/proto下的 Go 代码。
如需继续深入,推荐按以下路径阅读当前仓库:
- 协议定义:继续阅读 dapr/proto/internals/v1/service_invocation.proto 与 dapr/proto/internals/v1/reminders.proto 的完整注释;
- 生成工具链:Makefile 的
init-proto/gen-proto/check-proto-version三个目标,以及 tools/codegen.mk 中面向各 SDK 的旧版生成模板; - 调用方实现:pkg/messaging/direct_messaging.go 与 pkg/messaging/direct_messaging_test.go;
- 接收方实现:pkg/api/grpc/daprinternal.go(含
CallActorReminder等内部 API 的落地); - 跨包协议总览:dapr/README.md(含 Windows/WSL2 下的 protoc 安装变体与 e2e 测试应用依赖更新说明)。
【免费下载链接】daprDapr is a portable runtime for building distributed applications across cloud and edge, combining event-driven architecture with workflow orchestration.项目地址: https://gitcode.com/GitHub_Trending/da/dapr
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考