본문 바로가기
Performance

내가 만든 서비스는 얼마나 많은 사용자가 이용할 수 있을까? - 2편(nGrinder를 활용한 성능테스트)

by Hooligans 2020. 12. 27.

개요

 지난 시간 서비스의 성능을 알기 위해서 Throughput과 Latency 성능지표가 필요하다고 알게 되었습니다. 이번 시간에는 서비스의 성능 지표를 확인하기 위해서 부하를 발생시키는 방법에 대해 알아보겠습니다. 또한 AGORA 서비스에 실제 부하를 발생시켜서 단위 성능 테스트를 진행하고 AGORA 서비스의 현재 성능을 분석해보도록 하겠습니다.

 

 먼저 부하를 발생시키는 도구에 대해서 알아볼까요?

 

nGrinder에 대해서 알아보자!

 nGrinder는 네이버에서 The Grinder라는 성능 테스트 도구를 기반으로 제작한 오픈소스 성능 테스트 솔루션입니다. 스크립트 생성과 테스트 실행, 모니터링 및 결과 보고서 생성을 통합된 Web UI를 통해 사용할 수 있으므로 성능 테스트를 보다 쉽게 할 수 있습니다.

 

 추가적으로 단순히 부하만을 발생시키는 것이 아니라 Groovy, Jython 스크립트를 통해서 다양한 시나리오를 테스트할 수 있다는 점도 큰 장점입니다.

 

 무엇보다도 오픈소스 솔루션이기 때문에 무료로 사용할 수 있습니다. 물론 더 좋은 기능과 편리함을 제공하는 상용 툴들이 존재하지만 이들을 사용하기에는 금전적인 요소가 큰 부담이었습니다.

 

 이와 같이 오픈 소스임에도 쉽고 편리하게 성능 테스트를 할 수 있다는 점에서 이번 프로젝트에서는 nGrinder를 사용하기로 했습니다. 성능 테스트는 정확한 결과를 도출할 수 있는지가 중요한 것이지 어떤 툴을 사용하는지는 절대적이지 않습니다. 상용 툴 중에서는 더 좋은 툴들도 많이 있고, 오픈소스 중에서도 유명한 JMeter, 테스트 성능이 좋다는 Gatling 등 다양한 좋은 도구들이 있으니 필요에 따라 자신에게 맞는 툴을 선택해서 사용하시면 됩니다.

 

 지금까지 nGrinder에 대해서 알아보았습니다.

 

 그렇다면 다음은 nGrinder를 통해 테스트 환경을 구성하는 방법에 대해서 알아보겠습니다.

 

부하 테스트 도구도 결정했으니 테스트 환경을 구성해볼까요?

 

[그림 1] nGrinder 테스트 환경 구성

 

 

 nGrinder를 통해 성능 테스트를 하기 위해서는 위 그림과 같이 Controller, Agent, Target Server 가 각각 별도의 서버에 구성되어 있어야만 합니다.

 

 Controller의 역할은 테스트 스크립트를 작성하고, Agent에 부하 발생 명령을 해서 테스트를 시작할 수 있도록 하는 웹 애플리케이션 서버입니다. 추가적으로 성능 테스트를 위해서 UI를 제공하고 테스트 실행 절차를 설정할 수 있도록 하며, 실행 중인 테스트를 모니터링하거나, 테스트 결과를 수집해서 시각화해주는 역할을 합니다.

 

 다음은 Agent입니다. 실질적으로 부하를 발생시키는 주체로 프로세스와 스레드 수를 조정하여 vUser(가상 사용자)를 생성합니다. 통상 vUser 수 = 프로세스 수 × 스레드 수로 계산합니다. vUser는 Controller에서 실행한 테스트 스크립트에 따라 동작하여 Target Server에 부하를 발생시킵니다.

 

 Target 서버는 우리가 테스트하고자 하는 대상 서버를 의미하며, 테스트 중 Target 서버에서 발생한 오류들 혹은 실시간 CPU, Memory 상태 등 조금 더 자세한 정보를 확인하고 싶다면 Target 서버에 nGrinder Monitor를 설치하면 확인하실 수 있습니다.

 

 위의 세 가지 요소를 각각 다른 서버에 설치하는 이유는 Controller, Agent 모두 부하를 발생하고 모니터링하는 데 있어서 각각 CPU와 메모리 즉, 서버의 자원을 사용하기 때문입니다.

 

 만약 세 가지 요소가 하나의 서버에 존재한다면 서버는 자원을 나눠서 사용해야 하고, 그만큼 Context Switching이 발생하는 등 테스트에 있어서 불필요한 노이즈가 발생하게 되기 때문에 순수한 Target 서버의 성능을 도출하기 어려워집니다.

 

