akka-net-best-practices について
Akka.NET のアクターシステムを設計・運用する開発者向けのリファレンススキルです。通信、監督、エラー処理、作業分散に加え、ローカルとクラスター間の抽象化を扱い、よくある設計上の誤りも解説します。
主な機能
- 01EventStream と DistributedPubSub の使い分け
- 02アクターの監督とエラー処理
- 03Props と DependencyResolver の選択
- 04バックグラウンド作業の分散パターン
- 05ローカル・クラスター両モードの抽象化
- 06非同期キャンセルとアクターログ
活用シーン
- 01クラスター間のパブリッシュ・サブスクライブ方式の選択
- 02アクターの障害と再起動への対応
- 03複数ノードへのバックグラウンド処理の分散
- 04ローカルモードでのクラスター向け設計のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill akka-net-best-practicesインストール後、Claude Code で/akka-net-best-practicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
async-cancellation-patterns.mdcluster-local-abstractions.mdwork-distribution-patterns.md詳細
Akka.NET のベストプラクティス
このスキルを使用する場面
次のような場合にこのスキルを使用します:
- アクター間の通信パターンを設計する
- EventStream と DistributedPubSub のどちらを使用するか判断する
- アクターのエラー処理を実装する
- 監督戦略を理解する
- Props パターンと DependencyResolver のどちらを使用するか選択する
- ノード間の作業分散を設計する
- クラスターインフラストラクチャの有無にかかわらず実行できる、テスト可能なアクターシステムを作成する
- ローカルテストのシナリオで Cluster Sharding を抽象化する
参照ファイル
- work-distribution-patterns.md:データベースキュー、Akka.Streams のスロットリング、アウトボックスパターン
- cluster-local-abstractions.md:GenericChildPerEntityParent、IPubSubMediator、実行モードの配線
- async-cancellation-patterns.md:アクターのスコープに対応した CancellationToken、リンクされた CTS、タイムアウト処理
1. EventStream と DistributedPubSub
重要:EventStream はローカルのみ
Context.System.EventStream は、単一の ActorSystem プロセス内でのみ使用できます。クラスター内のノード間では機能しません。
// BAD: This only works on a single server
// When you add a second server, subscribers on server 2 won't receive events from server 1
Context.System.EventStream.Subscribe(Self, typeof(PostCreated));
Context.System.EventStream.Publish(new PostCreated(postId, authorId));
EventStream が適切な場面:
- 単一プロセス内でのログ記録と診断
- 真に単一プロセスで動作するアプリケーションのローカルイベントバス
- 開発/テストのシナリオ
マルチノードでは DistributedPubSub を使用する
複数のクラスター内ノードにまたがってアクターにイベントを届ける必要がある場合は、Akka.Cluster.Tools.PublishSubscribe を使用します:
using Akka.Cluster.Tools.PublishSubscribe;
public class TimelineUpdatePublisher : ReceiveActor
{
private readonly IActorRef _mediator;
public TimelineUpdatePublisher()
{
// Get the DistributedPubSub mediator
_mediator = DistributedPubSub.Get(Context.System).Mediator;
Receive<PublishTimelineUpdate>(msg =>
{
// Publish to a topic - reaches all subscribers across all nodes
_mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
});
}
}
DistributedPubSub の Akka.Hosting 設定
builder.WithDistributedPubSub(role: null); // Available on all roles, or specify a role
トピックの設計パターン
| パターン | トピック形式 | ユースケース |
|---|---|---|
| ユーザーごと | timeline:{userId} | タイムラインの更新、通知 |
| エンティティごと | post:{postId} | 投稿のエンゲージメント更新 |
| ブロードキャスト | system:announcements | システム全体への通知 |
| ロールベース | workers:rss-poller | 作業の分散 |
2. 監督戦略
重要な確認:監督の対象は子アクターです
アクターに定義された監督戦略が決めるのは、そのアクターが子アクターをどのように監督するかであり、そのアクター自身がどのように監督されるかではありません。
public class ParentActor : ReceiveActor
{
// This strategy applies to children of ParentActor, NOT to ParentActor itself
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
maxNrOfRetries: 10,
withinTimeRange: TimeSpan.FromSeconds(30),
decider: ex => ex switch
{
ArithmeticException => Directive.Resume,
NullReferenceException => Directive.Restart,
ArgumentException => Directive.Stop,
_ => Directive.Escalate
});
}
}
デフォルトの監督戦略
デフォルトの OneForOneStrategy には、すでにレート制限が含まれています:
- 1 秒以内に 10 回再起動すると、アクターは完全に停止します
- これにより、無限の再起動ループを防ぎます
特別な要件がない限り、カスタム戦略が必要になることはほとんどありません。
カスタム監督を定義する場面
適切な理由:
- アクターが、回復不能な状態の破損を示す例外をスローする -> 再起動
- アクターが、再起動を引き起こすべきではない例外(想定内の失敗)をスローする -> 再開
- 子アクターの失敗が兄弟アクターに影響する必要がある ->
AllForOneStrategyを使用する - デフォルトとは異なる再試行上限が必要
不適切な理由:
- 「念のため」— デフォルトですでに安全です
- アクターの動作を理解していない — まず動作を理解しましょう
3. エラー処理:監督と Try-Catch
Try-Catch を使用する場面(ほとんどの場合)
次の場合は try-catch を使用します:
- 失敗が想定内(ネットワークタイムアウト、無効な入力、外部サービスの停止)
- 例外が発生した理由を正確に把握している
- 適切に処理できる(再試行、エラーレスポンスの返却、ログを記録して処理を続行)
- 再起動しても効果がない(同じエラーが再び発生する)
public class RssFeedPollerActor : ReceiveActor
{
public RssFeedPollerActor()
{
ReceiveAsync<PollFeed>(async msg =>
{
try
{
var feed = await _httpClient.GetStringAsync(msg.FeedUrl);
var items = ParseFeed(feed);
// Process items...
}
catch (HttpRequestException ex)
{
// Expected failure - log and schedule retry
_log.Warning("Feed {Url} unavailable: {Error}", msg.FeedUrl, ex.Message);
Context.System.Scheduler.ScheduleTellOnce(
TimeSpan.FromMinutes(5), Self, msg, Self);
}
catch (XmlException ex)
{
// Invalid feed format - log and mark as bad
_log.Error("Feed {Url} has invalid format: {Error}", msg.FeedUrl, ex.Message);
Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
}
});
}
}
監督に処理させる場面
次の場合は例外を伝播させます(監督をトリガーします):
- 例外が発生した理由がまったくわからない
- アクターの状態が破損している可能性がある
- 再起動が有効(状態を初期化する、リソースに再接続する)
- プログラミングエラー(NullReferenceException、不適切なロジックによる InvalidOperationException)である
アンチパターン:不明な例外を握りつぶす
// BAD: Swallowing exceptions hides problems
catch (Exception ex)
{
_log.Error(ex, "Error processing work");
// Actor continues with potentially corrupt state
}
// GOOD: Handle known exceptions, let unknown ones propagate
catch (HttpRequestException ex)
{
// Known, expected failure - handle gracefully
_log.Warning("HTTP request failed: {Error}", ex.Message);
Sender.Tell(new WorkResult.TransientFailure());
}
// Unknown exceptions propagate to supervision
4. Props と DependencyResolver
通常の Props を使用する場面
次の場合は Props.Create() を使用します:
- アクターが
IServiceProviderやIRequiredActor<T>を必要としない - すべての依存関係をコンストラクター経由で渡せる
- アクターがシンプルで自己完結している
// Simple actor with no DI needs
public static Props Props(PostId postId, IPostWriteStore store)
=> Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
DependencyResolver を使用する場面
次の場合は resolver.Props<T>() を使用します。
- アクターが、スコープ付きサービスを作成するために
IServiceProviderを必要とする - アクターが
IRequiredActor<T>を使用して、ほかのアクターへの参照を取得する - アクターに、すでに DI コンテナーに登録されている依存関係が多数ある
// Registration with DI
builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor");
registry.Register<OrderProcessorActor>(actor);
});
リモートデプロイに関する考慮事項
リモートデプロイが必要になることは、ほとんどありません。 リモートデプロイを行わない場合(おそらく行わないでしょう)は、次のとおりです。
- クロージャーを使った
Props.Create(() => new Actor(...))で問題ありません - 「シリアル化の問題」に関する警告は当てはまりません
ほとんどのアプリケーションでは、リモートデプロイではなくクラスターシャーディングを使用します。分散を自動的に処理できます。
5. 作業分散のパターン
多数のバックグラウンドジョブ(RSS フィード、メール送信など)がある場合、一度にすべてを処理しないでください。サンダリングハード問題が発生する原因になります。
これを解決する 3 つのパターン:
- データベース駆動の作業キュー -
FOR UPDATE SKIP LOCKEDを使用して、ノード間で自然に分散します - Akka.Streams のレート制限 - 単一ノード内で処理を調整します
- 永続キュー(Outbox パターン) - 信頼性の高い処理を実現する、データベースを基盤としたアウトボックスです
コードサンプルの全容は、work-distribution-patterns.md を参照してください。
6. よくある間違いのまとめ
| 間違い | 問題となる理由 | 修正方法 |
|---|---|---|
| ノード間のパブリッシュ/サブスクライブに EventStream を使用する | EventStream はローカル専用です | DistributedPubSub を使用する |
| アクターを「保護」するために監督を定義する | 監督が保護するのは子アクターです | 階層構造を理解する |
| すべての例外をキャッチする | バグを隠し、状態を破損させます | 想定されるエラーのみをキャッチする |
| 常に DependencyResolver を使用する | 不要な複雑さが加わります | 可能な場合は通常の Props を使用する |
| すべてのバックグラウンドジョブを一度に処理する | サンダリングハードやリソース枯渇の原因になります | データベースキューとレート制限を使用する |
| 想定内の失敗に対して例外をスローする | 不要な再起動が発生します | 結果型を返し、メッセージングを使用する |
7. クイックリファレンス
通信パターンの判断フローチャート
Need to communicate between actors?
├── Same process only? -> EventStream is fine
├── Across cluster nodes?
│ ├── Point-to-point? -> Use ActorSelection or known IActorRef
│ └── Pub/sub? -> Use DistributedPubSub
└── Fire-and-forget to external system? -> Consider outbox pattern
エラー処理の判断フローチャート
Exception occurred in actor?
├── Expected failure (HTTP timeout, invalid input)?
│ └── Try-catch, handle gracefully, continue
├── State might be corrupt?
│ └── Let supervision restart
├── Unknown cause?
│ └── Let supervision restart
└── Programming error (null ref, bad logic)?
└── Let supervision restart, fix the bug
Props の判断フローチャート
Creating actor Props?
├── Actor needs IServiceProvider?
│ └── Use resolver.Props<T>()
├── Actor needs IRequiredActor<T>?
│ └── Use resolver.Props<T>()
├── Simple actor with constructor params?
│ └── Use Props.Create(() => new Actor(...))
└── Remote deployment needed?
└── Probably not - use cluster sharding instead
8. クラスター/ローカルモードの抽象化
クラスター化された本番環境とローカル/テスト環境の両方で実行する必要があるアプリケーションでは、実装を切り替える抽象化パターンを使用します。
AkkaExecutionModeenum - 使用する実装(LocalTest または Clustered)を制御しますGenericChildPerEntityParent- 同じIMessageExtractorを使用し、ローカルでシャーディングの動作を模倣しますIPubSubMediator- DistributedPubSub を抽象化し、ローカル/クラスターの実装を切り替えられるようにします
実装コードの全容は、cluster-local-abstractions.md を参照してください。
9. アクターのロギング
ILogger ではなく ILoggingAdapter を使用する
アクターでは、DI で注入した ILogger<T> ではなく、Context.GetLogger() から取得した ILoggingAdapter を使用します。
public class MyActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public MyActor()
{
Receive<MyMessage>(msg =>
{
_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 は不要です。アクターのコンテキストから直接取得できます
アクターに ILogger を注入しないでください。 Akka のロギング基盤を迂回してしまいます。
セマンティックロギング(v1.5.57 以降)
// Named placeholders for better log aggregation and querying
_log.Info("Order {OrderId} processed for customer {CustomerId}", order.Id, order.CustomerId);
// Prefer named placeholders over positional
// Good: {OrderId}, {CustomerId}
// Avoid: {0}, {1}
10. CancellationToken による非同期操作の管理
アクターが PipeTo 経由で非同期操作を開始した場合、適切に管理しなければ、その操作がアクターの終了後も継続することがあります。重要な実践方法は次のとおりです。
- PostStop でのアクター CTS -
PostStop()で必ずキャンセルし、破棄します - 操作ごとに新しい CTS を作成する - 新しい処理を開始する前に、前の処理をキャンセルします
- トークンをすべての処理に渡す - EF Core クエリ、HTTP 呼び出しなど
- タイムアウトにはリンクされた CTS を使用する - 外部呼び出しに短いタイムアウトを設定して、停止状態を防ぎます
- 適切に処理する - catch ブロックでタイムアウトとシャットダウンを区別します
実装コードの全容は、async-cancellation-patterns.md を参照してください。