发布日期: 2026-09-10
版本号: v1.16.20

Dapr 1.16.20 是一个缺陷修复版本,包含三项修复:第一,修复了 daprd 边车与 Placement 服务断连后无法重连的问题——此前当断连恰逢演员停用失败(如应用对演员 DELETE 调用返回非 2xx)时,客户端会因把停用失败视为致命错误而永久退出重连循环,受影响节点不再加入放置表,继续用过期的本地表路由调用,导致全命名空间演员调用挂起或超时、演员提醒不触发,且健康检查始终正常、只能靠重启 Pod 恢复;现在重连期间的停用改为尽力而为,失败仅记录日志并继续重连,重连失败改为每秒重试而不是终止客户端。第二,修复了 Placement 分发轮次中解锁指令晚于锁定指令超过 15 秒时,15 秒失效保护提前推进解锁计数器、迟到的解锁指令又推进一次导致两个计数器永久错位,此后所有解锁指令都配对失败而不释放表锁、失效保护也不再触发,整个主机的演员调用和状态操作永久挂起的问题——现在解锁处理器会在无配对锁定时重新同步计数器,失效保护完全不再依赖计数器,只要同一把锁在超时后仍被持有就释放,最坏情况只是 15 秒停顿而非主机卡死。第三,修复了可插拔发布/订阅组件向应用分发消息时串行阻塞、每个订阅同时只有一条在途消息、吞吐上限等于单条消息延迟倒数的问题——根因是先前为避免优雅停用时取消输入绑定事件而移除了 WaitGroup 并合并分发协程,副作用使接收循环阻塞在处理器上,同时批量订阅因缺少并发调用永远攒不满配置的 maxMessagesCount、每条消息都退化为独立请求;实测 RabbitMQ 组件以 5000 条/秒发布 30 万条消息时,消费从 5004 条/秒跌至 3026 条/秒、p95 端到端延迟从 15 毫秒涨到 37945 毫秒、批量均值从 10.1 条塌到 1 条;现在恢复了与输入绑定一致的并发分发,流上的发送仍由既有互斥锁串行化,并在关闭流前等待在途处理器完成以便其确认已处理的消息,优雅停用的排空、暂停和截止语义保持不变,内置发布/订阅组件不受此问题影响。

更新内容 (中文)

Dapr 1.16.20

本次更新包含以下问题修复:

Daprd 在 actor 停用失败后永远不再重连 Placement

问题

当 daprd 边车丢失与 Placement 服务的流连接时(例如 Placement Pod 重启、网络中断或分发超时后),边车可能无法重新连接,并保持永久断开状态,直到 Pod 重启为止。

边车日志显示:

Error communicating with placement: EOF
Failed to reconnect to placement: error whilst deactivating all actors when shutting down client

元数据端点报告 placement: disconnected 和 hostReady: false,而 Kubernetes 就绪探针和 Scheduler 连接仍然健康。

影响

只要 Placement 流断开与 actor 停用失败同时发生,任何 Dapr 1.16.x 版本都会受到影响——例如在负载下应用对 actor DELETE 调用返回非 2xx 状态码。

受影响的边车永远不会重新加入放置表(placement table)。 它继续使用过期的本地表路由 actor 调用,而集群的其余部分已将这些 actor 放置到其他位置,从而导致持续的 DaprBuiltInActorNotFoundRetries 警告、整个命名空间内 actor 调用挂起或超时,以及受影响主机上的 actor 提醒(reminder)不再触发。 副本数量越大,断开连接与停用失败同时发生的概率就越高。 唯一的恢复方式是重启受影响的 Pod,而 Kubernetes 永远不会自行执行此操作,因为边车仍然报告为健康。

根本原因

在重新连接 Placement 之前,客户端会停驻(halt)所有本地托管的 actor,以便它们能够在新位置被重新激活。 任何 actor 停用失败都被视为重连的致命错误,重连循环对该错误的响应是永久退出 Placement 客户端,而不是进行重试。 没有任何机制重启该客户端,运行时的其余部分也在没有它的情况下继续运行。

解决方案

Placement 重连期间的 actor 停驻现在是尽力而为的:停用失败会被记录日志,重连继续进行。 此外,重连尝试失败后现在每秒重试一次,而不是终止 Placement 客户端,这与其他断开路径的恢复行为保持一致。

Placement 分发轮次变慢后 actor 调用永远挂起

问题

在一次 Placement 分发轮次中,如果解锁指令的到达时间比加锁指令晚超过 15 秒,daprd 边车可能进入这样一种状态:该主机上的每次 actor 调用和每次 actor 状态操作都会挂起,直到调用方超时。 在此期间边车看起来完全健康:元数据端点报告 placement: connected、hostReady: true,actor 运行时为 RUNNING,Kubernetes 探针通过,并且边车持续为新表版本记录 Placement tables updated。

应用观察到 actor 方法调用在其配置的 HTTP 超时时超时,actor 方法内部发起的 actor 状态操作在 .NET HttpClient 默认的 100 秒后失败,边车日志显示如下错误:

