[テストJPA]負荷テストとN+1問題


現在のプロジェクトの進行中に、ある程度のコードが作成され、実際の導入を実行するために負荷テストが行われた場合、次のような状況が発生しました.
(テスト-砲兵.io)

1秒あたり5回のリクエスト
試験時間:60秒
最小:21 ms、最大:739 ms
http.200回答率:100%
グループ全体をクエリーするリクエストは、1秒に少なくとも5回実行されます.
しかし、私たちが現在展開しているサービスは少なくとも1000人が使用する必要があると考えています.そのため、私たちは毎秒1000回のリクエストを送信することにしました.

1秒あたり1000回のリクエスト
試験時間:20秒(10秒後に停止)
最小:1ミリ秒、最大:9505
http.200回答率:約24%
サーバが突然混乱し、平均応答時間は4秒になりました.簡単なお願いだったので、変なところを見つけて、どこが問題なのか探し始めました.

🤔 サーバの問題


まず疑うのはAWSサーバーです.
フリータイヤのEC 2を使用しているので、2 GBのRAMでは計算できないと推測されます.
(監視指標)


検証の結果、CPUが使用されていないのは7%で、ネットワークの問題ではありません.これは、サーバ上のコードに異常が発生し、Debugを開始して検索することを意味します.

😢 N+1の問題が発生しました。


複数の人が要求すると、GET関数が急に無理になり、作業効率が低下する可能性があります.
しかし、私が使っている方法はJPAのRepositoryの基本関数で、唯一疑わしいのはN+1です.

1つの要求では、約7〜10回のクエリがあり、一般的なN+1の問題が発生した.

🔥 N+1問題は


N+1は、JPAがEntity情報をマッピングする際に発生する問題である.
1:N関連関係の情報からN個の情報を取得するために、再びselectクエリが発行され、N+1と呼ばれる.
JPAが関連関係の情報を取得する際にN+1の問題が発生した原因は以下の通りである.
group(1)-people(N)関係で1つのグループに複数の関連関係がある場合、group情報をインポートする際にpeopleに関する情報も必要です.
このとき、JPAがグループに関する情報をインポートすると、関連関係にあるユーザがプロキシオブジェクトとしてインポートされます.
つまり、次の情報が得られます.

JPAはまずグループに関する情報を提供する.
selectクエリーが表示され、proxy(任意のエージェントオブジェクト)を使用してgroupオブジェクトが作成され、Entity Managerによってグループ内の情報とユーザのリストが保存されます.
個人情報を取得するには、関連する個人を選択してN個のクエリーを追加します.

🤔 どうしてN+1が現れるのですか?


それがJPAがEntityの管理方法を知っている理由です
JPAが関連関係から情報を取得する方法は2つあります.
  • FetchType.Lazy
  • FetchType.Eager
  • 2つのFetch typeの違いは、関連関係をクエリーするタイミングによって異なります.
    Lazyは,Nのオブジェクト情報を1:N関係でエージェントを用いて置き換え,実際にデータが必要なときにデータベースに要求してデータを取得する.逆に、関連関係の情報を1つの時点でインポートします.
    つまり、Fetch TypeがEagerであれば、Entityのクエリ時にすべての情報が1つの時点で取得されます.ここでは一時的に概念を混同し、
    1つの時点でインポートがクエリーにインポートされると勘違いしています.
    Eagerの場合、1つの時点でインポートするのは正しいですが、1:Nの場合、結果は複数のselectで結合されます.
    そこで,N+1に関する問題はFetchタイプがEagerであるかLazyであるかに関係なく,解決策を再検討することにした.

    🤗 N+1トラブルシューティング方法。


    では、JPAのN+1問題をどう防ぐのか.
    JPAは、ORM技術の説明に従ってオブジェクト向けのコードを要求し、独自のSQL形式でデータをマッピングする.これは、JPAが要求したオブジェクト情報をチェックし、JDBCを介してSQLに情報を要求することを意味します.
    例)

    (出典:JPAという組織ブログ)
    JPAは、私たちが要求したオブジェクト情報に基づいてJDBCに要求を発行するので、別の方法でJDBCに要求を発行することで解決することができる.つまり、SQL文のjoinを使用することで、fetch joinというクエリからオブジェクトのすべての情報を取得できます.
    @Repository
    public interface ChallengeRepository extends JpaRepository<Challenge, Long> {
    
    	@Query("select distinct c from challenge c join fetch c.userChallengeList")
    	Optional<Challenge> findById(Long id);
        ...
    }
    fetch joinメソッドでは、@Query宣言によりSQLを直接作成し、クエリーをJDBCに転送できます.その後、挑戦中のすべてのユーザーをjoinに入れ、クエリーから情報を取得できます.
    *注意:fetchjoinではなく通常のjoinを使用する場合、JPAはJDBCに複数のselectを要求し、eneityManager内部joinによって結果を返します.つまり,N+1問題は解決できない.
    Lazy、EagerでN+1問題を解決できると思ったのは私の錯覚です.すでに導入が完了しており、実際のサービスでこのような過負荷が発生した場合は、最悪です.
    また、EagerとLazyに関する記事もいくつか閲覧しました.
    ~ToOneの場合、デフォルト値はEagerです.
    ~ToManyのデフォルト値はLazyです.
    以上の結果から,~ToManyは関連関係の所有者ではない可能性が高いため,デフォルト値はLazyであることが分かる.
    SQLで、関連関係の所有者でない場合は、selectを使用して関連関係のエンティティを一度にインポートできないため、上記の設定が設定されている可能性があります.
    +追加する内容や不足点があれば、メッセージを残してください!:)