그러므로 우리는 각각 서버를 분리하여 설치해야만 하고, 필요에 따라 vUser 수를 늘리기 위해서 Agent 서버를 추가적으로 설치할 수 있습니다.

 

 지금까지 nGrinder 테스트 환경이 어떻게 구성되는지 알아보았습니다.

 

 구체적인 nGrinder 설치 과정을 설명드리는 것은 포스팅 길이가 길어지기 때문에 링크로 대체하겠습니다.

 

 Docker를 사용하면 쉽게 설치하실 수 있으나, Docker를 이용해 보지 못하신 분들은 nGrinder 공식 Github 혹은 생활 코딩 nGrinder 페이지를 참고하시면 쉽게 설치하실 수 있습니다. 참고로 생활코딩 자료는 비교적 오래된 자료이기 때문에 현재 버전과 맞지 않는 내용이 있을 수 있습니다. 최신 업데이트된 요소를 공식 사이트에서 확인하시길 바랍니다.

 

내 서비스는 얼마나 많은 사용자가 이용할 수 있을까?

 지금부터 AGORA 서비스의 단위 성능 테스트를 해보도록 하겠습니다.

 

 이번 단위 성능 테스트에서는 AGORA 서비스의 단일 피드 조회 API 성능을 확인해보도록 하겠습니다.

 

 우선 AGORA 서비스의 테스트 환경 구성부터 확인해볼까요?

 

 아래 보시는 바와 같이 테스트에 필요한 Controller, Agent, Target Server 가 기본적으로 구성되어 있고, Target 서버 앞에는 이후에 Scale-Out 시에 사용할 로드밸런서를 미리 구성했습니다.

 

 단일 서버를 사용하는 중에도 로드 밸런서를 두고 테스트하는 이유는 테스트 대상을 고립시키기 위해서입니다. Scale-out을 하려면 로드 밸런서가 필요하게 되고, 로드 밸런서와 서버를 동시에 추가하게 된다면 어떤 대상에 의해서 성능이 증가하고 감소했는지 정확하게 확인하기 어렵기 때문입니다.

 

 이처럼 단일 서버 구성에도 로드밸런서를 추가함으로써 Scale-out 시에도 순수하게 서버가 늘어났을 때 증가하는 성능에 대해서 측정을 할 수 있습니다.

 

 그 외에는 데이터베이스와 세션을 저장한 레디스가 추가적으로 구성되어 있습니다.

 

 

[그림 2] Agora 서비스 테스트 환경

 

 테스트 스크립트는 nGrinder 개발자분이 작성하신 'Groovy 스크립트 구조' 포스팅을 참고하여 작성했습니다! 아래 링크 첨부하겠습니다.

 

 아래는 위 포스팅을 참고해서 제가 작성한 테스트 스크립트를 첨부하니 필요하신 분들은 참고하시길 바랍니다. 더보기를 클릭하시면 확인하실 수 있습니다.

 

더보기
import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.plugin.http.HTTPRequest
import net.grinder.plugin.http.HTTPPluginControl
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
import net.grinder.scriptengine.groovy.junit.annotation.AfterThread
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import java.util.Date
import java.util.List
import java.util.ArrayList

import HTTPClient.Cookie
import HTTPClient.CookieModule
import HTTPClient.HTTPResponse
import HTTPClient.NVPair

import groovy.json.JsonOutput

@RunWith(GrinderRunner)
class TestRunner {

	public static GTest test
	public static HTTPRequest request
	public static NVPair[] headers = []
	public static Cookie[] cookies = []

	@BeforeProcess
	public static void beforeProcess() {
		HTTPPluginControl.getConnectionDefaults().timeout = 6000
		test = new GTest(1, "123.123.123.123")//Target IP
		request = new HTTPRequest()
        
		List<NVPair> headerList = new ArrayList<>()
		headerList.add(new NVPair("Content-Type", "application/json"))
		headers = headerList.toArray()
	}

	@BeforeThread 
	public void beforeThread() {
		test.record(request)
		grinder.statistics.delayReports=true;
		
        def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
        cookies = CookieModule.listAllCookies(threadContext)
        cookies.each {
            CookieModule.removeCookie(it, threadContext)
        }
		
		def loginBody = JsonOutput.toJson([userId: 'testuser1', password: 'password'])
		HTTPResponse res = request.POST("http://123.123.123.123/users/login", loginBody.getBytes(), headers);
		cookies = CookieModule.listAllCookies(threadContext)
	}
	
	@Before
	public void before() {
		request.setHeaders(headers)
		cookies.each { CookieModule.addCookie(it, HTTPPluginControl.getThreadHTTPClientContext()) }
	}

