ngrinder 성능테스트 [서버 부하 테스트]
🎐성능 테스트 목표🎐
성능 테스트 전략 수립 단계 중 가장 핵심은 바로 서비스할 소프트웨어에 대한 명확한 목표를 수립하는 것이다. 이 목표를 달성하기 위해 성능 테스트와 부하 테스트를 수행하고 튜닝작업을 진행한다.
만일 튜닝을 해도 목표를 달성하지 못한다면 하드웨어 증설이나 아키텍처 변경을 고려해 볼 필요가 있다.
- 서비스가 얼마나 빠른지(Time)
- 일정 시간 동안 얼마나 많이 처리할 수 있는지(TPS)
- 얼마나 많은 사람들이 동시에 사용할 수 있는지(Users)에 대해 이야기해야 한다.
성능 테스터의 관점에선 사용자가 Concurrent User 인지 Active User 인지가 중요합니다.
- Concurrent User : 웹 페이지를 띄어놓은 사용자처럼, 언제든지 부하를 줄 수 있는 사용자를 의미합니다.
- Active User는 메뉴나 링크를 누르고 결과가 나오기를 기다리는 등 실제로 서버에 부하를 주고 있는 사용자를 의미합니다.
Active User 와 Concurrent User 의 비율은 서비스의 성격에 따라 다르므로 이 점을 감안하고 성능테스트를 계획해야 한다. (성능 테스트시에 VUser는 Active User와 유사합니다.) 가령 수강신청의 경우, 특정 시간대엔 그 비율이 90%에 육박할 수 있어, 전체 평균을 기준으로 테스트할 경우 잘못된 판단을 이끌어낼 수 있다.
성능 테스트 시엔 실제 지연시간이 발생하는 구간을 파악하여야 합니다
- Server 구간 : DB와 애플리케이션 간 연결의 문제, 프로그램 로직 상의 문제 혹은 서버의 리소스 부족 등을 의심해 볼 수 있다.
- 네트워크 이슈의 경우 테스트하는 환경에 따라 달라질 수도 있다. 따라서 성능 테스트하는 소프트웨어는 가급적 테스트할 서버와 같은 네트워크 환경에 존재하는 것이 좋다.
- 지연 현상은 사용자의 이탈과 매우 밀접하기에 개선되어야 하지만, 단순히 서버를 늘린다고(Scale out) 해결되는 것은 아니라고 합니다!
- 따라서 출시 전에 테스트를 하여 최대 응답시간을 파악하고 있어야 하며, 상위 5%의 화면이 95% 사용자 요청을 받는다는 점을 감안하고 튜닝의 대상을 선정해야 한다.
- Time과 달리, TPS (Transaction Per Seconds)는 Scale out 혹은 Scale up을 통해 증가시킬 수 있습니다.
- User 증가 시 TPS는 어느 정도 증가하다가 더 이상 증가하지 않게 된다.
- Time은 일정하게 유지되다 점차적으로 증가합니다.
- 반면, 부하가 증가할 경우(TPS가 증가) 지연시간은 변곡점에 이르기도 하는데, 이 경우 시스템 리소스가 누수되고 있는 것은 아닌지 확인해봐야 한다.
테스트 환경 구축
1. 네트워크 환경
성능 테스트하는 소프트웨어는 가급적 테스트할 서버와 같은 네트워크 환경에 존재하는 것이 좋다. 그렇지 않으면 네트워크의 성능 차이때문에 성능 측정 결과가 변질될 소지가 있다.
2. 테스트 데이터
데이터베이스의 경우 축적된 데이터의 양에 따라 성능 결과가 크게 다를 수 있기 때문에 실제 운영환경을 고려해서 데이터를 준비해야 한다.
3. 다양한 요청 데이터
각각의 사용자들이 동일한 파라미터로 요청하는 경우는 극히 드물다. 그러므로 실제 서비스 환경을 고려해서 사용자들의 요청 데이터를 다양하게 준비할 필요가 있다.
4. 구간 모니터링 방안
최근 개발되는 애플리케이션은 멀티 티어 환경으로 구현되기 때문에 성능 테스트를 수행한 결과값을 각 구간별로 수집하고 구분할 수 있어야 한다. 예를 들어 A라는 요청은 데이터베이스 처리를 20번하고 파일 처리를 1번 한다면 각각의 항목의 수치값을 뽑을 수 있어야 향후 성능 튜닝에 참조할 수 있다.
nGrinder 사용
우선 nGrinder를 선택한 이유로는 웹 기반 테스트 + groovy언어로 작성할 수 있다는게 컸다. 다양한 부하테스트 도구들이 있었지만 선택하는데 어려움이 있었는데 Jmeter같은 경우는 100% 자바로 작성되어있어서 스프링부트와 하면 좋을것 같았지만 UI마저 swing으로 작성되어있어서 안예쁘다는것 같다. 그래서 그나마 UI괜찮고 groovy언어를 사용하는 nGrinder를 선택했다.
💻nGrinder 구성
Controller
- 웹 기반의 GUI 시스템
- 유저 관리 (각자 시나리오 작성 가능)
- 에이전트 관리
- 부하테스트 실시 & 모니터링
- 부하 시나리오 작성 테스트 내역을 저장하고 재활용 가능
agent
- 부하를 발생시키는 대상(주체)
- controller의 지휘를 받는다.
- 여러 머신에 설치해서 controller의 신호에 따라 일시에 부하를 발생가능
- 테스트하려는 머신에 agent 설치
target
- 테스트하려는 target 머신이다
필요 사항
- java JDK 1.6이상 (JRE 안됨!)
- tomcat 6.x 이상
주의사항
- 성능 테스트 시 기본적으로 Agent는 12000~ port를 이용하기 때문에 해당 포트 번호를 사전에 열어놓아야 합니다.
- 공식 문서는 Controller 컨테이너가 동작 중인 머신과 Agent 컨테이너를 구동하지 말것을 강력하게 권고합니다. 여러 Agent들이 동작하다 보면, 부하를 발생시키는 머신의 자원을 모두 소모할 수 있기 때문입니다.
- 1개의 머신에 2개 이상의 agent를 사용할 경우, 현재 이 머신이 어느정도의 free 메모리를 가지고 있는지 에이전트가 판단하기 힘들어 테스트 프로세스의 메모리 할당을 제대로 할 수 없습니다. 그리고 상호 간섭 현상(네트웍 트래픽)이 발생하여 테스트의 측정 결과를 신뢰할 수 없습니다.
http://ngrinder.373.s1.nabble.com/process-thread-td2636.html#a2640
Process와 Thread개수
OS 가 현재 떠 있는 프로세스간에 context switching을 하기 때문에 프로세스 개수를 늘리면 그 만큼 에이전트가 cpu를 사용하는 기회가 늘어나긴 하지만, 자바가 기본적으로 사용하는 메모리도 늘어나기 때문에, 과도한 프로세스 개수는 에이전트의 메모리 사용량을 소모시켜, threshing 상태로 빠질 수 있습니다.
만약 EC2두대로 Agent와 Controller를 나눈다면 아래처럼 이용하면 된다.
docker run -d --name agent --link controller:controller ngrinder/agent
하지만 그럴 여유가 없으므로 한대의 EC2에서 진행해보겠다.
docker-compose.yml
version: '3.7'
services:
controller:
container_name: ngrinder-controller
image: ngrinder/controller:latest
environment:
- TZ=Asia/Seoul
ports:
- "9000:80"
- "16001:16001"
- "12000-12009:12000-12009"
volumes:
- /tmp/ngrinder-controller:/opt/ngrinder-controller
sysctls:
- net.core.somaxconn=65000
agent-1:
container_name: ngrinder-agent-1
image: ngrinder/agent:latest
links:
- controller
environment:
- TZ=Asia/Seoul
sysctls:
- net.core.somaxconn=65000
agent-2:
container_name: ngrinder-agent-2
image: ngrinder/agent:latest
links:
- controller
environment:
- TZ=Asia/Seoul
sysctls:
- net.core.somaxconn=65000
아래 처럼 설정을 많이 줘도 되지만 잘 모른다면 그냥 위 처럼 보편적으로 적용하자!🙈
version: '3.7'
services:
controller:
container_name: ngrinder-controller
image: ngrinder/controller:latest
environment:
- TZ=Asia/Seoul
ports:
- "8880:80"
- "16001:16001"
- "12000-12009:12000-12009"
volumes:
- /tmp/ngrinder-controller:/opt/ngrinder-controller
sysctls:
- net.core.somaxconn=65000
agent-1:
container_name: ngrinder-agent-1
image: ngrinder/agent:latest
links:
- controller
environment:
- TZ=Asia/Seoul
sysctls:
- net.core.somaxconn=65000
ulimits:
memlock:
soft: -1
hard: -1
nproc:
soft: 1024000
hard: 1024000
nofile:
soft: 1024000
hard: 1024000
agent-2:
container_name: ngrinder-agent-2
image: ngrinder/agent:latest
links:
- controller
environment:
- TZ=Asia/Seoul
sysctls:
- net.core.somaxconn=65000
ulimits:
memlock:
soft: -1
hard: -1
nproc:
soft: 1024000
hard: 1024000
nofile:
soft: 1024000
hard: 1024000
SOMAXCONN(socket max connection 의 약어) : 네트워크 연결 최대 개수 [윈도우 Default=1000]
--ulimit memlock : 메모리 주소 공간 최대 size ( default 64kb ) , -1 swap 사용 X
soft : 새로운 프로그램을 생성하면 기본적으로 적용되는 한도
hard : 소프트한도에서 최대로 늘릴 수 있는 한도
nproc
- 해당 도메인 (사용자, 그룹)의 최대 프로세스 개수
- 한 사용자에게 허용 가능한 프로세스(user processes)의 최대 개수 제한
nofile
- 해당 도메인 (사용자, 그룹)이 오픈할 수 있는 최대 파일 개수
- 오픈할수 있는 파일기술자(FD: file descriptor)의 최대 개수 제한
이제 localhost:9000에 들어가고 quick start에 http://google.com을(ngrinder실습할 때, 국룰...구글..) 쳐주고 아래처럼 설정해준다.
- 테스트명 : 추후 해당 테스트 기록을 식별하는데 사용합니다.
- Agent : 해당 테스트에 사용할 Agent 개수를 선택할 수 있습니다.
- VUser : virtual user로 동시에 접속하는 유저의 수를 의미
- 사용할 수 있는 최대 VUser의 총합 개수는 Agent 개수 * process * thread 개수입니다.
- 해당 부하 테스트를 수행할 기간 및 실행 회수 등을 세밀하게 조정할 수 있습니다.
- 저장 후 시작 버튼을 통해 테스트를 실행합니다.
- 테스트를 바로 시작할 수 있으며, 특정 시간에 예약을 걸어둘 수 있습니다.
- TPS : 초당 트랜잭션의 수 - 초당 처리 수
- 트랜잭션 : HTTP Request가 성공할 때마다, 트랜잭션 수가 1씩 증가.
- 최고 TPS : 초당 처리 수의 최대치.
- 평균 테시트 시간 : 사용자가 request한 시점에서 시스템이 response할 때까지 걸린 시간.
- 총 실행 테스트 : 테스트 시간동안 실행한 테스트의 수
간단히 tps는 높을수록 테스트시간과 에러는 적을수록 좋습니다.
그리고 save and start를 해준다.
3분이 길어서 하다가 멈췄는데 이런 고양이 모양이 나왔다.
detailed Report를 클릭하면 TPS 이외의 지표들을 확인할 수 있으며, CSV 다운로드를 제공합니다.
그리고 너무 많은 에러가 발생하는 경우 테스트가 중단됩니다.
계~~속 agent들이 쉬지않고 일하는(?) 모습도 볼수 있었다.
하지만 이렇게 quickStart를 하면 스크립트 파일에 테스트 수행 코드를 작성하고 저장하더라도, 이후 메인 페이지에서 실수로 동일 URL에 대해 Quick Start 테스트 시작 버튼을 누르면 overwrite 되서 사라진다.
따라서 Scripts를 이용하자!
Groovy Scripts
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 static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
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
/**
* A simple example using the HTTP plugin that shows the retrieval of a
* single page via HTTP.
*
* This script is automatically generated by ngrinder.
*
* @author admin
*/
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static NVPair[] headers = []
public static NVPair[] params = []
public static Cookie[] cookies = []
@BeforeProcess
public static void beforeProcess() {
HTTPPluginControl.getConnectionDefaults().timeout = 6000
test = new GTest(1, "ec2-11.11.2.3.ap-northeast-2.compute.amazonaws.com")
//test = new GTest(1, "{도메인주소orIP주소}")
request = new HTTPRequest()
grinder.logger.info("before process.");
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
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 thread. init headers and cookies");
}
@Test
public void test(){
HTTPResponse result = request.GET("http://ec2-11.11.2.3.ap-northeast-2.compute.amazonaws.com/api/member", params)
// HTTPResponse result = request.GET("{도메인주소orIP주소}", 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));
}
}
참고로 위 Scripts는 전형적인 scripts이다. 커스터마이징하려면 @Test부분을 손보면 된다.
이후, 전제 조건, 시나리오 대상, 테스트 환경등을 따져보고 성능을 측정하자!😉😊💻
아래는 참고한 블로그에서 가져온 예시이다. 위에 글로만으로는 감이 안잡혀서 어떤 느낌인지 맛만 보자!
전제 조건(예시)
- 1일 예상 사용자(DAU) : ex) 10만명
- 1명당 1일 평균 접속 수 : ex) 15회
- 1일 총 접속 횟수 : ex) 150만회
- 1일 평균 RPS(Request Per Second) : ex) 17회
- 1일 최대 RPS(Request Per Second) : ex) 50회
- Latency : ex) 50 ~ 100ms 이하
- Throughput : ex) 17회~50회
부하 테스트에서 사용할 VUser는 공식에 의거해 105.
스트레스 테스트를 위한 VUser는 300 ~ 600 등 유동적으로 적용.
부하 및 스트레스 테스트 모두 30분 ~ 1시간을 진행.
시나리오 대상
접속 빈도가 높으며, 많은 DB 리소스를 조합해 결과를 보여주는 부분을 시나리오 대상으로 선정
테스트 환경
테스트를 위한 요소는 테스트 대상 시스템에 절대로 영향을 미쳐서는 안 되기 때문에, S3 이미지 업로드 및 GitHub API와 같은 외부 시스템은 테스트 대상 시스템과 완벽히 분리된 Mock 서버를 만들어 배포한 다음 테스트를 진행했습니다.
Reference