Dapr 1.5.3 发布详解:修复 Actor 状态存储缺失导致客户端调用失败的问题
2026/9/12 16:54:38 网站建设 项目流程

Dapr 1.5.3 发布详解:修复 Actor 状态存储缺失导致客户端调用失败的问题

【免费下载链接】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

导读

本文基于 docs/release_notes/v1.5.3.md 发布说明,深入剖析 Dapr 1.5.3 中修复的 Actor 状态存储配置行为缺陷:修复前,只要没有配置 actor state store 组件,所有 Actor API 都会报错;修复后,仅作为调用方(client)的服务即使没有 actor state store 也能正常使用 Actor API,而注册 Actor 的服务依旧强制要求提供状态存储。读完本文,你将掌握该问题的现象、根因、修复方案,以及从源码层理解 Actor runtime 初始化、Actor 状态存储解析与热重载的完整机制,并学会如何正确配置 actorStateStore 属性。

一、问题背景:Actor API 与 Actor 状态存储的关系

在 Dapr 中,Actor 模型的运行依赖于两个核心基础设施:

  • Placement 服务:负责 Actor 实例在多个节点间的分布与负载均衡;
  • Actor 状态存储(actor state store):负责持久化 Actor 的状态数据,是 Actor 有状态特性的基石。

按 Dapr 的设计,一个服务可以扮演两种角色:

角色行为是否必须有 Actor 状态存储
Actor 宿主(host)注册并承载 Actor 类型,处理 Actor 方法调用必须,因为要保存 Actor 状态
Actor 客户端(client)仅通过 Actor API 发起对远端 Actor 的调用不需要,状态由宿主侧存储

修复之前,Dapr 在初始化 Actor API 时采用了一刀切的策略:只要运行时中没有可用的 Actor 状态存储组件,无论该服务是否注册了 Actor,所有 Actor API 都会直接返回错误。这导致一类非常常见的场景被误伤——纯客户端应用(例如 e2e 测试中的actortestclient)仅仅为了调用其他服务的 Actor 而启动 daprd,却因为没有配置状态存储而无法使用InvokeActor等 API。

二、根因分析:Actor API 初始化对状态存储的过度依赖

发布说明明确指出根因在于:

The code that initializes the Actor API raises an error when there is no actor state storage component available, regardless of whether or not the service registers actors.

即 Actor API 的初始化逻辑在“没有 Actor 状态存储组件”时就抛错,而没有区分“该服务是否注册了 Actor”。从当前仓库源码可以印证这一历史问题被修复后的演进形态。在 pkg/actors/actors.go 的Init方法中,Actor runtime 初始化时通过GetStateStoreActorWithRevision()探测状态存储:

_, a.hostingName, a.hostingRev, a.hostingActive = a.compStore.GetStateStoreActorWithRevision() if !a.hostingActive { log.Info("Actor state store not configured - actor hosting disabled until one is configured, but invocation enabled") }

注意这一行日志——"actor hosting disabled until one is configured,but invocation enabled"。这正是修复后引入的关键语义:托管(hosting)能力与调用(invocation)能力被解耦。没有状态存储时,Actor runtime 依然可以正常初始化并对外提供调用 API,只是暂时无法托管 Actor 类型。

三、解决方案:按需禁用 Actor 托管,而不是禁用整个 Actor API

发布说明中的修复思路可总结为:

  1. 当没有 actor state store 时,只要服务不注册 Actor,Actor API 就正常初始化并可用(供纯客户端调用);
  2. 当服务注册了 Actor 却没有提供 actor state store 时,Actor API 继续保持不可用(这是正确行为,因为宿主必须有状态存储);
  3. 未注册 Actor 的服务,无论有没有 actor state store,Actor API 都保持可用。

3.1 源码印证:托管与调用的解耦

在 pkg/actors/actors.go 中,InithostingActive的状态传递给 Actor 表:

a.table = table.New(table.Options{ ReentrancyStore: a.reentrancyStore, StartSuspended: !a.hostingActive, Timers: func() internaltimers.Storage { return a.timerStorage }, })

