Skip to content

대기열 도입

min9805 edited this page Aug 22, 2024 · 12 revisions

개발 방향성

현재 t3.small 인스턴스를 사용하여 서버를 배포하고 있다.

인스턴스 사양의 한계로, 가장 간단한 API 에 대해 최대 RPS 700 정도를 가진다.

이를 높이기 위한 방법으로는 두 가지 방법을 고려할 수 있다.

  1. Scale Out
  2. Scale Up

Scale Out 같은 경우, t3.small 인스턴스를 사용하고 있는 상황에서는 Scale Out 이 아닌 Scale Up 이 적용되어야할 상황이라 생각한다. 또한 트래픽이 몰리는 시간대가 정해져 있기에 Scale Out 할 시점이 정해져 있지만, 해당 시간이 너무나도 짧으며 Scale Out 에 사용되는 비용이 과도하다 판단한다. Scale Up 같은 경우, 현재 교육 과정의 AWS 계정을 사용하고 있기 때문에 임의로 사양 변경이 불가능해 사용할 수 없다.

따라서 본 팀은 t3.small 인스턴스 환경은 변경하지 않되 가장 좋은 성능을 지향한다.

이를 위한 방법으로 대기열을 구현하였다.

대기열

앞서 말했다시피 해당 사이트는 선착순 이벤트가 시작되는 시점에 트래픽이 몰린다. 예측된 트래픽을 대비해 Scale Out 을 하더라도, 트래픽이 폭주하는 짧은 순간이 지나가면 대부분의 자원은 낭비될 것이다. 따라서 본 서비스에서는 Redis 를 사용한 대기열을 통해, 해당 문제를 해결하고자 한다.

image

(이미지 예시)

구현

1. Create Token

    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 값을 그대로 사용하였습니다.

2. Enqueue to Waiting Queue

    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 에 토큰을 집어넣습니다.

3. 프론트엔드 조회

대기열 생성 시 해당 시점의 대기열에 존재하는 총 인원 수를 반환하고, polling 방식으로 token 을 통해 현재 순번을 확인할 수 있습니다.

4. Scheduler

    @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 을 생성합니다.

5. 검증

    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 를 등록한 토큰인지 함께 검증합니다.