技術本部 Sansan Engineering Unit Data Hubグループの茂木(@shinnopo_)です。普段はSansanのデータ統合プラットフォームであるSansan Data Hubの開発をしています。
今回は、リソースのクリーンアップをし忘れたことでAzure Service Busの同時接続数の上限に達してしまった問題と、その解決方法について紹介します。
背景
Sansan Data HubはC#で開発されています。マイクロサービスで構成されており、各サービスがキューでつながっている非同期メッセージングアーキテクチャです。そして、その非同期メッセージングを行うメッセージブローカーにはAzure Service Busを採用しています。
Sansan Data Hubのアーキテクチャに興味がある方は以下の記事を参考にしてください。 https://buildersbox.corp-sansan.com/entry/2023/02/16/110000
ある日、Service Busの同時接続数上限に引っかかるエラーが発生しました。このエラーは、特定の機能が大量の接続を生成してしまうことが原因で、サービス全体に影響を及ぼしました。
Exception while executing function: [FunctionName] <--- Cannot allocate more handles. The maximum number of handles is 4999. (QuotaExceeded).
エラーの原因
このエラーの主な原因は、Service Busへの送受信を行うSenderやReceiverなどのオブジェクトが適切にクリーンアップされず、不要な接続が残ってしまうことでした。これにより、接続数が増え続け、最終的に上限に達してしまいました。
修正方法
クリーンアップは基本的なことですが、意外と見落としがちです。例えば、以下のコードではsender
のクリーンアップを忘れています。
var sender = serviceBusClient.CreateSender("example"); await sender.SendAsync(message);
この場合、以下のようにしてクリーンアップを実装します。
await using var sender = serviceBusClient.CreateSender("example"); await sender.SendAsync(message);
簡単な修正ではありますが、クリーンアップは意外と説明が省かれがちです。この記事では、ここを少しだけ深掘りしてみます。
DisposeとDisposeAsync
C#は、アンマネージドリソースのクリーンアップを行うためにdisposeパターンの実装を行うのが一般的です。IDisposable
とその非同期版であるIAsyncDisposable
インターフェースを利用して実装します。つまり、IDisposable
またはIAsyncDisposable
が実装されていれば、これを呼び出せばいいのです。
詳細はMicrosoft公式ドキュメント*1*2を参照してください。
usingとDispose
とはいえ、Dispose
をいちいち呼び出すのは面倒ですよね。そこで一般的にはusing
ステートメントを使用します。using
ステートメントは、IDisposable
を実装しているオブジェクトの寿命を管理し、スコープを抜ける際に自動的にDispose
メソッドを呼び出します。これにより、リソースリークを防げます。
await usingとDisposeAsync
await using
ステートメントは、前述したusing
ステートメントと同様、リソースのクリーンアップをする必要がある場合に実装します。using
との違いはawait
を使用することで、非同期のクリーンアップが可能になります。await using
を使用する場合は、対象のオブジェクトにIAsyncDisposable
を実装する必要があります。
終わりに
今回のエラーに遭遇したことで、改めてリソースのクリーンアップの重要性を認識しました。願わくば、すべてGCにやってもらいたいところですが、仕組み上仕方がないですね...。上記のようにusing
ステートメントやDispose
メソッドを活用することで、リソースリークを防ぎ、安定したシステム運用が可能になります。