-
선착순 쿠폰 발급 시스템 성능테스트프로젝트/선착순 쿠폰 발급 시스템 2023. 6. 18. 00:01728x90
개요
모든 구현이 끝났고 성능테스트만 남게 되었습니다.
성능테스트는 nGrinder로 진행할 예정입니다.
nGrinder 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 automatically generated by ngrinder. * * @author admin */ @RunWith(GrinderRunner) class TestRunner { public static GTest test public static HTTPRequest request public static Map<String, String> headers = [:] public static Map<String, Object> params = ["memberId": "string", "eventName": "event1"] public static List<Cookie> cookies = [] @BeforeProcess public static void beforeProcess() { HTTPRequestControl.setConnectionTimeout(300000) test = new GTest(1, "127.0.0.1") request = new HTTPRequest() // Set header data headers.put("Content-Type", "application/json") 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) CookieManager.addCookies(cookies) grinder.logger.info("before. init headers and cookies") } @Test public void test() { HTTPResponse response = request.POST("http://127.0.0.1:8080/coupons/event", params) if (response.statusCode >= 500) { grinder.logger.error("Error. The server encountered an error. The response code was {}.", response.statusCode) } else if (response.statusCode == 301 || response.statusCode == 302) { grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode) } else if (response.statusCode == 201 || response.statusCode == 404) { // Test passed } else { grinder.logger.error("Error. The response code {} is not expected.", response.statusCode) // Handle other unexpected status codes } } }
201과 404 응답의 경우에는 문제가 없으니 아무것도 작성하기 않고 500 이상이나 이외의 응답이 내려오는 경우 error로 측정합니다.
1000명의 유저
10개의 프로세스와 100개의 스레드입니다.
쿠폰은 10000개를 발급하여 놓았고, 이후의 요청에는 404가 떨어지며 10000개 요청 이후로는 DB 네트워크 지연시간은 없습니다.
평균 TPS는 4000, 평균 응답시간은 180ms입니다.
로그도 다운로드하여 볼 수 있습니다.
에러는 다음과 같은 Exception이 발생합니다.
2023-05-28 12:36:08,200 ERROR java.util.concurrent.ExecutionException: java.io.IOException: Connection reset by peer java.io.IOException: Connection reset by peer at org.apache.hc.core5.reactor.IOSessionImpl.read(IOSessionImpl.java:193) at org.apache.hc.core5.reactor.InternalDataChannel.read(InternalDataChannel.java:344) at org.apache.hc.core5.http.impl.nio.SessionInputBufferImpl.fill(SessionInputBufferImpl.java:143) at org.apache.hc.core5.http.impl.nio.AbstractHttp1StreamDuplexer.onInput(AbstractHttp1StreamDuplexer.java:265) at org.apache.hc.core5.http.impl.nio.AbstractHttp1IOEventHandler.inputReady(AbstractHttp1IOEventHandler.java:64) at org.apache.hc.core5.http.impl.nio.ClientHttp1IOEventHandler.inputReady(ClientHttp1IOEventHandler.java:39) at org.apache.hc.core5.reactor.InternalDataChannel.onIOEvent(InternalDataChannel.java:124) at org.apache.hc.core5.reactor.InternalChannel.handleIOEvent(InternalChannel.java:51) at org.apache.hc.core5.reactor.SingleCoreIOReactor.processEvents(SingleCoreIOReactor.java:179) at org.apache.hc.core5.reactor.SingleCoreIOReactor.doExecute(SingleCoreIOReactor.java:128) at org.apache.hc.core5.reactor.AbstractSingleCoreIOReactor.execute(AbstractSingleCoreIOReactor.java:85) at org.apache.hc.core5.reactor.IOReactorWorker.run(IOReactorWorker.java:44)
Connect rest by peer 예외의 경우에는 다음과 같은 경우에 발생합니다.
- 원격 서버에서 Connection을 reset 처리하거나
- 종료된 커넥션을 재사용하려고 할 때
- 클라이언트(브라우저)에서 정지버튼을 누르거나, 브라우저를 종료하거나, 다른 화면으로 이동하는 등의 이유로 서버 측에서 작업 결과를 전달할 곳이 없어졌을 때
- Connection에서 Timeout 발생
- 메모리부족
- 소켓 고갈 등등…
3000명의 유저
10개의 프로세스와와 300개의 스레드
평균 응답시간이 현저하게 떨어졌습니다.
결론
3000명의 유저기준으로 테스트하였을 때도 평균 응답시간이 3.4초대로 떨어지게 됩니다.
한정된 자원을 효율적으로 사용하기 위해선 여기에 대기열 시스템이 추가되면 좋을 것 같습니다.
또한 여기엔 Redis로 적재된 event쿠폰을 pop 하고, DB에 회원 id와 coupon 메타쿠폰를 적재합니다.
쿠폰을 pop하고, DB에 회원 id와 coupon을 적재하는 것이 병목이 될 수 있습니다.
예를 들어 지금은 100개의 coupon을 저장하지만 짧은 시간이 10000개의 쿠폰을 DB에 적재해야 한다면 이는 부하로 이어질 수 있습니다.
이를 방지하기 위해서는 10000개의 coupon을 이벤트가 끝나고 batch 등을 활용하여 적재할 수 있습니다.
앞으로의 개선방안
1. 쿠폰 발급요청서버와, 발행서버를 분리하기
2. 처리할 수 있는 만큼만 받자 (대기열을 만들고, 주기적인 폴링(예를 들어 1초)을 통해 특정인원(예를 들어 50명)만 처리)
'프로젝트 > 선착순 쿠폰 발급 시스템' 카테고리의 다른 글
Redis 주의사항 (0) 2023.07.01 Redis는 싱글스레드인가? (2) 2023.06.19 RedisTemplate으로 Set 자료구조 사용하기 (0) 2023.06.09 Spring Event 사용하기 (0) 2023.05.30 Kotlin JPA Update Query 작성하기 (0) 2023.05.28