티스토리 뷰

공연에 대한 조회 시 조회한 공연의 조회수를 늘려주는 기능이 존재한다.

 

해당 조회수는 다른 API의 조회 기능에서 사용되며, 조회수를 단순히 1 늘려주는 작업이며 DB에 접근하며 수정이 진행된다.

여러 트랜잭션에서 조회수 갱신 시도 시 조회수가 예상보다 적은 수치로 기록되는 동시성 문제가 발생한다.

 

  • 공연을 조회하는 API에서 조회수 갱신이 같이 수행되고 있다.
  • 현재, 조회수를 갱신하는 API에는 크게 두가지 문제점이 존재한다.

 

문제1: 조회수 동시성 문제

  • 여러 트랜잭션이 동시에 조회수 증가 업데이트 요청 시 조회수가 정확하지 않게 갱신될 수 있다.

문제2: 공연 조회 API 의 성능 문제

  • 조회수 갱신 로직의 성능이 조회 API의 성능에 영향을 준다.
  • 현재 방법으로는, 클라이언트에서 조회수 갱신이 완료되어야 공연 조회 결과를 받아볼 수 있다.

 

  • 위 두 문제에 대해 다음과 같이 접근했다.

문제1 접근 (Lock)

  1. 조회수 column에 베타 Lock 적용 (낙관 락은 충돌 우려가 크다.)
    • 랭킹 특성상 조회가 많으며, 조회수 특성상 갱신도 많다. 따라서 베타 Lock으로 접근 시 타 읽기 API 성능 저하가 우려된다.
  2. ReentrantLock
    • 현재, core 서버는 단일 서버로 이루어져 있고, 수평 확장은 따로 설정되어있지 않다. (알림 서버가 존재하지만 알림만 발송한다.)
      • Backend Application 에서의 Lock으로 해결 가능하다 판단했으며, 읽기 작업에는 영향을 주지 않을 것이라 예상했다.
    • ReentrantLock으로 해결하는 방법을 우선 테스트하기로 했다.

문제2 접근 (비동기 분리)

  • 비동기로 흐름을 분리하면, 클라이언트에서 조회수 갱신을 기다리지 않고 응답을 받아볼 수 있을 것이라 기대했다. 
  • 비동기를 수행하기 위해 현재 인프라를 보면 @Async 어노테이션 방법과 Redis Pub/Sub 이 먼저 떠올랐다.
    • Redis List 는 스케줄링이 추가로 필요하며, Stream 은 원하는 방향성과는 약간 다른 특징을 갖고 있었다.
    • Redis Pub/Sub은 유실 가능성이 크고, 타 MQ 도입을 할 만큼 중요하게 안정적인 비동기가 필요하지는 않았다.
    • @Async 어노테이션을 사용하고, 스레드풀을 적합하게 지정해보기로 결정했다.
  • 비동기 작업이면서, 의미 상으로 조회 이벤트로 바라보았으며 원래 흐름과 분리가 목적이었기에 @EventListener를 적용하기로 했다.
  • 커스텀 스레드풀 설정의 경우 수학적인 계산 후 테스트해서 줄이거나 늘리는 작업을 할 예정이다.
    •  

해결 과정

  • 현재 테스트코드 인프라는 DB와 상호작용하기에 어려움이 있어, 최대한 비슷한 환경을 테스트하도록 코드로 테스트하였다.
    • 팀에 중간에 합류하여 알게된 사실이며, DB 상호작용 테스트를 하도록 인프라를 모두 바꿔버리기에는 부담스러웠다.
  • 다음을 테스트하기로 한다:
    • 문제1: ReentrantLock으로 동시성 문제를 해소
    • 문제2: @Async + @EventListener + @Transactional 적용하여 조회 로직과 조회수 갱신 로직 분리

 

문제1 해결 과정

