이전 글에서는 nGrinder와 pinpoint를 사용하여 병목 원인을 분석하였고, 성능을 최적화할 수 있는 방안을 생각해보았습니다. 이제 실제로 대용량 서버 프로젝트의 성능을 최적화하고 확인하는 과정을 기록해보려고 합니다.
상품 목록 보기 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));
}
}
}
평균 TPS : 662
피크 TPS : 921
2,674
쿠폰 발급하기 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)
}
}
TPS 그래프
✔️ TPS가 대략 400에서 538까지인 성능 개선 전과 달리 성능 개선 후엔 TPS가 대략 450에서 667까지 도달하였습니다.
CPU 사용량
✔️ 스케일 아웃으로 서버를 확장한 결과, CPU사용률이 절반으로 줄어든 것을 확인할 수 있습니다.
캐싱을 적용하거나 스케일아웃으로 서버를 확장하였다고 해도 실제 환경에서 정말 성능이 개선된 것인지 확인하기 어렵습니다. 하지만 이렇게 nGrinder, pinpoint툴을 사용하여 어떤 부분에서 성능이 얼마나 개선되었는지 눈으로 볼 수 있었습니다.