(동시요청 문제해결) 선착순으로 제공되는 서비스에서 동시요청 문제를 어떻게 해결할까? (Naver는 어떻게?)
동시요청 문제해결 (Naver 사례)
이번에 선착순 쿠폰을 발급하는 프로모션을 진행했는데, 난리가 아니었습니다.
오류난 데이터를 집계하고 고치면서 어떤 방법이 좋았을까 찾아보았습니다.
네이버에서 선착순으로 컨퍼런스 신청을 받을때는 어떻게 구현했을까?
당연히 짧은시간에 엄청난 수의 요청이 들어올것이다.
이전 컨퍼런스 신청 때 구현한 코드를 개선하여 오픈하였다고 한다.
개발 순서
- 성능 테스트 계획 수립
- 성능 테스트& 문제발견
- 데이터베이스 교착 상태 해결
- 1 일차
- 신청 전: 메모리 누수 문제 해결
- 신청 후: 성능문제, 정원초과 문제 - 2일차
- 신청 전: 글로벌 캐시, 로컬 캐시 방식 적용
- 신청 후: 결과확인
1. 성능 테스트 계획 수립
작년, 재작년의 마감 시간을 기준으로 하여
x초안에 x번의 요청을 가정하고 테스트 계획수립
2. 성능 테스트& 문제발견
A. nGrinder로 웹 API의 성능 테스트
: nGrinder로 생성한 스크립트를 수정해서 Groovy로 성능 테스트용 스크립트를 작성
B. Pinpoint로 테스트 결과 모니터링
: Pinpoint에서 보여주는 메모리 사용량 그래프를 통해 확인
문제발견
a. 불규칙적으로 요청이 실패하는 현상
b. 메모리 누수 현상
3. 데이터베이스 교착 상태 해결 (불규칙적으로 요청이 실패하는 현상)
문제
- DeadLock 발생
먼저, 기존 코드 테스트시
UPDATE 일차별_참가신청 SET 현재_사람수 = 현재_사람수 + 1
WHERE 행사일차 = ?
AND 현재_사람수 < 정원
AND NOT EXISTS (SELECT 1 FROM 참가자 WHERE 행사일차 = ? AND email = ?)
이 부분에서 DeadLock이 발생했다고한다.
https://lion-king.tistory.com/entry/SpringJPATransaction-Write-skew-Phantom-2 (여기서도 비슷한경험)
해결
- Where 절 단순화
UPDATE 일차별_참가신청 SET 현재_사람수 = 현재_사람수 + 1
WHERE 행사일차 = ?
Where 절을 단순화 시키니 더이상 DeadLock이 발생하지 않았다고한다.
하지만 데이터베이스 isolation level에 따라 몇명 정도 초과할 가능성이 있다. 이 문제는 감수하기로 함.
4.1 (신청 전) 메모리 누수문제 해결
A. 분석
이용하여 힙 덤프 파일을 분석
B. 결과
데이터베이스 커넥션 풀 라이브러리인 commons-dbcp2와 관련된 객체가 과도하게 생성된 것을 확인, 라이브러리 버그로 의심
버전 업드레이드 결정
- commons-dbcp2: 2.0.1 버전 -> 2.2.1 버전
- commons-pool2: 2.2 버전 -> 2.4.2 버전
- Pinpoint의 GC(garbage collection) 그래프에서 메모리 누수 문제 해결 확인
4.2 (신청 후) 성능 문제, 정원초과 문제 확인
Pinpoint를 통해 모니터링
- 정원보다 초과하여 신청가능
- 최대 20초까지의 수행시간
5.1 (신청 전) 글로벌 캐시, 로컬 캐시 방식 적용
배포후 1일차 신청을 받았는데, 정원 초과 문제가 있었고 최대 20초까지 걸리는 등의 문제가 있었다고한다.
데이터베이스의 UPDATE 쿼리로 신청자 수를 확인하는 부분이 한계가 있다고 판단했고,
변경되는 신청자 수에는 글로벌 캐시 방식을 적용 / 변경되지 않는 신청 시간, 정원 등은 로컬 캐시 방식을 적용
으로 결정했다.
해결
- 글로벌 캐시 적용: 변경되는 신청자 수 조회 및 카운팅 (nbase-arc)
- 로컬 캐시 적용: 변경되지 않는 신청 시간, 정원 등
글로벌 캐시 저장소로 nbase-arc를 사용했다고 하는데 찾아본 결과 Redis Cluster를 구현한 것이고
결국 Redis 사용한 것으로 받아들였다. Redis는 싱글스레드로 동작하기 때문에 동시성 문제를 해결하기 위해 선택한 것으로 생각된다.
public int increment(Long 신청일차) {
String key = KEY_PREFIX + 신청일차;
ValueOperations<String, String> operation = redisClient.opsForValue();
return operation.increment(key, 1L).intValue();
}
핵심 코드는 위 부분이라고 한다.
신청수는 이제 순서대로 잘 증가가 될것으로 보이고, 조회도 정확한 데이터가 조회될 것으로 보인다.
5.2 (신청 후) 결과 확인
pinpoint로 안정된 성능 확인
아쉬운점
- 조회 API, 신청 API, 웹 페이지 접근까지 포함해 실제 사용자가 사용할 경로에
더 가깝게 테스트 하지 못함 (운영 상황에 가까운 부하를 재현)
- 운영체제나 웹 서버인 Nginx의 설정 등 애플리케이션 서버 외에 다른 요소까지 튜닝하지 못함
여기서 궁금한 점은 신청수를 데이터베이스에 저장하는 시점은 신청이 다 끝나고나서 캐시에 저장된 데이터를 저장하도록 했을까?
아니면 신청 중에 시간 간격 또는 신청 수 간격으로 데이터베이스에 저장 및 수정을 했을까? 입니다.
저였으면 신청이 마감되고 신청수를 데이터베이스에 저장했을것 같은데 네이버에서는 어떻게 했을지 궁금하네요.