-
Notifications
You must be signed in to change notification settings - Fork 2
대기열 도입
현재 t3.small 인스턴스를 사용하여 서버를 배포하고 있다.
인스턴스 사양의 한계로, 가장 간단한 API 에 대해 최대 RPS 700 정도를 가진다.
이를 높이기 위한 방법으로는 두 가지 방법을 고려할 수 있다.
- Scale Out
- Scale Up
Scale Out 같은 경우, t3.small 인스턴스를 사용하고 있는 상황에서는 Scale Out 이 아닌 Scale Up 이 적용되어야할 상황이라 생각한다. 또한 트래픽이 몰리는 시간대가 정해져 있기에 Scale Out 할 시점이 정해져 있지만, 해당 시간이 너무나도 짧으며 Scale Out 에 사용되는 비용이 과도하다 판단한다. Scale Up 같은 경우, 현재 교육 과정의 AWS 계정을 사용하고 있기 때문에 임의로 사양 변경이 불가능해 사용할 수 없다.
따라서 본 팀은 t3.small 인스턴스 환경은 변경하지 않되 가장 좋은 성능을 지향한다.
이를 위한 방법으로 대기열을 구현하였다.
앞서 말했다시피 해당 사이트는 선착순 이벤트가 시작되는 시점에 트래픽이 몰린다. 예측된 트래픽을 대비해 Scale Out 을 하더라도, 트래픽이 폭주하는 짧은 순간이 지나가면 대부분의 자원은 낭비될 것이다. 따라서 본 서비스에서는 Redis 를 사용한 대기열을 통해, 해당 문제를 해결하고자 한다.
(이미지 예시)
public String createToken(User authenticatedUser, WaitingEnqueueBodyRequest waitingEnqueueBodyRequest) {
String combinedData = authenticatedUser.getId() + ":"
+ waitingEnqueueBodyRequest.getSubEventId() + ":"
+ UUID.randomUUID().toString();
return Base64.getEncoder().encodeToString(combinedData.getBytes()).replaceAll("=+$", "");
}
대기열 진입 시 이를 위한 Token 을 생성합니다.
Token 에 엔티티의 id 값을 직접적으로 사용하는 것을 지양하고 있지만, 디버깅 및 설명 등을 위해 Id 값을 그대로 사용하였습니다.
public String addWaitingQueue(long subEventId, String token) {
redisTemplate.opsForZSet().add(WAIT_QUEUE_KEY + subEventId, token, System.currentTimeMillis());
redisTemplate.expire(WAIT_QUEUE_KEY, 60 * 60, TimeUnit.SECONDS);
return token;
}
실제 대기열에 현재 서버 시간을 Score 로 SortedSet 에 토큰을 집어넣습니다.
대기열 생성 시 해당 시점의 대기열에 존재하는 총 인원 수를 반환하고, polling 방식으로 token 을 통해 현재 순번을 확인할 수 있습니다.
@Scheduled(fixedDelay = 1000)
private void EventScheduler() {
for (Long subEventId : subEventIds) {
Set<String> tokens = queueService.popTokensFromWaitingQueue(subEventId, POP_CNT);
queueService.addTokensToWorkingQueue(tokens);
log.info("waiting queue pop tokens size from subEventId: {} is {}", subEventId, tokens.size());
}
}
스케쥴러를 통해서 1초마다 N 명을 대기열에서 POP 하고, 해당 토큰을 Key-Value 로 등록하며 30분의 TTL 을 생성합니다.
public void validateToken(String token, Long id, Long subEventId) {
String[] decodedToken = new String(Base64.getDecoder().decode(token)).split(":");
if (decodedToken.length != 3 ||
!decodedToken[0].equals(id.toString()) ||
!decodedToken[1].equals(subEventId.toString())) {
throw new WaitingInvalidTokenException();
}
if (redisTemplate.opsForValue().get(token) == null) {
throw new WaitingInvalidTokenException();
}
}
대기열 진입 이후 사용되는 API 에서는 token 을 함께 받아 대기열에서 Key-Value 를 등록한 토큰인지 함께 검증합니다.