[Project : Gold-Rush-Lab] 2. 동시성 문제에서의 락
동시성 문제를 해결하기 위해 비관적 락과 낙관적 락 중 어떤 것을 선택해야 할까? 그리고 Lock은 서버 성능에 어떠한 영향을 끼칠까?
개요
v0.1 단계에서는 동시성 문제에 대한 대비 없이 VU 10 ~ 500 까지의 부하를 걸었다. 결과로서 데이터 정합성이 VU가 1 일 때를 제외하고 모두 정합성이 어긋나는 현상을 확인할 수 있었다.
그렇다면 이러한 상황에서 최소한의 방지를 하기 위해 어떤 것을 할 수 있을까?
먼저 동일한 자원을 동시에 수정하는 트랜잭션 간의 충돌을 제어해야 한다.
이를 위해 데이터 변경 시 충돌을 감지하거나, 특정 트랜잭션이 자원을 수정하는 동안 다른 트랜잭션의 충돌 가능한 변경을 제한하는 Lock을 적용할 수 있다.
Lock에는 두 가지 종류가 있다.
- Optimistic Lock(낙관적 락)
- 동시성 충돌이 자주 발생하지 않을 것이라고 가정한다.
데이터를 조회할 때는 별도의 Lock을 획득하지 않고, 수정 시점에
Version값을 비교하여 다른 트랜잭션이 먼저 수정했는지를 확인한다. 충돌이 발생하면 예외를 발생시키거나 재시도한다.
- 동시성 충돌이 자주 발생하지 않을 것이라고 가정한다.
데이터를 조회할 때는 별도의 Lock을 획득하지 않고, 수정 시점에
- Pessimistic Lock(비관적 락)
- 동시성 충돌이 자주 발생할 것이라고 가정한다. 데이터를 조회하는 시점부터 DB Row Lock을 획득하여 다른 트랜잭션의 충돌 가능한 수정 작업을 대기시킨 뒤 현재 트랜잭션의 작업을 수행한다.
이번 실험에서는 v0.1 의 모놀리식 아키텍처에서 낙관적, 비관적 락만 적용하여 실험을 진행한다. 또한 이러한 적용점이 서버 처리량에 어떠한 영향을 미치는지 알아보고자 한다.
가설
Lock에 대해서 알아본 후, 그 내용을 토대로 v0.1 과 같이 가설을 설정했다.
가설 1. 낙관적 락은 Lost Update를 방지하여 데이터 정합성을 유지할 것이다.
- 광산의 잔량을 수정하는 Update 연산에 ‘충돌감지’ 라는 완충지대가 생겼다. 이는 현재 작업하고 있는 스레드가 다른 스레드의 결과를 덮어씌우는 것을 방지할 것이다.
가설 2. 낙관적 락은 오류를 반환하여 어떤 유저는 100개를 채굴하지 못 할 것이다.
- 충돌이 발생하면 재시도하거나 예외를 발생시킨다고 했다. 재시도 횟수 설정이 필요할 것이라는 예상과 함께 설정한 재시도 횟수를 넘기면 오류를 반환할 것 이라는 생각이 들었다.
가설 3. 비관적 락은 Lost Update를 방지하여 데이터 정합성을 유지할 것이다.
- 비관적 락은 조회하는 시점부터 접근을 제한한다. 정합성을 유지할 것이다.
가설 4. 비관적 락은 부하가 늘어남에 따라 TPS가 떨어질 것이다.
- 조회하는 시점부터 접근을 제한한다. 이는 동시 요청들이 직렬화 될 것이므로 v0.1(커넥션 풀 10) 보다 TPS가 낮게 나올 것이다.
가설 5. 비관적 락은 Lock 대기가 길어지면 일부 요청에서 타임아웃이 발생할 것이다.
- 낙관적 락과 마찬가지로 어떤 유저는 100개를 채굴하지 못 할 것이다. 부하가 충분히 커진다면 대기가 길어져 일부 요청에서 Timeout이 발생할 것으로 예상했다.
가설 6. 낙관적, 비관적 락을 적용함으로써 DB에서의 Connection Pool Size와 관련된 병목은 해결될 것이다.
- v0.1에서 커넥션 풀을 늘리며 스프링 → DB 로 전이된 병목은 동시성 제어가 애플리케이션 레벨에서 수행되며 다시 스프링에서의 병목으로 전이될 것이다. 즉, DB에서의 wait_event 는 그 수가 줄고 Spring에서의 병목이 커질 것으로 예상된다.
실험 방법
- 실험 방법은 v0.1과 동일하게 진행한다.
- 단, v0.2/optimistic-lock, v0.2/pessimistic-lock 브랜치에서 각각에 대한 구현을 수행 후 실험한다.
- Optimistic Lock 과 Pessimistic Lock의 성능을 비교하기 위해 다음을 수행한다.
- 각 풀 사이즈마다 VU 10으로 Warm-up을 수행한다.
- 서버 부트 후의 첫 요청에서 다음이 발생하기 때문이다.
- JVM JIT 컴파일
- 클래스 로딩
- Hikari 커넥션 생성
- SQL 실행 경로 준비
- Spring 내부 초기화
- 각 커넥션 실험마다 3회 반복한다.
- 결과의 비교는 3회의 평균 값을 활용한다.
- 각 실행마다 System CPU, Process CPU, Heap Usage, Hikari Pool 안정을 확인 후 실행한다.
-
각 실행마다 DB는 다음의 쿼리로 초기화한다.
TRUNCATE TABLE app_user, mine, mining_log RESTART IDENTITY CASCADE;
- 각 풀 사이즈마다 VU 10으로 Warm-up을 수행한다.
Optimistic Lock
CREATE TABLE mine
(
id BIGINT GENERATED ALWAYS AS IDENTITY,
remaining_amount BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
version BIGINT NOT NULL DEFAULT 0, --추가된 version 컬럼
CONSTRAINT pk_mine
PRIMARY KEY (id),
CONSTRAINT chk_mine_remaining_amount
CHECK (remaining_amount >= 0)
);
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@FieldDefaults(level = AccessLevel.PRIVATE)
@Table(name = "mine")
public class MineEntity extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
Long remainingAmount;
@Version
Long version; // 추가된 필드
...
}
- Mine 엔티티, 테이블에 version 필드, 컬럼을 추가하여 구현한다.
@Version이 적용되면 Hibernate는 기존 version 값을 UPDATE WHERE 조건에 포함하고, 수정에 성공하면 version 값을 증가시킨다. 이미 다른 트랜잭션이 데이터를 수정하여 조건에 맞는 행이 없다면, 변경된 행의 수가 0이 되며 낙관적 락 예외가 발생한다.
Pessimistic Lock
- Lock을 적용하고 싶은 도메인의 조회 쿼리에
FOR UPDATE구문을 적용한다.- 예시
SELECT id, remaining_amount, created_at, version FROM mine WHERE id = ? FOR UPDATE; -- 추가된 FOR UPDATE 구문 - JPA에서는 다음과 같이 적용할 수 있다.
- 예시 : MineJpaRepository
- 구현체인 MinRepositoryJpaAdapter 에서는 Optional이 처리된 MineEntity 객체가 반환된다.
@Lock(LockModeType.PESSIMISTIC_WRITE) @Query(""" select m from MineEntity m where m.id = :id """) Optional<MineEntity> findByIdForUpdate(@Param("id") Long id);@Query어노테이션을 적용한 이유는 함수 이름을 통해 구분을 하기 위함이다.JPA는
메서드 이름 분석→JPQL 생성→SQL 변환의 과정을 거치는데,@Query를 적용함으로써메서드 이름 분석(생략)→JPQL 생성(직접 작성)→SQL 변환의 과정을 거치게된다.이를 적용했다고 해서 성능상의 차이가 유의미하게 있다고 보기는 어렵다고 판단하여 함수를 추가했다.
@Override @Transactional public void mine(UUID sessionId, Long amount) { LockStrategy strategy = LockStrategy.from(configuredLockStrategy); try { UserEntity foundUser = userRepository.findBySessionId(sessionId); // 기존 조회 코드 // MineEntity foundMine = foundUser.getMine(); // Pessimistic Lock 적용 조회 코드 MineEntity foundMine = mineRepository.findByIdForUpdate(foundUser.getMine().getId()); ... } ... } }또한
findByIdForUpdate를 실행하기 위해getId()함수를 호출하는 부분이 존재한다.일반적으로
UserEntity가MineEntity를LAZY로 참조하고 있기 때문에getId()를 호출하는 과정에서 추가적인 조회가 발생하는 것이 아닌지 의문을 가질 수 있다.public class UserEntity{ ... @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "mine_id", nullable = false) MineEntity mine; ... }하지만 Hibernate는 연관 엔티티를
LAZY로 로딩할 경우 실제 엔티티 대신 Proxy 객체를 주입한다. 이 Proxy는 연관 엔티티의 PK 를 이미 알고 있기 때문에getId()호출만으로는 Proxy 초기화가 발생하지 않는다.즉,
Long mineId = foundUser.getMine().getId();는
MineEntity를 조회하기 위한SELECT를 수행하지 않으며, Proxy가 보유하고 있는 식별자만 반환한다.이후
MineEntity foundMine = mineRepository.findByIdForUpdate(foundUser.getMine().getId());를 통해 최초 조회부터
PESSIMISTIC_WRITE가 적용된SELECT ... FOR UPDATE가 수행된다.결과적으로 기존 방식
MineEntity foundMine = foundUser.getMine();역시
MineEntity를 최초 접근하는 시점에SELECT가 한 번 수행되고,변경된 방식
MineEntity foundMine = mineRepository.findByIdForUpdate(foundUser.getMine().getId());역시
SELECT ... FOR UPDATE가 한 번 수행된다.즉, SQL 실행 횟수는 기존과 동일하다.
기존에는
MineEntity를 일반 조회한 후 비즈니스 로직을 수행했다면, 변경된 방식은 최초 조회부터FOR UPDATE를 적용하여 Row Lock을 획득한다는 차이가 있다.따라서
getId()호출로 인해 추가적인 조회가 발생하거나 성능 저하가 발생하지 않으며, 동일한 SQL 실행 횟수로 비관적 락을 적용할 수 있었다. - 예시 : MineJpaRepository
결과
Process CPU
낙관적 락과 비관적 락의 Process CPU 사용률 비교
정합성
락 전략별 데이터 정합성 검증 결과
Optimistic Lock
첫 Warm-up 시도와 함께 Optimistic Lock 과 관련된 예외가 발생했다.
낙관적 락 충돌로 발생한 ObjectOptimisticLockingFailureException
따라서 오류는 그대로 HttpStatus.CONFLICT로 반환하되 후속 실험에서 재시도 로직을 추가하여 대표적인 사례만 비교하고자 한다.
실행 결과 표 (스압)
| 버전 | Hikari Max Pool Size | VU | TPS (req/s) | System CPU Peak (%) | Process CPU Peak (%) | JVM Heap Peak (%) | Avg Latency (ms) | P95 (ms) | P99 (ms) | Error Rate (%) | 초기 잔량 | 사용자 총 채굴량 | Mining Log 총 채굴량 | 실제 잔량 | 정합성 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| v0.2_optimistic-lock | 10 | 10 | 483.325 | 1.49 | 1.005 | 12.089 | 19.956 | 30.196 | 37.097 | 82.3 | 1000 | 177 | 177 | 823 | ✅ |
| v0.2_optimistic-lock | 10 | 10 | 646.412 | 9.496 | 9.548 | 9.724 | 14.705 | 24.429 | 34.75 | 82.5 | 1000 | 175 | 175 | 825 | ✅ |
| v0.2_optimistic-lock | 10 | 10 | 784.314 | 74.51 | 66.01 | 11.411 | 12.092 | 19.044 | 26.589 | 83 | 1000 | 170 | 170 | 830 | ✅ |
| v0.2_optimistic-lock | 10 | 10 | 853.242 | 47.777 | 41.089 | 11.848 | 11.221 | 17.081 | 22.333 | 83.6 | 1000 | 164 | 164 | 836 | ✅ |
| v0.2_optimistic-lock | 10 | 10 | 914.077 | 0.5 | 1 | 7.607 | 10.435 | 15.587 | 17.809 | 83.2 | 1000 | 168 | 168 | 832 | ✅ |
| v0.2_optimistic-lock | 10 | 50 | 915.583 | 98.505 | 84.653 | 11.099 | 52.199 | 112.411 | 146.722 | 84.5 | 5000 | 775 | 775 | 4225 | ✅ |
| v0.2_optimistic-lock | 10 | 50 | 1077.586 | 46.465 | 38.889 | 9.832 | 44.297 | 93.719 | 124.143 | 84.84 | 5000 | 758 | 758 | 4242 | ✅ |
| v0.2_optimistic-lock | 10 | 50 | 1213.592 | 97.025 | 79.602 | 10.404 | 39.471 | 86.014 | 113.473 | 85.16 | 5000 | 742 | 742 | 4258 | ✅ |
| v0.2_optimistic-lock | 10 | 50 | 1238.85 | 57.639 | 47.264 | 11.379 | 38.462 | 86.167 | 114.494 | 85.2 | 5000 | 740 | 740 | 4260 | ✅ |
| v0.2_optimistic-lock | 10 | 50 | 1468.429 | 87.245 | 69.388 | 8.974 | 32.486 | 69.17 | 91.337 | 85.92 | 5000 | 704 | 704 | 4296 | ✅ |
| v0.2_optimistic-lock | 10 | 100 | 1647.989 | 96.99 | 69.802 | 10.133 | 57.352 | 129.953 | 175.004 | 85.95 | 10000 | 1405 | 1405 | 8595 | ✅ |
| v0.2_optimistic-lock | 10 | 100 | 1707.65 | 96.059 | 71.5 | 11.888 | 55.702 | 121.798 | 170.831 | 86.02 | 10000 | 1398 | 1398 | 8602 | ✅ |
| v0.2_optimistic-lock | 10 | 100 | 1665.002 | 92.195 | 64.878 | 9.738 | 57.417 | 128.699 | 169.961 | 86.03 | 10000 | 1397 | 1397 | 8603 | ✅ |
| v0.2_optimistic-lock | 10 | 100 | 1722.356 | 94.54 | 65.842 | 9.729 | 55.427 | 121.22 | 169.774 | 86.1 | 10000 | 1390 | 1390 | 8610 | ✅ |
| v0.2_optimistic-lock | 10 | 100 | 1656.726 | 91.779 | 64.251 | 12.119 | 57.32 | 126.453 | 172.352 | 85.77 | 10000 | 1423 | 1423 | 8577 | ✅ |
| v0.2_optimistic-lock | 10 | 300 | 1694.341 | 95.039 | 70.297 | 14.449 | 168.905 | 397.067 | 507.532 | 86.163 | 30000 | 4151 | 4151 | 25849 | ✅ |
| v0.2_optimistic-lock | 10 | 300 | 1692.811 | 95.074 | 66.337 | 17.038 | 169.197 | 390.56 | 499.097 | 86.11 | 30000 | 4167 | 4167 | 25833 | ✅ |
| v0.2_optimistic-lock | 10 | 300 | 1675.51 | 93.525 | 67.164 | 16.364 | 170.599 | 393 | 497.968 | 86.293 | 30000 | 4112 | 4112 | 25888 | ✅ |
| v0.2_optimistic-lock | 10 | 300 | 1621.446 | 94.189 | 67.476 | 13.927 | 177.247 | 410.809 | 530.22 | 86.05 | 30000 | 4185 | 4185 | 25815 | ✅ |
| v0.2_optimistic-lock | 10 | 300 | 1663.156 | 91.714 | 62.319 | 16.162 | 173.007 | 398.443 | 504.387 | 86.027 | 30000 | 4192 | 4192 | 25808 | ✅ |
| v0.2_optimistic-lock | 10 | 500 | 1669.672 | 95.434 | 67.662 | 17.992 | 291.206 | 522.438 | 634.987 | 86.098 | 50000 | 6951 | 6951 | 43049 | ✅ |
| v0.2_optimistic-lock | 10 | 500 | 1666 | 92.67 | 65.104 | 18.538 | 291.834 | 521.254 | 627.034 | 85.944 | 50000 | 7028 | 7028 | 42972 | ✅ |
| v0.2_optimistic-lock | 10 | 500 | 1692.162 | 94.118 | 66.01 | 18.306 | 287.281 | 516.879 | 628.025 | 86.258 | 50000 | 6871 | 6871 | 43129 | ✅ |
| v0.2_optimistic-lock | 10 | 500 | 1668.613 | 92.921 | 64.455 | 18.139 | 291.046 | 523.209 | 627.758 | 86.148 | 50000 | 6926 | 6926 | 43074 | ✅ |
| v0.2_optimistic-lock | 10 | 500 | 1685.204 | 95.02 | 68.02 | 19.736 | 287.888 | 514.186 | 618.194 | 86.112 | 50000 | 6944 | 6944 | 43056 | ✅ |
| v0.2_optimistic-lock | 30 | 10 | 458.085 | 1.01 | 1.508 | 8.131 | 21.022 | 33.119 | 39.746 | 82.3 | 1000 | 177 | 177 | 823 | ✅ |
| v0.2_optimistic-lock | 30 | 10 | 668.449 | 1.99 | 1 | 10.359 | 14.372 | 23.023 | 30.661 | 83 | 1000 | 170 | 170 | 830 | ✅ |
| v0.2_optimistic-lock | 30 | 10 | 759.878 | 1.99 | 0.503 | 11.724 | 12.504 | 21.435 | 31.061 | 82.9 | 1000 | 171 | 171 | 829 | ✅ |
| v0.2_optimistic-lock | 30 | 10 | 843.17 | 21.5 | 18.5 | 11.097 | 11.391 | 16.964 | 21.078 | 83.5 | 1000 | 165 | 165 | 835 | ✅ |
| v0.2_optimistic-lock | 30 | 10 | 900.09 | 83.746 | 72.727 | 12.02 | 10.608 | 15.826 | 20.657 | 84.1 | 1000 | 159 | 159 | 841 | ✅ |
| v0.2_optimistic-lock | 30 | 50 | 1025.431 | 99.495 | 83.824 | 7.521 | 46.727 | 86.187 | 104.415 | 89.52 | 5000 | 524 | 524 | 4476 | ✅ |
| v0.2_optimistic-lock | 30 | 50 | 1105.461 | 99.505 | 84.08 | 11.707 | 43.826 | 77.392 | 95.668 | 89.16 | 5000 | 542 | 542 | 4458 | ✅ |
| v0.2_optimistic-lock | 30 | 50 | 1235.178 | 99.5 | 83.582 | 11.817 | 39.052 | 71.343 | 88.318 | 89.72 | 5000 | 514 | 514 | 4486 | ✅ |
| v0.2_optimistic-lock | 30 | 50 | 1348.072 | 18.812 | 16.832 | 12.15 | 35.813 | 63.692 | 80.005 | 89.94 | 5000 | 503 | 503 | 4497 | ✅ |
| v0.2_optimistic-lock | 30 | 50 | 1507.386 | 92.424 | 75 | 12.474 | 31.892 | 59.972 | 79.229 | 89.74 | 5000 | 513 | 513 | 4487 | ✅ |
| v0.2_optimistic-lock | 30 | 100 | 1687.479 | 97.505 | 76.617 | 12.421 | 56.642 | 128.3 | 166.535 | 89.92 | 10000 | 1008 | 1008 | 8992 | ✅ |
| v0.2_optimistic-lock | 30 | 100 | 1721.17 | 97.5 | 71.5 | 11.984 | 55.716 | 121.433 | 158.444 | 90.07 | 10000 | 993 | 993 | 9007 | ✅ |
| v0.2_optimistic-lock | 30 | 100 | 1759.324 | 96.049 | 69.802 | 10.829 | 54.607 | 116.276 | 148.869 | 90.09 | 10000 | 991 | 991 | 9009 | ✅ |
| v0.2_optimistic-lock | 30 | 100 | 1747.335 | 83.756 | 58.794 | 9.711 | 54.706 | 116.147 | 150.798 | 90.12 | 10000 | 988 | 988 | 9012 | ✅ |
| v0.2_optimistic-lock | 30 | 100 | 1725.03 | 96.069 | 70.647 | 11.271 | 55.511 | 116.43 | 146.509 | 90.06 | 10000 | 994 | 994 | 9006 | ✅ |
| v0.2_optimistic-lock | 30 | 300 | 1740.846 | 96.117 | 70.388 | 15.101 | 165.721 | 372.076 | 472.363 | 90.053 | 30000 | 2984 | 2984 | 27016 | ✅ |
| v0.2_optimistic-lock | 30 | 300 | 1746.827 | 96.04 | 69.118 | 17.192 | 164.254 | 365.278 | 463.372 | 89.967 | 30000 | 3010 | 3010 | 26990 | ✅ |
| v0.2_optimistic-lock | 30 | 300 | 1699.909 | 92.137 | 65.816 | 15.736 | 169.269 | 375.022 | 477.505 | 90.257 | 30000 | 2923 | 2923 | 27077 | ✅ |
| v0.2_optimistic-lock | 30 | 300 | 1740.745 | 95.098 | 68.293 | 16.305 | 164.953 | 365.236 | 463.027 | 90.143 | 30000 | 2957 | 2957 | 27043 | ✅ |
| v0.2_optimistic-lock | 30 | 300 | 1777.883 | 97 | 70.149 | 16.884 | 161.995 | 361.325 | 455.793 | 90.107 | 30000 | 2968 | 2968 | 27032 | ✅ |
| v0.2_optimistic-lock | 30 | 500 | 1713.091 | 96.939 | 69.388 | 17.364 | 283.931 | 501.614 | 602.795 | 90.05 | 50000 | 4975 | 4975 | 45025 | ✅ |
| v0.2_optimistic-lock | 30 | 500 | 1687.251 | 92.676 | 67.5 | 17.305 | 288.067 | 505.189 | 605.988 | 90.188 | 50000 | 4906 | 4906 | 45094 | ✅ |
| v0.2_optimistic-lock | 30 | 500 | 1707.417 | 93.768 | 66.341 | 18.561 | 284.375 | 496.288 | 596.171 | 90.076 | 50000 | 4962 | 4962 | 45038 | ✅ |
| v0.2_optimistic-lock | 30 | 500 | 1747.824 | 96.116 | 67.317 | 19.554 | 278.29 | 485.248 | 584.456 | 90.038 | 50000 | 4981 | 4981 | 45019 | ✅ |
| v0.2_optimistic-lock | 30 | 500 | 1756.235 | 96.469 | 69.036 | 19.824 | 276.924 | 484.397 | 582.98 | 90.22 | 50000 | 4890 | 4890 | 45110 | ✅ |
| v0.2_optimistic-lock | 50 | 10 | 458.716 | 95.455 | 87.5 | 7.664 | 21.004 | 33.03 | 43.237 | 81.9 | 1000 | 181 | 181 | 819 | ✅ |
| v0.2_optimistic-lock | 50 | 10 | 600.601 | 32.828 | 29.851 | 9.469 | 15.898 | 24.805 | 32.736 | 83.1 | 1000 | 169 | 169 | 831 | ✅ |
| v0.2_optimistic-lock | 50 | 10 | 697.35 | 0.5 | 0.5 | 10.332 | 13.634 | 22.208 | 31.829 | 82.3 | 1000 | 177 | 177 | 823 | ✅ |
| v0.2_optimistic-lock | 50 | 10 | 844.595 | 1.49 | 0.995 | 11.047 | 11.338 | 17.048 | 24.781 | 83.5 | 1000 | 165 | 165 | 835 | ✅ |
| v0.2_optimistic-lock | 50 | 10 | 844.595 | 9.545 | 8.04 | 12.429 | 11.177 | 18.879 | 25.9 | 83 | 1000 | 170 | 170 | 830 | ✅ |
| v0.2_optimistic-lock | 50 | 50 | 937.559 | 96.597 | 83.824 | 11.153 | 50.639 | 104.727 | 135.669 | 90.36 | 5000 | 482 | 482 | 4518 | ✅ |
| v0.2_optimistic-lock | 50 | 50 | 1089.799 | 99.505 | 85.5 | 12.157 | 43.779 | 84.047 | 105.068 | 90.92 | 5000 | 454 | 454 | 4546 | ✅ |
| v0.2_optimistic-lock | 50 | 50 | 1186.24 | 36.275 | 29.412 | 10.579 | 40.517 | 76.796 | 95.352 | 90.58 | 5000 | 471 | 471 | 4529 | ✅ |
| v0.2_optimistic-lock | 50 | 50 | 1362.027 | 98.015 | 79.602 | 13.112 | 35.006 | 74.809 | 109.93 | 91.62 | 5000 | 419 | 419 | 4581 | ✅ |
| v0.2_optimistic-lock | 50 | 50 | 1464.987 | 99.479 | 81.538 | 9.909 | 32.365 | 68.511 | 102.62 | 91.8 | 5000 | 410 | 410 | 4590 | ✅ |
| v0.2_optimistic-lock | 50 | 100 | 1522.302 | 99.005 | 79.104 | 12.77 | 63.592 | 129.485 | 165.052 | 90.91 | 10000 | 909 | 909 | 9091 | ✅ |
| v0.2_optimistic-lock | 50 | 100 | 1581.028 | 88.319 | 60.976 | 12.886 | 60.866 | 122.594 | 179.642 | 91.46 | 10000 | 854 | 854 | 9146 | ✅ |
| v0.2_optimistic-lock | 50 | 100 | 1676.165 | 96.078 | 70.647 | 12.59 | 57.746 | 108.141 | 134.612 | 90.7 | 10000 | 930 | 930 | 9070 | ✅ |
| v0.2_optimistic-lock | 50 | 100 | 1685.204 | 92.939 | 67.347 | 12.022 | 57.331 | 108.657 | 134.156 | 90.89 | 10000 | 911 | 911 | 9089 | ✅ |
| v0.2_optimistic-lock | 50 | 100 | 1633.72 | 87.435 | 60 | 11.164 | 59.089 | 114.661 | 155.577 | 91.43 | 10000 | 857 | 857 | 9143 | ✅ |
| v0.2_optimistic-lock | 50 | 300 | 1549.107 | 96.454 | 72.959 | 16.383 | 186.694 | 389.911 | 489.449 | 92.81 | 30000 | 2157 | 2157 | 27843 | ✅ |
| v0.2_optimistic-lock | 50 | 300 | 1638.002 | 91.318 | 65.347 | 18.45 | 176.829 | 362.787 | 449.236 | 90.79 | 30000 | 2763 | 2763 | 27237 | ✅ |
| v0.2_optimistic-lock | 50 | 300 | 1624.519 | 92.751 | 66.502 | 18.711 | 177.698 | 368.644 | 465.187 | 91.327 | 30000 | 2602 | 2602 | 27398 | ✅ |
| v0.2_optimistic-lock | 50 | 300 | 1712.818 | 95.928 | 68.02 | 18.971 | 169.359 | 351.371 | 441.05 | 91.377 | 30000 | 2587 | 2587 | 27413 | ✅ |
| v0.2_optimistic-lock | 50 | 300 | 1764.291 | 97.475 | 69.5 | 17.256 | 163.222 | 341.969 | 427.999 | 90.627 | 30000 | 2812 | 2812 | 27188 | ✅ |
| v0.2_optimistic-lock | 50 | 500 | 1702.649 | 92.195 | 65.5 | 20.057 | 286.611 | 478.722 | 565.85 | 91.332 | 50000 | 4334 | 4334 | 45666 | ✅ |
| v0.2_optimistic-lock | 50 | 500 | 1651.473 | 91.95 | 65.072 | 18.415 | 295.292 | 491.214 | 586.819 | 91.594 | 50000 | 4203 | 4203 | 45797 | ✅ |
| v0.2_optimistic-lock | 50 | 500 | 1707.825 | 92.54 | 66 | 19.77 | 286.183 | 476.408 | 562.898 | 90.964 | 50000 | 4518 | 4518 | 45482 | ✅ |
| v0.2_optimistic-lock | 50 | 500 | 1623.166 | 91.638 | 64.563 | 21.403 | 300.184 | 498.886 | 595.214 | 92.566 | 50000 | 3717 | 3717 | 46283 | ✅ |
| v0.2_optimistic-lock | 50 | 500 | 1677.008 | 92.829 | 64.286 | 22.092 | 290.92 | 488.595 | 581.021 | 91.354 | 50000 | 4323 | 4323 | 45677 | ✅ |
Pessimistic Lock
실행 결과 표 (스압)
| Hikari Max Pool Size | VU | TPS (req/s) | System CPU Peak (%) | Process CPU Peak (%) | JVM Heap Peak (%) | Avg Latency (ms) | P95 (ms) | P99 (ms) | Error Rate (%) | 초기 잔량 | 사용자 총 채굴량 | Mining Log 총 채굴량 | 실제 잔량 | 정합성 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10 | 10 | 264.69 | 61.65 | 50.481 | 11.472 | 36.722 | 75.852 | 94.368 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 10 | 10 | 336.7 | 78.802 | 65.385 | 12.21 | 28.545 | 60.417 | 84.16 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 10 | 10 | 374.672 | 78.268 | 62.085 | 8.894 | 25.405 | 47.567 | 65.727 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 10 | 10 | 405.515 | 71.3 | 54.286 | 12.53 | 23.467 | 48 | 64.501 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 10 | 10 | 433.839 | 63.633 | 42.925 | 11.481 | 21.741 | 45.011 | 59.117 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 10 | 50 | 482.765 | 73.215 | 55.981 | 12.432 | 101.897 | 137.831 | 164.872 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 10 | 50 | 515.996 | 45.603 | 21.127 | 12.178 | 95.511 | 125.497 | 140.106 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 10 | 50 | 522.63 | 44.985 | 26.19 | 9.448 | 94.175 | 123.73 | 153.164 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 10 | 50 | 526.205 | 48.144 | 22.791 | 11.244 | 93.503 | 123.458 | 140.997 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 10 | 50 | 527.037 | 48.832 | 25.822 | 10.786 | 93.29 | 121.089 | 139.954 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 10 | 100 | 522.903 | 47.664 | 23.256 | 12.602 | 188.729 | 220.035 | 246.377 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 10 | 100 | 533.646 | 47.043 | 22.749 | 12.307 | 185.188 | 214.762 | 234.261 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 10 | 100 | 532.51 | 47.412 | 23.364 | 12.595 | 185.412 | 214.242 | 234.955 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 10 | 100 | 533.191 | 46.931 | 23.223 | 12.756 | 185.372 | 215.475 | 234.216 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 10 | 100 | 531.887 | 46.045 | 23.333 | 12.667 | 185.548 | 217.251 | 333.618 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 10 | 300 | 522.339 | 46.122 | 22.222 | 13.777 | 559.684 | 950.958 | 1288.799 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 10 | 300 | 506.406 | 47.226 | 23.81 | 14.106 | 578.067 | 993.698 | 1284.62 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 10 | 300 | 523.716 | 47.064 | 22.072 | 14.644 | 557.561 | 951.299 | 1291.236 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 10 | 300 | 518.744 | 46.537 | 22.222 | 14.425 | 562.779 | 956.14 | 1288.496 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 10 | 300 | 525.357 | 46.724 | 22.43 | 15.346 | 555.556 | 948.539 | 976.372 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 10 | 500 | 527.437 | 47.358 | 22.535 | 15.853 | 931.02 | 1324.831 | 1659.504 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 10 | 500 | 528.793 | 46.979 | 22.857 | 16.752 | 928.619 | 1319.871 | 1657.138 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 10 | 500 | 528.362 | 47.923 | 26.168 | 16.527 | 929.453 | 1321.106 | 1653.319 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 10 | 500 | 529.341 | 46.204 | 23.474 | 16.6 | 927.631 | 1319.927 | 1653.405 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 10 | 500 | 529.078 | 46.3 | 22.43 | 17.5 | 928.327 | 1318.806 | 1652.585 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 30 | 10 | 276.549 | 82.856 | 72.906 | 13.918 | 34.561 | 66.706 | 83.688 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 30 | 10 | 334.672 | 62.202 | 49.282 | 14.122 | 28.699 | 57.906 | 90.935 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 30 | 10 | 380.373 | 16.154 | 12.255 | 14.259 | 25.374 | 55.183 | 74.55 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 30 | 10 | 411.692 | 0.495 | 0.503 | 11.889 | 22.861 | 46.991 | 64.036 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 30 | 10 | 440.335 | 0.99 | 0 | 12.855 | 20.937 | 44.881 | 65.388 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 30 | 50 | 483.325 | 60.28 | 37.5 | 14.473 | 98.054 | 262.753 | 406.489 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 30 | 50 | 514.774 | 50.905 | 25 | 9.534 | 91.808 | 258.958 | 408.495 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 30 | 50 | 523.78 | 47.485 | 21.596 | 11.553 | 90.257 | 250.881 | 398.169 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 30 | 50 | 524.384 | 52.813 | 28.111 | 14.524 | 90.26 | 258.515 | 390.696 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 30 | 50 | 528.318 | 47.443 | 22.43 | 12.819 | 89.784 | 252.765 | 390.179 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 30 | 100 | 525.597 | 46.976 | 21.801 | 14.862 | 184.428 | 354.526 | 506.411 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 30 | 100 | 527.287 | 46.513 | 20.93 | 14.909 | 184.173 | 354.005 | 507.003 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 30 | 100 | 528.513 | 46.084 | 21.327 | 14.262 | 183.481 | 356.412 | 507.983 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 30 | 100 | 526.815 | 46.971 | 21.495 | 13.14 | 184.324 | 350.98 | 492.472 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 30 | 100 | 529.297 | 46.759 | 20.853 | 13.052 | 182.99 | 349.888 | 496.712 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 30 | 300 | 517.215 | 49.065 | 27.103 | 16.358 | 566.233 | 933.768 | 1181.082 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 30 | 300 | 522.73 | 47.464 | 22.535 | 17.07 | 560.709 | 924.346 | 1174.662 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 30 | 300 | 522.266 | 46.534 | 21.86 | 16.972 | 560.747 | 921.314 | 1172.912 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 30 | 300 | 522.839 | 47.01 | 22.326 | 16.871 | 560.293 | 921.814 | 1173.56 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 30 | 300 | 523.469 | 48.177 | 22.222 | 17.484 | 559.431 | 923.755 | 1171.828 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 30 | 500 | 522.706 | 47.425 | 21.963 | 17.668 | 940.471 | 1315.279 | 1563.52 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 30 | 500 | 522.854 | 47.96 | 22.897 | 17.593 | 940.571 | 1308.353 | 1558.165 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 30 | 500 | 522.237 | 47.884 | 22.699 | 18.797 | 941.952 | 1310.172 | 1557 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 30 | 500 | 522.171 | 46.768 | 22.685 | 19.713 | 941.578 | 1310.848 | 1553.148 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 30 | 500 | 516.705 | 45.992 | 22.326 | 20.148 | 952.707 | 1309.231 | 1569.608 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 50 | 10 | 267.881 | 0.5 | 0.503 | 10.303 | 35.701 | 76.761 | 111.083 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 50 | 10 | 345.066 | 77.824 | 62.5 | 10.306 | 27.478 | 58.829 | 87.371 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 50 | 10 | 372.995 | 78.259 | 62.019 | 11.521 | 25.35 | 56.353 | 83.185 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 50 | 10 | 415.455 | 70.507 | 52.133 | 12.364 | 22.549 | 55.685 | 113.313 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 50 | 10 | 442.674 | 63.15 | 41.509 | 12.26 | 21.462 | 45.605 | 58.762 | 0 | 1000 | 1000 | 1000 | 0 | ✅ |
| 50 | 50 | 468.121 | 74.519 | 60.386 | 11.78 | 100.061 | 331.414 | 538.325 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 50 | 50 | 502.917 | 50.472 | 23.944 | 12.831 | 91.4 | 307.747 | 481.653 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 50 | 50 | 506.894 | 47.614 | 23.077 | 13.28 | 91.813 | 299.958 | 444.389 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 50 | 50 | 500.25 | 46.984 | 18.692 | 10.694 | 93.196 | 313.696 | 481.347 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 50 | 50 | 511.3 | 46.441 | 19.159 | 13.444 | 91.63 | 285.471 | 464.803 | 0 | 5000 | 5000 | 5000 | 0 | ✅ |
| 50 | 100 | 499.6 | 49.056 | 25.701 | 12.058 | 192.89 | 424.657 | 603.514 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 50 | 100 | 494.829 | 47.923 | 21.918 | 13.545 | 193.93 | 427.088 | 622.355 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 50 | 100 | 495.909 | 50.212 | 25.926 | 12.432 | 194.761 | 424.855 | 620.377 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 50 | 100 | 496.081 | 49.336 | 22.172 | 13.577 | 194.417 | 423.102 | 622.366 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 50 | 100 | 497.636 | 49.091 | 21.429 | 12.316 | 193.82 | 412.312 | 588.599 | 0 | 10000 | 10000 | 10000 | 0 | ✅ |
| 50 | 300 | 497.249 | 48.901 | 20.961 | 14.652 | 589.066 | 967.476 | 1178.925 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 50 | 300 | 495.54 | 46.825 | 21.86 | 15.424 | 590.187 | 965.091 | 1172.53 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 50 | 300 | 449.001 | 47.685 | 24.444 | 14.906 | 654.873 | 1092.45 | 1392.663 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 50 | 300 | 485.57 | 46.521 | 22.535 | 15.453 | 602.966 | 978.092 | 1212.009 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 50 | 300 | 417.223 | 45.997 | 23.113 | 16.561 | 699.39 | 1182.596 | 1452.286 | 0 | 30000 | 30000 | 30000 | 0 | ✅ |
| 50 | 500 | 452.435 | 47.345 | 25.11 | 16.782 | 1087.633 | 1538.136 | 1811.782 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 50 | 500 | 453.141 | 45.966 | 25.238 | 17.333 | 1087.942 | 1522.567 | 1789.442 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 50 | 500 | 468.27 | 47.473 | 22.541 | 18.078 | 1050.641 | 1455.055 | 1692.177 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 50 | 500 | 468.384 | 48.823 | 24 | 18.735 | 1051.139 | 1443.575 | 1655.91 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
| 50 | 500 | 486.519 | 46.975 | 23.913 | 18.059 | 1012.998 | 1395.309 | 1613.651 | 0 | 50000 | 50000 | 50000 | 0 | ✅ |
분석
가설 1. 낙관적 락은 Lost Update를 방지하여 데이터 정합성을 유지할 것이다.
낙관적 락은 성공적으로 Lost Update를 방지했다. 하지만 Error Rate Peak 값 92.81%, DB 커넥션 사이즈별(10, 30, 50) 각각 86%, 90%, 91%대의 높은 수치를 보였다. VU가 달라짐에 따라서는 유의미한 상승폭을 보이지 않았다.
| 초기 잔량 | VU | 사용자 총 채굴량 | 실제 잔량 | 정합성 |
|---|---|---|---|---|
| 30000 | 300 | 2157 | 27843 | ✅ (2157 + 27843 = 30000) |
| 50000 | 500 | 3717 | 46283 | ✅ (3717 + 46283 = 50000) |
가설 2. 낙관적 락은 오류를 반환하여 어떤 유저는 100개를 채굴하지 못 할 것이다.
실제로 가설1 에서의 표를 참고했을 때, 어떤 유저가 아닌 거의 모든 유저가 목표량(100개)를 채굴하지 못했다. 최소 관측 VU(10)에서도 높은 확률로 에러가 발생했으며, 거의 모든 유저가 목표량(100개)을 채굴하지 못했다.
| Hikari Max Pool Size | 초기 잔량 | VU | 사용자 총 채굴량 | 실제 잔량 | Error Rate (%) |
|---|---|---|---|---|---|
| 10 | 1000 | 10 | 164 | 836 | 83.6 |
| 10 | 1000 | 10 | 168 | 832 | 83.2 |
| 10 | 1000 | 10 | 170 | 830 | 83 |
| 10 | 1000 | 10 | 175 | 825 | 82.5 |
| 10 | 1000 | 10 | 177 | 823 | 82.3 |
| 30 | 1000 | 10 | 159 | 841 | 84.1 |
| 30 | 1000 | 10 | 165 | 835 | 83.5 |
| 30 | 1000 | 10 | 170 | 830 | 83 |
| 30 | 1000 | 10 | 171 | 829 | 82.9 |
| 30 | 1000 | 10 | 177 | 823 | 82.3 |
| 50 | 1000 | 10 | 165 | 835 | 83.5 |
| 50 | 1000 | 10 | 169 | 831 | 83.1 |
| 50 | 1000 | 10 | 170 | 830 | 83 |
| 50 | 1000 | 10 | 177 | 823 | 82.3 |
| 50 | 1000 | 10 | 181 | 819 | 81.9 |
가설 3. 비관적 락은 Lost Update를 방지하여 데이터 정합성을 유지할 것이다.
가설 4. 비관적 락은 부하가 늘어남에 따라 TPS가 떨어질 것이다.
비관적 락은 Lost Update를 방지하여 데이터 정합성을 성공적으로 유지하면서도, 에러를 발생시키지 않았다. 하지만 그 Trade Off로 TPS가 감소했고, P95 수치가 약 3배가량 크게 증가했다. 경험상 수강 신청, 티켓팅과 같은 도메인은 용인될 수 있겠지만(요청에 성공만 한다면) 게임과 같은 형식을 지니는 Gold-Rush-Lab에서는 이와 같은 상황이 벌어진다면 유저 수를 크게 잃을 가능성이 크다는 생각이 들었다.
낙관적 락과 비관적 락의 P95 응답 시간 비교
낙관적 락과 비관적 락의 Error Rate 비교
가설 5. 비관적 락은 Lock 대기가 길어지면 일부 요청에서 타임아웃이 발생할 것이다.
가설은 맞지 않았다.
모든 부하 조건에서 Error Rate는 0%를 유지했으며, 타임아웃으로 인해 실패한 요청은 관측되지 않았다.
다만 부하가 증가할수록 Lock을 획득하기 위한 대기 시간이 증가하면서 응답 지연이 크게 증가하였다. 특히 P95 수치는 Connection Pool Size 10 기준 약 55 ms(VU 10) 에서 1,320 ms(VU 500) 까지 증가했으며, Pool Size 50에서는 1,470 ms 수준까지 증가하였다.
즉, 이번 실험 환경에서는 비관적 락이 요청을 실패시키기보다 대기(Blocking) 시키는 방식으로 동작하였으며, 타임아웃이 발생할 정도로 Lock Wait Timeout 설정에 도달하지는 않았다.
따라서 비관적 락의 특징은 “실패를 허용하지 않는 대신 응답 시간이 증가한다.”는 점으로 해석할 수 있다.
가설 6. 낙관적, 비관적 락을 적용함으로써 DB에서의 Connection Pool Size와 관련된 병목은 해결될 것이다.
이번 실험 결과를 통해 Connection Pool Size를 증가시키는 것만으로는 병목이 해결되지 않음을 확인하였다.
낙관적 락에서는 Connection Pool Size를 10에서 30, 50으로 증가시켜도 TPS는 거의 증가하지 않았으며, 오히려 동시에 더 많은 트랜잭션이 동일한 데이터(Row)에 접근하면서 Version 충돌이 증가하여 Error Rate가 약 86% → 93% → 95% 수준으로 높아졌다.
낙관적 락과 비관적 락의 TPS 비교
비관적 락에서도 Connection Pool Size를 증가시킨다고 처리량이 증가하지는 않았다. 오히려 Pool Size가 커질수록 동시에 Lock을 기다리는 트랜잭션 수가 증가하면서 P95 Latency가 더욱 증가하는 경향을 보였다. 특히 Pool Size 50에서는 TPS가 소폭 감소하는 반면 대기 시간은 가장 크게 증가하였다.
이는 두 락 방식 모두 Connection Pool이 병목의 원인이 아니라, 하나의 Row에 대한 Hotspot 경쟁 자체가 병목임을 의미한다. Connection Pool은 병목의 위치를 바꾸거나 경쟁 양상을 변화시킬 뿐, Hotspot 환경에서는 근본적인 해결책이 될 수 없었다.
따라서 Scale-out이나 Distributed Lock과 같은 다음 단계의 실험은 Connection Pool을 조정하는 것이 아니라 Hotspot 자체를 어떻게 완화할 것인가를 검증하는 과정이라고 볼 수 있다.
결론
1. 락과 TPS
락을 적용하지 않은 버전 v0.1과 낙관적 락, 비관적 락을 적용한 v0.2의 TPS는 그 성능이 높은 순으로 나열 했을 때 다음과 같았다.
- DB Pool Size 10, VU 500, 평균 값 기준, req/s(소수점 아래 생략)
- Optimistic Lock(1676) > No Lock(669) ≥ Pessimistic Lock(528)
- 단, No Lock과 Pessimistic Lock은 다른 VU, Connection Pool Size에서 순위가 뒤바뀌기도 하므로 ‘같거나 크다’ 로 표현하였다.
먼저 낙관적 락은 가장 높은 TPS를 보여줬다. 이는 현재 수정하려는 엔티티의 버전이 충돌하는 경우 대기하지 않고 바로 클라이언트에게 409_Conflict 를 전달했기 때문에 가능한 수치로 확인되었다.
빠른 TPS를 기록하는 대신 오류율이라는 Trade Off가 존재했다.
다음으로 락을 획득하지 않은채로 처리하는 v0.1, No Lock 버전이다. 이 글을 읽는 사람이라면 이미 알고 있는 사실이겠지만 v0.1은 사실 데이터 정합성, 그중에서도 Lost Update를 고려한다면 비교 대상이 될 수 없다. 또한 비관적 락과 비교했을 때 TPS가 극적으로 차이 나지 않았기 때문에, 동시성 이슈 가능성이 있을 수 있는 엔티티, 그리고 그에 대한 회피 정책이 마련되어 있지 않다면 낙관적 락이 그 첫 번째 고려 대상이 될 것이다.
다음으로 근소하게 가장 낮은 TPS를 보인 비관적 락은 어떠한 오류와 동시성 문제 없이 테스트를 마쳤다. 하지만 역시 낮은 TPS 라는 Trade Off 가 존재했다.
성능과 그 파급효과를 고려하였을 때, 낙관적 락과 비관적 락의 선택은 정책과 도메인의 특성에 달려 있다.
- 낙관적 락
- 요청 후, 성공하지 못하더라도 빠른 피드백이 필요한 경우
- 요청 후, 실패했다면 또 다른 비즈니스 로직으로 유도 되어야 하는경우
- 높은 처리량(TPS)이 중요한 서비스
- 프로필 수정, 게시글 수정 등
- 비관적 락
- 요청 후, 늦더라도 항상 성공해야하는 경우
- 동일 자원에 대한 동시 수정 자체를 허용하면 안 되는 경우
- 계좌 이체, 쿠폰 선착순 지급, 좌석 예약 등
2. 락과 DB 병목
DB에서의 병목이 ‘해결’ 될 것 이라는 예상과는 달리 완화된 형태로 여전히 존재했다.
낙관적 락 실행 중 관측된 PostgreSQL 대기 이벤트
어떠한 쿼리가 실제로 병목의 원인이 되는지 파악 하기 위해 아래 쿼리를 통해 확인해보았다.
SELECT
blocked.pid AS blocked_pid,
blocked.wait_event_type,
blocked.wait_event,
now() - blocked.query_start AS blocked_duration,
blocked.query AS blocked_query,
blocker.pid AS blocker_pid,
now() - blocker.query_start AS blocker_duration,
blocker.query AS blocker_query
FROM pg_stat_activity blocked
CROSS JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS bpid
JOIN pg_stat_activity blocker
ON blocker.pid = bpid
WHERE blocked.wait_event_type = 'Lock'
ORDER BY blocked_duration DESC;
pg_stat_activity로 확인한 낙관적 락의 병목 쿼리
실제로 병목을 일으키는 위 이미지의 쿼리를 통해 낙관적 락의 작동방식과 병목의 원인을 이해하게 되었다.
update mine set remaining_amount=$1,version=$2 where id=$3 and version=$4
낙관적 락을 이해하는 방식을 보통 다음과 같이 이해하고는 한다.
- 데이터 조회 → 데이터 수정 → 검증 → 수정 성공
하지만 실제 쿼리의 구조는 다음과 같다.
- 데이터 조회 → (데이터 수정, 검증) → 수정 성공
검증은 version을 UPDATE 구문의 WHERE 조건절에 함께 끼워 넣음으로써 현재 버전을 판단한다.
또한 버전을 미리 검사하고 UPDATE 쿼리를 DB로 전송하는 것이 아니기 때문에, UPDATE 쿼리는 DB에서 처리하게 된다.
위 과정중에서 한번의 쿼리가 또 들어오게 되면, 이미 실행중인 트랜잭션과 같은 hotspot row 에 대해서 commit 이나 rollback을 기다려야 한다. 경합이 일어나는 쿼리의 수는 결과적으로 v0.1, No Lock과 같다는 것이다.
그렇다면 왜 v0.1보다 낙관적 락이 더 높은 TPS를 보이는 것일까 ?
하나의 row에 대한 UPDATE 는 하나의 트랜잭션에만 영향을 받는다. 그 근거는 공식문서의 두 섹션으로부터 얻을 수 있었다.
… Note that a transaction can hold conflicting locks on the same row, even in different subtransactions; but other than that, two transactions can never hold conflicting locks on the same row. Row-level locks do not affect data querying; they block only writers and lockers to the same row. …
하나의 트랜잭션은 서로 충돌하는 Lock을 동일한 행(Row) 에 대해 보유할 수 있으며, 이는 서로 다른 서브트랜잭션(Subtransaction) 간에도 가능하다. 그러나 이를 제외하면, 두 개의 서로 다른 트랜잭션이 동일한 행에 대해 서로 충돌하는 Lock을 동시에 보유하는 일은 절대 발생하지 않는다.
Row-level Lock은 데이터 조회(Query) 에는 영향을 주지 않는다. 대신 동일한 행을 수정하려는 쓰기 작업(Writer)이나 해당 행에 Lock을 획득하려는 작업(Locker)만 차단(Block) 한다.
…
UPDATE,DELETE,SELECT FOR UPDATE, andSELECT FOR SHAREcommands behave the same asSELECTin terms of searching for target rows: they will only find target rows that were committed as of the command start time. However, such a target row might have already been updated (or deleted or locked) by another concurrent transaction by the time it is found. In this case, the would-be updater will wait for the first updating transaction to commit or roll back (if it is still in progress). If the first updater rolls back, then its effects are negated and the second updater can proceed with updating the originally found row. If the first updater commits, the second updater will ignore the row if the first updater deleted it, otherwise it will attempt to apply its operation to the updated version of the row. The search condition of the command (theWHEREclause) is re-evaluated to see if the updated version of the row still matches the search condition. If so, the second updater proceeds with its operation using the updated version of the row. In the case ofSELECT FOR UPDATEandSELECT FOR SHARE, this means it is the updated version of the row that is locked and returned to the client. …
*UPDATE,DELETE,SELECT FOR UPDATE,SELECT FOR SHARE명령은 대상 Row를 검색하는 방식에서는SELECT와 동일하게 동작한다. 즉, 명령이 시작된 시점까지 Commit된 Row만 검색 대상이 된다.*그러나 검색된 대상 Row는, 조회된 시점에는 존재했더라도 다른 동시 실행 트랜잭션에 의해 이미 수정(UPDATE), 삭제(DELETE) 또는 Lock이 획득되었을 수도 있다.
이 경우, 후행 트랜잭션은 선행 트랜잭션이 Commit 또는 Rollback될 때까지 기다린다(아직 실행 중이라면).선행 트랜잭션이 Rollback되면, 해당 변경 사항은 취소되므로 후행 트랜잭션은 원래 검색했던 Row를 그대로 수정할 수 있다. 선행 트랜잭션이 Commit되면, 후행 트랜잭션은 선행 트랜잭션이 Row를 삭제한 경우에는 해당 Row를 무시하고, 그렇지 않은 경우에는 갱신된(Row의 최신 버전)에 대해 자신의 작업을 수행하려고 시도한다.
*이때 명령문의 검색 조건(
WHERE절)은 갱신된 Row에 대해 다시 평가(re-evaluate)된다.갱신된 Row가 여전히WHERE조건을 만족하면, 후행 트랜잭션은 최신 버전의 Row를 대상으로 작업을 계속 수행한다.SELECT FOR UPDATE와SELECT FOR SHARE의 경우에는 최신 버전의 Row에 Lock을 획득한 뒤, 그 Row를 클라이언트에 반환한다.
요약하자면, 하나의 Row는 하나의 트랜잭션 만이 수정할 수 있고, 두 쿼리가 동시에 들어온 경우 선행 트랜잭션이 Commit 되거나 Rollback 될 때 까지 Row Lock을 획득하기 위해 대기 후 재평가한 다는 것이다.
이를 토대로 다시 v0.1에서 발생한 쿼리를 바라보면 다음과 같은 모습일 것이다.
update mine set remaining_amount=$1 where id=$2
▲
|
| 경합 발생! ROW LOCK 획득을 대기한다.
| (transactionid wait)
|
update mine set remaining_amount=$1 where id=$2
...(DB Connection Pool 이 커질수록 대기도 길어진다.)
공식문서에서 낙관적 락의 의문을 해결해줄 또 하나의 핵심적인 문장을 확인할 수 있었다.
이때 명령문의 검색 조건(WHERE 절)은 갱신된 Row에 대해 다시 평가(re-evaluate)된다.
이제 v0.2 optimistic lock 을 적용한 쿼리를 바라보면 다음과 같은 모습일 것이다.
update mine set remaining_amount=$1,version=$2 where id=$3 and version=1 -- 수행! 수정 성공, version 변경
-- version = 2
update mine set remaining_amount=$1,version=$2 where id=$3 and version=1 -- affected 0 row 실패!
update mine set remaining_amount=$1,version=$2 where id=$3 and version=1 -- affected 0 row 실패!
결국, v0.1과 v0.2 optimistic lock 사이에서 병목 발생의 수는 비슷할 것이나, 낙관적 락은 충돌한 요청을 빠르게 실패시켜 트랜잭션의 대기 시간을 줄이고 TPS를 보장했다.
마지막으로 v0.2 pessimistic lock은 어떨까?
비관적 락에서는 새로운 형태의 wait_event를 확인할 수 있었다.
비관적 락 실행 중 관측된 PostgreSQL 대기 이벤트
또 한번, 병목이 되는 쿼리를 확인하기 위해 pg_stat_activity 테이블을 확인했다.
pg_stat_activity로 확인한 비관적 락의 병목 쿼리
이미지에서의 blocker, blocked 쿼리는 동일하게 다음과 같았다.
select me1_0.id,me1_0.created_at,me1_0.remaining_amount,me1_0.version
from mine me1_0
where me1_0.id=$1
for no key update of me1_0
하나의 의외인 점이 존재했다. transactionid에 의해 블로킹되는 쿼리는 UPDATE일 것이라 예상했는데, 이 또한 tuple과 동일한 SELECT 구문이었다.
처음에는 SELECT 문이 어떻게 transactionid를 기다릴 수 있는지 의문이 들었다. 일반적인 SELECT는 단순 조회만 수행하며 Row-Level Lock을 획득하지 않기 때문이다.
하지만 Hibernate가 생성한 SQL을 확인해보니, 실제 실행된 쿼리는 단순 SELECT가 아니라 FOR NO KEY UPDATE가 포함된 Locking Read였다.
FOR NO KEY UPDATE는 조회와 동시에 대상 Row에 Row Lock을 획득하는 명령이다. 따라서 여러 트랜잭션이 동일한 Row에 대해 동시에 해당 쿼리를 수행하면, Lock을 먼저 획득한 트랜잭션을 제외한 나머지 트랜잭션은 대기 상태에 진입하게 된다.
select me1_0.id,me1_0.created_at,me1_0.remaining_amount,me1_0.version
from mine me1_0
where me1_0.id=$1
for no key update of me1_0
▲ (Row Lock 획득 !)
|
| (Row Lock 획득 대기..(tuple) -> 선행 트랜잭션 종료 대기(transactionid)
select me1_0.id,me1_0.created_at,me1_0.remaining_amount,me1_0.version
from mine me1_0
where me1_0.id=$1
for no key update of me1_0
이 과정에서 PostgreSQL은 Lock 획득 단계에 따라 서로 다른 wait_event를 기록한다. 먼저 동일한 Tuple에 대한 Lock 획득을 시도하는 과정에서는 tuple이 관측될 수 있으며, 이미 다른 트랜잭션이 해당 Row를 점유하고 있는 것이 확인되면 선행 트랜잭션의 종료를 기다리는 단계로 전환되어 transactionid가 기록된다.
따라서 tuple과 transactionid는 서로 다른 병목이 아니라, 동일한 Row-Level Lock 획득 과정에서 관측되는 서로 다른 대기 단계라고 볼 수 있다.
즉, 비관적 락에서는 Row-Level Lock을 UPDATE 시점이 아니라 SELECT ... FOR NO KEY UPDATE 단계에서 미리 획득한다.
이로 인해 동일 Row에 접근하는 후행 트랜잭션은 비즈니스 로직을 수행하기 전부터 Lock 대기에 진입하게 된다. 결과적으로 Lock을 보유하는 시간이 가장 길어지고 병렬성이 크게 감소하여, 세 가지 방식 중 가장 낮은 TPS를 기록하였다.
추가
CPU와 관련된 이슈
v0.2에서의 실험만 138 회 실험을 모두 마친후에 VM에서 vCPU의 수를 ec2의 vCPU와 맞추지 않은 것을 알아챘다..
- DB Connection Pool(10, 30, 50)
- VU (warm-up, 10, 50, 100, 300, 500)
- 총 재시도 횟수 3회
- 3(1 + (3 * 3 * 5))
실의에 빠져 있다가, 이 참에 실험을 자동화하면 휴먼 에러도 줄고, 실험과 동시에 결과를 분석하는 시간을 가질 수 있을 것이라는 생각이 들어 Codex와 함께 자동화했다.
자동화
자동화는 DB 초기화 → 요청 → 기록 → csv 생성 → VU 증가 와 같은 흐름으로 구성했다. 처음에는 k6의 TPS와 Grafana의 TPS가 일치하지 않아서 꽤 많은 시간을 사용했지만 두 가지 문제가 있는 것을 확인했다.
- Prometheus의 갱신 주기, Grafana의 갱신 주기가 맞지 않음 → Grafana에서의 해상도 저하
- Grafana가 그래프에 표현하기 위해 TPS를 계산하는 구간이 1분으로 설정 → Grafana에서의 해상도 저하
Grafana에서의 갱신 간격은 모두 1초로 맞추었지만, TPS의 최댓값을 얻어내기에 가장 정확한 것은 k6를 통해 얻는 순간적인 TPS라서, CSV 내에서는 k6의 기록을 활용하기로 했다.
Optimistic-Lock 에서의 재시도
Optimistic-Lock에서는 version 충돌 시 ObjectOptimisticLockingFailureException 반환하기 때문에 try-catch를 통해서 재시도 로직을 통해 기다릴지, 해당 요청을 버릴지 등 정책을 적용할 수 있다.
다만 이 실험에서는 낙관적 락의 기본 특성만으로 실험하고자 했기 때문에 연달아 실험하지 않았다.
추후 재시도 처리를 적용한 낙관적 락과 비관적 락의 차이점도 알아볼 예정이다. (Lost Update, 데이터 정합성과 관련한 문제는 둘 모두 해결 할 수 있지만 조회 시점에서 획득한 락: 비관적 락이 성능 상 조금 더 유리 할 것으로 예상이된다.)
댓글남기기