13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Loggerくらい new でいいでしょ』が客先障害になった話(ASP.NET Core / DI)

Last updated at Posted at 2025-12-23

はじめに

ASP.NET Core で API を開発していたときの話です。
ログ出力には NLog を使っていました。

当時の自分は、

「Loggerなんて軽いし、new しても問題ないでしょ」

と思っていました。
DI(Dependency Injection)を使わず、
Controller 側でLogger を生成していたのですが、
これが 客先での障害につながることになります。

この記事では、

  • 何が起きたのか
  • なぜ開発中に気づけなかったのか
  • 次から何をルール化したか

を、事故の教訓としてまとめます。


開発当時の前提

  • ASP.NET Core Web API
  • ログライブラリ:NLog
  • 小〜中規模の API
  • 開発環境・結合テストでは特に問題なし

Controller では、DIを使わずに Logger を扱っていました。


何が起きたか(事象)

本番相当の環境で、負荷をかけたところ次のような現象が起きました。

  • プロセスのメモリ使用量が 徐々に増え続ける
  • GC が走ってもメモリが元に戻らない
  • 数時間〜半日程度でプロセスが不安定になる

ポイントは、

一気に増えるのではなく、じわじわ増える

という点でした。

通常操作では気づかず、
負荷がかかって初めて表に出るタイプの問題でした。


なぜ発見が遅れたか

原因が分かるまでに時間がかかった理由はシンプルです。

  • 開発環境では再現しない
  • 単体テスト・結合テストでは問題なし
  • しばらく動かし続けないと症状が出ない

つまり、

「動いている」状態が長く続いてしまった

のが落とし穴でした。


どうやって原因に辿り着いたか

JMeter を使って、同じ API を一定の TPS で叩き続けました。

その際に見ていたのは、

レスポンスタイムやエラー率

ではなく、

プロセスのメモリ使用量の推移

です。

負荷を止めてもメモリが戻らないことから、

「これはリークっぽい」

と疑い、実装を一つずつ確認していきました。


問題の実装

簡略化すると、次のような実装でした。

public class SampleController : ControllerBase
{
    private readonly Logger _logger;

    public SampleController()
    {
        // Loggerを直接生成
        _logger = /* Loggerをnew */;
    }
}
  • DI を使っていない
  • Controller が生成されるたびに Logger が作られる

当時は、

「Loggerなんて軽いオブジェクトだろう」

と深く考えていませんでした。


なぜこれが問題になるのか

Logger は一見すると軽そうに見えます。
しかし内部では、

  • バッファ
  • ファイルハンドル
  • 非同期キュー
  • タイマー

などを持っており、
アプリケーション全体で共有される前提にて設計されています。

DI を使わずにリクエスト単位で生成すると、

  • 破棄されない内部リソースが溜まる
  • 高負荷時にそれが一気に効いてくる

という状況になります。


このとき取得した、プロセスのリソース使用状況が次の図です。

image.png

CPU 使用率が大きく跳ね上がっている箇所は、
JMeter から同一 API に対して、
およそ 1,000 前後のリクエストを送信しているタイミングに対応しています。

一見すると、

  • 負荷をかけたので CPU が上がっている
  • よくある高負荷時の挙動

のようにも見えます。

しかし注目すべきだったのは、
負荷を止めた後の挙動でした。

GC が発生し、CPU 使用率が落ち着いた後も、
プロセスのメモリ使用量が元に戻らず、
時間とともにじわじわ増え続けていることが分かります。

この挙動から、

  • 一時的な高負荷による問題ではない
  • GC 管理外のリソースが解放されていない可能性がある
  • 実装やライフサイクル管理に原因があるのではないか

と考え、Logger 周りの実装を重点的に見直すことにしました。


修正後の実装

修正はシンプルでした。

public class SampleController : ControllerBase
{
    private readonly ILogger<SampleController> _logger;

    public SampleController(ILogger<SampleController> logger)
    {
        _logger = logger;
    }
}
  • Logger の生成は DI に任せる
  • Controller は「使うだけ」
  • 設定はアプリ起動時に一度だけ

これで、負荷試験時のメモリ増加は解消しました。


この事故から学んだこと(ルール化)

この件以降、次のようなルールを決めました。

  • Controller / Service で new しない
  • Logger / HttpClient / DbContext は必ず DI 経由
  • 負荷試験では「最大値」ではなくメモリが増え続けないかを見る
  • 「軽そうなオブジェクトほど疑う」

特に、

DI は便利だから使うのではなく、
ライフサイクルを守るために使う

という意識に変わりました。


おわりに

今回は Logger でしたが、
同じことは他のオブジェクトでも起き得ます。

  • 動いているから正しいとは限らない
  • 問題は、負荷がかかってから表に出ることもある

「Loggerくらい new でいいでしょ」という油断が、
客先障害につながったという教訓でした。

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?