error getting actor state: context canceled
error invoke actor method: context canceled

影响

只要某次分发轮次超过边车 15 秒的解锁保险机制,任何 Dapr 1.16.x 版本都会受到影响——这在大型 actor 部署中最有可能发生,因为一台慢主机会拖慢所有人的轮次。 一旦触发,受影响的边车对其拥有的几乎所有 actor 调用都会失败,而它向其他主机上 actor 发起的调用则继续成功。 该状况永远不会自愈,且对健康检查不可见;唯一的恢复方式是重启受影响的 Pod。

根本原因

边车使用两个计数器配对 Placement 的加锁和解锁指令,当解锁指令迟到时,一个 15 秒的保险机制会强制解锁 actor 表。 当保险机制触发时,它推进解锁计数器以追赶;当迟到的解锁指令随后到达时,计数器又被第二次推进,导致两个计数器永久失去同步。 此后每条解锁指令都未能通过配对检查并直接返回,没有释放表锁,而保险机制也永远不会再次触发,因为它的触发条件同样依赖于已失步的计数器。 表永远处于写锁状态,阻塞该主机上的所有 actor 查找和 actor 状态操作。

解决方案

解锁处理器现在会在解锁指令到达却没有配对的加锁指令时(例如保险机制已经释放了该轮次之后)重新同步计数器,而不是任由计数器偏移。 保险机制不再依赖计数器:只要同一次加锁在超时后仍被持有,它就会释放表锁,因此任何计数器状态都无法阻止其触发。 有了这两项更改,任何畸形或延迟的指令序列最坏的情况也只是 15 秒的一次停顿,而不是主机被永久卡死。

可插拔发布/订阅组件每次只向应用投递一条消息

问题

从可插拔发布/订阅组件接收的消息被串行分发给应用。 接收循环阻塞在处理器上——处理器调用应用、等待其响应,然后才进行确认,因此在上一条消息被完全处理之前无法接收下一条消息。 每个订阅的在途消息被限制为一条,其吞吐上限变成单条消息延迟的倒数。 发布不受影响,因此订阅者逐渐落后于它之前还能跟上的发布者,队列持续增长且没有记录任何错误。

批量订阅也随之退化。 运行时通过并发的处理器调用来组装批量批次,因此一次只有一个调用方时,配置的 maxMessagesCount 永远无法达到,每条消息都变成了单独的应用请求。

影响

如果你在 v1.16.0 或更高版本上通过可插拔发布/订阅组件消费消息,就会受到影响。 以一个 RabbitMQ 可插拔组件按 5,000 msg/s 发布 300,000 条消息进行测量,消费量从 5,004 msg/s 下降到 3,026 msg/s,p95 端到端延迟从 15 毫秒上升到 37,945 毫秒,批量批次从平均 10.1 条消息骤降到恰好 1 条。 吞吐上限与处理器延迟成正比,因此应用处理器越慢,影响越严重:一个耗时 60 毫秒的处理器,无论 broker 或组件容量如何,吞吐上限都在 16 msg/s 左右。 内置发布/订阅组件不受影响。

根本原因

这种串行化是一项变更的连带损害,该变更原本旨在阻止在优雅关闭期间在途的输入绑定事件被取消。 该变更移除了 pullMessages 用于等待在途处理器的 WaitGroup——当时它已经冗余,因为运行时自己在 pkg/runtime/subscription 中跟踪在途消息——并连同分发 goroutine 一并折叠掉。 输入绑定在同一变更中保留了其并发分发,而为保证并发分发安全而存在的 safeSend 互斥锁也在这里被保留了下来。

解决方案

消息恢复为并发分发,沿用输入绑定已有的形式,因此每个订阅可以有多条消息在途。 流上的发送仍由现有的互斥锁串行化。 在流关闭之前现在会等待在途处理器完成,因此在订阅被拆除时仍在运行的处理器仍可确认它已处理的消息,而不是留待重新投递。 优雅关闭语义保持不变:排空、暂停和截止时间都在 pkg/runtime/subscription 中,统计的是处理器而非流 goroutine。

更新内容 (原始)

Dapr 1.16.20

This update contains the following bug fixes:

Daprd never reconnecting to Placement after a failed actor deactivation

Problem

When a daprd sidecar lost its stream to the Placement service (for example after a Placement pod restart, a network disconnect, or a dissemination timeout), the sidecar could fail to reconnect and remained permanently disconnected until the pod was restarted.

The sidecar logged:

Error communicating with placement: EOF
Failed to reconnect to placement: error whilst deactivating all actors when shutting down client

and the metadata endpoint reported placement: disconnected and hostReady: false, while the Kubernetes readiness probe and the Scheduler connection remained healthy.

Impact

You were affected on any Dapr 1.16.x version when a Placement stream disconnect coincided with an actor deactivation failure, for example the application returning a non-2xx status to the actor DELETE call while under load.

