
目次
はじめに
本記事ではAzure Functions を使用して Azure Blob Storage 間でファイルを移動する際に発生するメモリ関連などの問題と、その対処方法について解説します。
処理の安定化とパフォーマンス改善を目的とし、エラーの見え方・原因・具体的な回避策をプロジェクトの実体験を元に整理して記載しています。
Azure 初心者の C# 開発者が同様の処理を実装・運用する際の参考情報としてご活用ください。
発生した事象:Azure Functionsで発生した不具合の事例
Azure Functions で Blob ファイルの移動処理を行っていたところ、以下のような不審な挙動が Azure ポータル上で確認されました。
- Application Insights (※ Application Insights=Azureのログ監視ツール)に明確なエラーが表示されない
- Function の実行が失敗しているのに、ログ上では「開始」だけが繰り返されており、処理中のログが出力されない
- 複数の実行インスタンスが並列で立ち上がったように見える(多重起動に見える)
- メトリックで「Memory Working Set(メモリ使用量)」が関数実行時に異常に上昇し、数分間ピーク状態が続いている
- 関数が定期的に自動再起動されているが、明確な理由が分からない
これらの事象は、Azure Functions の処理中に発生した異常終了がログに残らず、再起動によって表面化するという特徴を持っています。原因の特定には、ログだけでなくメトリックの確認が必要です。
原因:メモリ不足で関数が強制終了する仕組み
Blob ファイルの移動処理で blobClient.DownloadStreamingAsync() を使用し、Blob の内容をストリームで読み込んでから別の場所へアップロードする方式を採用していた場合、ファイルサイズや並列処理数によってはAzure Functions の実行環境で大量のメモリを消費します。
Azure Functions は実行中にメモリ使用量が上限に達すると、OutOfMemory 例外を出さずにプロセスが強制終了され、自動で再起動される挙動をとります。
そのため、Application Insights 上には明確な例外ログが残らず、Function が突然失敗し再起動したような動きを取ります。
この結果、Function の「開始ログ」だけが並び処理中のログが表示されない、あるいは複数回起動(多重起動)されたような挙動に見えるという事象につながります。
実際にはストリーム処理によりメモリ不足が発生し、処理が途中で落ちAzureの仕様で再起動していたことが根本原因でした。
解決策:StartCopyFromUriAsyncによるサーバーサイドコピー
メモリ使用量を抑えて安定したファイル移動処理を実現するために、blobClient.DownloadStreamingAsync() を用いたストリームコピーではなく、StartCopyFromUriAsync() を使用したサーバーサイドコピーへ切り替えます。
この方法では、Blob ストレージ内で直接ファイルをコピーするため、Azure Functions 側でデータを保持する必要がなく、メモリ使用量をほぼゼロに抑えることが可能です。
実装例(C#)
var sourceBlobClient = sourceContainerClient.GetBlobClient("source-folder/sample.txt");
var destinationBlobClient = destinationContainerClient.GetBlobClient("processed-folder/sample.txt");
// Azure Blob Storage 内でファイルをサーバーサイドコピー
await destinationBlobClient.StartCopyFromUriAsync(sourceBlobClient.Uri);
// コピー完了後、元ファイルを削除して「移動」処理を完了
await sourceBlobClient.DeleteIfExistsAsync();
この方法により、Function 実行中のメモリ消費を抑え、Blob 間の移動処理を安定かつ高速に実行できるようになります。
ファイルサイズや処理件数が多いケースでも安全に運用可能です。
追加の注意点(CosmosDBなどのシステムと連携している場合)
CosmosDBへの連携時の注意点と対策
StartCopyFromUriAsync() によりファイル移動処理が高速化されると、後続の処理である CosmosDBへのログ書き込みに負荷が集中し「Request Units(RU)枯渇によるエラー(HTTP 429)」が発生する可能性があります。
特に、Parallel.ForEachAsync などでファイルを並列処理している場合は移動が短時間で完了するため、CosmosDBへの書き込みリクエストが同時多発的に送られるようになります。
これにより、スループット制限に引っかかり、処理が一部失敗するケースがあります。
主な事象
CreateItemAsync実行時に 429(Too Many Requests)エラー- 書き込み処理がリトライを繰り返すことで全体のパフォーマンスが低下
- Function の実行時間が延びる、またはタイムアウト
有効な対策
並列数の制御ParallelOptions で MaxDegreeOfParallelism を設定し、同時に実行される処理数を制限することで、CosmosDB へのアクセス集中を緩和します。
var options = new ParallelOptions { MaxDegreeOfParallelism = 3 };
await Parallel.ForEachAsync(blobList, options, async (blobItem, token)
=> { await destinationBlobClient.StartCopyFromUriAsync(blobItem.Uri);
await sourceBlobClient.DeleteIfExistsAsync(); await cosmosDbClient.CreateItemAsync(logData); });CosmosDB の自動スケーリングを有効化
RUの使用量に応じて自動でスループットが拡張されるように設定することで、ピーク負荷への対応力を上げます。
スロットリング制御の導入(例:SemaphoreSlim)
書き込み処理に手動で制限をかけることで、制御しやすくなります。
using var semaphore = new SemaphoreSlim(3);
await Task.WhenAll(blobList.Select(async blobItem
=> { await semaphore.WaitAsync(); try { await destinationBlobClient.StartCopyFromUriAsync(blobItem.Uri);
await sourceBlobClient.DeleteIfExistsAsync(); await cosmosDbClient.CreateItemAsync(logData); } finally { semaphore.Release(); } }));ログ書き込みのバッチ処理
一定数のデータをまとめて書き込むことで、リクエスト数とRU消費を抑制できます(※用途に応じて実装要検討)。
これらの対策を組み合わせることで、Blob 処理の高速化と CosmosDB 側の負荷制御を両立させることが可能です。Function の処理安定性を保つためには、「処理を早くする」だけでなく「後続リソースの耐性を高める/負荷をコントロールする」ことも重要です。
まとめ
- DownloadStreamingAsyncはAzureFunctionのメモリを消費する
- StartCopyFromUriAsyncでメモリ消費ゼロに
- 並列数を制御してCosmosDBの負荷を回避
Azure Functionsを使ったBlobファイルの移動処理は一見すると簡単そうに見えますが、実際には実行環境のリソース制約や外部サービスの特性をしっかり考慮する必要があります。
最終的には、並列数の制御(スレッド制限)をまず試し、それでも不十分な場合は自動スケーリングやスロットリング、バッチ処理といった多角的なアプローチを組み合わせることで、全体の安定性を保つことができました。
Azureでの開発では、「1つの問題を解決すると別の課題が顔を出す」ことが珍しくありません。
これからAzure FunctionsとBlob Storageを組み合わせた処理を検討されている方は、ぜひ今回の件を参考に構築してみてください。