💡 성능 테스트의 종류
성능 테스트는 시스템이나 애플리케이션이 요구되는 표준 내에서 얼마나 효과적으로 작동하는지 확인하는 과정입니다.
다양한 유형의 성능 테스트가 있으며, 각각은 시스템 성능의 다른 측면을 측정합니다.
1. 부하 테스트(Load Test)
일정시간 동안 부하를 가하여 애플리케이션이 예상 사용량을 처리할 수 있는지 확인하기 위해 실시합니다.
이 테스트는 애플리케이션의 반응 시간, 처리량 및 리소스 사용량을 측정하여 성능 문제를 식별합니다.
2. 스트레스 테스트(Stress Test)
애플리케이션의 한계를 결정하기 위해 비정상적으로 높거나 예상치 못한 부하를 적용합니다.
목적은 시스템이 과부하 상태에서 어떻게 실패하는지 관찰하고, 최대 용량을 파악하는 것입니다.
3. 지속성 테스트(Endurance Test)
애플리케이션이 장기간에 걸쳐 안정적으로 실행될 수 있는지 평가합니다.
메모리 누수나 자원 고갈과 같은 문제를 식별하는 데 유용합니다.
4. 스파이크 테스트(Spike Test)
시스템이 짧은 시간 동안 급격히 증가하는 부하를 얼마나 잘 처리할 수 있는지를 평가합니다.
예를 들어, 이벤트 시작 시 티켓 판매와 같은 급격한 트래픽 증가 상황을 시뮬레이션합니다.
5. 중단점 테스트(BreakPoint Test)
시스템이나 애플리케이션의 성능 한계점을 결정하기 위해 실시되는 스트레스 테스트의 일종입니다.
이 테스트는 시스템에 점진적으로 부하를 증가시켜 가면서, 시스템이 어느 시점에서 더 이상 새로운 요청을 처리할 수 없게 되는지, 즉 '중단점'이나 '장애 지점'을 찾아내는 과정입니다. 시스템의 최대 용량과 한계를 이해하는 데 중요합니다.
🚀 성능 테스트 도구 비교
JMeter | nGrinder | k6 | |
개발사 | 아파치 소프트웨어 재단 | 네이버 | 로드임팩트(LoadImpact) |
장점 | - 다양한 프로토콜 지원 - GUI 기반 인터페이스 - 확장성 - 대규모 테스트 지원 |
- 분산 테스트 지원 - 웹 기반 인터페이스 - 스크립트 관리와 결과 분석 용이 |
- 개발자 친화적인 도구 - CI/CD 통합 용이 - 경량화 및 효율적인 리소스 사용 |
단점 | - 리소스 소모 - 초보자에게 다소 복잡한 학습 곡선 |
- 초기 설정과 환경 구축에 시간 소요 - 한정된 언어 지원(Groovy, Java 등) |
- 코드 기반 접근 방식 - GUI 부재 |
사용 | - 웹 애플리케이션 및 서비스의 성능 테스트 - 다양한 프로토콜을 사용하는 환경에서의 테스트 필요 시 |
- 대규모 분산 테스트가 필요한 경우 - 웹 인터페이스를 통한 테스트 관리와 실행을 원하는 경우 |
- 개발자가 주도하는 성능 테스트 - CI/CD 파이프라인과의 통합이 중요한 프로젝트 - 서버리스 또는 경량화된 테스트 환경 필요 |
인터페이스 | GUI 및 CLI | 웹 인터페이스 | CLI |
언어/스크립팅 | 다양한 프로토콜 지원을 위한 자체 형식 및 Java | Groovy, Java | JavaScript |
🌐 Latency와 Throughput
Latency(지연 시간)
정의) 요청이 처리되고 응답이 반환되기까지 걸리는 시간입니다.
단위) 밀리초(ms) 또는 초(s).
지표의 의미) 낮은 지연 시간은 시스템의 반응 속도가 빠르다는 것을 의미하며, 사용자 경험에 직접적인 영향을 미칩니다. 웹 페이지 로딩, API 응답 등 사용자와의 상호작용에서 중요한 지표입니다.
Throughput(처리량)
정의) 단위 시간당 시스템이 처리할 수 있는 작업의 양입니다.
단위) 초당 요청 수(RPS), 초당 트랜잭션 수(TPS), 또는 초당 비트 수(bps) 등.
지표의 의미) 높은 처리량은 시스템이 많은 요청을 효율적으로 처리할 수 있음을 의미합니다. 시스템의 성능 및 스케일링 능력을 나타내는 중요한 지표로, 서버의 용량 계획이나 네트워크 대역폭 할당 등에 사용됩니다.
Latency와 Throughput의 관계
대표적인 지표인 Response Time과 TPS간의 관계를 통해 이해해보겠습니다.
1. 시스템의 처리 용량 내에서는 요청이 많아질수록 Throughput이 증가합니다.
→ 시스템의 포화 상태 이전에는 시스템이 요청을 일정하게 처리할 수 있으므로, 요청 수가 증가함에 따라 Throughput도 증가
2. 시스템의 처리 용량 포화점(Saturation Point)부터 Throughput은 유지 또는 하락되며 Latency가 기하급수적으로 증가하기 시작합니다.
→ 시스템 자원이 포화되어 추가 요청을 즉각적으로 처리할 수 없어서 대기 시간이 발생
Saturation point는 해당 시스템의 최대 처리량을 나타내는 지점입니다.
이상적인 서비스의 경우 Saturation Point 이후 TPS는 수렴합니다.
만약 TPS가 떨어진다면 개선해야합니다.
성능 테스트의 한 형태인 Critical Performance Test (임계 성능 테스트)의 경우, Saturation Point를 찾아내는 것을 목표로 합니다.
✅ 서비스 수준 목표(SLO) 설정
서비스 수준 목표(Service Level Objective, SLO)는 IT 서비스 관리(ITSM)와 서비스 수준 관리(SLM)의 일환으로, 서비스 제공자가 고객이나 사용자와 합의한 성능 기준을 충족하는 것을 목표로 설정한 구체적이고 측정 가능한 성능 지표입니다.
SLO는 서비스 품질을 정의하고, 이를 통해 서비스가 일관된 수준의 성능과 가용성을 제공할 수 있도록 합니다.
먼저, Latency와 Throughput의 목표를 설정하기 전에 현재 브리핑 앱의 DAU를 확인해보겠습니다.
20~50사이인 것을 확인할 수 있습니다.
서비스가 성장해서 일일 활성 사용자가 현재(DAU 약 50)의 100배일 때 홈화면에서 정상적으로 빠른 조회 가능할지 여부를 기준으로 계획을 세워보겠습니다. (비지니스 트랜잭션 : "홈화면의 브리핑 목록 조회")
Latency
브리핑 목록 조회 API 호출의 평균 응답 시간은 300ms 이내
Throughput
목표 DAU) 목표 5000 (현재 : 20~50)
1명당 평균 요청 횟수) 20회
1일 평균 접속 수에 대한 피크 트래픽 배율) 3배
안전 계수) 3배
목표 TPS 계산) (5000 x 20 / 86400초) x 3 x 3 = 10.42
Concurrent Users
Think Time이 10초라고 가정하고, 리틀의 법칙으로 동시 사용자 수를 계산해보겠습니다.
동시 사용자 수 = Throughput x (단위 처리 응답시간 + Think Time)
Concurrent Users = 10.42 x (300ms + 10s) = 약 107명
Latency의 경우 일반적으로 평균보다 신뢰성있는 백분위 수(중앙 값이나 99% 등)로 설정을 하지만 이번에는 평균 응답 시간으로 설정해보겠습니다.
또한, 전체 시스템의 성능을 예상하기 위해서는 여러 단위 테스트를 종합하고 시나리오 테스트 및 성능 개선 과정 등을 거쳐야하지만, 이번에는 사용자가 가장 많이 호출하게 되는 홈화면 진입시 사용되는 목록 조회 API를 기준으로 단위 성능 테스트를 수행해보겠습니다.
♻️ nGrinder 개요
위 그림은 nGrinder의 시스템 아키텍쳐입니다. 사용자, 컨트롤러, 에이전트, 타겟 서버 간의 상호작용을 설명합니다.
사용자(User)
사용자는 성능 테스트를 실행하도록 nGrinder 컨트롤러에 요청합니다.
이 과정에서 테스트 스크립트와 테스트 설정이 포함됩니다.
nGrinder 컨트롤러
PerfTestRunnable) 사용자로부터 테스트 실행 요청을 받는 부분입니다. 이는 테스트 스크립트와 설정을 처리하고 실행 준비를 합니다.
ConsoleManager) 사용자와 에이전트 간의 커뮤니케이션을 관리하는 부분입니다. 각 테스트에 대한 콘솔을 생성하고 관리합니다.
AgentManager) 사용 가능한 에이전트를 관리하며, 테스트에 필요한 에이전트를 할당합니다.
SingleConsole) 실제 테스트 실행을 관리하고, 테스트 결과를 모니터링합니다.
AgentControllerServer) 에이전트와의 통신을 관리하고, 에이전트에게 명령을 전송합니다.
MonitorController) 테스트 대상 서버의 성능 모니터링을 시작합니다.
에이전트(Agent Controller & Agent)
에이전트는 테스트 부하를 생성하고 실행합니다.
각 에이전트 컨트롤러는 하나 이상의 에이전트를 관리할 수 있으며, 이 에이전트들은 컨트롤러로부터 테스트 스크립트와 명령을 받아 대상 서버에 부하를 생성합니다.
테스트 대상(Target Server)
대상 서버는 에이전트로부터 생성된 부하를 받아 처리합니다.
대상 서버의 모니터는 시스템의 성능을 모니터링하고, 해당 데이터를 컨트롤러로 보냅니다.
통신 프로토콜
nGrinder TCP Communication) 컨트롤러와 에이전트 간의 기본 통신입니다.
Grinder TCP Communication) 에이전트가 대상 서버에 테스트 부하를 생성할 때 사용하는 통신입니다.
Monitoring(JMX)) 대상 서버의 성능 모니터링을 위해 사용되는 통신 프로토콜로, 주로 Java Management Extensions(JMX)을 사용합니다.
상호작용 과정
1. 사용자는 nGrinder 컨트롤러에 테스트를 실행하라는 요청을 합니다.
2. 컨트롤러는 ConsoleManager를 통해 사용 가능한 콘솔을 가져옵니다.
3. AgentManager를 통해 사용 가능한 에이전트를 조회합니다.
4. 에이전트가 선택되면, 해당 에이전트는 SingleConsole에 바인딩됩니다.
5. SingleConsole은 에이전트에 테스트 스크립트를 분배하고 스트레스 테스트를 시작합니다.
6. 에이전트는 대상 서버에 부하를 생성하며, MonitorController는 대상 서버의 성능 모니터링을 시작합니다.
7. 테스트가 실행되는 동안 컨트롤러는 에이전트로부터 테스트 결과와 모니터링 데이터를 수집합니다.
8. 모든 테스트 데이터는 최종 사용자에게 결과로 제공되기 전에 컨트롤러에서 처리됩니다.
🚀 브리핑 목록 조회 성능 테스트
기존의 목표에 따라 테스트에서는 두 가지를 관찰하겠습니다.
1. 107명의 동시 유저에서 300ms이하의 Latency와 10.42 이상의 TPS를 만족하는지 관찰
2. Saturation Point에서의 TPS, Vusers, Mean Test Time 관찰
Test Configuration
Vuser의 변화에 따른 TPS를 관찰하기 위해 Ramp-Up을 활성화하여 Vuser를 0부터 500까지 점차 증가시켰습니다.
테스트는 20분간 수행했습니다.
Script
import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
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 org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.ngrinder.http.cookie.CookieManager
/**
* A simple example using the HTTP plugin that shows the retrieval of a
* single page via HTTP.
*
* This script is auto generated by ngrinder.
*
* @author userName
*/
@RunWith(GrinderRunner)
public class TestNewsBriefing {
public static GTest test;
public static HTTPRequest request;
@BeforeProcess
public static void beforeClass() {
test = new GTest(1, "Test News Briefing API");
request = new HTTPRequest();
test.record(request);
grinder.logger.info("before process.");
}
@BeforeThread
public void beforeThread() {
grinder.statistics.delayReports = true;
grinder.logger.info("before thread.");
}
public void initialSleep() {
grinder.sleep(grinder.threadNumber * 1000, 0);
}
@Test
public void test() {
if (grinder.runNumber == 0) {
initialSleep();
}
HTTPResponse result = request.GET("https://[스테이징 서버 주소]/v2/briefings?type=SOCIAL");
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));
}
}
}
초기에 스크립트를 작성할 때는 응답 시간을 계산하여 목표 Latency(300ms) 초과의 결과들에 대해선 실패로 처리하게 작성했었습니다.
그러나 다수의 요청이 실패로 판정되며 테스트가 종료됨에 따라 해당 로직을 제외했습니다.
Test Report
테스트에 대한 요약입니다.
TPS, 평균 응답 시간 등을 확인할 수 있습니다.
TPS & Vuser & Mean Test Time
1. 107명의 동시 유저에서 300ms이하의 Latency와 10.42 이상의 TPS를 만족하는지 관찰
(그래프에서 vUser가 105 다음 109값을 가져서 107보다 큰 109로 값을 확인했습니다.)
TPS) 151.5
Mean Test Time) 약 692.4 ms
TPS는 쉽게 만족했으나, Latency는 개선이 필요해보입니다.
2. Saturation Point에서의 TPS, Vusers, Mean Test Time 관찰
TPS) 233
Vuser) 215
Mean Test Time) 약 857.1 ms
Saturation Point에서 값들을 관찰하며 최대 TPS, 그 때의 Latency, 최대 허용 동시 사용자를 파악할 수 있었습니다.
🚧 병목지점 이해하기
1. 서비스의 Throughput은 하위 시스템들 중 가장 낮은 Throughput입니다.
WAS와 DB가 아무리 3000, 2000TPS여도 결국, Web Server에서 초당 500개의 트랜잭션만을 수행할 수 있으므로 결과적으로 해당 서비스는 초당 500개의 트랜잭션만 수행할 수 있습니다.
따라서 위의 예시에서 병목 지점은 Web Server가 되며 Throughput 관점에서의 성능 개선은 병목 지점을 지속적으로 개선하며 전체 서비스의 성능을 올리는 것입니다.
만약, Web Server의 Thoughput이 4000TPS로 개선되었다면 병목지점은 Database로 바뀝니다.
2. 서비스의 Latency는 하위 시스템들 Latency의 총 합(+ 대기시간)입니다.
사용자가 요청 후 응답 받기까지의 과정을 생각해보면 하위 시스템들의 Latency를 모두 더해야합니다.
(200ms + 30ms + 50ms + 대기시간)
하나의 하위 시스템 Latency가 줄어들면 전체 서비스의 Latency도 줄어들기때문에 Latency 관점에서의 성능 개선은 병목 구간을 찾기보단 Latency가 가장 큰 하위 시스템을 개선하는 것입니다.
3. Throughput 개선은 Latency 개선에도 영향을 줄 수 있습니다.
Throughput을 개선하면 대기시간이 줄어들어 Latency를 줄일 수 있습니다.
📝 성능 개선 방법들
Web Server
정적 파일 캐싱) 이미지, CSS 등 캐싱
WAS
스레드 풀 설정) maxThreads, minSpareThreads 등
커넥션 풀 설정) DB 커넥션 풀을 최적화하여 DB 연결 지연 최소
JVM 옵션 튜닝) 메모리할당량(Xms, Xmx), 가비지 컬렉션 옵션 등 JVM 설정 최적화
DB
인덱싱) 복합 인덱스, 커버링 인덱스 등
쿼리 튜닝) 실행 계획 분석 및 쿼리 재작성
테이블 파티셔닝) 대용량 데이터 처리를 위한 테이블 분할
버퍼 풀 조정) InnoDB 버퍼 풀 크기 조정 DB 서버 메모리 사용 최적화
레플리케이션 활용) 읽기 부하 분산을 위한 마스터-슬레이브 레플리케이션 구성
커넥션 관리) max_connections 조정
ETC
API 응답 캐싱) Redis 등을 사용한 응답 캐싱
API 응답 캐싱 참고 글) https://dev-peter.online/78
📚 마무리
이번 브리핑 목록 조회 단위 성능 테스트를 진행하며, 성능 개선을 위한 초석을 다질 수 있었던 것 같습니다. 앞으로 작성할 글들에서 실제 개선과정을 기록하고 공유해보겠습니다!
[참고]
https://naver.github.io/ngrinder/
https://github.com/naver/ngrinder/wiki/Architecture
https://chance-story.tistory.com/67
https://hyuntaeknote.tistory.com/11
https://youtu.be/3cTn53dtzJI?si=12S1Cmehh-DGsMpv
https://yummy0102.tistory.com/685
https://performance.tistory.com/4
https://hub.docker.com/r/ngrinder/controller/
https://velog.io/@calaf/Docker-이용한-nGrinder-설치-방법
https://velog.io/@max9106/nGrinderPinpoint-test1
http://www.jidum.com/jidums/view.do?jidumId=562
https://jh-labs.tistory.com/624
https://developer-cheol.tistory.com/72
https://github.com/naver/ngrinder/wiki/How-to-ramp-up-by-threads
'💻 Service > Briefing' 카테고리의 다른 글
[Briefing] Facade로 계층 구조 개선하기 (2) | 2024.01.15 |
---|---|
[Briefing] API 응답 캐싱을 통한 조회 속도 개선 (0) | 2024.01.06 |
[Briefing] Spotless로 코드 포맷 유지하기 (0) | 2023.12.27 |