	@Test
	public void getSingleFeedTest(){
		HTTPResponse result = request.GET("http://123.123.123.123/feeds/test")
	}
	
	@AfterThread
	public void afterThread() {
		HTTPResponse result = request.POST("http://123.123.123.123/users/logout")
	}
}

 

 이렇게 테스트 스크립트 작성이 끝나면 테스트 설정을 합니다.

 

 

테스트 설정

 

 가장 중요한 것은 vUser 수를 선정하는 것으로 500명, 1000명의 사용자를 기준으로 각각 테스트해보았습니다. 테스트 시간은 Ramp-up 시간을 포함하여 유효한 결과를 도출하고자 10분 동안 테스트했습니다.

 

 아래는 vUser를 500명 기준으로 테스트한 결과입니다.

 

 

vUser 500명 기준 테스트 결과

 

 전체적인 결과도 중요하지만 우리에게 가장 필요한 데이터는 Throughput과 Latency였습니다. 위의 결과를 보면 TPS가 Throughput을 의미하고, 평균 테스트 시간이 Latency를 의미합니다. 결과적으로 Agora 서비스는 vUser 500명 기준, 약 2000 TPS 처리량을 갖고, 테스트 요청당 230ms의 지연시간이 걸린다는 것을 알 수 있습니다.

 

 다음은 vUser를 1000명으로 테스트한 결과입니다.

 

vUser 500명 기준 테스트 결과

 

 보시는 바와 같이 사용자가 늘어났는데 Throughput은 약 2000 TPS로 동일하고, Latency만 두 배로 증가했습니다. 이 부분은 아래 그래프를 보시면 이해하실 수 있습니다.

 

 

동시 활동 사용자 증가에 따른 Throughput 변화

 

 vUser 수가 최고점에 다다르면 고부하 영역에 들어가게 되고, 고부하 영역에서 일정 User 수를 초과하면 Buckle zone으로 진입해서 처리량이 줄어드는 현상이 발생합니다. 현재 vUser 500명 기준과 1000명 기준일 때, 처리량은 일정하므로 현재 테스트한 API에 대한 시스템의 성능은 약 2000 TPS 정도로 추정할 수 있습니다.

 

 하지만 이는 단위 성능 테스트이므로 전체 시스템의 성능이라고 단정할 수 없고, 여러 단위 테스트를 종합하고 성능 개선을 한 이후, 통합 시나리오 테스트 및 성능 개선 과정까지 끝내야 전체 시스템의 성능을 예상할 수 있습니다.

 

현재 상태에서 WAS의 CPU 사용량을 확인해봅시다.

 

vUser 500명 기준 테스트 중 WAS 리소스 사용 상태

 

 vUser 500명을 기준으로 테스트하는 중간에 htop 명령을 통해 확인한 결과, WAS의 CPU 사용량이 90% 이상 점유하는 것을 보면 현재 WAS에서 최대 처리량에 거의 도달했음을 볼 수 있습니다.

 

 지금까지 AGORA 서비스의 단위 성능 테스트를 진행해보았습니다. 현재 500명 vUser 기준, 2000 TPS의 처리량과 200ms정도의 Latency가 소요되는 성능을 보이는 것을 알 수 있습니다. 현재 WAS 상태에 대해 진단을 해보았으니, JVM 튜닝 및 슬로우 쿼리 분석 등 다양한 성능 개선 작업을 시도하면서 서비스의 성능을 최대한 올려보겠습니다.

 

결론

  1. nGrinder를 활용하면 스크립트 생성과 테스트 실행, 모니터링 및 결과 보고서 생성을 통합된 Web UI를 통해 사용할 수 있으므로 성능 테스트를 보다 쉽게 할 수 있습니다.
  2. nGrinder를 활용한 성능 테스트를 하기 위해서는 Controller, Agent, Target 서버가 필요하고, 이는 각각 다른 서버로 구성되어 있어야 합니다.
  3. vUser가 증가함에 따라 Throughput이 한계에 도달하면 현 상태의 최대 처리량으로 추정할 수 있습니다. 단, 실제 서비스의 성능은 이보다 더 적을 수 있습니다.
  4. 단위 성능 테스트 결과는 전체 서비스의 성능을 나타내지 않습니다. 하지만 이를 개선함으로써 전체적인 성능을 증가시킬 수 있습니다.

 

참고

 

프로젝트 참고

 

f-lab-edu/sns-agora

소셜 네트워크 서비스 AGORA입니다. Contribute to f-lab-edu/sns-agora development by creating an account on GitHub.

github.com

 

다음 포스팅에는

 다음 포스팅에는 현재 AGORA 서비스의 JVM 옵션을 변경해보고 이에 따라 성능이 어떻게 변하는지 확인해보도록 하겠습니다.