SuperAPI AI 工具集

akka-net-best-practices

编程与开发

查看项目

更新于 10-15 09:47

技能介绍

Akka.NET 最佳实践

一、用途与适用时机

当你需要设计 Actor 通信模式、在 EventStream 与 DistributedPubSub 之间做选择、在 Actor 中实现错误处理、理解监督策略、在 Props 模式与 DependencyResolver 之间做选择、设计跨节点工作分发、构建可在有或没有集群基础设施下运行的可测试 Actor 系统,或为本地测试场景抽象 Cluster Sharding 时,可使用本技能。

配套参考文件包括:

  • work-distribution-patterns.md:数据库队列、Akka.Streams 限流、outbox 模式
  • cluster-local-abstractions.md:GenericChildPerEntityParent、IPubSubMediator、执行模式装配
  • async-cancellation-patterns.md:Actor 作用域 CancellationToken、链接的 CTS、超时处理

二、EventStream 与 DistributedPubSub

关键点:EventStream 仅限本地。Context.System.EventStream 仅在单个 ActorSystem 进程内有效,不跨集群节点。示例中给出反面写法:订阅 Self 与 Publish 事件仅在单服务器上有效,增加第二台服务器后,服务器 2 上的订阅者收不到服务器 1 的事件。

EventStream 适用的场景:单进程内的日志与诊断、真正单进程应用的本地事件总线、开发/测试场景。

多节点需使用 DistributedPubSub:对于必须送达多个集群节点上 Actor 的事件,使用 Akka.Cluster.Tools.PublishSubscribe。示例通过 DistributedPubSub.Get(Context.System).Mediator 获取中介者,并用 _mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update)) 将消息发布到主题。

Akka.Hosting 配置:builder.WithDistributedPubSub(role: null); 可对所有角色可用,也可指定角色。

主题设计模式:按用户 timeline:{userId}(时间线更新、通知)、按实体 post:{postId}(帖子互动更新)、广播 system:announcements(系统级通知)、按角色 workers:rss-poller(工作分发)。

三、监督策略

关键澄清:监督针对的是子 Actor。在某个 Actor 上定义的监督策略决定该 Actor 如何监督其子级,而非该 Actor 自身如何被监督。示例中 ParentActor 的 SupervisorStrategy 返回 OneForOneStrategy,设置 maxNrOfRetries: 10、withinTimeRange: TimeSpan.FromSeconds(30),并按异常类型分别返回 Directive.Resume(ArithmeticException)、Directive.Restart(NullReferenceException)、Directive.Stop(ArgumentException)、Directive.Escalate(其他)。

默认监督策略已包含速率限制:1 秒内重启 10 次即永久停止该 Actor,防止无限重启循环。除非有特定需求,很少需要自定义策略。

定义自定义监督的合理理由:Actor 抛出的异常表明状态不可恢复性损坏 → Restart;异常不应导致重启(预期失败)→ Resume;子级失败应影响同级 → 使用 AllForOneStrategy;需要不同于默认的重试上限。不合理的理由:“为了保险”——默认已安全;不了解 Actor 做什么——应先理解。

四、错误处理:监督与 Try-Catch

多数情况使用 Try-Catch:当失败是预期的(网络超时、无效输入、外部服务不可用)、你明确知道异常原因、能优雅处理(重试、返回错误响应、记录并继续)、重启也无济于事(同样错误会再发生)时使用。示例 RssFeedPollerActor 中,捕获 HttpRequestException 时记录警告并调度 5 分钟后重试,捕获 XmlException 时记录错误并回复 FeedPollResult.InvalidFormat。

交由监督处理的情况:完全不知道异常原因、Actor 状态可能已损坏、重启有帮助(全新状态、重连资源)、属于编程错误(NullReferenceException、因逻辑错误导致的 InvalidOperationException)。

反模式:吞掉未知异常。反面写法是捕获 Exception 后仅记录日志,Actor 带着可能损坏的状态继续;正确做法是只处理已知异常,未知异常向上传播至监督。

五、Props 与 DependencyResolver

使用普通 Props 的时机:Actor 不需要 IServiceProvider 或 IRequiredActor<T>、所有依赖可通过构造函数传入、Actor 简单且自包含。示例:public static Props Props(PostId postId, IPostWriteStore store) => Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));