문제1 : 테스트 1

  • 단일 ReentrantLock 사용과 공연 PK 별 ReentrantLock 사용을 고려했으며, count에 낙관적 락 사용 여부도 중요한 포인트였다.
  • 개별 ReentrantLock의 경우 ConcurrentHashMap을 사용해 테스트 했으며, 낙관적 락은 JPA의 버전 기반 락과 비슷한 CAS 연산을 제공하는 AtomicInteger로 우선 테스트하였다.

 

  • 테스트 1 시나리오
    1. 개별 ReentrantLock
    2. 단일 ReentrantLock
      • 단일 Lock으로도 트래픽을 견디며 성능이 비슷할 경우를 고려해 단일 Lock 활용
  • 테스트 1 세팅
    • ExecutorService와 CountDownLatch를 통해 [50, 100, 200, 300] 의 동시 요청을 수행한다.
      • 100tps가 목표이지만, 좀 더 확실한 결과를 보고자 비교적 더 높은 tps도 테스트하였다.
    • 하나의 요청은 count++; 를 수행한다. 앞뒤로 db 접근 등을 고려해 Thread.sleep(5);을 수행한다.
  • 테스트 1 결과
    • 단일 Lock을 전역으로 사용한 경우 100개 요청을 1초 이상 소요되며 처리한다.
    • 개별 Lock의 경우 100개 요청을 약 260ms 소요되며 처리한다.
      • 하지만, 개별 Lock의 경우 1% ~ 3% 정도의 오차를 보이는 경우가 빈번했다.
  • 테스트 1 결론
    • 단일 ReentrantLock 은 성능 상 위험하다고 판단했다. 개별 ReentrantLock을 개선할 점을 찾아보기로 했다.

 

문제1 : 테스트 2

  • ReentrantLock과 JPA 낙관적 락을 같이 사용하는 방법을 고려했다.
    • ReentrantLock을 적용하였을때 충돌이 많지 않았기에 낙관적 락을 통한 충돌도 적을 것이라 기대했으며, 동시성을 더 해결해줄 것이라 기대했다.

 

  • 테스트 2 시나리오
    • 어떤 방법이 적합할지 다음과 같은 방법들을 테스트
      • 개별 ReentrantLock + AtomicInteger
        • AtomicInteger의 경우 cas 연산을 수행하므로 JPA 낙관적 락과 유사한 방법으로 동시성을 제어한다.
  • 테스트 2 세팅은 테스트 1과 동일하게 진행했다.

 

  • 테스트 2 결과
    • 100개의 동시 요청 시 약 250ms 가 소요되어 성능은 테스트 1과 매우 비슷했으며, 동시성 문제가 테스트의 모든 경우에 해결되었다.

문제1 결론

  • Map을 활용한 개별 ReentrantLock을 사용하며, JPA의 낙관적 락을 활용한다.
    • 낙관적 락의 재시도는 multiplier와 random 옵션으로 백오프를 주어 재시도를 할 수 있도록 구성했다.
      • 이 부분은 경험에 의해 적용한 부분이다. 해당 옵션을 주어야 재시도 횟수가 확연히 줄어들며 해결되었었다.
  • Map에 무수히 많은 요소가 저장되는 것을 방지하고자, Map에서 가져온 ReentrantLock은 lock 해제 이후 다른 lock이 잡히지 않은 경우 Map에서 제거하도록 구현했으며, 이렇게 구현한 방법과 그렇지 않은 방법의 테스트의 결과는 동일하여 로직에 영향은 없었다.

 

문제1 개선 필요한 부분

  • ReentrantLock 의 tryLock timeout을 1.5초로 설정했으며, 고도화가 필요하다.
    • 테스트 결과 1초로도 충분했지만, DB 작업이 오래 걸리는 경우를 고려해 다소 늘려두었다.
  • 재시도 백오프 수치는 직접 DB와 상호작용하는 테스트로 고도화가 필요해 보인다.

 

문제2 해결 과정

  • @Async + @EventListener + @Transactional 를 활용하여 비동기로 조회수 갱신을 시도할 수 있도록 했다.
  • 기본 스레드풀을 사용하면 Integer.MAX_VALUE 까지 스레드 개수가 비정상적으로 많아질 수 있기에 따로 스레드 풀을 설정하기로 결정했다.
  • 스레드 풀의 corePoolSize 는 수식으로 계산한 이후 테스트를 통해 설정하기로 한다.
  • DB 작업과 같이 I/O가 많은 작업에 대한 스레드 풀의 경우 다음의 계산식이 권장된다.
    • Number of threads = CPU 코어수 * CPU 사용률 * (1 + I/O입력시간대기효율)
    • 1 * 0.2 * (1 + 0.7) = 0.85
  • 정수로 나누어 떨어져야 하기에 계산 결과 1 이라 볼 수 있다.
  • 따라서 corePoolSize는 1로 설정하였다. 다른 설정값의 경우 우리가 사용하여 성능을 개선할 만한 설정값이 아니라고 생각이 들었던 점과 실제로도 설정하지 않는 것을 여러 레퍼런스에서 권장한다는 점 등을 고려해 설정하지 않았다.

 

문제 상황 2 개선 필요한 부분

  • 스레드 풀의 설정값 테스트 및 고도화 필요

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함