[Project : Gold-Rush-Lab] 4. 분산 시스템과 분산 락
개요
v0.3 버전에서는 API 서버를 증설하고, 낙관적 락에 대한 재시도 로직을 추가하여 TPS, P95, P99 등 유저에게 직접적으로 영향을 끼치는 성능을 개선하고자 하였다.
하지만 API 서버 증설, 스케일 아웃만으로는 근본적인 DB 병목을 효과적으로 개선하지 못했다. 이로 인해 DB에서의 병목을 완화하고 병목을 관리할 수 있는 애플리케이션 레이어로 이동시키기 위해 분산 락(Distributed Lock)을 적용하기로 했다.
왜 Redis를 선택했을까?
막상 v0.4까지 와서 적용하려니, “왜 Redis여야만 하는지”에 대한 의문이 생겼다. 그래서 분산 락을 구현할 수 있는 수단을 AI를 활용해 검색했더니, 사용 사례까지 친절하게 알려줬다.
| 방식 | 특징 | 사용 사례 |
|---|---|---|
| Redis | 빠르고 구현이 쉬움 | 가장 많이 사용 |
| ZooKeeper | 강한 일관성, 분산 코디네이션 | Hadoop, Kafka(예전) |
| etcd | Kubernetes가 사용하는 분산 KV 저장소 | Kubernetes, Cloud Native |
| Consul | 서비스 디스커버리 + 분산 락 | HashiCorp 생태계 |
| DB Lock | DB 자체의 Lock 사용 | 규모가 작거나 DB 중심 시스템 |
| Hazelcast / Ignite | In-memory Data Grid | Java 기반 클러스터 |
우선 이미 실험 과정에 포함되었던, 그리고 v0.4의 취지에 맞지 않는 DB Lock과 Gold-Rush-Lab의 상황에 맞지 않는 etcd는 바로 제외했다. Consul은 MSA에서 동적 스케일 아웃 시 서비스를 등록하는 서비스 디스커버리가 주된 기능이고, 분산 락은 KV 스토어가 존재해서 지원하는 부가적인 기능과 같은 인상을 주었다. Hazelcast는 같은 느낌으로 분산 자료구조, Ignite는 분산 데이터베이스가 주된 기능이되, 분산 락을 지원한다고 한다.
사실상 etcd와 DB Lock을 제외한 모두가 후보군으로 보인다. (Redis와 ZooKeeper에 대해선 이미 알고 있다.) 모두 구현할 수는 없다. Gold-Rush-Lab은 DB와 같은 특정 노드를 제외하고, 메모리도 CPU도 넉넉하지 않아 EC2의 medium급에 해당하는 인스턴스만 사용할 수 있다. 따라서 ‘가용 자원’을 우선적인 기준으로 다시 소거해 나가고자 했다.
그런 의미에서 Consul은 애초에 활용조차 불가능했다.
Consul 공식 문서의 서버 권장 사양
Hazelcast도 마찬가지였다.
Hazelcast 공식 문서의 서버 권장 사양
Ignite는 공식 문서에서 권장 하드웨어 정보를 찾을 수는 없었지만, 감사하게도 이미 차이를 확인해본 분의 게시글을 통해 소거할 수 있었다. 게시글의 내용을 토대로 Ignite는 위에서 언급한 것과 같이 분산 데이터베이스가 주된 기능이고, 분산 락만 적용하고자 하는 Gold-Rush-Lab에는 다소 무거울 것이라는 결론을 내려 제외하기로 결정했다.
결국 Redis와 ZooKeeper가 최종 후보로 남았다.
두 기술 모두 분산 락을 구현할 수 있었지만, Gold-Rush-Lab에서 분산 락이 필요한 구간은 PostgreSQL에서 동일한 광산 데이터를 조회하고 수정한 뒤 트랜잭션을 커밋하는 짧은 구간뿐이었다.
반면 ZooKeeper는 분산 락 외에도 여러 서버의 역할을 조정하거나(Leader Election), 서버 간 설정을 공유하고(Configuration Management), 장애가 발생한 서버를 감지하는 등 여러 서버가 서로 협력하며 동작하도록 관리하는 기능까지 제공하는 분산 코디네이션 시스템이다.
Gold-Rush-Lab에서는 이러한 기능은 오버엔지니어링이며, Redis만으로도 동일한 데이터에 대한 동시 접근을 효과적으로 제어할 수 있었다. 또한 개발과 운영 복잡도 역시 상대적으로 낮았다.
따라서 이번 실험에서는 프로젝트의 요구사항과 운영 환경에 가장 적합한 선택으로 Redis 기반의 분산 락을 적용하기로 결정했다.
가설
가설 1. DB의 병목(lock wait)이 애플리케이션으로 전이되어, DB 자체의 병목은 해소될 것이다.
- Redis를 통해 DB의 병목을 앞단인 애플리케이션 레이어로 전이시켰다. 그로 인해 한 번에 발생하는 트랜잭션의 수가 줄어들며 자연스럽게 DB 내부의 병목도 줄어들 것이다.
가설 2. Lost Update는 일어나지 않고, 모든 유저들은 성공적으로 100개의 금을 채굴할 것이다.
- Redis 분산 락은 하나의 광산에 대해 동시에 하나의 요청만 트랜잭션을 수행하도록 보장한다. 이는 동일한 자원을 여러 사용자가 동시에 수정하더라도 요청이 직렬화되어 Lost Update가 발생하지 않으며, 모든 사용자가 정상적으로 100개의 금을 채굴할 수 있을 것이다.
가설 3. TPS, P95, P99는 비관적 락과 비슷한 수치를 보일 것이다.
- Redis 분산 락 역시 하나의 광산에 대해 요청을 순차적으로 처리하므로, Hot Spot 환경에서는 동시에 하나의 요청만 실제 비즈니스 로직을 수행하게 된다. 따라서 처리 방식이 비관적 락과 유사해져 TPS와 P95, P99 또한 비슷한 수준을 보일 것으로 예상하였다.
가설 4. 애플리케이션의 대기 스레드가 늘어남에 따라 가용 스레드 또한 줄어들 것이다.
- Redis 분산 락은 락을 획득하지 못한 요청이 애플리케이션에서 대기하게 된다. 대기 중인 요청은 스레드를 점유하므로 동시 요청이 증가할수록 대기 스레드 수가 증가하고, 반대로 즉시 요청을 처리할 수 있는 가용 스레드 수는 감소할 것이다.
실험 방법
- 분산 락 구현
@Service
@RequiredArgsConstructor
public class DistributedMiningService {
private final RedissonClient redissonClient;
private final MineService mineService;
public void mine(UUID userSessionId, Long mineId) {
RLock lock = redissonClient.getLock(
"gold-rush:mine:" + mineId);
boolean acquired = false;
try {
// 최대 3초간 락 획득 대기
acquired = lock.tryLock(3, TimeUnit.SECONDS);
if (!acquired) {
throw new LockAcquireException();
}
mineService.mine(userSessionId, mineId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
// 현재 권한이 있는 스레드라면 락 해제
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
@RestController
@RequiredArgsConstructor
public class MineController {
private final UserService userService;
private final MineService mineService;
private final DistributedMiningService distributedMiningService;
...
@PostMapping("/mine")
public ApiResponse<MineRequestDto> mine(
@RequestParam(name = "sessionId") UUID sessionId
) {
// mineService.mine(sessionId, 1L); // 기존 mine 함수 비활성화
distributedMiningService.mine(sessionId, 1L); // 분산 락 적용 mine 활성화
UserEntity foundUser = userService.findBySessionId(sessionId);
Long totalMinedGold = foundUser.getTotalMinedGold();
Long remainingAmount = foundUser.getMine().getRemainingAmount();
return ApiResponse.success(new MineRequestDto(1L, totalMinedGold, remainingAmount));
}
}
- 지표 추가 - Lock Wait
-
자동화 스크립트에서 DB 내부의 Lock Wait를 측정하기 위해
pg_locks와pg_stat_activity를 조회하는 쿼리를 추가하였다.SELECT l.pid, a.backend_xid, l.virtualtransaction, a.xact_start, l.waitstart, clock_timestamp() - l.waitstart AS lock_wait_duration, l.locktype, l.mode, l.relation::regclass AS relation, l.page, l.tuple, l.transactionid, pg_blocking_pids(l.pid) AS blocking_pids, a.query FROM pg_locks l JOIN pg_stat_activity a ON a.pid = l.pid WHERE l.granted = false AND l.waitstart IS NOT NULL ORDER BY l.waitstart; - 해당 쿼리를 1초마다 폴링(Polling) 하여 락 대기 시간(
lock_wait_duration), 락 종류(locktype), 락 모드(mode), 대기 중인 Relation, Blocking Session 등을 수집하였다. - 이를 통해 TPS와 응답 시간뿐만 아니라 DB 내부에서 실제로 얼마나 오랫동안 Lock을 기다렸는지를 함께 분석할 수 있도록 하였다.
- 이를 v0.3에 추가하여 재측정한 결과를 바탕으로 비교한다.
-
실험 환경
Gold-Rush-Lab v0.4 분산 락 실험 환경
- 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
- Redis 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: macOS Tahoe 26
- CPU: M1
- Memory: 8GB
결과
⚠️ Lock Wait 측정 시 주의: Lock Wait는 PostgreSQL의
pg_locks와pg_stat_activity를 1초 간격으로 Polling하여 수집한 보조 지표이다. Polling 기반 측정 특성상 모든 Lock Wait를 포착하지 못할 수 있으므로, 절대적인 수치보다는 병목의 발생 여부와 경향을 분석하기 위한 지표로 활용하였다.**
Scale-Out : Distributed Lock 결과 기록 표(스압)
| Record Type | Target Type | Target | Version | Hikari Max Pool Size | VU | Run | TPS (req/s) | System CPU Peak (%) | Process CPU Peak (%) | JVM Heap Peak (%) | Tomcat Available Threads Min | Tomcat Available Threads Avg | Hikari Active Peak | Avg Latency (ms) | P95 (ms) | P99 (ms) | Error Rate (%) | DB Observed Lock Waits | DB Lock Wait Total (ms) | DB Lock Wait Avg (ms) | DB Lock Wait Max (ms) | DB Lock Wait Poll Interval (s) | Redis Lock Wait Samples | Redis Lock Wait Total (ms) | Redis Lock Wait Avg (ms) | Redis Lock Wait Max (ms) | Redis Lock Timeout Count | 초기 잔량 | 사용자 총 채굴량 | Mining Log 총 채굴량 | 실제 잔량 | 정합성 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| WARMUP | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 10 | 1 | 63.694 req/s | 0 | 140.984 | 662.981 | 832.294 | 0 | 0 | 0 | 0 | 0 | 1 | 108.55 | 12699.375 | 116.991 | 750.592 | 0 | 100 | 100 | 100 | 0 | ✅ | |||||
| WARMUP | BACKEND | 192.168.0.41:8080 | v0.4 | 10 | 1 | 12.192 req/s | 3.005 | 2.538 | 10.255 | 194 | 198.2 | |||||||||||||||||||||
| WARMUP | BACKEND | 192.168.0.46:8080 | v0.4 | 10 | 1 | 0.000 req/s | 1.5 | 1.508 | 13.405 | 193 | 198.3 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 10 | 1 | 157.654 req/s | 0 | 60.565 | 178.841 | 270.668 | 0 | 0 | 0 | 0 | 0 | 1 | 1076.923 | 55103.501 | 51.168 | 750.592 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 10 | 1 | 84.891 req/s | 49.091 | 39.72 | 13.433 | 190 | 197.214 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 10 | 1 | 84.891 req/s | 54.735 | 48.095 | 12.766 | 189 | 196.5 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 10 | 2 | 200.965 req/s | 1 | 47.317 | 136.205 | 229.933 | 0 | 0 | 0 | 0 | 0 | 1 | 1083.333 | 43426.704 | 40.086 | 750.592 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 10 | 2 | 108.856 req/s | 47.685 | 38.785 | 12.999 | 193 | 197.769 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 10 | 2 | 108.856 req/s | 46.181 | 37.736 | 14.153 | 193 | 197.385 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 10 | 3 | 222.519 req/s | 1 | 42.344 | 123.754 | 196.244 | 0 | 0 | 0 | 0 | 0 | 1 | 1083.333 | 38996.751 | 35.997 | 426.456 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 10 | 3 | 120.531 req/s | 43.683 | 34.112 | 12.752 | 190 | 197.077 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 10 | 3 | 120.531 req/s | 36.738 | 25 | 14.116 | 193 | 198 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 10 | 4 | 233.481 req/s | 1 | 41.169 | 118.427 | 177.375 | 0 | 0 | 0 | 0 | 0 | 1 | 1090.909 | 38093.429 | 34.919 | 391.093 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 10 | 4 | 127.353 req/s | 51.451 | 43.128 | 14.968 | 190 | 197.5 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 10 | 4 | 127.353 req/s | 59.016 | 50.943 | 15.041 | 196 | 198.417 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 10 | 5 | 252.461 req/s | 0 | 37.445 | 99.979 | 144.396 | 0 | 0 | 0 | 0 | 0 | 1 | 1090.909 | 34756.179 | 31.86 | 346.578 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 10 | 5 | 137.706 req/s | 3.96 | 2.5 | 13.337 | 191 | 196.667 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 10 | 5 | 137.706 req/s | 19.8 | 14.01 | 14.488 | 193 | 198.083 | |||||||||||||||||||||
| AVERAGE | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 10 | AVERAGE | 213.416 req/s | 0.6 | 45.768 | 131.441 | 203.723 | 0 | 0 | 0 | 0 | 0 | 1 | 1085.082 | 42075.313 | 38.806 | 533.062 | 0 | 1000 | 1000 | 1000 | 0 | ✅ | |||||
| AVERAGE | BACKEND | 192.168.0.41:8080 | v0.4 | 10 | AVERAGE | 115.867 req/s | 39.174 | 31.649 | 13.498 | 190.8 | 197.245 | |||||||||||||||||||||
| AVERAGE | BACKEND | 192.168.0.46:8080 | v0.4 | 10 | AVERAGE | 115.867 req/s | 43.294 | 35.157 | 14.113 | 192.8 | 197.677 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 50 | 1 | 265.520 req/s | 1 | 177.42 | 546.744 | 925.057 | 0 | 0 | 0 | 0 | 0 | 1 | 5192.308 | 891979.778 | 171.789 | 2099.804 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 50 | 1 | 137.866 req/s | 39.066 | 28.571 | 16.363 | 151 | 178.815 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 50 | 1 | 137.866 req/s | 36.817 | 24.091 | 15.579 | 164 | 188.556 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 50 | 2 | 284.139 req/s | 2 | 163.309 | 556.542 | 918.643 | 0 | 0 | 0 | 0 | 0 | 1 | 5200 | 823829.694 | 158.429 | 2099.804 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 50 | 2 | 147.752 req/s | 41.092 | 31.019 | 17.513 | 150 | 173.308 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 50 | 2 | 147.752 req/s | 46.138 | 35.484 | 16.219 | 180 | 195.231 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 50 | 3 | 295.735 req/s | 1 | 158.505 | 583.258 | 998.158 | 0 | 0 | 0 | 0 | 0 | 1 | 5208.333 | 801520.686 | 153.892 | 2099.804 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 50 | 3 | 154.029 req/s | 27.193 | 15.596 | 16.182 | 149 | 170.84 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 50 | 3 | 154.029 req/s | 27.837 | 15.455 | 16.059 | 180 | 195.48 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 50 | 4 | 301.550 req/s | 1 | 156.061 | 578.926 | 885.703 | 0 | 0 | 0 | 0 | 0 | 1 | 5208.333 | 788893.342 | 151.468 | 1676.817 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 50 | 4 | 157.057 req/s | 25.339 | 13.953 | 13.838 | 149 | 172.4 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 50 | 4 | 157.057 req/s | 26.241 | 12.273 | 16.428 | 170 | 194.12 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 50 | 5 | 300.643 req/s | 1 | 155.813 | 552.36 | 834.331 | 0 | 0 | 0 | 0 | 0 | 1 | 5200 | 786753.006 | 151.299 | 1837.93 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 50 | 5 | 156.335 req/s | 24.885 | 12.558 | 17.104 | 150 | 173.385 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 50 | 5 | 156.335 req/s | 28.25 | 14.953 | 16.654 | 176 | 195.731 | |||||||||||||||||||||
| AVERAGE | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 50 | AVERAGE | 289.518 req/s | 1.2 | 162.222 | 563.566 | 912.378 | 0 | 0 | 0 | 0 | 0 | 1 | 5201.795 | 818595.301 | 157.375 | 1962.832 | 0 | 5000 | 5000 | 5000 | 0 | ✅ | |||||
| AVERAGE | BACKEND | 192.168.0.41:8080 | v0.4 | 50 | AVERAGE | 150.608 req/s | 31.515 | 20.34 | 16.2 | 149.8 | 173.749 | |||||||||||||||||||||
| AVERAGE | BACKEND | 192.168.0.46:8080 | v0.4 | 50 | AVERAGE | 150.608 req/s | 33.057 | 20.451 | 16.188 | 174 | 193.823 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 100 | 1 | 299.043 req/s | 2 | 313.694 | 978.89 | 1641.81 | 0 | 0 | 0 | 0 | 0 | 1 | 10243.902 | 3166135.617 | 309.075 | 3229.33 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 100 | 1 | 153.168 req/s | 35.332 | 24.651 | 18.188 | 100 | 151.286 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 100 | 1 | 153.168 req/s | 25.426 | 12.963 | 16.595 | 103 | 171.024 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 100 | 2 | 298.374 req/s | 1 | 312.247 | 1152.257 | 1893.436 | 0 | 0 | 0 | 0 | 0 | 1 | 10243.902 | 3133865.395 | 305.925 | 3481.192 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 100 | 2 | 152.826 req/s | 25.331 | 13.023 | 19.057 | 99 | 134.905 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 100 | 2 | 152.826 req/s | 26.939 | 13.303 | 18.815 | 142 | 188.048 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 100 | 3 | 300.933 req/s | 2 | 309.926 | 996.736 | 1638.3 | 0 | 0 | 0 | 0 | 0 | 1 | 10243.902 | 3128333.4 | 305.385 | 3765.873 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 100 | 3 | 154.136 req/s | 24.654 | 13.084 | 19.949 | 100 | 140.048 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 100 | 3 | 154.136 req/s | 25.926 | 13.889 | 19.191 | 122 | 184.857 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 100 | 4 | 301.405 req/s | 2 | 312.367 | 955.056 | 1515.567 | 0 | 0 | 0 | 0 | 0 | 1 | 10250 | 3154849.727 | 307.79 | 3765.873 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 100 | 4 | 154.470 req/s | 24.851 | 12.617 | 20.346 | 119 | 173.854 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 100 | 4 | 154.470 req/s | 24.967 | 13.488 | 19.433 | 101 | 148.415 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 100 | 5 | 301.178 req/s | 1 | 310.824 | 1173.146 | 1882.615 | 0 | 0 | 0 | 0 | 0 | 1 | 10243.902 | 3137078.229 | 306.239 | 3873.55 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 100 | 5 | 154.262 req/s | 25.179 | 12.903 | 20.569 | 99 | 130.571 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 100 | 5 | 154.262 req/s | 28.176 | 15.068 | 20.269 | 138 | 192.619 | |||||||||||||||||||||
| AVERAGE | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 100 | AVERAGE | 300.186 req/s | 1.6 | 311.812 | 1051.217 | 1714.346 | 0 | 0 | 0 | 0 | 0 | 1 | 10245.122 | 3144052.474 | 306.883 | 3623.164 | 0 | 10000 | 10000 | 10000 | 0 | ✅ | |||||
| AVERAGE | BACKEND | 192.168.0.41:8080 | v0.4 | 100 | AVERAGE | 153.772 req/s | 27.07 | 15.256 | 19.622 | 103.4 | 146.133 | |||||||||||||||||||||
| AVERAGE | BACKEND | 192.168.0.46:8080 | v0.4 | 100 | AVERAGE | 153.772 req/s | 26.287 | 13.742 | 18.861 | 121.2 | 176.992 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 300 | 1 | 297.359 req/s | 2 | 948.632 | 3184.498 | 4656.585 | 0 | 0 | 0 | 0 | 0 | 1 | 30275.229 | 21660211.78 | 715.443 | 8652.835 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 300 | 1 | 150.044 req/s | 26.708 | 14.865 | 24.888 | 0 | 21.709 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 300 | 1 | 150.044 req/s | 25.452 | 13.615 | 20.983 | 60 | 182.2 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 300 | 2 | 292.597 req/s | 2 | 960.714 | 3324.88 | 4755.627 | 0 | 0 | 0 | 0 | 0 | 1 | 30272.727 | 20803519.89 | 687.203 | 8852.417 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 300 | 2 | 147.629 req/s | 29.83 | 16.981 | 27.682 | 0 | 20.892 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 300 | 2 | 147.629 req/s | 31.258 | 18.265 | 21.977 | 70 | 190.333 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 300 | 3 | 291.783 req/s | 2 | 969.064 | 3243.116 | 4754.233 | 0 | 0 | 0 | 0 | 0 | 1 | 30270.27 | 21794924.79 | 720.011 | 9609.087 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 300 | 3 | 147.206 req/s | 26.096 | 13.441 | 29.318 | 0 | 24.402 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 300 | 3 | 147.206 req/s | 26.147 | 14.884 | 23.393 | 41 | 180.446 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 300 | 4 | 295.264 req/s | 2 | 954.394 | 3009.419 | 4612.313 | 0 | 0 | 0 | 0 | 0 | 1 | 30275.229 | 22890226.26 | 756.071 | 9609.087 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 300 | 4 | 148.986 req/s | 26.699 | 13.158 | 30.199 | 0 | 39.836 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 300 | 4 | 148.986 req/s | 29.179 | 15.982 | 25.411 | 0 | 151.718 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 300 | 5 | 295.960 req/s | 2 | 949.961 | 3278.374 | 4696.975 | 0 | 0 | 0 | 0 | 0 | 1 | 30275.229 | 20757765.37 | 685.635 | 11450.975 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 300 | 5 | 149.338 req/s | 26.022 | 13.757 | 31.82 | 0 | 21.673 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 300 | 5 | 149.338 req/s | 26.306 | 13.084 | 26.731 | 78 | 189.973 | |||||||||||||||||||||
| AVERAGE | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 300 | AVERAGE | 294.593 req/s | 2 | 956.553 | 3208.057 | 4695.147 | 0 | 0 | 0 | 0 | 0 | 1 | 30273.737 | 21581329.62 | 712.873 | 9634.88 | 0 | 30000 | 30000 | 30000 | 0 | ✅ | |||||
| AVERAGE | BACKEND | 192.168.0.41:8080 | v0.4 | 300 | AVERAGE | 148.641 req/s | 27.071 | 14.44 | 28.781 | 0 | 25.702 | |||||||||||||||||||||
| AVERAGE | BACKEND | 192.168.0.46:8080 | v0.4 | 300 | AVERAGE | 148.641 req/s | 27.669 | 15.166 | 23.699 | 49.8 | 178.934 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 1 | 295.170 req/s | 2 | 1596.51 | 4593.353 | 6043.085 | 0 | 0 | 0 | 0 | 0 | 1 | 50279.33 | 35776160.78 | 711.548 | 11450.975 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 1 | 148.409 req/s | 28.103 | 16.216 | 30.966 | 0 | 99.654 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 1 | 148.409 req/s | 27.077 | 14.352 | 28.781 | 0 | 185.511 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 2 | 294.048 req/s | 1 | 1605.846 | 4654.524 | 6055.355 | 0 | 0 | 0 | 0 | 0 | 1 | 50280.899 | 35062504.55 | 697.332 | 11451.658 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 2 | 147.850 req/s | 25.965 | 13.115 | 30.23 | 0 | 95.773 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 2 | 147.850 req/s | 25.997 | 13.242 | 27.655 | 0 | 191.078 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 3 | 293.522 req/s | 2 | 1606.983 | 4602.889 | 6066.439 | 0 | 0 | 0 | 0 | 0 | 1 | 50279.33 | 36520197.35 | 726.346 | 11451.658 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 3 | 147.581 req/s | 26.374 | 13.761 | 31.514 | 0 | 88.111 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 3 | 147.581 req/s | 27.218 | 13.889 | 27.655 | 0 | 182.794 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 4 | 294.891 req/s | 2 | 1603.981 | 4654.92 | 6053.922 | 0 | 0 | 0 | 0 | 0 | 1 | 50280.899 | 35746890 | 710.944 | 10872.922 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 4 | 148.274 req/s | 26.683 | 12.844 | 36.378 | 0 | 86.333 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 4 | 148.274 req/s | 26.072 | 12.963 | 29.778 | 0 | 186.408 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 5 | 298.315 req/s | 2 | 1579.832 | 4579.682 | 6044.282 | 0 | 0 | 0 | 0 | 0 | 1 | 50284.091 | 35699509.72 | 709.956 | 11570.5 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 5 | 150.078 req/s | 26.866 | 13.5 | 33.841 | 0 | 88 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 5 | 150.005 req/s | 32.147 | 18.062 | 31.531 | 0 | 187.486 | |||||||||||||||||||||
| AVERAGE | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | AVERAGE | 295.189 req/s | 1.8 | 1598.63 | 4617.073 | 6052.617 | 0 | 0 | 0 | 0 | 0 | 1 | 50280.91 | 35761052.48 | 711.225 | 11359.543 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| AVERAGE | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | AVERAGE | 148.438 req/s | 26.798 | 13.887 | 32.586 | 0 | 91.574 | |||||||||||||||||||||
| AVERAGE | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | AVERAGE | 148.424 req/s | 27.702 | 14.501 | 29.08 | 0 | 186.655 |
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 2 | 147.85 req/s | 25.965 | 13.115 | 30.23 | 0 | 95.773 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 2 | 147.85 req/s | 25.997 | 13.242 | 27.655 | 0 | 191.078 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 3 | 293.522 req/s | 2 | 1606.983 | 4602.889 | 6066.439 | 0 | 0 | 0 | 0 | 0 | 1 | 50279.33 | 36520197.35 | 726.346 | 11451.658 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 3 | 147.581 req/s | 26.374 | 13.761 | 31.514 | 0 | 88.111 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 3 | 147.581 req/s | 27.218 | 13.889 | 27.655 | 0 | 182.794 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 4 | 294.891 req/s | 2 | 1603.981 | 4654.92 | 6053.922 | 0 | 0 | 0 | 0 | 0 | 1 | 50280.899 | 35746890 | 710.944 | 10872.922 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 4 | 148.274 req/s | 26.683 | 12.844 | 36.378 | 0 | 86.333 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 4 | 148.274 req/s | 26.072 | 12.963 | 29.778 | 0 | 186.408 | |||||||||||||||||||||
| RUN | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | 5 | 298.315 req/s | 2 | 1579.832 | 4579.682 | 6044.282 | 0 | 0 | 0 | 0 | 0 | 1 | 50284.091 | 35699509.72 | 709.956 | 11570.5 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| RUN | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | 5 | 150.078 req/s | 26.866 | 13.5 | 33.841 | 0 | 88 | |||||||||||||||||||||
| RUN | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | 5 | 150.005 req/s | 32.147 | 18.062 | 31.531 | 0 | 187.486 | |||||||||||||||||||||
| AVERAGE | LOAD_BALANCER | http://192.168.0.47/api | v0.4 | 20 | 500 | AVERAGE | 295.189 req/s | 1.8 | 1598.63 | 4617.073 | 6052.617 | 0 | 0 | 0 | 0 | 0 | 1 | 50280.91 | 35761052.48 | 711.225 | 11359.543 | 0 | 50000 | 50000 | 50000 | 0 | ✅ | |||||
| AVERAGE | BACKEND | 192.168.0.41:8080 | v0.4 | 500 | AVERAGE | 148.438 req/s | 26.798 | 13.887 | 32.586 | 0 | 91.574 | |||||||||||||||||||||
| AVERAGE | BACKEND | 192.168.0.46:8080 | v0.4 | 500 | AVERAGE | 148.424 req/s | 27.702 | 14.501 | 29.08 | 0 | 186.655 |
분석
가설 1. DB의 병목(lock wait)이 애플리케이션으로 전이되어, DB 자체의 병목은 해소될 것이다.
버전별 DB Lock Wait 평균
버전별 DB Lock Wait 최댓값
v0.4의 분산 락은 낙관적 락과 비관적 락에서 발생하던 DB 내부 병목을 애플리케이션으로 이동시킨 것으로 확인되었다. 이는 Redis가 성공적으로 채굴 함수의 호출을 직렬화하여, DB 내부에서 하나의 Row에 대한 경합을 제거한 것으로 보인다.
가설 2. Lost Update는 일어나지 않고, 모든 유저들은 성공적으로 100개의 금을 채굴할 것이다.
버전별 실제 광산 잔량
Lost Update는 발생하지 않았으며 모든 유저가 100개의 금을 성공적으로 채굴했다. (타임아웃 60초) 이는 위에서 언급한 것과 같이 Redis 분산 락이 적절히 작동하여 하나의 Row에 대해 하나의 트랜잭션을 발생시켰다는 것을 확인할 수 있는 점이었다.
하지만 이는 타임아웃을 60초로 설정한 경우이며, 아래 그래프는 그 설정값에 따라 실패율이 달라질 수 있었음을 보여준다.
Redis Lock Wait 최댓값: 일부 요청은 약 12초 동안 대기
Redis Lock Wait 평균: VU 300 이상에서 약 0.7초의 평균 대기 시간
가설 3. 분산 락의 TPS, P95, P99는 비관적 락과 비슷한 수치를 보일 것이다.
TPS, P95, P99는 약 2배에서 최대 3배의 차이로 분산 락의 성능이 더 낮았다.
분산 락은 mine() 함수를 실행시키기 위해 락을 획득한 이후부터 DB에 접근할 수 있기 때문에, 같은 광산에 대한 요청이 애플리케이션 레이어에서 직렬화되었다. 버전별 DB Lock Wait Max 그래프와 Redis Lock Wait Max 이미지를 통해 v0.4의 DB Lock Wait는 사라졌지만 Redis Lock Wait이라는 새로운 병목으로 전환되었다.
분산 락의 처리량은 비관적 락보다 약 2배 낮게 측정
분산 락의 P95는 비관적 락보다 2배 이상 느리게 측정
분산 락의 P99는 비관적 락보다 약 3배 느리게 측정
분산 락은 DB의 경합을 효과적으로 감소시켰지만, 병목 자체는 Redis로 전이됐을 뿐이었다. 따라서 “핫스팟 환경에서 성능을 개선하기 위해 분산 락을 도입했다”는 적절한 트레이드오프가 아님을 확인할 수 있었다.
가설 4. 애플리케이션의 대기 스레드가 늘어남에 따라 가용 스레드 또한 줄어들 것이다.
가설과 달리, 분산 락(v0.4)은 평균적으로 가장 많은 Tomcat 가용 스레드를 유지했다.
이는 분산 락이 Redis에서 동일한 자원에 대한 접근을 먼저 직렬화하면서, DB까지 동시에 도달하는 요청 수 자체를 줄였기 때문으로 판단된다. 그 결과 애플리케이션 내부에서 동시에 비즈니스 로직을 수행하는 요청 수가 감소했고, 스레드 풀 사용량 또한 상대적으로 낮게 유지되었다.
VU 300 기준 가용 스레드 최솟값: 분산 락 > 낙관적 락 > 비관적 락
VU 500 기준 가용 스레드 평균값: 분산 락 ≥ 비관적 락 > 낙관적 락
반면 낙관적 락과 비관적 락은 더 많은 요청이 동시에 애플리케이션 내부에서 실행되며 DB까지 진입했기 때문에, 평균 가용 스레드가 더 적게 나타났다.
다만 가용 스레드를 가장 많이 유지했음에도 불구하고, 분산 락의 TPS는 비관적 락 대비 약 절반 수준으로 가장 낮았다. 이는 매 요청마다 Redis와의 네트워크 왕복(락 획득, 해제)이 추가되면서 발생하는 오버헤드 때문으로 보인다.
핫스팟 환경에서 이 네트워크 비용이 누적되어 전체 처리 시간을 크게 증가시켰다고 판단되며, 결과적으로 스레드 효율성은 높았지만 실제 처리량(TPS)과 응답 시간(P95, P99)은 오히려 저하되는 결과를 가져왔다.
결론
1. 락 전략은 비즈니스보다 앞서지 않는다.
이번 실험에서 적용한 낙관적 락, 비관적 락, 분산 락은 모두 서로 다른 장단점을 가지고 있었다.
낙관적 락은 가장 높은 TPS와 빠른 응답 속도를 보였지만, 재시도 로직이 없다면 높은 실패율을 감수해야 했다. 반대로 비관적 락과 분산 락은 모든 요청의 정합성을 보장했지만 처리량과 응답 시간에서는 큰 손해를 감수해야 했다.
결국 어떤 락이 가장 좋은가를 찾는 것이 아니라, 서비스가 어떤 비즈니스 요구사항을 가지고 있는가가 먼저 결정되어야 한다는 점을 확인할 수 있었다.
Gold-Rush-Lab에서는 모든 사용자가 반드시 100개의 금을 획득해야 한다는 요구사항이 존재했기 때문에, 단순히 TPS가 높다는 이유만으로 낙관적 락을 선택할 수는 없었다.
결론적으로 동시성 제어 전략은 성능을 위해 존재하는 것이 아니라 비즈니스 요구사항을 만족시키기 위한 수단이라는 점을 확인할 수 있었다.
2. 미들웨어는 문제를 마법처럼 해결해주지 않는다.
Redis를 적용한 이후 DB Lock Wait는 사라졌다. 하지만 TPS와 P95, P99는 오히려 악화되었고, Redis Lock Wait라는 새로운 대기 시간이 발생하였다.
Redis는 병목을 제거한 것이 아니라 DB에서 발생하던 병목을 Redis 앞단으로 이동시킨 것이었다. 이번 실험을 통해 특정 기술을 도입한다고 해서 병목이 자동으로 해결되는 것은 아니라는 점을 확인할 수 있었다.
미들웨어는 시스템의 병목을 없애는 것이 아니라 보다 관리하기 쉬운 위치로 이동시키는 역할을 수행할 뿐이며, 결국 전체 처리량은 시스템에서 가장 느린 지점의 영향을 받는다.
3. 병목은 제거되는 것이 아니라 이동한다.
Gold-Rush-Lab은 버전이 올라갈수록 하나의 병목을 해결하면 새로운 병목이 나타나는 과정을 반복했다.
- v0.1: Lost Update 발생
- v0.2: DB Lock 병목 발생
- v0.3: API 증설, DB Lock 병목 유지
- v0.4: DB Lock Wait 제거, Redis Lock Wait 발생
병목은 하나의 기술로 제거되는 것이 아니다. 시스템 구조에 따라 다른 위치로 이동하는 특성을 가진다는 점을 확인할 수 있었으며 그로 인해 성능 개선의 목표는 병목을 완전히 없애는 것이 아니라, 현재 시스템에서 가장 관리 가능하고 예측 가능한 위치로 이동시키는 것임을 알 수 있었다.
이러한 결과를 바탕으로 다음 버전(v0.5)에서는 요청을 동기적으로 처리하는 구조 자체를 변경하여, Kafka를 이용한 비동기 처리와 Queue 기반의 병렬 처리를 통해 Redis Lock Wait까지 완화할 수 있는지 실험하고자 한다.
또한, 후속 실험을 통해 요구사항을 추가하고 분산 락이 적절히 활용될 수 있는 비즈니스 상황을 재현해보고자 한다.
댓글남기기