使用 DependencyResolver 的时机:Actor 需要 IServiceProvider 创建作用域服务、使用 IRequiredActor<T> 获取其他 Actor 引用、依赖众多且已在 DI 容器中。示例在 builder.WithActors((system, registry, resolver) => { var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor"); registry.Register<OrderProcessorActor>(actor); }); 中注册。

远程部署注意事项:几乎不需要远程部署。如果不做远程部署(很可能确实不做),带闭包的 Props.Create(() => new Actor(...)) 没问题,“序列化问题”的警告不适用。多数应用应使用集群分片而非远程部署,集群分片会自动处理分发。

六、工作分发模式

当有大量后台任务(RSS 源、邮件发送等)时,不要一次性全部处理——会造成惊群问题。三种解决模式:数据库驱动的工作队列(使用 FOR UPDATE SKIP LOCKED 实现自然的跨节点分发);Akka.Streams 限流(在单节点内限制处理速率);持久队列(Outbox 模式,用数据库支持的 outbox 实现可靠处理)。完整代码示例见 work-distribution-patterns.md。

七、常见错误总结

用 EventStream 做跨节点发布/订阅——错误原因是 EventStream 仅限本地——应使用 DistributedPubSub。 定义监督来“保护”某个 Actor——监督保护的是子级——应理解层级关系。 捕获所有异常——会隐藏 bug、损坏状态——只捕获预期错误。 总是使用 DependencyResolver——增加不必要的复杂度——尽量使用普通 Props。 一次性处理所有后台任务——惊群、资源耗尽——使用数据库队列加限流。 对预期失败抛出异常——触发不必要的重启——返回结果类型,使用消息传递。

八、快速参考决策树

通信模式:需要 Actor 间通信?仅同进程 → EventStream 即可;跨集群节点 → 点对点使用 ActorSelection 或已知 IActorRef,发布/订阅使用 DistributedPubSub;对外部系统的即发即忘 → 考虑 outbox 模式。

错误处理:Actor 中发生异常?预期失败(HTTP 超时、无效输入)→ Try-catch 优雅处理并继续;状态可能损坏 → 交由监督重启;原因未知 → 交由监督重启;编程错误(空引用、错误逻辑)→ 交由监督重启并修复 bug。

Props:创建 Actor Props?需要 IServiceProvider → 使用 resolver.Props<T>();需要 IRequiredActor<T> → 使用 resolver.Props<T>();带构造参数的简单 Actor → 使用 Props.Create(() => new Actor(...));需要远程部署 → 大概率不需要,改用集群分片。

九、集群/本地模式抽象

对于既要在集群化生产环境运行、又要在本地/测试环境运行的应用,可使用抽象模式在实现之间切换:AkkaExecutionMode 枚举控制使用哪套实现(LocalTest 与 Clustered);GenericChildPerEntityParent 在本地用相同的 IMessageExtractor 模拟分片行为;IPubSubMediator 抽象 DistributedPubSub 以便切换本地/集群实现。完整实现代码见 cluster-local-abstractions.md。

十、Actor 日志

使用 ILoggingAdapter 而非 ILogger:在 Actor 中使用 Context.GetLogger() 得到的 ILoggingAdapter,而不是 DI 注入的 ILogger<T>。示例在 Actor 中定义 private readonly ILoggingAdapter _log = Context.GetLogger();,并调用 _log.Info("Processing message for user {UserId}", msg.UserId) 与 _log.Error(ex, "Failed to process {MessageType}", msg.GetType().Name)。

使用 ILoggingAdapter 的理由:与 Akka 的日志管线和监督集成;自 v1.5.57 起支持语义/结构化日志;方法名为 Info()、Debug()、Warning()、Error()(不是 Log* 变体);无需 DI,直接从 Actor 上下文获取。不要把 ILogger 注入 Actor——那会绕过 Akka 的日志基础设施。

语义日志(v1.5.57+):使用具名占位符以便更好地聚合与查询,例如 _log.Info("Order {OrderId} processed for customer {CustomerId}", order.Id, order.CustomerId);优先使用具名占位符而非位置占位符(优选 {OrderId}、{CustomerId},避免 {0}、{1})。

十一、使用 CancellationToken 管理异步操作

当 Actor 通过 PipeTo 启动异步操作时,若管理不当,这些操作的生命周期可能超过 Actor。关键实践:在 PostStop 中取消并释放 Actor 的 CTS;每次操作新建 CTS,开始新工作前取消上一个;将 token 传递到各处(EF Core 查询、HTTP 调用等);使用链接的 CTS 处理超时,为外部调用设置短超时以防挂起;在 catch 块中区分超时与关闭并优雅处理。完整实现代码见 async-cancellation-patterns.md。