-
주문 시스템 성능 개선기개발 2024. 9. 3. 18:45
개요
우아한 테크 캠프 팀 프로젝트로 배달의 민족(일반 커머스에 가까운) 서비스 중 메뉴를 담고 장바구니에 등록한 뒤 주문하는 기능을 만들었습니다.
저희는 고도화 과정을 통해 성능을 개선하려고 했는데요, 저는 대규모 시스템에서 안정적이고 빠른 주문 시스템을 만드는 작업을 했습니다.
접근 방식
저희는 클라우드 자원으로 ec2 t3.small과 rds t3.micro를 지원받았습니다. 그렇기 때문에 한정된 자원에서 성능향상을 이루기 위해서 현재 가장 부하가 많은 곳을 개선하려고 노력했습니다.
서버에서 가장 많은 병목이 발생하는 부분은 DB라고 생각했기 떄문에 DB 락을 중심으로 문제를 접근했습니다.
우리가 시도한 방법들
비관락
처음으로 적용한 방식은 비관락입니다. 적용한이유는 가장 간단히 적용할 수 있었기 때문이었습니다.
하지만, 다음과 같은 상황에서 트랜잭션이 오래 락을 점유하고 있으면 다음 트랜잭션들이 DB 커넥션을 계속 차지해 DB 커넥션이 낭비될 수 있다는 단점이 있었습니다.
그래서 저희는 락의 주체를 다른 쪽으로 옮겨 보자고 생각했습니다.
낙관락
메뉴에 버전을 두어 조회 시점의 버전보다 늘은 경우 재시도를 하도록 하는 로직을 적용했습니다.
이렇게 하면 오히려 WAS에서는 재시도 하면서 DB I/O가 증가하고 DB에서는 실패할 쿼리들이 연결되어 오히려 DB 커넥션을 낭비하게 됩니다. 성능도 오히려 비관락보다 안좋았습니다.
그래서 저희는 여러번 DB를 접근하지 않고 다른 곳에서 아예 락을 관리해보자고 생각합니다.
분산락
그래서 'Redis를 통해 락을 관리하면 DB 커넥션을 효율적이게 쓸 수 있지 않을까?' 라는 생각으로 분산락을 적용하게 됩니다.
사실 분산 서버가 아닌 상황에서 분산락을 적용할 이유가 거의 없지만 장바구니에서 redis를 사용하기도 하고 DB 커넥션을 최적화할 수 있다고 생각해서 적용해봤습니다.
하지만, 성능 향상이 크게 없었습니다. 가장 큰 이유는 결국 락이 존재하기 때문에 병목은 발생할 수 밖에 없다는 점이었습니다. 그래서 아예 재고에 대한 경쟁 상태가 발생하지 않는 방식으로 설계해보았습니다.
재고 캐싱
다음과 같이 재고를 캐싱해서 redis 에서 재고를 관리하고 일정 기간마다 DB에 값을 맞춰 주는 방식을 적용해봤습니다.
재고를 감소시킬 때 Redis 에서 제공하는 원자적 연산으로 한번에 감소시키는 방식을 사용했습니다. 이런 구조를 적용하면 재고를 감소할 때 경쟁 상태가 발생하지 않으며 DB I/O 횟수도 크게 감소시킬 수 있습니다.
이 방식은 성능은 향상시킬 수 있었지만 DB 정합성에 대한 문제가 있을 수도 있습니다. 때문에 Redis 가 장애가 났을 때 DB에 싱크를 맞출 수 있도록 적절한 조치가 필요할 수도 있습니다.
성능 비교
성능 테스트는 Locust 로 진행하였고 User 는 1만명 ramp up 은 100으로 두었습니다. 시나리오는 한 매장에 모든 메뉴를 여러 사용자가 주문하는 방식으로 경쟁 상태가 많이 발생하도록 구성했습니다.
성능은 캐시가 응답속도 면에서 2-3배 빠른 향상을 보였습니다. 처음 테스트를 진행할 때는 장애 대비가 되어 있지 않아 DB 싱크를 매 순간마다 해줘서 성능의 차이가 없었지만 DB 싱크를 따로 스케줄링을 돌려 처리하니 큰 성능 향상을 보이는 것을 알 수 있었습니다.
(Redis 장애에 대한 대비가 되어 있지 않았지만 데모 데이 피드백을 받고 한번 적용을 해보았습니다.)
비관락
Type Name # Requests # Fails Average (ms) Min (ms) Max (ms) Average size (bytes) RPS Failures/s POST /orders 13486 0 17538.89 74 50876 38.18 30.92 0 Method Name 50%ile (ms) 60%ile (ms) 70%ile (ms) 80%ile (ms) 90%ile (ms) 95%ile (ms) 99%ile (ms) 100%ile (ms) POST /orders 14000 16000 19000 31000 39000 43000 49000 51000 재고 캐싱
Type Name # Requests # Fails Average (ms) Min (ms) Max (ms) Average size (bytes) RPS Failures/s POST /orders 26718 0 8715.08 479 18227 39 48.95 0 Method Name 50%ile (ms) 60%ile (ms) 70%ile (ms) 80%ile (ms) 90%ile (ms) 95%ile (ms) 99%ile (ms) 100%ile (ms) POST /orders 8400 8900 9900 11000 14000 15000 17000 18000 결론
캐싱을 활용하는 방식은 DB에서 실시간으로 재고 수량의 정합성이 맞춰지진 않지만 큰 성능 향상을 보여 주문이 많이 몰리는 상황에서는 좋은 구조가 될 수 있을 것 같습니다.
회고
이번 고도화를 진행하면서 많은 것을 배웠고 반성하게 되었습니다.
단일 서버 환경에서 분산락을 적용한 것은 불필요한 복잡성을 추가한 것 같습니다. 앞으로는 기술 적용 전에 해당 기술의 필요성과 적합성을 더욱 신중하게 평가해야할 것 같습니다.
또한, 테스트 상황을 좀 더 다양하게 두어 실제 많이 발생하는 시나리오를 바탕으로 의사결정을 하는 방식도 괜찮았을 것 같습니다. 한 매장에 여러 명이 동시에 같은 메뉴를 시키는 일은 생각보다 빈번히 발생하지 않을 수 있는 시나리오일 수도 있다는 생각이 들었습니다. 조금 더 다양한 시나리오로 각 방식에 대해서 장단점을 분석해보았으면 더 좋았을 것 같습니다.
++ 캐시 웜업
저희 서비스는 주문을 하기 위해서는 카트를 담는 행위가 선행되어야 하기 때문에 카트에 담을 때 재고를 redis에 동기화 해주었습니다.
서버를 처음 실행할 때에 모든 메뉴를 동기화하는 것은 750만개의 데이터를 모두 담는 것은 서버에 큰 부하가 있을 것 같아 패스했습니다.
'개발' 카테고리의 다른 글
동시성 제어에 관하여 (0) 2024.09.10 CSV Driver SELECT 요청 개선기 (4) 2024.08.05 내가 만든 was, Maven에 올렸지 (0) 2024.07.20 Java 프로그램 바이트 코드를 분석해보자 (0) 2024.07.10 ServerSocket의 내부 동작 살펴보기: TCP 연결 설정의 로우레벨 이해 (0) 2024.07.08