성능 개선하기

sieun·2022년 4월 6일
2
post-thumbnail

개요

이전 글에서는 nGrinder와 pinpoint를 사용하여 병목 원인을 분석하였고, 성능을 최적화할 수 있는 방안을 생각해보았습니다. 이제 실제로 대용량 서버 프로젝트의 성능을 최적화하고 확인하는 과정을 기록해보려고 합니다.


상품 목록 보기 API 성능 개선

상품 목록 보기 API는 Disk I/O를 줄이기 위한 방법으로 Redis 캐시를 생각하였습니다.

캐시를 적용하여 위와 같이 기존에 Disk I/O에만 의존하던 Data Access가 Redis에도 분산된 것을 확인할 수 있습니다.


테스트 스크립트

@RunWith(GrinderRunner)
class TestRunner {

	public static GTest test1
	public static GTest test2
	public static GTest test3
	
	public static HTTPRequest request
	public static NVPair[] headers = []
	public static NVPair[] params = []
	public static Cookie[] cookies = []
	
	public static MAX_RECORDS = 99800

	@BeforeProcess
	public static void beforeProcess() {
		HTTPPluginControl.getConnectionDefaults().timeout = 6000
		test1 = new GTest(1, "27.96.135.51")
		test2 = new GTest(2, "27.96.135.51")
		test3 = new GTest(3, "27.96.135.51")
		
		request = new HTTPRequest()
		grinder.logger.info("before process.");
	}

	@BeforeThread 
	public void beforeThread() {
		test1.record(this, "test1")
		test2.record(this, "test2")
		test3.record(this, "test3")
		
		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. init headers and cookies");
	}

	@Test
	public void test1(){
		String origin = "http://27.96.135.51:8080/products"
		String deliveryType = "ROCKET"
		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
		HTTPResponse result = request.GET(origin + 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));
		}
	}
	
	@Test
	public void test2(){
		String origin = "http://27.96.135.51:8080/products"
		String deliveryType = "ROCKET_FRESH"
		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
		HTTPResponse result = request.GET(origin + 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));
		}
	}
	
	@Test
	public void test3(){
		String origin = "http://27.96.135.51:8080/products"
		String deliveryType = "ROCKET_GLOBAL"
		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
		HTTPResponse result = request.GET(origin + 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));
		}
	}
}

성능 개선 후 부하테스트 결과

  • nGrinder report
  • 지표
    • TPS : 1,124
    • Peak TPS : 1,547
    • CPU Usage : 100%
    • Errors : 0

성능개선 전/후 비교하기

  • TPS 개선 [전]

    평균 TPS : 662

    피크 TPS : 921

✔️ 평균 TPS가 662에서 그쳤던 이전 설계에서 Redis 캐시를 적용하여 평균 TPS를 1,124까지 약 70% 가까이 성능을 개선했습니다.
  • Error 개선 [전]

    2,674

✔️ Error 발생이 2,674 건이 발생했던 이전 설계에서 Redis 캐시를 적용하여 Error 발생을 0 건으로 완전히 개선할 수 있었습니다.




쿠폰 발급하기 API 성능 개선

쿠폰 발급하기 API의 성능을 개선하기 위해, nginx를 활용하여 scale-out으로 서버를 구성하였습니다. nginx가 로드밸런서 역할을 하도록 만들어 트래픽을 하나의 서버가 아닌 2개의 서버로 분산시키기 위함입니다.

테스트 스크립트

@RunWith(GrinderRunner)
class TestRunner {
	public static GTest test
	public static HTTPRequest request
	public Object cookies = []
	
	def nvs(def map) {
		def nvs = []
		map.each {
			key, value ->  nvs.add(new NVPair(key, value))
		}
		return nvs as NVPair[]
	}

	@BeforeProcess
	public static void beforeProcess() {
		HTTPPluginControl.getConnectionDefaults().timeout = 6000
		test = new GTest(1, "test : [POST] /available-coupons/1") 
		request = new HTTPRequest()
		test.record(request); 
		grinder.logger.info("before process.");
	}

	@BeforeThread 
	public void beforeThread() {
		// reset to the all cookies
        def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
        cookies = CookieModule.listAllCookies(threadContext)
        cookies.each {
            CookieModule.removeCookie(it, threadContext)
        }
        
		int randomNum = Math.abs(new Random().nextInt() % 50000) + 1 
		String email = Integer.toString(randomNum) + "@naver.com" 
		HTTPResponse result = request.POST("http://[nginxIP]:8080/users/login", nvs(["email":email, "password":"1234"])) 
		
		cookies = CookieModule.listAllCookies(threadContext)
		grinder.statistics.delayReports=true;
		grinder.logger.info("before thread.");
	}
	
	@Before
    public void before() {
        def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
        cookies.each {
            CookieModule.addCookie(it ,threadContext)
            net.grinder.script.Grinder.grinder.logger.info("{}", it)
        }
    }
	
	@Test
	public void couponTest() {
		int randomNum = Math.abs(new Random().nextInt() % 20000) + 1
		String couponId = Integer.toString(randomNum)
		request.POST("http://[nginxIP]:8080/available-coupons/" + couponId)
	}
}



성능 개선 후 부하테스트 결과

  • nGrinder report
✔️ 여기서 Errors가 발생한 이유를 log를 통해 살펴보니, 다음과 같은 경우였습니다. - 존재하지 않는 쿠폰을 발급받으려는 상황 - 한 사람이 똑같은 쿠폰을 여러 번 발급하려는 상황 결국 성능이 떨어진 것이 아닌, 올바른 서비스 로직을 통한 `BAD_REQUEST` 임을 확인하였습니다.



성능개선 전/후 비교하기

  • TPS 그래프

    ✔️ TPS가 대략 400에서 538까지인 성능 개선 전과 달리 성능 개선 후엔 TPS가 대략 450에서 667까지 도달하였습니다.
  • CPU 사용량

    ✔️ 스케일 아웃으로 서버를 확장한 결과, CPU사용률이 절반으로 줄어든 것을 확인할 수 있습니다.



후기

캐싱을 적용하거나 스케일아웃으로 서버를 확장하였다고 해도 실제 환경에서 정말 성능이 개선된 것인지 확인하기 어렵습니다. 하지만 이렇게 nGrinder, pinpoint툴을 사용하여 어떤 부분에서 성능이 얼마나 개선되었는지 눈으로 볼 수 있었습니다.

profile
열심히 공부중입니다😇

0개의 댓글