[Project : Gold-Rush-Lab] 3. 분산 시스템에서의 Lock
핫스팟 환경에서 API 서버를 증설하면 TPS가 높아질까?
개요
이전 실험에서는 hot spot 상황의 모놀리식 시스템에서 Lock이 끼치는 영향과 그 내부에서 일어나는 일과 DB Connection Pool Size에 대해서 알아보았다.
v0.1 버전에서의 병목 변수가 되었던 DB Connection Pool Size가 v0.2 버전의 Lock을 통해서 병목이 해소될 것이라는 예상과는 다르게 여전히 병목이 될 수 있음을 확인했고, 낙관적 락을 적용했을 경우 빠른 TPS, 응답 시간을 얻지만 높은 오류율이라는 트레이드 오프가 있음을 확인했다. 또한 비관적 락을 적용한 경우는 100%의 응답률을 보이나 TPS가 가장 낮다는 트레이드 오프가 존재했다.
Gold Rush는 동시 접속자 중 가장 빠르게 100개의 금을 채굴한 사람이 승리한다. 따라서 빠른 응답 속도와 더불어 사용자의 요청에 동시성 문제로 인한 오류를 반환해서는 안 된다.
위 요구사항에 맞춘 최적의 다음 시나리오를 구성하기 위해 다음을 적용하여 실험하고, 그 성능을 비교하고자 한다.
- API 1대 증설
- API 1대 증설에 따른 LoadBalancer 추가
- 재시도 로직(20회)이 포함된 낙관적 락을 적용한 v0.3-optimistic_lock 적용 및 실험
- 기존 비관적 락을 적용한 v0.3-pessimistic_lock 적용 및 실험
가설
가설 1. 재시도 로직이 포함된 낙관적 락의 오류율은 1% 이상일 것이다.
- 낙관적 락은 동일한 Row에 대한 충돌을 예외로 감지하고 재시도를 수행한다. 그러나 재시도 횟수(20회)를 초과하거나 요청 시간이 제한되는 경우 일부 요청은 최종적으로 실패할 가능성이 있다.
가설 2. 재시도 로직이 포함된 낙관적 락은 비관적 락보다 평균 응답시간과 P95 응답시간이 증가할 것이다.
- 충돌이 발생한 요청은 여러 번의 재시도를 수행해야 하므로 단일 요청이 완료되기까지의 시간이 증가할 것으로 예상된다.
가설 3. API를 증설하더라도 Hot Spot에서는 DB가 여전히 병목이 될 것이다.
- Load Balancer는 HTTP 요청을 여러 API 인스턴스로 분산하지만, 모든 요청은 동일한 Mine Row를 대상으로 한다. 따라서 병목은 애플리케이션이 아니라 동일 Row에 대한 DB 접근에서 계속 발생할 것으로 예상된다.
가설 4. API를 Scale-out 하면 단일 API 인스턴스의 Process CPU Peak 평균값은 감소할 것이다.
- 요청이 여러 API 인스턴스로 분산되므로 각 인스턴스가 처리해야 하는 요청 수가 감소하여 CPU 부하가 분산될 것으로 예상된다.
실험 방법
- k6는 App VM(192.168.0.41)로 직접 요청하지 않고 Load Balancer VM(192.168.0.47)에 요청한다.
- 각 실험은 v0.3/optimistic-lock, v0.3/pessimistic-lock 브랜치에 구현 후 수행한다.
- v0.3/optimistic-lock 은 재시도 20회 로직을 추가한다.
- DB Connection Pool Size의 영향은 v0.1과 v0.2에서 이미 확인하였다. 이번 실험부터는 기본값(10)을 유지하여 실험한다.
- VU(가상 유저)는 10, 50, 100, 300, 500으로 실험한다.
- 각 실험마다 자동화 스크립트를 통해 DB 초기화 → 부하 → 기록 → 30초 대기(실험 구간 분리)를 5회 반복한다.
실험 환경
- Host Machine
- OS: Windows 11
- CPU: AMD Ryzen 5 5600X
- Memory: 32 GB
- Virtualization
- Hyper-V
- Network
- External Virtual Switch, 모든 VM은 동일한 Hyper-V Virtual Switch를 사용
- APP VM-01, APP VM-02
- OS: Ubuntu Server 24.04
- vCPU: 2
- Memory: 2 GB
- DB VM
- OS: Ubuntu Server 24.04
- vCPU: 2
- Memory: 2 GB
- LB(LoadBalancer) VM
- OS: Ubuntu Server 24.04
- vCPU: 2
- Memory: 2 GB
- Monitoring VM
- OS: Ubuntu Server 24.04
- vCPU: 2
- Memory: 2 GB
- Load Generator
- OS : Mac OS Tahoe(26)
- CPU: M1
- Memory: 8GB
결과
Optimistic Lock
재시도 로직
재시도 로직은 다음과 같이 구현했다. @Retryable을 통해 최대 20회의 재시도, 최초 1회 총 21회의 시도를 수행하며 5ms의 딜레이에 50%의 가중치가 추가되며, 이 증가된 수치는 0.1초, 100ms를 넘어가지 않도록 구성하였다.
또한 재시도 횟수에 대한 지표를 얻기 위해 OptimisticRetry 지표를 Prometheus에 추가하였다.
지표 수집은 최초 1회 시도를 ‘재시도 지표’로서 추가하지 않기 위해 @EventListener를 적용한 별도의 함수로써 구성하였다.
@Service
@RequiredArgsConstructor
public class MiningRetryService {
private final MineService mineService;
private final MiningMetrics miningMetrics;
private final MiningFailureClassifier miningFailureClassifier;
@Retryable(
includes = OptimisticLockingFailureException.class,
maxRetries = 20L,
delay = 5,
multiplier = 1.5,
maxDelay = 100
)
public void mine(UUID sessionId, Long amount) {
mineService.mine(sessionId, amount);
}
@EventListener
public void recordRetry(MethodRetryEvent event) {
if (event.getMethod().getDeclaringClass() != MiningRetryService.class) {
return;
}
if (event.isRetryAborted()) {
MiningFailureType failureType = miningFailureClassifier.classify(event.getFailure());
if (failureType == MiningFailureType.OPTIMISTIC_LOCK) {
miningMetrics.recordFailure(LockStrategy.OPTIMISTIC, failureType);
}
return;
}
miningMetrics.incrementOptimisticRetry();
}
}
이후 컨트롤러에서는 재시도 기능을 포함한 채굴 함수를 호출하기 위해 MineService.mine()이 아닌 MiningRetryService.mine()을 호출한다.
@PostMapping("/mine")
public ApiResponse<MineRequestDto> mine(
@RequestParam(name = "sessionId") UUID sessionId
) {
miningRetryService.mine(sessionId, 1L);
UserEntity foundUser = userService.findBySessionId(sessionId);
Long totalMinedGold = foundUser.getTotalMinedGold();
Long remainingAmount = foundUser.getMine().getRemainingAmount();
return ApiResponse.success(new MineRequestDto(1L, totalMinedGold, remainingAmount));
}
MineService.mine()에도 변경 사항이 추가되었다.
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW) // 추가된 변경 사항 - 의도 명시적 선언
public void mine(UUID sessionId, Long amount) {
LockStrategy strategy = LockStrategy.OPTIMISTIC;
try {
UserEntity foundUser = userRepository.findBySessionId(sessionId);
MineEntity foundMine = foundUser.getMine();
foundMine.mine(amount);
foundUser.addGold(amount);
miningLogRepository.save(
MiningLogEntity.create(foundUser, foundMine, amount)
);
mineRepository.flush(); // 추가된 변경 사항
recordSuccessAfterCommit(strategy);
} catch (OptimisticLockingFailureException exception) { // 추가된 변경 사항
throw exception;
}
catch (RuntimeException exception) {
MiningFailureType failureType = miningFailureClassifier.classify(exception);
miningMetrics.recordFailure(strategy, failureType);
throw exception;
}
}
먼저 MiningRetryService.mine(...)에 @Transactional이 적용되어도 MineService.mine(...)이 지장받지 않기 위해 명시적으로 매 실행마다 새로운 트랜잭션이 실행되게 propagation 옵션을 추가했다. 이는 현재 MiningRetryService.mine(...)에 @Transactional이 적용되어 있지 않기 때문에, Propagation.REQUIRES_NEW 옵션이 존재하지 않아도 의도한 retry 로직은 수행될 것이다.
mineRepository.flush()는 OptimisticLock 예외를 메서드 내부에서 발생시켜 Retry가 즉시 수행될 수 있도록 flush()를 호출했다.
마지막으로 기존 OptimisticLockingFailureException은 GlobalExceptionHandler 클래스가 HttpStatus 409를 반환하지 않게 하기 위해 미리 try-catch 구문을 통해 별도의 실패 처리 로직을 수행하지 않고 그대로 전파했다.
결과 기록 표 (스압)
| Run | Target Type | Record Type | Version | Hikari Max Pool Size | VU | TPS (req/s) | System CPU Peak (%) | Process CPU Peak (%) | JVM Heap Peak (%) | Hikari Active Peak | Avg Latency (ms) | P95 (ms) | P99 (ms) | Error Rate (%) | 초기 잔량 | 사용자 총 채굴량 | Mining Log 총 채굴량 | 실제 잔량 | 정합성 | Optimistic Retry Count |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 10 | 230.044 req/s | 2 | 36.814 | 129.501 | 471.102 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | 1470 | |||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 115.137 req/s | 52.619 | 42.308 | 11.051 | ||||||||||||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 103.750 req/s | 70.854 | 59.135 | 11.621 | ||||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 10 | 237.023 req/s | 6 | 36.17 | 130.316 | 459.69 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | 1398 | |||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 84.275 req/s | 65.821 | 51.691 | 9.267 | ||||||||||||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 102.973 req/s | 56.092 | 43.478 | 12.756 | ||||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 10 | 275.786 req/s | 5 | 30.174 | 92.004 | 352.884 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | 1371 | |||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 110.314 req/s | 24.645 | 16.019 | 10.736 | ||||||||||||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 136.054 req/s | 40.313 | 25.118 | 10.521 | ||||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 10 | 282.566 req/s | 8 | 29.642 | 112.122 | 447.938 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | 1317 | |||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 151.958 req/s | 43.382 | 24.171 | 10.553 | ||||||||||||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 132.806 req/s | 56.013 | 38.756 | 11.069 | ||||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 10 | 297.442 req/s | 1 | 27.451 | 111.789 | 446.227 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | 1259 | |||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 165.246 req/s | 0.5 | 0.5 | 9.203 | ||||||||||||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 10 | 145.747 req/s | 0.495 | 0.5 | 11.333 | ||||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_optimistic-lock | 20 | 10 | 264.572 req/s | 4.4 | 32.05 | 115.146 | 435.568 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | 1363 | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 10 | 125.386 req/s | 37.393 | 26.938 | 10.162 | ||||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 10 | 124.266 req/s | 44.753 | 33.397 | 11.46 | ||||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 50 | 328.558 req/s | 20 | 139.528 | 370.585 | 933.476 | 0.18 | 5000 | 4991 | 4991 | 9 | ✅ | 10613 | |||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 170.245 req/s | 57.427 | 38.942 | 12.254 | ||||||||||||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 164.802 req/s | 59.419 | 38.647 | 12.955 | ||||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 50 | 351.395 req/s | 20 | 133.859 | 342.778 | 940.61 | 0.08 | 5000 | 4996 | 4996 | 4 | ✅ | 10579 | |||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 184.482 req/s | 46.357 | 23.301 | 12.297 | ||||||||||||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 179.612 req/s | 50.701 | 25 | 12.591 | ||||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 50 | 351.939 req/s | 16 | 132.165 | 339.765 | 850.78 | 0.14 | 5000 | 4993 | 4993 | 7 | ✅ | 10440 | |||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 181.442 req/s | 47.311 | 22.816 | 13.519 | ||||||||||||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 184.768 req/s | 46.335 | 22.927 | 12.899 | ||||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 50 | 354.988 req/s | 20 | 130.058 | 339.958 | 833.03 | 0.16 | 5000 | 4992 | 4992 | 8 | ✅ | 10513 | |||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 186.368 req/s | 49.029 | 23.786 | 12.699 | ||||||||||||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 182.268 req/s | 50.471 | 24.038 | 13.601 | ||||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 50 | 352.535 req/s | 20 | 129.975 | 333.493 | 880.661 | 0.16 | 5000 | 4992 | 4992 | 8 | ✅ | 10622 | |||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 185.081 req/s | 49.511 | 23.077 | 13.356 | ||||||||||||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 50 | 182.490 req/s | 47.83 | 21.698 | 14.236 | ||||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_optimistic-lock | 20 | 50 | 347.883 req/s | 19.2 | 133.117 | 345.316 | 887.712 | 0.144 | 5000 | 4992.8 | 4992.8 | 7.2 | ✅ | 10553.4 | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 50 | 181.524 req/s | 49.927 | 26.384 | 12.825 | ||||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 50 | 178.788 req/s | 50.951 | 26.462 | 13.256 | ||||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 100 | 362.858 req/s | 20 | 260.698 | 528.978 | 1073.041 | 0.1 | 10000 | 9990 | 9990 | 10 | ✅ | 21256 | |||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 178.734 req/s | 49.699 | 24.51 | 15.692 | ||||||||||||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 178.024 req/s | 51.433 | 24.88 | 17.003 | ||||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 100 | 357.270 req/s | 20 | 264.17 | 556.03 | 1082.051 | 0.15 | 10000 | 9985 | 9985 | 15 | ✅ | 20850 | |||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 183.154 req/s | 50.952 | 24.762 | 17.422 | ||||||||||||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 180.947 req/s | 48.772 | 24.272 | 16.895 | ||||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 100 | 361.337 req/s | 20 | 266.105 | 513.931 | 1057.116 | 0.14 | 10000 | 9986 | 9986 | 14 | ✅ | 21545 | |||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 185.982 req/s | 48.31 | 24.155 | 18.122 | ||||||||||||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 185.462 req/s | 51.209 | 24.286 | 18.192 | ||||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 100 | 360.192 req/s | 20 | 263.69 | 543.582 | 1070.941 | 0.17 | 10000 | 9983 | 9983 | 17 | ✅ | 21272 | |||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 179.616 req/s | 51.2 | 23.923 | 18.846 | ||||||||||||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 182.918 req/s | 52.848 | 24.645 | 18.519 | ||||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 100 | 358.680 req/s | 20 | 264.888 | 538.784 | 1089.866 | 0.16 | 10000 | 9984 | 9984 | 16 | ✅ | 21251 | |||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 184.615 req/s | 48.786 | 23.077 | 19.557 | ||||||||||||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 100 | 184.615 req/s | 51.676 | 24.762 | 19.561 | ||||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_optimistic-lock | 20 | 100 | 360.067 req/s | 20 | 263.91 | 536.261 | 1074.603 | 0.144 | 10000 | 9985.6 | 9985.6 | 14.4 | ✅ | 21234.8 | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 100 | 182.420 req/s | 49.79 | 24.085 | 17.928 | ||||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 100 | 182.393 req/s | 51.188 | 24.569 | 18.034 | ||||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 300 | 358.822 req/s | 20 | 797.582 | 1896.815 | 2673.12 | 0.163 | 30000 | 29951 | 29951 | 49 | ✅ | 63774 | |||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 181.404 req/s | 53.046 | 24.645 | 28.211 | ||||||||||||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 181.404 req/s | 54.241 | 26.941 | 23.731 | ||||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 300 | 370.343 req/s | 20 | 779.49 | 2002.691 | 2593.458 | 0.183 | 30000 | 29945 | 29945 | 55 | ✅ | 63325 | |||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 185.940 req/s | 53.627 | 25.962 | 31.294 | ||||||||||||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 184.042 req/s | 52.867 | 25.263 | 28.213 | ||||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 300 | 356.214 req/s | 20 | 797.656 | 2406.773 | 2739.164 | 0.15 | 30000 | 29955 | 29955 | 45 | ✅ | 63715 | |||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 180.064 req/s | 50.714 | 24.528 | 25.381 | ||||||||||||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 180.064 req/s | 54.765 | 30.688 | 32.891 | ||||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 300 | 360.343 req/s | 20 | 800.053 | 1683.25 | 2438.138 | 0.183 | 30000 | 29945 | 29945 | 55 | ✅ | 63314 | |||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 182.173 req/s | 53.893 | 26.573 | 29.007 | ||||||||||||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 182.173 req/s | 54.838 | 24.651 | 28.638 | ||||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 300 | 357.859 req/s | 20 | 798.541 | 2062.908 | 2704.584 | 0.187 | 30000 | 29944 | 29944 | 56 | ✅ | 64092 | |||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 180.896 req/s | 52.359 | 26.257 | 31.21 | ||||||||||||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 300 | 180.896 req/s | 51.195 | 25.837 | 31.171 | ||||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_optimistic-lock | 20 | 300 | 360.716 req/s | 20 | 794.664 | 2010.488 | 2629.693 | 0.173 | 30000 | 29948 | 29948 | 52 | ✅ | 63644 | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 300 | 182.095 req/s | 52.728 | 25.593 | 29.021 | ||||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 300 | 181.716 req/s | 53.581 | 26.676 | 28.929 | ||||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 500 | 358.446 req/s | 20 | 1326.436 | 3057.502 | 3781.776 | 0.17 | 50000 | 49920 | 49920 | 80 | ✅ | 105941 | |||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 180.449 req/s | 51.658 | 24.638 | 28.837 | ||||||||||||
| 1 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 180.435 req/s | 51.058 | 24.599 | 31.79 | ||||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 500 | 358.562 req/s | 20 | 1329.903 | 2900.944 | 3704.258 | 0.16 | 50000 | 49921 | 49921 | 79 | ✅ | 105377 | |||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 180.500 req/s | 51.415 | 24.286 | 30.843 | ||||||||||||
| 2 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 180.500 req/s | 52.485 | 25.962 | 29.798 | ||||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 500 | 360.114 req/s | 20 | 1335.699 | 2757.162 | 3549.046 | 0.194 | 50000 | 49908 | 49908 | 92 | ✅ | 105238 | |||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 181.290 req/s | 53.333 | 24.631 | 33.756 | ||||||||||||
| 3 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 181.290 req/s | 52.885 | 25.854 | 31.769 | ||||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 500 | 356.926 req/s | 20 | 1330.266 | 3371.348 | 3824.927 | 0.146 | 50000 | 49934 | 49934 | 66 | ✅ | 105843 | |||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 179.677 req/s | 49.5 | 22.807 | 33.963 | ||||||||||||
| 4 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 179.677 req/s | 49.958 | 23.333 | 31.197 | ||||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_optimistic-lock | 20 | 500 | 359.813 req/s | 20 | 1321.154 | 3219.888 | 3705.327 | 0.218 | 50000 | 49897 | 49897 | 103 | ✅ | 105898 | |||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 181.139 req/s | 53.302 | 25.238 | 30.184 | ||||||||||||
| 5 | BACKEND | RUN | v0.3_optimistic-lock | 500 | 181.139 req/s | 52.839 | 25.532 | 34.79 | ||||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_optimistic-lock | 20 | 500 | 358.772 req/s | 20 | 1328.692 | 3061.369 | 3713.067 | 0.178 | 50000 | 49916 | 49916 | 84 | ✅ | 105659.4 | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_optimistic-lock | 500 | 180.611 req/s | 51.842 | 24.32 | 31.517 |
Pessimistic Lock
비관적 락은 v0.2에서의 로직과 같다.
결과 기록 표 (스압)
| Run | Target Type | Record Type | Version | Hikari Max Pool Size | VU | TPS (req/s) | System CPU Peak (%) | Process CPU Peak (%) | JVM Heap Peak (%) | Hikari Active Peak | Avg Latency (ms) | P95 (ms) | P99 (ms) | Error Rate (%) | 초기 잔량 | 사용자 총 채굴량 | Mining Log 총 채굴량 | 실제 잔량 | 정합성 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 10 | 224.366 req/s | 11 | 42.656 | 81.329 | 113.223 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 114.023 req/s | 49.491 | 40.976 | 12.734 | ||||||||||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 116.820 req/s | 65.842 | 59.406 | 11.738 | ||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 10 | 275.938 req/s | 1 | 33.077 | 60.194 | 98.185 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 114.668 req/s | 0.995 | 0.5 | 11.717 | ||||||||||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 142.568 req/s | 9.506 | 6.965 | 11.166 | ||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 10 | 340.020 req/s | 10 | 28.412 | 46.986 | 54.826 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 188.900 req/s | 49.038 | 38.35 | 11.663 | ||||||||||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 178.700 req/s | 66.687 | 55.288 | 10.681 | ||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 10 | 359.195 req/s | 10 | 26.954 | 44.469 | 50.774 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 148.707 req/s | 61.312 | 50.971 | 12.446 | ||||||||||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 120.420 req/s | 54.593 | 42.995 | 10.73 | ||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 10 | 390.778 req/s | 5 | 24.447 | 40.305 | 45.442 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 218.933 req/s | 43.794 | 30.476 | 12.654 | ||||||||||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 10 | 183.763 req/s | 24.141 | 17.822 | 12.236 | ||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_pessimistic-lock | 20 | 10 | 318.060 req/s | 7.4 | 31.109 | 54.657 | 72.49 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 10 | 157.046 req/s | 40.926 | 32.254 | 12.243 | ||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 10 | 148.454 req/s | 44.154 | 36.495 | 11.31 | ||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 50 | 426.148 req/s | 19 | 111.389 | 232.105 | 337.898 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 211.169 req/s | 37.415 | 22.967 | 11.906 | ||||||||||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 220.103 req/s | 53.883 | 40.58 | 11.454 | ||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 50 | 479.846 req/s | 20 | 100.314 | 200.844 | 242.82 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 254.918 req/s | 37.379 | 22.816 | 13.743 | ||||||||||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 250.126 req/s | 33.965 | 17.788 | 12.485 | ||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 50 | 500.601 req/s | 12 | 95.542 | 195.173 | 240.785 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 265.944 req/s | 30.547 | 15.534 | 13.444 | ||||||||||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 257.753 req/s | 34.693 | 18.269 | 10.982 | ||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 50 | 513.347 req/s | 20 | 93.101 | 191.276 | 227.541 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 272.716 req/s | 28.155 | 13.3 | 12.817 | ||||||||||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 272.716 req/s | 30.09 | 13.592 | 10.85 | ||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 50 | 506.586 req/s | 20 | 93.311 | 191.321 | 234.648 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 269.124 req/s | 28.84 | 11.962 | 12.234 | ||||||||||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 50 | 269.124 req/s | 30.909 | 13.527 | 10.812 | ||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_pessimistic-lock | 20 | 50 | 485.306 req/s | 18.2 | 98.731 | 202.144 | 256.738 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 50 | 254.774 req/s | 32.467 | 17.316 | 12.829 | ||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 50 | 253.964 req/s | 36.708 | 20.751 | 11.317 | ||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 100 | 510.882 req/s | 20 | 186.536 | 384.808 | 473.816 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 265.266 req/s | 29.172 | 12.195 | 14.938 | ||||||||||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 262.984 req/s | 31.276 | 12.981 | 12.817 | ||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 100 | 512.821 req/s | 19 | 184.904 | 386.675 | 426.419 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 266.272 req/s | 29.636 | 14.078 | 16.159 | ||||||||||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 266.272 req/s | 30.744 | 13.235 | 13.32 | ||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 100 | 512.085 req/s | 20 | 184.242 | 384.811 | 441.993 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 265.890 req/s | 28.565 | 12.019 | 16.102 | ||||||||||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 265.890 req/s | 30.42 | 13.3 | 14.026 | ||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 100 | 511.326 req/s | 20 | 185.168 | 385.788 | 434.882 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 265.358 req/s | 29.844 | 12.44 | 16.207 | ||||||||||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 259.242 req/s | 29.952 | 12.919 | 13.968 | ||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 100 | 507.924 req/s | 20 | 186.704 | 386.933 | 476.36 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 263.730 req/s | 29.185 | 12.195 | 16.654 | ||||||||||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 100 | 263.730 req/s | 33.652 | 15.61 | 14.089 | ||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_pessimistic-lock | 20 | 100 | 511.007 req/s | 19.8 | 185.511 | 385.803 | 450.694 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 100 | 265.303 req/s | 29.28 | 12.585 | 16.012 | ||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 100 | 263.624 req/s | 31.209 | 13.609 | 13.644 | ||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 300 | 504.142 req/s | 20 | 564.099 | 1219.466 | 1908.668 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 255.890 req/s | 34.605 | 15.484 | 22.387 | ||||||||||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 255.890 req/s | 34.356 | 17.961 | 18.966 | ||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 300 | 506.757 req/s | 20 | 558.003 | 1199.157 | 1900.115 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 257.217 req/s | 40.905 | 27.586 | 23.554 | ||||||||||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 257.217 req/s | 32.692 | 14.423 | 20.028 | ||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 300 | 508.087 req/s | 20 | 556.027 | 1190.48 | 1891.604 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 256.456 req/s | 29.293 | 12.121 | 24.658 | ||||||||||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 257.952 req/s | 33.014 | 14.493 | 21.275 | ||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 300 | 505.834 req/s | 20 | 558.607 | 1189.251 | 1898.649 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 256.236 req/s | 30.65 | 12.207 | 27.518 | ||||||||||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 256.749 req/s | 31.885 | 14.078 | 21.601 | ||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 300 | 509.710 req/s | 20 | 554.626 | 1191.41 | 1894.553 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 258.103 req/s | 31.404 | 12.871 | 28.612 | ||||||||||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 300 | 258.776 req/s | 32.21 | 13.876 | 23.218 | ||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_pessimistic-lock | 20 | 300 | 506.906 req/s | 20 | 558.272 | 1197.953 | 1898.718 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 300 | 256.780 req/s | 33.371 | 16.054 | 25.346 | ||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 300 | 257.317 req/s | 32.831 | 14.966 | 21.018 | ||||||||||
| 1 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 500 | 507.898 req/s | 20 | 929.593 | 1991.689 | 2682.762 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 256.345 req/s | 32.769 | 12.921 | 26.376 | ||||||||||
| 1 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 256.345 req/s | 32.857 | 14.493 | 25.608 | ||||||||||
| 2 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 500 | 506.509 req/s | 20 | 931.57 | 1985.268 | 2692.831 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 255.644 req/s | 29.444 | 12.291 | 28.017 | ||||||||||
| 2 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 255.644 req/s | 34.615 | 16.346 | 25.114 | ||||||||||
| 3 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 500 | 506.940 req/s | 20 | 931.558 | 1991.311 | 2693.573 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 255.861 req/s | 31.567 | 13.725 | 29.19 | ||||||||||
| 3 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 255.861 req/s | 33.799 | 14.286 | 24.938 | ||||||||||
| 4 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 500 | 508.911 req/s | 20 | 927.939 | 1979.643 | 2670.234 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 256.879 req/s | 29.41 | 13.043 | 29.977 | ||||||||||
| 4 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 256.879 req/s | 32.8 | 15.049 | 25.798 | ||||||||||
| 5 | LOAD_BALANCER | RUN | v0.3_pessimistic-lock | 20 | 500 | 508.368 req/s | 20 | 933.533 | 1983.886 | 2686.337 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 256.605 req/s | 29.003 | 12.745 | 23.735 | ||||||||||
| 5 | BACKEND | RUN | v0.3_pessimistic-lock | 10 | 500 | 256.605 req/s | 32.686 | 14.493 | 27.271 | ||||||||||
| AVERAGE | LOAD_BALANCER | AVERAGE | v0.3_pessimistic-lock | 20 | 500 | 507.725 req/s | 20 | 930.839 | 1986.36 | 2685.148 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 500 | 256.267 req/s | 30.439 | 12.945 | 27.459 | ||||||||||
| AVERAGE | BACKEND | AVERAGE | v0.3_pessimistic-lock | 10 | 500 | 256.267 req/s | 33.351 | 14.933 | 25.746 |
분석
가설 1. 재시도 로직이 포함된 낙관적 락의 오류율은 1% 이상일 것이다.
모든 VU(10, 50, 100, 300, 500)에서 0.2% 미만의 낮은 오류율을 보였다. 오류는 1회 요청, 총 20회의 재시도에서 모두 실패하여 사용자에게 HttpStatus 409를 반환한 응답이다.
| VU | Mining Log 총 채굴량 평균값 | 실제 잔량 평균값 | Error Rate |
|---|---|---|---|
| 10 | 1000 | 0 | 0 |
| 50 | 4992.8 | 7.2 | 0.144 |
| 100 | 9985.6 | 14.4 | 0.144 |
| 300 | 29948 | 52 | 0.173 |
| 500 | 49916 | 84 | 0.178 |
하지만 그 증가폭이 유의미하다고 보기는 어려운 수치이다. 그 이유는 분산 시스템을 적용했다고 해서, v0.2에서 발생한 DB 병목이 애플리케이션 레이어로 이동한 것이 아니며, 단지 재시도 로직을 통해서 동시성 문제를 해결이 아닌 ‘완화’ 시킨 것이기 때문이다.
v0.3 낙관적 락, 비관적 락 로드밸런서의 평균 오류율
결국 DB의 병목을 만들어내는 변수는 한 행에 대한 Row-Level Lock이며, Gold-Rush-Lab에서 Row에 대해 경합하는 트랜잭션의 수는 DB Connection Pool Size에 의해 결정되기 때문이다. 즉, 오류율의 증가폭을 결정하는 것은 VU가 아닌 대기 중인 트랜잭션의 수다.
가설 2. 재시도 로직이 포함된 낙관적 락은 비관적 락보다 평균 응답시간과 P95 응답시간이 증가할 것이다.
실제로 더 높은 P95 응답 시간을 보인 버전은 낙관적 락이었다.
v0.3 버전의 낙관적 락, 비관적 락 P95
이는 재시도의 비용과 관련이 있다. 실제로 재시도 비용은 다음과 같다.
- TRANSACTION(SELECT 유저, SELECT 광물, UPDATE 유저, UPDATE 광물)
위와 같은 트랜잭션을 낙관적 락에서는 한 요청당 최악의 경우 21번 반복한다.
이 비용은 비관적 락이 SELECT 광물 시에 FOR UPDATE를 통해 row-level lock을 획득하고, 이를 처리하는 비용보다 더 높았던 것이다.
가설 3. API를 증설하더라도 Hot Spot에서는 DB가 여전히 병목이 될 것이다.
v0.2와 v0.3 사이에 재시도 정책이 달라 직접적인 Scale-out 성능 비교에서는 제외하였다. 대신 v0.3 내부에서 VU 증가에 따라 재시도가 급증하고 TPS가 약 360에서 포화되는 현상을 통해 동일 Row 경합이 처리량을 제한하는 보조 근거로 활용하였다.
v0.2, v0.3 낙관적 락에서의 평균 TPS
VU에 따른 급증하는 재시도 횟수
비관적 락에서는 API 서버를 2대로 증설했음에도 TPS는 v0.2와 유사한 수준에서 유지되었다. 두 API 서버가 동시에 요청을 처리하더라도 결국 동일 Row의 배타적 락을 획득해야 하므로 트랜잭션은 DB에서 직렬화된다. 따라서 API 계층의 처리 능력은 증가했지만, 최종 처리량은 동일 Row에 대한 DB의 Row-Level Lock 처리 속도에 의해 결정되었다.
v0.2, v0.3 비관적 락에서의 평균 TPS
Scale-out은 API 계층의 처리 능력을 높이는 방법이지만, Hot Spot 환경에서는 모든 요청이 동일한 데이터(Row)에 집중된다.
API 1대 --------
|
API 2대 -----> 동일 Mine(Row) -----> PostgreSQL
와 같이 모든 요청이 결국 하나의 Row에 도달하기 때문에 API 인스턴스를 추가하더라도 DB의 동시성 제약은 그대로 유지된다.
특히 비관적 락은 DB에서 Row-Level Lock을 획득하기 위해 대기하고, 낙관적 락은 버전 충돌로 인해 재시도가 반복된다. 두 방식 모두 병목의 위치는 API가 아니라 DB라는 공통점을 가진다.
가설 4. API를 Scale-out 하면 단일 API 인스턴스의 Process CPU Peak 평균값은 감소할 것이다.
낙관적 락에서 API 서버를 1대에서 2대로 증설한 결과, 단일 API 인스턴스의 Process CPU Peak는 v0.2보다 크게 감소하였다.
v0.3에서는 Retry 로직이 추가되어 동일 요청이 여러 번 실행될 수 있었음에도, 두 개의 API 서버가 요청을 분산 처리하면서 개별 인스턴스가 담당하는 요청 수는 감소하였다. 그 결과 각 API 인스턴스의 CPU 사용률은 약 25~30% 수준에서 안정적으로 유지되었다.
즉, Retry로 인해 전체 시스템의 작업량은 증가했지만, API 계층을 Scale-out함으로써 개별 인스턴스의 CPU 부담은 효과적으로 분산되었다.
v0.2와 v0.3 낙관적 락의 Process CPU Peak 평균값
비관적 락에서도 동일한 결과를 확인하였다.
v0.2에서는 단일 API가 모든 요청을 처리했기 때문에 Process CPU Peak가 높게 나타났지만, v0.3에서는 요청이 두 API 서버로 분산되면서 각 인스턴스의 CPU 사용률이 절반 수준까지 감소하였다.
비록 동일 Row에 대한 DB 락으로 인해 TPS는 크게 증가하지 않았지만, API 계층 자체의 부하는 두 인스턴스로 분산되었음을 확인할 수 있었다.
v0.2와 v0.3 비관적 락의 Process CPU Peak 평균값
결론
1. 핫스팟 환경에서는 API 증설만으로 TPS를 유의미하게 개선시킬 수 없다.
API를 1대에서 2대로 증설하여 애플리케이션 계층의 처리 능력을 확장하였지만, 핫스팟 환경에서는 TPS가 서버 수에 비례하여 증가하지 않았다. 낙관적 락에서는 Retry 비용이 증가하였고, 또 그에 따라 버전 충돌 또한 증가했다.
비관적 락에서는 동일 Row에 대한 Row-Level Lock으로 인해 트랜잭션이 직렬화되었다. 결과적으로 API를 증설해도 병목은 여전히 DB에 존재하였으며, Hot Spot 환경에서는 API 증설만으로 처리량 한계를 해결할 수 없음을 확인하였다.
2. 핫스팟 환경에서는 재시도를 적용한 낙관적 락은 비관적 락보다 더 늦은 응답을 반환한다.
재시도를 적용한 낙관적 락은 대부분의 버전 충돌을 성공적으로 복구하여 정합성을 유지하였다. 그러나 충돌이 발생할 때마다 새로운 트랜잭션에서 조회와 갱신을 반복하고 재시도마다의 딜레이가 적용되며 응답 시간이 크게 증가하였다. 반면 비관적 락은 DB에서 대기 시간이 발생하더라도 한 번의 트랜잭션으로 작업을 완료하므로, 핫스팟 환경에서는 평균 응답 시간과 P95, P99 모두 재시도를 적용한 낙관적 락보다 낮게 나타났다.
3. API 서버 증설은 단일 API 인스턴스의 부하를 효과적으로 분산시킨다.
API를 증설한 이후 단일 API 인스턴스의 Process CPU Peak는 v0.2 대비 유의미하게 감소하였다. 이는 Load Balancer가 요청을 여러 API 인스턴스로 분산하여 애플리케이션 계층의 연산 부하를 효과적으로 분산시켰기 때문이다. 낙관적 락에서는 재시도로 인해 전체 연산량이 증가했음에도 각 API 인스턴스의 CPU 사용률은 감소하였으며, 비관적 락에서도 동일한 경향을 확인하였다.
4. API 증설은 부하를 분산했으나 ‘병목을 제거하진 못했다.’
스케일 아웃한 시스템은 단일 인스턴스의 CPU 사용률을 줄이고, 부하를 성공적으로 분산했다. 하지만 낙관적 락에서는 여전히 동시성과 관련된 오류를 발생시켰고, 비관적 락에서는 TPS 증가, P95, P99 감소를 보여주진 못했다.
이는 애플리케이션의 처리 능력이 아닌, 동일 데이터에 대한 DB 경합이 시스템의 병목으로 남아 있었기 때문이다. 따라서 단순히 API를 증설하는 것만으로는 한계가 있으며, DB에 도달하기 전에 동일 자원에 대한 동시성을 제어하여 병목 지점을 애플리케이션 계층으로 이동시키는 전략이 필요하다고 판단했다.
댓글남기기