StartSuspended: !a.hostingActive表示:没有状态存储时,Actor 表以“挂起”状态启动——不激活 Actor 实例、不注册到 placement,但整个 runtime 照常运行。而Run阶段在 pkg/actors/actors.go 中,也只在存在 Actor 状态存储时才等待宿主注册完成:

if _, _, ok := a.compStore.GetStateStoreActor(); ok { select { case <-a.registerDoneCh: case <-ctx.Done(): return ctx.Err() } } return a.placement.Run(ctx)

也就是说,纯客户端模式(无状态存储)下,placement 客户端照常启动以接收 Actor 位置信息,但不会注册任何宿主 Actor 类型。

3.2 源码印证:注册宿主时的校验

在 pkg/actors/actors.go 的RegisterHosted方法中,当HostedActorTypes为空(即服务没有注册任何 Actor)时直接返回:

if len(cfg.HostedActorTypes) == 0 { return nil }

这从 API 层面保证:纯客户端服务无需等待状态存储即可完成初始化;而一旦传入非空 Actor 类型列表(注册 Actor 的宿主),后续状态读写操作会通过状态存储解析层强制校验。

四、状态存储解析层的强制校验

当宿主服务确实要读写 Actor 状态时,底层会走到 pkg/actors/state/state.go 的stateStore()方法:

func (s *state) stateStore() (string, Backend, error) { storeS, storeName, ok := s.compStore.GetStateStoreActor() if !ok { return "", nil, messages.ErrActorRuntimeNotFound } store, ok := storeS.(Backend) if !ok || !contribstate.FeatureETag.IsPresent(store.Features()) || !contribstate.FeatureTransactional.IsPresent(store.Features()) { return "", nil, errors.New(errStateStoreNotConfigured) } return storeName, store, nil }

这段代码揭示了两层校验:

  1. 组件存在性GetStateStoreActor()返回 false 时,直接返回ErrActorRuntimeNotFound。对应的错误文案定义在 pkg/messages/predefined.go:

    the state store is not configured to use the actor runtime. Have you set the - name: actorStateStore value: "true" in your state store component file?

  2. 能力校验:即使存在组件,也必须同时支持ETag事务(Transactional)特性,否则返回errStateStoreNotConfigured(定义于 pkg/actors/state/state.go)。这是因为 Actor 状态读写依赖事务性操作(如Multi批量提交,见 pkg/actors/state/state.go)与并发控制(ETag)。

该解析层每次调用都实时从组件存储中解析,因此天然支持状态存储的热重载,见 pkg/actors/state/state_test.go 的Test_stateStore测试:它在添加、删除、更换不同名称的 actor state store 后逐一验证解析行为。

五、如何配置 Actor 状态存储:actorStateStore 属性

要在 Dapr 中把某个状态存储指定为 Actor 状态存储,需要在 Component 配置的metadata中设置actorStateStore: true。该属性由状态组件处理器在初始化时解析,见 pkg/runtime/processor/state/state.go:

