nGrinder性能テスト


概要
E-コマースの大型サーバープロジェクトの実際のトラフィック受信時のパフォーマンスをテストします.そこで,実際の環境を作成し,nGrinderを用いてサーバがどれだけのユーザを収容できるかをテストすることにした.
nGrinder
:Naverが開発した負荷テストツール
  • コントローラ:WebベースのGUIシステムで、包括的なタスク管理とロードスクリプト作成機能を提供します.
  • エージェント:コントローラの制御に基づいて実際に負荷が発生します.
  • コントローラのインストールと実行
    # 서버 접속 명령어
    ssh root@[서버접속용IP] -p [포트번호]
    
    # 관리자 비밀번호 입력
    root@[IP]'s password: 
    
    # 서버 접속 
    # 자바 설치
    [root@ngrinder-server ~]# yum list java*jdk-devel
    
    [root@ngrinder-server ~]# yum install java-11-openjdk-devel.x86_64
    
    # 설치 확인
    [root@ngrinder-server ~]# which javac
    /usr/bin/javac
    [root@ngrinder-server ~]# java -version
    openjdk version "11.0.14" 2022-01-18 LTS
    OpenJDK Runtime Environment 18.9 (build 11.0.14+9-LTS)
    OpenJDK 64-Bit Server VM 18.9 (build 11.0.14+9-LTS, mixed mode, sharing)
    [root@ngrinder-server ~]# javac -version
    javac 11.0.14
    
    [root@ngrinder-server ~]# rpm -qa java*jdk-devel
    java-11-openjdk-devel-11.0.14.0.9-1.el7_9.x86_64
    
    [root@ngrinder-server ~]# readlink /etc/alternatives/javac
    /usr/lib/jvm/java-11-openjdk-11.0.14.0.9-1.el7_9.x86_64/bin/javac
    
    [root@ngrinder-server ~]# ls -l /usr/bin/javac
    lrwxrwxrwx 1 root root 23  130 14:06 /usr/bin/javac -> /etc/alternatives/javac
    
    # 환경설정
    [root@ngrinder-server ~]# vi /etc/profile
    
    # 아래 코드 추가
    export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.14.0.9-1.el7_9.x86_64
    export PATH=$JAVA_HOME/bin:$PATH
    export JAVA_OPTS=Dfile.encoding=UTF-8
    export CLASSPATH="."
    
    
    [root@ngrinder-server ~]# source /etc/profile
    
    # 서버 재시작
    [root@ngrinder-server ~]# reboot
    Connection to 106.10.50.203 closed by remote host.
    Connection to 106.10.50.203 closed.
    
    # nGrinder 다운로드
    [root@ngrinder-server ~]# wget https://github.com/naver/ngrinder/releases/download/ngrinder-3.5.3-20201127/ngrinder-controller-3.5.3.war
    
    # nGrinder 실행
    java -jar ngrinder-controller-3.5.3.war
    
    エージェントのインストールと実行
    [root@ngrinder-server ~]# cd /usr/local
    
    # ngrinder 웹페이지 우측 상단에 admin 토글을 클릭하여 Download Agent를 눌러 agent tar 파일을 다운
    # agent 다운로드
    [root@ngrinder-server local]# wget --content-disposition http://[IP]:8080/agent/download
    
    [root@ngrinder-server local]# ls
    bin  games    lib    libexec                                  sbin   src
    etc  include  lib64  ngrinder-agent-3.5.3-[IP].tar  share
    
    [root@ngrinder-server local]# tar xvf ngrinder-agent-3.5.3-[IP].tar
    
    [root@ngrinder-server local]# ls
    bin    include  libexec                                  sbin
    etc    lib      ngrinder-agent                           share
    games  lib64    ngrinder-agent-3.5.3-1.[IP].tar  src
    
    [root@ngrinder-server local]# cd ngrinder-agent
    [root@ngrinder-server ngrinder-agent]# ls
    __agent.conf   run_agent.sh            run_agent_internal.sh
    lib            run_agent_bg.sh         stop_agent.bat
    run_agent.bat  run_agent_internal.bat  stop_agent.sh
    
    [root@ngrinder-server ngrinder-agent]# cp __agent.conf agent.conf 
    [root@ngrinder-server ngrinder-agent]# ls
    __agent.conf  run_agent.bat    run_agent_internal.bat  stop_agent.sh
    agent.conf    run_agent.sh     run_agent_internal.sh
    lib           run_agent_bg.sh  stop_agent.bat
    
    # agent 실행
    [root@ngrinder-server ngrinder-agent]# sh run_agent.sh
    
    テストスクリプトの作成
    テスト機能
  • カタログ
  • クーポン
  • を発行
    1)カタログAPIの表示
    @RunWith(GrinderRunner)
    class TestRunner {
    
    	public static GTest test1
    	public static GTest test2
    	public static GTest test3
    	
    	public static HTTPRequest request
    	public static NVPair[] headers = []
    	public static NVPair[] params = []
    	public static Cookie[] cookies = []
    	
    	public static MAX_RECORDS = 99800
    
    	// 프로세스가 생성될 때 동작해야하는 작업 정의
    	@BeforeProcess
    	public static void beforeProcess() {
    		HTTPPluginControl.getConnectionDefaults().timeout = 6000
    		// 각 테스트 통계를 수집할 때 사용되는 GTest인스턴스 정의
    		test1 = new GTest(1, "Test1")
    		test2 = new GTest(2, "Test2")
    		test3 = new GTest(3, "Test3")
    		
    		request = new HTTPRequest()
    		grinder.logger.info("before process.");
    	}
    
    	// 각 쓰레드가 실행되기 전에 동작해야하는 작업 정의
    	@BeforeThread 
    	public void beforeThread() {
    		//request 인스턴스에 대해 메소드를 호출하게 되면 테스트 별로 TPS증가시켜 기록
    		test1.record(this, "test1")
    		test2.record(this, "test2")
    		test3.record(this, "test3")
    		
    		grinder.statistics.delayReports=true;
    		grinder.logger.info("before thread.");
    	}
    	
    	@Before
    	public void before() {
    		request.setHeaders(headers)
    		cookies.each { CookieModule.addCookie(it, HTTPPluginControl.getThreadHTTPClientContext()) }
    		grinder.logger.info("before. init headers and cookies");
    	}
    
    	// 테스트 동작 정의
    	@Test
    	public void test1(){
    		String origin = "http://[public IP]:8080/products"
    		String deliveryType = "ROCKET"
    		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
    		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
    		HTTPResponse result = request.GET(origin + params)
    
    		if (result.statusCode == 301 || result.statusCode == 302) {
    			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode); 
    		} else {
    			assertThat(result.statusCode, is(200));
    		}
    	}
    	
    	@Test
    	public void test2(){
    		String origin = "http://[public IP]:8080/products"
    		String deliveryType = "ROCKET_FRESH"
    		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
    		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
    		HTTPResponse result = request.GET(origin + params)
    
    		if (result.statusCode == 301 || result.statusCode == 302) {
    			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode); 
    		} else {
    			assertThat(result.statusCode, is(200));
    		}
    	}
    	
    	@Test
    	public void test3(){
    		String origin = "http://[public IP]:8080/products"
    		String deliveryType = "ROCKET_GLOBAL"
    		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
    		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
    		HTTPResponse result = request.GET(origin + params)
    
    		if (result.statusCode == 301 || result.statusCode == 302) {
    			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode); 
    		} else {
    			assertThat(result.statusCode, is(200));
    		}
    	}
    }
    2)クーポンAPI取得
    @RunWith(GrinderRunner)
    class TestRunner {
    	public static GTest test
    	public static HTTPRequest request
    	public Object cookies = []
    	
    	// map에서 NVPair로 convert하는 함수 
    	def nvs(def map) {
    		def nvs = []
    		map.each {
    			key, value ->  nvs.add(new NVPair(key, value))
    		}
    		return nvs as NVPair[]
    	}
    
    	// 프로세스가 생성될 때 동작해야하는 작업 정의
    	@BeforeProcess
    	public static void beforeProcess() {
    		HTTPPluginControl.getConnectionDefaults().timeout = 6000
    		// 각 테스트 통계를 수집할 때 사용되는 GTest인스턴스 정의
    		test = new GTest(1, "Test1") 
    		request = new HTTPRequest()
    		//request 인스턴스에 대해 메소드를 호출하게 되면 테스트 별로 TPS증가시켜 기록
    		test.record(request); 
    		grinder.logger.info("before process.");
    	}
    
    	// 각 쓰레드가 실행되기 전에 동작해야하는 작업 정의
    	@BeforeThread 
    	public void beforeThread() {
    		// reset to the all cookies
            def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
            cookies = CookieModule.listAllCookies(threadContext)
            cookies.each {
                CookieModule.removeCookie(it, threadContext)
            }
            
    		// 테스트 전에 사전 작업으로 로그인 처리
    		int randomNum = Math.abs(new Random().nextInt() % 50000) + 1 //
    		String email = Integer.toString(randomNum) + "@naver.com" //
    		HTTPResponse result = request.POST("http://[public IP]:8080/users/login", nvs(["email":email, "password":"1234"])) //
    		
    		// HTTPResponse result = request.POST("http://101.101.209.54:8080/users/login", nvs(["email":"[email protected]", "password":"1234"]))
    		cookies = CookieModule.listAllCookies(threadContext)
    		grinder.statistics.delayReports=true;
    		grinder.logger.info("before thread.");
    	}
    	
    	@Before
        public void before() {
            // set cookies for login state
            def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
            cookies.each {
                CookieModule.addCookie(it ,threadContext)
                net.grinder.script.Grinder.grinder.logger.info("{}", it)
            }
        }
    	
    	// 테스트 동작 정의
    	@Test
    	public void couponTest() {
    		int randomNum = Math.abs(new Random().nextInt() % 20000) + 1
    		String couponId = Integer.toString(randomNum)
    		request.POST("http://[public IP]:8080/available-coupons/" + couponId)
    	}
    	
    }
    負荷テストの実行
    1)商品リストの表示-VUser 500

    2)クーポン取得-VUser 500

    テスト結果の分析
    1)商品リストの表示-VUser 500
    VUserを500に設定し、10分間のテストを行いました.
    しひょう
    平均TPS:662
    ピークTPS:921
    CPU Usage : 100%
    Errors : 2,674
    2)クーポン取得-VUser 500
    VUserを500に設定した後、10分間のテストを行いましたが、結果は以下の通りです.
    しひょう
    平均TPS:426
    ピークTPS:538
    CPU Usage : 100%
    Errors : 0
    ポスト
    初めて性能テストを行いますが、nGrinderの使い方は公式文書や技術ブログに多くの資料があるので便利です.ただし、scriptでエラーが発生した場合、問題点を特定するのは難しいですが、nGrinderはログファイルを提供し、問題を参照して解決するのに役立ちます.nGrinder以外にも、JMeterなどの他の性能テストツールがありますが、次回は他のツールを使用して比較します.
    📕 Reference
    公式ドキュメント-インストールガイド
    公式ドキュメント-ユーザーガイド
    Groovyスクリプト作成方法