An affected sidecar never rejoined the placement table. It kept routing actor invocations using its stale local table while the rest of the cluster placed those actors elsewhere, causing persistent DaprBuiltInActorNotFoundRetries warnings, hung or timed-out actor invocations across the namespace, and actor reminders on the affected host not firing. The larger the replica count, the more likely a disconnect was to coincide with a failed deactivation. The only recovery was restarting the affected pod, which Kubernetes never did on its own because the sidecar still reported healthy.

Root Cause

Before reconnecting to Placement, the client halts all locally hosted actors so they can be reactivated in their new location. A failure to deactivate any actor was treated as a fatal error for the reconnect, and the reconnect loop responded to that error by exiting the Placement client permanently instead of retrying. Nothing restarted the client, and the rest of the runtime kept running without it.

Solution

Halting actors during a Placement reconnect is now best effort: a failed deactivation is logged and the reconnect proceeds. In addition, a failed reconnect attempt is now retried every second instead of terminating the Placement client, matching the recovery behavior of the other disconnect paths.

Actor calls hanging forever after a slow Placement dissemination round

Problem

After a Placement dissemination round in which the unlock order arrived more than 15 seconds after the lock order, a daprd sidecar could enter a state where every actor invocation and every actor state operation on that host hung until the caller timed out. The sidecar looked healthy the whole time: the metadata endpoint reported placement: connected, hostReady: true, and actor runtime RUNNING, Kubernetes probes passed, and the sidecar kept logging Placement tables updated for new table versions.

Applications observed actor method calls timing out at their configured HTTP timeout, actor state operations from inside actor methods failing after the .NET HttpClient default of 100 seconds, and the sidecar logging errors such as:

error getting actor state: context canceled
error invoke actor method: context canceled

Impact

You were affected on any Dapr 1.16.x version whenever a dissemination round overran the sidecar’s 15 second unlock failsafe, which is most likely in large actor deployments where one slow host delays the round for everyone. Once triggered, the affected sidecar failed effectively all actor calls for the actors it owned, while calls it made to actors on other hosts kept succeeding. The condition never self-healed and was invisible to health checks; the only recovery was restarting the affected pod.

Root Cause

The sidecar pairs Placement lock and unlock orders with two counters, and a 15 second failsafe force-unlocks the actor table when an unlock order is late. When the failsafe fired, it advanced the unlock counter to catch up; when the late unlock order then arrived anyway, it advanced the counter a second time, leaving the two counters permanently out of step. From then on every unlock order failed the pairing check and returned without releasing the table lock, and the failsafe never fired again because its condition also relied on the desynchronized counters. The table stayed write locked forever, blocking every actor lookup and actor state operation on the host.

Solution

The unlock handler now resynchronizes the counters when an unlock order arrives with no paired lock, such as after the failsafe already released the round, instead of letting the counters drift apart. The failsafe no longer depends on the counters at all: it now releases the table lock whenever the same lock acquisition is still held after the timeout, so no counter state can prevent it from firing. With both changes, the worst case for any malformed or delayed order sequence is a single 15 second stall instead of a permanently wedged host.

Pluggable pub/sub components delivered messages to the application one at a time

Problem

Messages received from a pluggable pub/sub component were dispatched to the application serially. The receive loop blocked on the handler, which invokes the application, waits for its response and only then acknowledges, so the next message could not be received until the previous one was fully handled. Each subscription was limited to a single message in flight and its ceiling became the inverse of the per-message latency. Publishing was unaffected, so a subscriber fell steadily behind a publisher it had previously kept up with, and the queue grew without any error being logged.

Bulk subscribe degraded along with it. The runtime assembles bulk batches out of concurrent handler calls, so with one caller at a time the configured maxMessagesCount could never be reached and every message became its own application request.

Impact

You were affected if you consumed messages through a pluggable pub/sub component on v1.16.0 or later. Measured against a RabbitMQ pluggable component publishing 300,000 messages at 5,000 msg/s, consumption fell from 5,004 to 3,026 msg/s, p95 end-to-end latency rose from 15 ms to 37,945 ms, and bulk batches collapsed from a mean of 10.1 messages to exactly 1. The throughput ceiling tracks handler latency, so the slower the application handler, the more severe the effect: a handler taking 60 ms tops out near 16 msg/s regardless of broker or component capacity. Built-in pub/sub components were not affected.

Root Cause

The serialization was collateral damage of a change that set out to stop in-flight input binding events from being cancelled during graceful shutdown. That change removed the WaitGroup pullMessages used to wait on in-flight handlers, by then redundant because the runtime tracks in-flight messages itself in pkg/runtime/subscription, and collapsed the dispatching goroutine along with it. Input bindings kept their concurrent dispatch in that same change, and the safeSend mutex that exists to make concurrent dispatch safe was left in place here.

Solution

Messages are dispatched concurrently again, following the shape input bindings already use, so more than one message can be in flight per subscription. Sends on the stream remain serialized by the existing mutex. In-flight handlers are now waited on before the stream is closed, so a handler that is still running when the subscription is torn down can still acknowledge the message it has already processed instead of leaving it to be redelivered. Graceful shutdown semantics are unchanged: draining, pausing and the deadline live in pkg/runtime/subscription and count handlers rather than stream goroutines.

下载链接