ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 선착순 쿠폰 발급 시스템 성능테스트
    프로젝트/선착순 쿠폰 발급 시스템 2023. 6. 18. 00:01
    728x90

    개요

    모든 구현이 끝났고 성능테스트만 남게 되었습니다.

    성능테스트는 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명)만 처리)

    댓글

Designed by Tistory.