[my-klas]負荷テストシミュレーション



シミュレーションの定義


アプリケーションの実装は終わりに近づいており、負荷テストを試みます.
目標性能は、実際の学校で発生する可能性のある通信量を基準とします.
私たちの学校では1学期の授業申請を4回に分けて行い、学科や学年によって日程が異なります.
これは川の余石を公平に調整するためであるが,流量の分散効果も得られる.
学校が提供した入試結果資料を基準にして計算すると、学年ごとに2000人近くの学生がいて、在校生だけなら8000人から1万人程度です.
これを4組に分けると、1回の選択授業に2000人以上の生徒が参加する.
このとき申請単位は15~21点であり,学生1人当たり6科目を申請したと考えると,一瞬にして12000件のリクエストが発生する.
問題は連続して講義を申し込むことだ.したがって,応答時間が最も速くてこそユーザ体験が増加し,いくら遅くても1秒を超えてはならない.
シミュレーションの定義は次のとおりです.
  • 000人のユーザーが6回連続で受講を申し込んだ.
  • ユーザーは4年生の1学期の学生で、3年(計6学期)の受講経験を持っている.
  • 鋼は1200個、余石は各10個ある.
  • 課には10の人気授業がある.50%のユーザーは、人気のあるコースから応募を選ぶに違いありません.
  • の受講条件に違反したり、複数席がない場合は申請に失敗し、400 Bad Requestの回答を受けます.
  • シミュレーション実装


    初期化スクリプト:
    def main():
        # 모든 데이터 삭제
        request('POST', '/admin/clear')
    
        # 임의의 학생 생성
        studentIds = []
        for i in range(NUM_STUDENTS):
            student = request('POST', '/students', body={ 'studentNumber': random_student_number()})
            studentIds.append(student['id'])
        
        # 임의의 강의 생성
        lectureIds = []
        for i in range(NUM_LECTURES):
            level = i
            lecture = request('POST', '/lectures', body={
                'lectureNumber': random_lecture_number(level=level),
                'term': '2021-1',
                'name': f"test-{str(i)}",
                'subject': f"test-{str(i)}",
                'level': level,
                'credit': 3,
                'capacity': 10,
                'schedules': []})
            lectureIds.append(lecture['id'])
    
        # 인기 강의 선택
        popLectureIds = lectureIds[:NUM_POPULAR_LECTURES]
    
        # CSV 파일 생성 (학생이 어떤 강의를 수강할 것인지 결정)
        with open('registrations.csv', 'w', newline='') as csvfile:
            spamwriter = csv.writer(csvfile, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
            spamwriter.writerow(["studentId", "lectureIds"]) # Header
    
            for studentId in studentIds:
                registLectureIds = []
                
                # 50% 확률로 인기 강의 중 하나를 골라 수강
                if random.random() < 0.5:
                    registLectureIds.append(random.choice(popLectureIds))
                
                # 정해진 수만큼 강의 수강
                while len(registLectureIds) < NUM_LECTURES_PER_STUDENT:
                    registLectureIds.append(random.choice(lectureIds))
    
                spamwriter.writerow([str(studentId), ' '.join(str(id) for id in registLectureIds)])
    gatlingシミュレーションスクリプト:
    class RegisterSimulation extends Simulation {
    
      // 설정 파일 로드
      val simConfig: Config = ConfigFactory.load("simulation.conf")
    
      val httpProtocol = http
        .baseUrl(simConfig.getString("baseUrl"))
    
      // 수강신청 시나리오 정의
      val scn = scenario("RegisterSimulation")
        .feed(csv("registrations.csv").queue)
        .foreach(session => session("lectureIds").as[String].split(" ").toSeq, "lectureId") {
          exec(http("register lectures")
            .post("/students/${studentId}/register")
            .body(StringBody("""{"lectureId":${lectureId}}""")).asJson
            .check(status.is(200)))
        }
    
      // 시나리오 실행
      setUp(
        scn.inject(atOnceUsers(simConfig.getInt("register.numUsers")))
      ).protocols(httpProtocol)
      
    }

    初回実行結果



    平均応答時間はなんと4秒!ユーザーが待つ時間が長すぎます.
    しかし、負荷テストでは、JVMのクラス遅延ロードと割り込みプロセスを考慮する必要があります.
    JVMのInterpreterは、JIT方式でバイトコードをタイムリーにコンパイルします.また,頻繁に実行されるコードがあれば,それをコンパイルして再利用し,速度を向上させることができる.
    JVMのClass Loaderは、必要なクラスをタイムリーにロードし、メモリにロードします.
    したがって、この2つのタスクを事前に実行すると、アプリケーションの実行速度が速くなります.これをJVM Warm-upと呼びます.

    JVM Warm-up


    最も簡単な暖房方法は、予めシミュレーションすることです.これは、必要なクラスをロードし、使用するコードをコンパイルしたためです.
    ただし、デフォルト設定を使用しているJVMでは、1回のシミュレーションのみではコードを正常に動作させることはできません.これは、コンパイルするには、コードが特定の制限点以上で実行されなければならないためです.

    -XX:CompileThreshold=invocations


    Sets the number of interpreted method invocations before compilation. By default, in the server JVM, the JIT compiler performs 10,000 interpreted method invocations to gather information for efficient compilation.
    ...
    This option is ignored when tiered compilation is enabled.
    しきい値は、-XX:CompileThreshold=invocations因子で設定できます.
    デフォルト値は10000に達しますが、12000リクエストのうち約2000リクエストが失敗しているため、失敗に関連するコードを熱くするには、5回前にシミュレーションする必要があります.そのため、1000程度に設定すれば適切なはずです.
    しかしながら、この因子を用いるためには、-XX:-TieredCompilation因子を適用する必要がある.
    階層型コンパイルは、Java 8からデフォルトで有効になっている機能で、頻繁に実行することでコンパイルレベルを向上させることで、コンパイル時間と最適化の程度の間でトレードオフを提供します.
    JVMにはクライアント(-client)とサーバ(-server)方式のJITコンパイラがある.クライアントコンパイラのコンパイル速度は速いが、実行性能は悪い.サーバコンパイラのコンパイル速度は遅いが、実行性能は良い.階層コンパイルは,コンパイラの階層化によって実現されるといえる.
    この機能はJVMヒーターを妨げ、コンパイル制限を設定できないため、オフにする必要があります.
    つまり、次のパラメータを使用してサーバ・アプリケーションを実行します.
    -XX:-TieredCompilation -XX:CompileThreshold=1000
    次に,実際の測定前にシミュレーションを1回行い,必要なすべてのクラスがロードされ,使用するコードが十分に加熱された場合に実験を行うことができる.

    JVM Warup後の実行結果



    平均応答時間4239 ms→3253 ms
    1000 ms程度改善されたが,満足できる結果は得られなかった.
    生徒の立場に立って6時間の授業を受けなければならない.ボタンを押すたびに3秒も待たなければならないなら、本当に憂鬱だ.
    問題はもう定義されているので、後でこれを解決します.