본문 바로가기
Java/트러블 슈팅

nGrinder를 사용한 성능 테스트: 비관적 락 vs 분산 락

by oneny 2024. 1. 21.

nGrinder를 사용한 성능 테스트: 비관적 락 vs 분산 락

분산 환경에서는 비관적 락과 분산 락의 성능이 비교해보려고 한다.

 

비관적 락과 분산 락 비즈니스 로직

성능 테스트를 위해 두 비즈니스 로직을 똑같게 만들기 위해서 상품을 조회한 후 재고를 차감할 후 있는지 확인한 후 가능하다면 재고를 차감시킬 수 있도록 구현했다.

 

비관적 락과 분산 락 로직 차이

<select id="findById" resultType="Product">
    SELECT *
    FROM product
    WHERE id = #{id}
</select>

<select id="findByIdForUpdate" resultType="Product">
    SELECT *
    FROM product
    WHERE id = #{id}
        FOR UPDATE
</select>

가장 먼저, 상품을 조회했을 때 비관적 락은 SELECT ... FOR UPDATE를 통해서 조회한 레코드에 대해 배타락을 획득하는 반면, 분산 락은 애플리케이션 단에서 락을 획득하고 반납하기 때문에 FOR UPDATE없이 조회한다.

 

그리고 락을 획득하고 트랜잭션 진입하는지와 트랜잭션 진입하고 락을 획득하는지에 대한 차이가 있는데 위에서 보는 것처럼 분산 락의 경우에는 트랜잭션 진입하기 전에 분산 락을 획득한 스레드에서만 트랜잭션에 진입한 후 비즈니스 로직을 수행할 수 있다. 비관적 락의 경우에는 트랜잭션 진입한 후 비즈니스 로직을 수행하면서 비관적 락을 획득하게 된다. 분산 락을 사용한 내용에 대해서는 아래 블로깅을 참고하면 좋다.

 

분산 락 사용 시 상위 트랜잭션이 있으면 안되는 이유

분산 락 사용 시 상위 트랜잭션이 있으면 안되는 이유 주문 기능 중 재고 차감을 데이터 정합성을 위해 비관적 락으로 먼저 구현하고, 다음으로 Redisson을 사용하여 분산 락으로 구현하려고 했지

oneny.tistory.com

 

nGrinder 사용

이제 비관적 락과 분산 락을 사용했을 때 성능이 어떻게 보이는지를 위해 nGrinder를 사용하여 테스트할 예정이다. nGrinder를 사용하면 부하 테스트 또는 스트레스 테스트를 통해 일부러 시스템에 부하를 발생시켜 얼마만큼의 부하를 버틸 수 있는지 평가할 수 있다. 그리고 스크립트를 작성해서 테스트 시나리오를 만들 수 있다. nGrinder를 구성하는 중요한 컴포넌트는 아래 두 가지가 있다.

  • Controller: 웹 기반의 GUI 시스템을 제공하여 에이전트를 관리하고, 부하 테스트를 실시하여 모니터링할 수 있다.
  • Agent: 부하를 발생시키는 대상으로 Controller의 지휘를 받는다.

 

nGrinder Script

import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.plugin.http.HTTPRequest
import net.grinder.plugin.http.HTTPPluginControl
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 java.util.Date
import java.util.List
import java.util.ArrayList

import HTTPClient.Cookie
import HTTPClient.CookieModule
import HTTPClient.HTTPResponse
import HTTPClient.NVPair

/**
 * 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 NVPair[] headers = []
	public static NVPair[] params = []
	public static Cookie[] cookies = []
	public static String body = "{\"productId\":1,\n \"quantity\":1}"

	@BeforeProcess
	public static void beforeProcess() {
		HTTPPluginControl.getConnectionDefaults().timeout = 6000
		test = new GTest(1, "49.50.161.66")
		request = new HTTPRequest()
		
		// Set header datas
		List<NVPair> headerList = new ArrayList<>()
		headerList.add(new NVPair("Content-Type", "application/json"))
		headers = headerList.toArray()
		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)
		cookies.each { CookieModule.addCookie(it, HTTPPluginControl.getThreadHTTPClientContext()) }
		grinder.logger.info("before. init headers and cookies");
	}

	@Test
	public void test(){
		
		HTTPResponse result = request.POST("http://49.50.161.66:80/products/pessimisticLock", body.getBytes())

		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));
		}
	}
}

@BeforeProcess에서 POST 요청 시 Content-Type 헤더와 RequestBody를 설정한 후 @Test에서 테스트를 진행한다. /products/pessimisticLock은 비관적 락에 대한 요청이고, /products/distributedLock으로 요청하는 경우에는 분산 락을 사용한다.

 

nGrinder Performance Test

위 사진을 보는 것처럼 Performance Test에서 설정해야 할 항목들은 아래와 같다.

  • Agent: Controller에 연결 승인된 수만큼까지만 지정 가능
  • Vuser per agent: agent당 가상 유저수로 process와 thread로 구성되어 있고 수정 가능
  • Script: 위에서 작성한 테스트 시 수행할 Script 설정하는 부분
  • Target Host: 부하를 일으킬 호스트
  • Duration: 테스트를 진행할 시간
  • Run Count: 테스트를 실행할 횟수로 한 마디로 각 가상 유저가 몇 번을 부하 일으킬지 설정하는 부분
  • Enable Ramp-Up: 트래픽을 서서히 증가하게 하는 설정

 

성능 테스트 진행

분산 락을 사용한 경우에는 TPS가 104.0가 나온 것을 확인할 수 있다.

 

 

비관적 락을 사용한 경우에는 205.5가 나온 것을 확인할 수 있다. 이전 게시글에서는 분산 락이 In-memory DB를 사용하고, Redisson이 Non-blocking I/O로 동작하기 때문에 막연히 더 빠르겠지라고 생각했었다. 또한 Database에서 해당 row에 대한 update 동작하는 transaction의 경합 수가 적어지니 요청-응답 사이클도 빨라질 것이라 생각했지만 위 성능 테스트를 통해 다르다는 것을 알 수 있다.

 

고민할 여지

먼저 분산 락에서 무엇이 병목 지점을 만들었는지를 생각해볼 필요가 있을 것 같다. 분산 락은 분산 환경에서 동기화된 처리를 위해 사용된다. Scale Out을 사용하면서 수평적으로 확장되어 트래픽을 분산시킴으로써 성능이 올라갔지만 분산 락을 사용함으로써 결국 한 순간에 처리될 수 있는 요청을 하나밖에 없으므로 하나의 요청에서 DB를 통해 재고를 차감시키는 시간 자체는 향상되었지만 전체적으로 처리되는 양은 줄어들은 것 같다. 더 정확한 지표를 확인하기 위해서 APM을 사용하여 살펴볼 필요가 있을 것 같다.