Akka.NET ベストプラクティス
このスキルを使うべき時
以下のような状況でこのスキルを使用してください:
- アクター間の通信パターンを設計するとき
- EventStreamとDistributedPubSubのどちらを使うか決めかねているとき
- アクター内でエラーハンドリングを実装するとき
- 監視戦略を理解したいとき
- PropsパターンとDependencyResolverのどちらを選ぶか検討しているとき
- ノード間の作業分散を設計するとき
- クラスタインフラの有無にかかわらず実行可能なテスト可能なアクターシステムを作成するとき
- ローカルテストシナリオのためにクラスタシャーディングを抽象化するとき
リファレンスファイル
- work-distribution-patterns.md: データベースキュー、Akka.Streamsスロットリング、Outboxパターン
- cluster-local-abstractions.md: GenericChildPerEntityParent、IPubSubMediator、実行モードの配線
- async-cancellation-patterns.md: アクタースコープのCancellationToken、リンクされたCTS、タイムアウト処理
1. EventStream vs DistributedPubSub
重要: EventStreamはローカル専用
Context.System.EventStream は 1つのActorSystemプロセス内に限定 されます。クラスタノード間では動作しません。
// 悪い例: これは単一サーバーでのみ動作する
// 2台目のサーバーを追加すると、サーバー2の購読者はサーバー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()
{
// DistributedPubSubメディエーターを取得
_mediator = DistributedPubSub.Get(Context.System).Mediator;
Receive<PublishTimelineUpdate>(msg =>
{
// トピックに公開 - 全ノードの全購読者に到達
_mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
});
}
}
Akka.HostingでのDistributedPubSub設定
builder.WithDistributedPubSub(role: null); // 全ロールで利用可能。または特定のロールを指定
トピック設計パターン
| パターン | トピック形式 | ユースケース |
|---|---|---|
| ユーザー単位 | timeline:{userId} | タイムライン更新、通知 |
| エンティティ単位 | post:{postId} | 投稿エンゲージメント更新 |
| ブロードキャスト | system:announcements | システム全体の通知 |
| ロールベース | workers:rss-poller | 作業分散 |
2. 監視戦略
重要な明確化: 監視は子アクターに対して
アクターに定義された監視戦略は、そのアクターが子アクターをどのように監視するか を指示するものであり、そのアクター自身がどのように監視されるかではありません。
public class ParentActor : ReceiveActor
{
// この戦略はParentActorの子アクターに適用され、ParentActor自身には適用されない
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回の再起動 = アクターは永続的に停止
- これにより無限再起動ループが防止されます
特別な要件がない限り、カスタム戦略はめったに必要ありません。
カスタム監視を定義すべき時
良い理由:
- アクターが回復不可能な状態破損を示す例外をスローする -> Restart
- 再起動を引き起こすべきではない例外(予期される障害)をスローする -> Resume
- 子アクターの障害が兄弟アクターに影響を与えるべき場合 -> AllForOneStrategy を使用
- デフォルトと異なる再試行制限が必要な場合
悪い理由:
- 「念のために」 - デフォルトは既に安全です
- アクターの動作を理解していない - まず理解しましょう
3. エラーハンドリング: 監視 vs 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);
// アイテムを処理...
}
catch (HttpRequestException ex)
{
// 予期される障害 - ログ記録して再試行をスケジュール
_log.Warning("フィード {Url} は利用不可: {Error}", msg.FeedUrl, ex.Message);
Context.System.Scheduler.ScheduleTellOnce(
TimeSpan.FromMinutes(5), Self, msg, Self);
}
catch (XmlException ex)
{
// 無効なフィード形式 - ログ記録して不良としてマーク
_log.Error("フィード {Url} の形式が無効: {Error}", msg.FeedUrl, ex.Message);
Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
}
});
}
}
監視に任せるべき時
例外を伝播させる(監視をトリガーする)状況:
- 例外の原因が 全くわからない
- アクターの 状態が破損している可能性がある
- 再起動が有効である(新しい状態、リソースの再接続)
- プログラミングエラーである(NullReferenceException、論理エラーによるInvalidOperationException)
アンチパターン: 未知の例外を飲み込む
// 悪い例: 例外を飲み込むと問題が隠蔽される
catch (Exception ex)
{
_log.Error(ex, "作業処理中にエラー発生");
// アクターは潜在的に破損した状態で継続する
}
// 良い例: 既知の例外は処理し、未知の例外は伝播させる
catch (HttpRequestException ex)
{
// 既知の予期される障害 - 適切に処理
_log.Warning("HTTPリクエスト失敗: {Error}", ex.Message);
Sender.Tell(new WorkResult.TransientFailure());
}
// 未知の例外は監視に伝播
4. Props vs DependencyResolver
プレーンなPropsを使うべき時
Props.Create() を使用する状況:
- アクターが
IServiceProviderやIRequiredActor<T>を必要としない - すべての依存関係をコンストラクタで渡せる
- アクターがシンプルで自己完結している
// DI不要のシンプルなアクター
public static Props Props(PostId postId, IPostWriteStore store)
=> Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
DependencyResolverを使うべき時
resolver.Props<T>() を使用する状況:
- アクターがスコープ付きサービスを作成するために
IServiceProviderを必要とする - アクターが他のアクターへの参照を取得するために
IRequiredActor<T>を使用する - アクターに多くの依存関係があり、それらが既にDIコンテナに登録されている
// 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フィード、メール送信など)がある場合、それらをすべて一度に処理しないでください。群集崩壊(thundering herd)問題を引き起こします。
これを解決する3つのパターン:
- データベース駆動の作業キュー -
FOR UPDATE SKIP LOCKEDを使用して自然なクロスノード分散を実現 - Akka.Streams レート制限 - 単一ノード内で処理をスロットル
- 永続キュー(Outboxパターン) - データベースバックアップによる信頼性の高い処理
完全なコードサンプルについては work-distribution-patterns.md を参照してください。
6. よくある間違いのまとめ
| 間違い | なぜ間違いか | 修正方法 |
|---|---|---|
| クロスノードpub/subにEventStreamを使用する | EventStreamはローカル専用 | DistributedPubSubを使用する |
| アクターを「保護」するために監視を定義する | 監視は子アクターを保護する | 階層を理解する |
| すべての例外をキャッチする | バグを隠蔽し、状態を破損する | 予期されるエラーのみキャッチする |
| 常にDependencyResolverを使用する | 不必要な複雑さを追加する | 可能な場合はプレーンなPropsを使用する |
| すべてのバックグラウンドジョブを一度に処理する | 群集崩壊、リソース枯渇 | データベースキュー+レート制限を使用する |
| 予期される障害に対して例外をスローする | 不必要な再起動をトリガーする | 結果型を返し、メッセージングを使用する |
7. クイックリファレンス
通信パターン決定木
アクター間で通信する必要がありますか?
├── 同一プロセスのみ? -> EventStreamで十分
├── クラスタノード間?
│ ├── ポイントツーポイント? -> ActorSelectionまたは既知のIActorRefを使用
│ └── Pub/Sub? -> DistributedPubSubを使用
└── 外部システムへのファイアアンドフォーゲット? -> Outboxパターンを検討
エラーハンドリング決定木
アクター内で例外が発生しましたか?
├── 予期される障害(HTTPタイムアウト、無効な入力)?
│ └── Try-catchで適切に処理し続行
├── 状態が破損している可能性がある?
│ └── 監視に再起動させる
├── 原因不明?
│ └── 監視に再起動させる
└── プログラミングエラー(null参照、論理エラー)?
└── 監視に再起動させ、バグを修正する
Props決定木
アクターのPropsを作成しますか?
├── アクターがIServiceProviderを必要とする?
│ └── resolver.Props<T>() を使用
├── アクターがIRequiredActor<T>を必要とする?
│ └── resolver.Props<T>() を使用
├── コンストラクタパラメータを持つシンプルなアクター?
│ └── Props.Create(() => new Actor(...)) を使用
└── リモートデプロイが必要?
└── おそらく不要 - 代わりにクラスタシャーディングを使用
8. クラスタ/ローカルモードの抽象化
クラスタ化された本番環境とローカル/テスト環境の両方で実行する必要があるアプリケーションでは、抽象化パターンを使用して実装を切り替えます:
AkkaExecutionMode列挙型 - 使用する実装を制御します(LocalTest vs Clustered)GenericChildPerEntityParent- 同じIMessageExtractorを使用してローカルでシャーディング動作を模倣しますIPubSubMediator- 交換可能なローカル/クラスタ実装のためにDistributedPubSubを抽象化します
完全な実装コードについては cluster-local-abstractions.md を参照してください。
9. アクターログ記録
ILoggingAdapterを使用し、ILoggerは使用しない
アクター内では、DIで注入された ILogger<T> ではなく、Context.GetLogger() からの ILoggingAdapter を使用します:
public class MyActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public MyActor()
{
Receive<MyMessage>(msg =>
{
_log.Info("ユーザー {UserId} のメッセージを処理中", msg.UserId);
_log.Error(ex, "{MessageType} の処理に失敗しました", msg.GetType().Name);
});
}
}
ILoggingAdapter を選ぶ理由:
- Akkaのログパイプラインおよび監視と統合
- v1.5.57以降、セマンティック/構造化ログをサポート
- メソッド名:
Info(),Debug(),Warning(),Error()(Log*バリアントではない) - DI不要 - アクターコンテキストから直接取得
ILogger をアクターに注入しないでください - Akkaのログインフラストラクチャをバイパスします。
セマンティックログ記録(v1.5.57以降)
// 名前付きプレースホルダーを使用すると、ログの集約と検索が向上
_log.Info("注文 {OrderId} が顧客 {CustomerId} に対して処理されました", order.Id, order.CustomerId);
// 位置指定よりも名前付きプレースホルダーを推奨
// 良い: {OrderId}, {CustomerId}
// 避ける: {0}, {1}
10. CancellationTokenを使用した非同期操作の管理
アクターが PipeTo 経由で非同期操作を開始する場合、適切に管理されないと、それらの操作がアクターより長く存続する可能性があります。重要なプラクティス:
- PostStop内のアクターCTS -
PostStop()で必ずキャンセルおよび破棄を行う - 操作ごとに新しいCTS - 新しい作業を開始する前に前のものをキャンセル
- トークンをあらゆる場所に渡す - EF Coreクエリ、HTTP呼び出しなど
- タイムアウト用のリンクCTS - 外部呼び出しは短いタイムアウトを設定し、ハングを防止
- 適切な処理 - catchブロックでタイムアウトとシャットダウンを区別
完全な実装コードについては async-cancellation-patterns.md を参照してください。