if s.actorsEnabled { actorStoreSpecified := false for k, v := range props { if strings.ToLower(k) == PropertyKeyActorStateStore { actorStoreSpecified = kitstrings.IsTruthy(v) break } } if actorStoreSpecified { if err = s.compStore.AddStateStoreActor(comp.Name, store); err != nil { // ... } log.Info("Using '" + comp.Name + "' as actor state store") if s.actors != nil { s.actors.OnActorStateStoreChanged() } } }

关键细节:

  • 属性名大小写不敏感(strings.ToLower(k)与常量PropertyKeyActorStateStore = "actorstatestore"比较,见 pkg/runtime/processor/state/state.go);
  • 值通过kitstrings.IsTruthy解析,接受"true"等真值写法;
  • 添加成功后调用OnActorStateStoreChanged()通知 Actor runtime 收敛托管状态(见下文第六节)。

仓库自带的真实配置示例可以参考 tests/config/dapr_postgres_state_actorstore.yaml:

apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore-actors spec: type: state.postgres version: v2 metadata: - name: connectionString value: "host=dapr-postgres-postgresql.dapr-tests.svc.cluster.local user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" - name: tablePrefix value: v2actor - name: metadataTableName value: dapr_metadata_v2actor - name: actorStateStore value: true scopes: # actortestclient is deliberately omitted to ensure that `actor_features_test` works without a state store - actor1 - actor2 - actorapp - actorfeatures

同样,tests/config/dapr_cosmosdb_state_actorstore.yaml 为 Cosmos DB 状态存储设置了actorStateStore: true。两处配置的scopes注释都刻意写明:actortestclient(纯客户端测试应用)被有意排除在作用域之外,以验证无状态存储时客户端调用测试仍然通过——这正是本文所述修复在 e2e 测试中的直接体现。

组件存储层的注册与校验

当多个状态存储被标记为 actor state store 时,pkg/runtime/compstore/statestore.go 的AddStateStoreActor会拒绝重复注册:

func (c *ComponentStore) AddStateStoreActor(name string, store state.Store) error { if c.actorStateStore.store != nil && c.actorStateStore.name != name { return fmt.Errorf("detected duplicate actor state store: %s and %s", c.actorStateStore.name, name) } c.states[name] = store c.actorStateStore.name = name c.actorStateStore.store = store c.actorStateStore.rev++ return nil }

同时每个槽位维护一个自增的rev(revision),GetStateStoreActorWithRevision()(pkg/runtime/compstore/statestore.go)返回该版本号,用于检测状态存储的“增删换”等迁移事件——包括同名删除后重新添加这种 rev 相同但语义不同的情况。

六、热重载与托管收敛:状态存储变化时的动态调整

修复方案不仅覆盖启动阶段,还覆盖运行期的动态变化。Actor runtime 通过storeKickCh通道接收状态存储变更通知(pkg/actors/actors.go):

func (a *actors) OnActorStateStoreChanged() { select { case a.storeKickCh <- struct{}{}: default: } }

通知采用非阻塞的 coalesce 模式,避免频繁通知堆积。随后convergeHosting(pkg/actors/actors.go)根据最新状态收敛托管行为:

func (a *actors) convergeHosting(ctx context.Context) { _, name, rev, ok := a.compStore.GetStateStoreActorWithRevision() if rev == a.hostingRev { return } a.hostingRev = rev if ok && a.hostingActive && name == a.hostingName { log.Infof("Actor state store %s updated - actor hosting continues", name) return } if a.hostingActive { log.Info("Actor state store removed or replaced - draining hosted actors") if err := a.table.SuspendHosting(ctx); err != nil { log.Errorf("Error draining hosted actors after actor state store change: %s", err) } } if ok { log.Infof("Actor state store %s configured - enabling actor hosting", name) a.table.ResumeHosting() } a.hostingActive = ok a.hostingName = name }

该逻辑处理三类事件:

事件行为
同名热更新(如密钥轮换)数据路径每次调用实时解析组件存储,宿主无需排空,继续服务
状态存储被移除或替换挂起宿主并排空已托管的 Actor
状态存储被配置恢复宿主,开始托管 Actor

七、总结与验证建议

Dapr 1.5.3 的这项修复本质上是把“Actor 状态存储缺失”从Actor API 级错误降级为Actor 托管级限制

  • 纯客户端服务(只调用不托管):无状态存储也能正常使用 Actor API,提升 Actor 客户端部署的轻量性;
  • 宿主服务(注册 Actor):仍必须配置具备 ETag 与事务特性的状态存储,保证状态一致性与并发安全;
  • 运行期动态变化:通过 revision 驱动的convergeHosting实现状态存储的增删换热收敛。

如需在本地复现验证,可参考 pkg/actors/state/state_test.go 的单元测试逻辑:创建空组件存储 → 断言ErrActorRuntimeNotFound→ 添加 actor store 后断言解析成功 → 删除后再次断言报错,即可完整覆盖“无存储/有存储”两种路径。仓库的 e2e 测试应用 tests/apps/actorfeatures 与刻意不配置状态存储的 tests/apps/actorclientapp 则从集成层面验证了修复效果。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询