본문 바로가기

웹 프로그래밍/스프링

쿠버네티스 로드밸런싱 실험(nGrinder)

쿠버네티스의 성능

쿠버네티스를 이용해서 트래픽 변화에 따른 서버의 자동 증설 및 축소가 가능해졌는데 과연 이렇게 늘어난 서버가 정말로 제 역할을 해낼지 궁금해졌다. 만약 서버 증설을 성공하더라도 로드밸런서가 잘 작동되지 않는다고 가정하면(병목현상 등) n개의 서버를 증설한다고해도 성능은 n배가 되지 않을것이기 때문이다. 그래서 nGrinder라는 테스트 툴을 이용해서 서버가 증설될때 과연 요청 처리 속도가 얼마나 빨라지고 시간당 트랜잭션이 얼만큼 증가하는지 실험해보고자 한다.

 

nGrinder

nGrinder는 내 로컬에서 도커로 쉽게 설치가 가능하고 실험하고싶은 API URL과 실험하고자하는 시나리오(script)를 자바와 유사한 Groovy로 작성하면 가상 사용자수가 몇명일때 처리되는 요청을 분석할수 있다. 

이때 가상 사용자를 설정하기위해 Agent도 도커로 설치해야하는데 1개의 Agent당 최대 가상 사용자가 3000명이므로 실험하기위한 사용자가 더 필요하다면 여러개를 설치하는 방법을 생각해볼수 있다.(이번 실험에서는 한개의 Agent으로도 충분했다.)

 

1) nGrinder 설치

$ docker pull ngrinder/controller
$ docker run -d -v ~/ngrinder-controller:/opt/ngrinder-controller --name controller \
-p 80:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller

 

2) Agent 설치

docker pull ngrinder/agent
docker run -d --name agent --link controller:controller ngrinder/agent

 

테스트 시나리오는 가상사용자 3천명이 로그인 로직을 반복하는 상황을 가정해서 작성했고 Groovy 코드는 아래와 같다.

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 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

/**
 * HTTP 플러그인을 사용하는 간단한 예제로 HTTP를 통해 단일 페이지를 검색하는 방법을 보여줍니다.
 *
 * 이 스크립트는 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 = [:]
public static List<Cookie> cookies = []

@BeforeProcess
public static void beforeProcess() {
HTTPRequestControl.setConnectionTimeout(300000)
test = new GTest(1, "kube")
request = new HTTPRequest()
grinder.logger.info("프로세스 시작 전.")
}

@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true
grinder.logger.info("스레드 시작 전.")
}

@Before
public void before() {
request.setHeaders(headers)
CookieManager.addCookies(cookies)
grinder.logger.info("초기 헤더 및 쿠키 설정.")
}

@Test
public void loginTest() {
Map<String, Object> loginParams = [
    'id': 'user1',
    'password': '123'
]
HTTPResponse loginResponse = request.POST("http://223.130.154.217/user/login", loginParams)

assertThat(loginResponse.statusCode, is(200))
grinder.logger.info("로그인 성공: 상태 코드 ${loginResponse.statusCode}")

// 로그인 성공 후 추가 테스트를 진행할 수 있습니다.
}

@Test
public void test() {
HTTPResponse response = request.GET("http://223.130.154.217/", params)

if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("경고. 응답이 정확하지 않을 수 있습니다. 응답 코드는 ${response.statusCode}입니다.")
} else {
assertThat(response.statusCode, is(200))
}
}
}

 

실험결과는 아래와 같다.


 

실험결과를 더 자세히 보기위해 아래와 같이 그래프로 나타내었다.

그래프에서 x축은 쿠버네티스로 증설한 로드밸런싱이 되는 서버의 수이고

y축 좌측은 시간당 트랜잭션수(TPS), 우측은 성공한 총 테스트수(Successful Tests) 그리고 노란색 선은 평균 테스트 시간을 의미한다.(Mean Test Time)

그래프에서 알수 있듯이 서버의 개수가 1개 일때 보다 쿠버네티스로 4개로 증설했을때가 TPS는 약 4.17배 MSS는 약 4.92배 증가한것을 확인할수 있었다.

 

어떤 URL에 요청이 들어올때 로드밸런싱을 과정을 거치기 때문에 그에 따른 Overhead가 있을거라고 우려했던것과는 달리 로드밸런싱 성능은 우수해서 실제로 서버의 개수가 증가할때마다 성능지표가 비례해서 증가하는것을 확인하였다. 이런 실험들을 통해 실무에서 기획자가 요구하는 수준을 맞추기위해 얼만큼의 서버가 필요한지 미리 가늠해보는 것도 가능하다고 생각이 들었다. 처음에는 단순히 쿠버네티스의 로드밸런싱이 정말 잘 동작할까라는 의문에서 실험을 시작했지만 만약 기획을 할때 요구사항에서 최대 동시접속자가 몇명이고 최대한으로 걸리는 시간이 정해진다면 그에 맞는 쿠버네티스의 서버의 수를 계산할수있고 그 결과또한 예측할수 있다는것을 이 실험을 통해 알수있었다. 이렇게 결과에 대해서 의문이 드는 상황에서 단순히 머리속으로 몇배가 될것이다라고 뇌피셜로 생각하는것 보다 여러번의 실험을 통해 그 결과를 그래프로 만들어두는 습관을 가지는게 개발자의 덕목이 아닐까라고 생각이 들었다.