글 작성자: Key Ryung

개요

구매 부분은 대부분 서비스들의 핵심이고 커머스 도메인에서 재고 관리의 경우 비즈니스와 밀접한 연관이 있다고 생각합니다. 관리측면에서 보면 100개 밖에 없는 상품을 120개 판매했다고 기록한다면 추후 실제 재고를 관리하는 팀에서는 추가 발주를 진행해야 될 수도 있고 만약 추가 발주를 통해 재고가 확보가 안된다면 구매했던 고객들의 상품을 취소해야 합니다. 결국 이런 일들이 반복되면 비즈니스적으로 악영향을 끼칠 것이 분명합니다.

 

실물상 재고와 서비스상 재고가 일치하지 않는다면?

도메인마다, 서비스마다 다를 수도 있습니다! 결국 Trade-Off가 핵심 

10개의 제품을 10명이 1개씩 구매했지만 2개가 남아있는 상황같이 반대의 경우는 어떨지 생각해보았습니다. 현재 구매를 진행한 고객들은 상품을 잘 받았지만 재고가 있어 추가적으로 구매하려 했지만 실패하는 경우가 생길 수 있습니다.

 

재고와 구매 주문 수와의 불일치

다품목 구매 테스트와 단품목 구매 테스트를 진행했을 시 때로는 재고와 구매 주문 물품의 갯수가 일치하고 때로는 일치 않는 현상이 발견됐습니다. StackTrace에서 확인해보면 Update 쿼리가 수행될 때, 같은 재고로 수정하는 것을 확인할 수 있습니다.

구매 Transaction StackTrace(재고, PK 순)

쿼리 확인을 통해 재고 검증 과정에서 Read, 재고 차감 과정에서 Update하는 '구매 주문'이라는 Transaction이 문제라는 것을 알 수 있었습니다. 왜 이런 현상이 발생하는 것일까요?

 

Repeatable Read에서 발생하는 문제

Database에서 데이터의 무결성을 지키기 위해서는 ACID라는 특성을 갖게 됩니다. 실제 환경에서는 이 특성들을 서로 Trade-Off 하면서 속도 혹은 정합성을 고려해야 합니다. MySQL은 기본적인 Transaction의 Isolation Level(이하. TIL)로 Repetable Read를 채택하고 있습니다. Vendor 사마다 각자만의 장점이 있지만 MySQL의 장점으로는 이론적으로 Serializable 레벨에서 막을 수 있는 Phantom Read를 Repeatable Read 레벨에서도 막을 수 있습니다.

 

하지만 이런 MySQL에서도 Write-Skew라는 현상은 막을 수 없었습니다. Write-Skew란 Database에 있는 Record를 읽은 값을 통해 애플리케이션에서 판단한 후 다음 동작에서 해당 Record에 있는 값을 업데이트하면서 발생합니다. 구매 주문 과정과 연결시켜 본다면 '재고 검증' -> '구매 주문 접수' -> '재고 차감' 과정에서 재고 검증과 재고 차감이 Write-Skew 발생 조건에 부합합니다.

 

구매 주문과정 문제 상황 모식도

 

위 그림과 같이 하나의 트랜잭션은 재고 검증 단계에서 실패해야 되지만 잠금 없는 일관된 읽기를 지원하는 MySQL에서는 Ta와 Tb 모두 검증 단계를 통과해 구매가 성공하게 됩니다. Write-Skew가 발생해서 재고 데이터의 무결성이 깨지는 상황이 만들어지므로 테스트 시 겪었던 재고 불일치가 발생하는 것입니다.


* Lost-Update는 Write-Skew가 특정 시기, 특정 조건에서 발생하는 이상 현상으로 Write-Skew를 Lost-Update의 일반화된 것으로 생각할 수 있습니다.
* Data Intensive Design 출처

 

누가 동시성 문제를 해결할까?

이 모든 시작은 동시성 문제라고 생각했습니다. 서비스를 구성하고 있는 요소 중 하나라도 동시성을 해결하기 위한 장치를 마련한다면 데이터의 무결성을 지킬 수 있을 것이라고 판단했습니다. 현재 서비스를 구성하고 있는 요소 중 비즈니스 로직 혹은 상태를 담고 있는 것은 Application과 Database이므로 두 곳에서 해결책을 찾아 나갔습니다.

 

Synchronized

Synchronized 키워드를 사용하여 요청들을 Block하여 처리하면 요청들을 하나씩 처리할 수 있기 때문에 위와 같은 문제가 발생하지 않을 것이나 Scale-Out을 진행함으로써 적절한 해결책은 아닙니다. 요청들이 Load Balancer에 의해 분산될 것이므로 각 Application마다의 동시성 문제는 해결될 수 있으나 전체적인 관점에서 동시성 문제는 해결되지 않습니다. 분산 환경에 사용할 수 있는 해결책을 찾아야 합니다.

