Step Functions のインラインモードと分散モードのエラー動作の違いについて理解する
AWS Step Functions で複数のアイテムを並列処理する際、1 つのエラーで全体の処理が止まってしまう経験はありませんか。
私も以前、インラインモードの Map ステートを使った並列処理でこの問題に直面しました。並列処理をするステートマシンで、1 つのエラーが発生すると、他の正常な処理まですべて停止してしまいます。
これを避けるため、追加のエラーハンドリングロジックを実装したりしていたのですが、分散モードを使うと並列実行で一部が失敗しても中断されずに実行されることを最近知りました。
それでは、インラインモード(Inline Map)と分散モード(Distributed Map)のエラー動作の違いについて確認していきます。
やってみる
CDK でインラインモードと分散モードを実装して、動作確認していきます。
CDK のコードは以下に保存してあるので、詳細が気になる方はこちらをどうぞ。
インラインモードでのエラーハンドリング
インラインモードの Map ステートでは、並列実行中の 1 つのタスクでエラーが発生すると、他のすべての並列処理も即座に停止します。
これは、処理を中断させたくない場合に大きな課題となります。
例えば、100 個のアイテムを並列処理している最中に、5 番目のアイテムでエラーが発生すると以下のようになります。
アイテム1: ✅ 成功
アイテム2: ✅ 成功
アイテム3: ✅ 成功
アイテム4: ✅ 成功
アイテム5: ❌ エラー発生 → 全体が停止
アイテム6-100: ⏹️ 処理されない
実際に実行すると処理途中でエラーが発生した場合、失敗した処理以降は中断されてしまいます。
失敗した処理は Map のプルダウンから確認できます。
中断を回避するには追加の実装が必要
この問題を回避するために、インラインモードでは追加のエラーハンドリングロジックが必要になる場合があります。
-
子ステート内でのエラーキャッチ
- 各並列処理内で Try-Catch パターンを実装
- エラーを握りつぶして成功扱いにする
-
親ステートでの結果判定
- 各処理の結果を収集
- エラーがあった場合を判定
以下のような実装により、エラーが発生してもすべての処理が実行され、後でエラー率をチェックして全体の成功/失敗を判定します。
エラーハンドリングのためだけにこれらの追加実装が必要になるため、保守性の観点では課題があります。
CDK で実装するとこのくらいのコード量になります。
this.errorHandlingMap = new sfn.Map(this, 'ErrorHandlingMap', {
itemsPath: '$.items',
resultPath: '$.results',
comment: 'Process items with error tolerance',
});
const processTask = new tasks.LambdaInvoke(this, 'ProcessItemTask', {
lambdaFunction: props.processItemFunction,
resultPath: '$.processResult',
comment: 'Process individual item with Lambda',
retryOnServiceExceptions: true,
});
const errorFallback = new sfn.Pass(this, 'ErrorFallback', {
resultPath: '$.processResult',
result: sfn.Result.fromObject({
status: 'error',
timestamp: sfn.JsonPath.stringAt('$$.State.EnteredTime')
}),
comment: 'Handle processing errors gracefully',
});
processTask.addCatch(errorFallback, {
errors: ['States.ALL'],
resultPath: '$.errorDetails',
});
this.errorHandlingMap.itemProcessor(processTask);
const successState = new sfn.Pass(this, 'ProcessingComplete', {
result: sfn.Result.fromObject({
status: 'completed',
completedAt: sfn.JsonPath.stringAt('$$.State.EnteredTime')
}),
comment: 'Mark processing as successfully completed',
});
const prepareResults = new sfn.Pass(this, 'PrepareResults', {
parameters: {
'inputItems.$': '$.items',
'processedResults.$': '$.results',
'summary': {
'timestamp.$': '$$.State.EnteredTime',
'executionName.$': '$$.Execution.Name'
}
},
comment: 'Prepare results for error checking (zero tolerance)',
});
const errorCheck = new sfn.Choice(this, 'CheckForErrors', {
comment: 'Zero tolerance approach: complete failure is safer than partial success for data integrity',
})
.when(
sfn.Condition.isPresent('$.processedResults[0].errorDetails'),
new sfn.Fail(this, 'ProcessingFailed', {
error: 'ProcessingFailed',
cause: 'Zero tolerance policy: any processing error triggers complete workflow failure',
})
)
.otherwise(successState);
const definition = this.errorHandlingMap
.next(prepareResults)
.next(errorCheck);
this.stateMachine = new sfn.StateMachine(this, 'Inline MapStateMachine', {
definitionBody: sfn.DefinitionBody.fromChainable(definition),
stateMachineName: `${cdk.Stack.of(this).stackName}-Inline MapDemo`,
comment: 'Demonstrates strict error handling with zero tolerance for business-critical workflows',
logs: {
destination: props.logGroup,
level: sfn.LogLevel.ERROR,
},
timeout: props.timeout ?? cdk.Duration.minutes(30),
tracingEnabled: true,
});
分散モードによる解決策
分散モードでは、並列実行の一部が失敗したとしても中断せずに処理を実行します。
以下のようにエラーが発生した場合でも、インラインモードとは違い中断されずに5つの処理が完了しています。
マップ実行の詳細を見ると、並列実行された処理の詳細が確認できます。インラインモードより処理個別の内容が確認できて見やすいですね。
CDK では以下のようにシンプルな設定でできます。
const distributedMap = new DistributedMap(this, 'DistributedMap', {
itemsPath: '$.items',
resultPath: '$.results',
maxConcurrency: 10,
toleratedFailurePercentage: 10, // 10%までのエラーを許容
});
重要なのは**「エラー許容率(ToleratedFailurePercentage)」パラメータ**の設定です。これにより、指定した割合までのエラーを許容して Map 全体を成功として扱えます。
エラー許容率の設定で柔軟な制御
エラー許容率(ToleratedFailurePercentage)のパラメータがあり、どの程度の割合失敗しているかで Map としての成功/失敗を分けることができます。
ToleratedFailurePercentage = 0
0 に設定すると、1つでもエラーが発生した場合は Map が失敗になります。
アイテム1: ✅ 成功
アイテム2: ✅ 成功
アイテム3: ❌ エラー発生(他の処理は継続)
アイテム4: ✅ 成功
アイテム5: ✅ 成功
...
アイテム10: ✅ 成功
結果: 9/10成功 → しかしMap全体は失敗
ToleratedFailurePercentage = 10
10 に設定すると 10%の許容率となるため、10 個の並列実行のうち1つ失敗しても Map としては成功になります。
アイテム1: ✅ 成功
アイテム2: ✅ 成功
アイテム3: ❌ エラー発生(他の処理は継続)
アイテム4: ✅ 成功
アイテム5: ✅ 成功
...
アイテム10: ✅ 成功
結果: 9/10成功(10%のエラー率 = 10%の許容率)→ 全体は成功
許容率を 10%で実行した際、1つ処理は失敗していますが Map 全体は成功となっています。
こうしたエラー時の挙動を変更できるので Distributed Map は自由度があります。
このように並列実行の処理を中断させずに、処理をしたい場合は分散モードを使用することでシンプルにできます。
料金
インラインモード自体には料金がかからないのに対し、分散モードは状態遷移に対して料金がかかります。月 4000 回まで無料枠があるため、それ以上では 1000 回当たり$0.025 です。
月間 10000 回の状態遷移であれば、以下の料金です。
- 10000-4000(無料枠)= 6000 回
- 6000 * 0.025$ = 0.15$
少ない回数であればインパクトは少ないですが、どの程度の遷移数になるかを見積もった上で利用しましょう。
並列処理を止めたくない場合、追加のハンドリングロジックにより保守が大変になるため、分散モードを利用するのがおすすめです。
まとめ
10 分の 1 失敗する処理のケースでインラインモードと分散モードの動作の違いをまとめると以下のようになります。
他の処理の継続 | Map全体の成功/失敗 | |
---|---|---|
インラインモード | ❌ 即座に停止 | ❌ 失敗 |
分散モード(0%) | ✅ 継続 | ❌ 失敗 |
分散モード(10%) | ✅ 継続 | ✅ 成功 |
正直に言うと、分散モードの利点を十分に理解していなかったため、エラーハンドリングの観点から再評価しました。
この機能をもっと早く理解していれば、追加のコード実装に悩まされることもなかったなぁと反省しています。
この記事が同じような課題に直面している方の助けになれば幸いです。