핫스팟 환경에서 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회 반복한다.

실험 환경

Gold-Rush-Lab 분산 시스템 실험 환경

  • 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()를 호출했다.

마지막으로 기존 OptimisticLockingFailureExceptionGlobalExceptionHandler 클래스가 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 낙관적 락과 비관적 락의 평균 오류율 비교

v0.3 낙관적 락, 비관적 락 로드밸런서의 평균 오류율

결국 DB의 병목을 만들어내는 변수는 한 행에 대한 Row-Level Lock이며, Gold-Rush-Lab에서 Row에 대해 경합하는 트랜잭션의 수는 DB Connection Pool Size에 의해 결정되기 때문이다. 즉, 오류율의 증가폭을 결정하는 것은 VU가 아닌 대기 중인 트랜잭션의 수다.

가설 2. 재시도 로직이 포함된 낙관적 락은 비관적 락보다 평균 응답시간과 P95 응답시간이 증가할 것이다.

실제로 더 높은 P95 응답 시간을 보인 버전은 낙관적 락이었다.

v0.3 낙관적 락과 비관적 락의 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 비교

v0.2, v0.3 낙관적 락에서의 평균 TPS

VU 증가에 따른 낙관적 락 재시도 횟수

VU에 따른 급증하는 재시도 횟수

비관적 락에서는 API 서버를 2대로 증설했음에도 TPS는 v0.2와 유사한 수준에서 유지되었다. 두 API 서버가 동시에 요청을 처리하더라도 결국 동일 Row의 배타적 락을 획득해야 하므로 트랜잭션은 DB에서 직렬화된다. 따라서 API 계층의 처리 능력은 증가했지만, 최종 처리량은 동일 Row에 대한 DB의 Row-Level Lock 처리 속도에 의해 결정되었다.

v0.2와 v0.3 비관적 락의 평균 TPS 비교

v0.2, v0.3 비관적 락에서의 평균 TPS

VU에 따른 낙관적 락 재시도 횟수 추이

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와 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 평균값 비교

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에 도달하기 전에 동일 자원에 대한 동시성을 제어하여 병목 지점을 애플리케이션 계층으로 이동시키는 전략이 필요하다고 판단했다.

댓글남기기