추가적으로 확장성을 더 높이기 위해서는 Application Server를 무상태(Stateless)로 유지해야 됩니다.

 

Distributed Lock

Synchronized와 같이 분산 환경에서는 잠금(Blocking)을 공유할 수 없으므로 공유할 수 있도록 다른 Component를 통해 서로 잠금을 공유하는 것이 분산 락의 기본 원리입니다. Redis, Hazelcast, Ignite 등 In-memory 데이터 베이스에서 분산 락을 지원해주고 있습니다.

Distributed-Lock

기존에 사용하고 있는 Redis를 통해서 분산 락을 적용했습니다. Redis Client 중 Lettuce와 Redisson이라는 선택지 중 Redisson을 선택했습니다. Lettuce는 Spin-Lock 방식으로 지속적인 요청으로 Lock이 해제되었는지 확인하고 Redisson은 Pub/Sub 방식을 채택하여 Lock이 해제되면 Subscribe 하고 있는 클라이언트에게 원자적으로 알려주어 Redis의 부하를 줄였습니다. 이런 관점에서 Redis Client로 Redisson을 채택했습니다.

 

Database Lock

실제로 문제가 발생하는 것은 Database이므로 Database에서도 이런 동시성 문제들을 해결할 수 있는 방안을 마련해놨습니다. 바로 Intention Exclusive/Shared Lock입니다. 비관적 Lock이라고도 알려져 있습니다. 다음과 같이 'FOR UPDATE' 키워드를 적용하여 구현할 수 있습니다. 낙관적 Lock을 선택하지 않은 이유는 하나의 상품을 구매하려고 하는 것은 도메인에서 빈번하게 일어날 것이므로 재시도까지 생각했을 때 부하를 더 많이 줄 것으로 판단했습니다.

 

SELECT ... FOR UPDATE

* Pessimistic(비관적) Lock은 Pessimistic Concurrency Control과 혼용되어 쓰이는 것으로 보입니다.

 

Undo 로그에 있는 Multi Version에 대해서는 Lock을 수행할 수 없으므로 Update를 통해 수정된 값들을 순차적으로 조회합니다.

 

Distributed-Lock과 Database Lock 비교 테스트

현재 재고 데이터의 무결성과 TPS 성능 모두 포기할 수 없었기 때문에 2개의 방식을 모두 테스트하여 비교했습니다.

  Distributed-Lock Database Lock
TPS 158 411.8

Pinpoint에서 확인한 Distributed-Lock과 Database Lock 트랜잭션 비교

Pinpoint Transaction의 응답 속도만 비교했을 때 왼쪽이 전체적인 응답 속도가 빠르므로 Database Lock을 통한 결과로 보여집니다. 하지만 전체적인 성공 응답의 횟수는 오른쪽이 더 큰 것을 볼 수 있습니다. JVM의 지표들도 살펴봤습니다.

 

Pinpoint에서 확인한 Distributed-Lock과 Database Lock JVM 상태 비교

JVM의 상태를 비교해보면 메모리 사용량의 경우 비슷한 양상을 보이지만 CPU 사용량을 비교해보면 상단에 위치한 그래프에서 초기에 증가했다가 더 낮아지는 것을 볼 수 있습니다. 마지막으로 Database의 모니터링도 살펴보았습니다.

 

Grafana에서 확인한 Distributed-Lock과 Database Lock Database 상태 비교

데이터 베이스 모니터링 결과에서는 두개의 테스트 모두 비슷한 양상을 보이고 있습니다.

 

왜 Database Lock이 TPS 성능이 더 높을까?

첨부한 지표들이 각각 어떤 테스트에 대한 지표들인지는 의도적으로 밝히지 않았습니다. 이유는 'Database Lock이 TPS 성능이 더 좋다'라는 결론보다 '왜 Database Lock이 진행한 비교 테스트에서 더 TPS 성능이 잘 나올까'가 더 중요하다고 생각했기 때문입니다. 

 

2편에서 계속됩니다.

 

고민을 위한 여지

  • 왜 Database Lock이 성능이 더 좋을까?
해당 프로젝트는 네이버 클라우드를 활용하여 진행했습니다.

REF

http://www.ulogistics.co.kr/test/board.php?board=column2&command=body&no=558 

 

최영호의 물류, 기본이 중요하다(9) / 재고관리의 중요성-실물재고와 전산재고 일치의 중요성

재고관리의 중요성-실물재고와 전산재고 일치의 중요성     물류센터의 가장 큰 고민중 하나는 정확한 재고관리이다. 재고가 맞지 않다는 것은 실물재고와 전산재고가 일치하지 않는 것을 의

www.ulogistics.co.kr

https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html

 

레디스와 분산 락(1/2) - 레디스를 활용한 분산 락과 안전하고 빠른 락의 구현

레디스를 활용한 분산 락에 대해 알아봅니다. 그리고 성능을 높이고 일관성을 보장하는 방법에 대해 알아봅니다.

hyperconnect.github.io