Lua Script, Transaction, Redisson Lock 세 가지 방식 중
어떤 게 가장 빠르고 안정적일까? 직접 부하 테스트를 통해 수치로 비교해보겠습니다.
본 글에서는 구체적인 구현 방식을 다루지 않습니다.
코드가 궁금하시다면 👉 PR
크레용 서비스에는 하루에 보낼 수 있는 메일의 총 한도(50,000건)와 동아리 별 한도(300건)가 있다.
이 한도를 초과하지 않도록 Redission MulitLock를 적용해봤지만 최적의 선택인지 확신이 ^^...
그래서 실험을 통해 가장 적합한 동시성 제어 방식을 찾기로 했다.
EVAL
기반 원자 처리)WATCH-MULTI-EXEC
기반 낙관적 락, 재시도 3회)평균 응답 시간을 비교했을 때, Lua Script가 가장 빠른 속도를 보인다.
그렇다면 Lua Script가 가장 좋은 것일까?
평균 응답 시간도 중요하지만, 하나 더 비교해야 하는 지표가 있다.
실험 목표에서 스포했듯이 tail latency(꼬리 지연 시간)이다.
tail latency는 전체 응답 시간 중 가장 느린 쪽의 퍼포먼스를 백분위로 나타내는 지표이다.
사용하는 백분위는 95분위, 99분위, 99.9분위가 일반적이다.
구분 | 의미 |
---|---|
P95 | 전체 요청 중 95%가 이 시간보다 빠르게 응답됨 |
P99 | 99%가 이 시간보다 빠르게 응답됨 |
평균 응답시간은 대부분 요청의 속도를 보여주지만,
tail latency는 특정 순간에 느려지는 현상을 보여준다. 그리고 이는 서비스 신뢰 저하로 이어진다. 😰
그럼 각 방식의 tail latency를 확인해보자.
Lua script 방식은 99분위가 4ms로 가장 짧고 평균과 비교해 안정적이지만,
Transaction 방식은 99분위가 36ms으로 차이가 큰 것을 확인할 수 있다.
MultiLcok 방식 또한 99분위가 80ms으로 가장 느려며 차이가 크다.
이를 통해 Lua Script 방식이 최악의 상황에서도 일정한 성능을 보일 것으로 판단한다.
Transaction
낙관적 락을 사용하는 방식으로, WATCH
충돌 시 재시도한다. 이로 인해 지연이 증가할 수 있다.
MultiLock
동시 요청이 많은 경우, 락을 획득하기 위해 대기 시간이 길어질 수 있다. 이로 인해 long-tail이 발생한다.
Lua Script
원자적이고 재시도 로직이 없다. 덕분에 응답 시간이 안정적이다.
트랜잭션을 사용했을 때, 중간 상태가 노출되는 문제가 있다.
이를 이해하기 위해서는 먼저 MULTI-EXEC
를 사용할 때 분기 처리를 할 수 없다는 것을 짚고 넘어가겠다. 이로 인해 '검증(소비 가능하면) → 소비'가 아닌 '소비 → 검증(소비 가능했는지)' 순서로 진행해야 한다. 검증 단계에 분기 처리가 필요하기 때문이다.
그런데 만약 소비를 이미 했는데, 소비할 수 없는 상태였다면 어떨까?
소비했던만큼 다시 채워줘야 한다. 이를 롤백이라고 부르겠다.
롤백은 '소비 → 검증' 처리와 원자적으로 이루어질 수 있을까?
'검증'이라는 분기를 거쳐야 하기 때문에 원자적으로 처리할 수 없다.
원자적으로 처리할 수 없기 때문에, 롤백을 하기 전에 다른 작업이 진행될 수 있다.
그럼 다른 작업은 롤백 전인 '중간 상태'를 보게 되는 것이다.
즉, '소비와 롤백'이 원자적으로 동작하지 않아 중간 상태가 노출된다.
그리고 이로 인해 성공할 수 있었던 작업은 실패할 수 있다.
실제로 Lock과 Lua Script는 성공 → 실패
양상을 보이는 반면,
Transaction은 성공 → 실패 → 성공 → 실패 ...
결과가 나타났다.
동시성 제어를 할 때 레디스를 사용하신 이유가 궁금해요!
메일 발송 제한을 이미 레디스로 처리하고 있어서.?
아님 서버를 여러개 띄우고 있어서.?
Java에서 동기화를 위한 ReentrantLock을 제공하는데 이것도 고민해보셨는지 궁금해서요!