좌충우돌 레디스 활용 기 - 메세지 큐

2019-12-24

배경

입사한지 어느덧 1년이 조금 지난 유사개발자는 놀랍게도

직접 개발하고 서비스중인 API 서버 하나를 제외하고 어떤 서버도 두고 있지 않다! 두둥탁!

그러던 어느날 평화롭게 회식을 하던 유사개발자는 겁나 무시무시한 TTS 장애 티켓을 받게 되었다!!

그로 인해 배치서버의 필요성과 캐시가 필요하다는것을 인지하게 되었다…!

문제 발견

이전에는 다른 팀에서 구성해논 푸시 프록시 서버로 콜을 날리고 있었는데

문제는 1대1 푸시였기 때문에 100명에 유저에게 푸시를 보내려면 100번 호출 해야 했다!!!

예를 들면, 카카오톡 단톡방 푸시를 생각해보면 50명이 있는 단톡방에 한명이 톡을 보냈을때 나를 제외한 49명에게 푸시를 보내기 위해 49번을 호출해야 했다.

하지만 일반적으로 단톡방은 쉬지않고 카톡이 올라오기 때문에 49번 x 카톡 수 만큼 요청을 하게된다.

이 부하를 견디기엔 작고 소중한 내 API 서버는 너무나 연약했다.

더이상의 서버장애는 모 야메룽다!!!

문제 해결 전략

가장 큰 문제는 API 서버의 부하이고 이 부하를 줄이는게 포인트니니, 푸시만을 위한 서버를 따로두어 부하를 분산시키면 된다.

푸시 서버를 나누게 된다면 푸시서버에 부하가 생기면 푸시서버 팟 또는 워커 노드를 늘리면 된다.

즉, 스케일링에 용이하다.

푸시 서버는 결국 들어오는 요청 순서대로 푸시 요청을 처리 역할을 하게 된다.

여기서 “들어오는 요청 순서대로 처리” 라는 문구가 어디서 많이 봤던거였다.

결국엔 자료구조 수업중에 첫시간에 배우는 큐(Queue) 를 사용한다.

큐에 푸시 요청 콜을 담아서 배치 서버가 푸시 요청이 들어오면 푸시를 보낸다.

용어가 부정확할 수 있는데 결국 배치 서버이자 푸시 서버가 되었다.

중간정리

  1. 푸시 부하를 전담할 서버가 필요하다.
  2. 큐를 사용해 만들겠다.

작고 연약한 API 서버가 provider가 되고 푸시 서버가 consumer가 되겠다.

문제 해결 방법

메세지 큐를 사용한다고 했을때 내가 선택할 수 있는 선택지들이 생겨난다.

  1. Kafka / Pub - Sub 구조에 특화돤 분산 큐
  2. 아마존 SQS / 클라우드 짱짱
  3. RabitMQ / 얼랭기반으로 높은 신뢰성과 많은 레퍼런스들
  4. Redis / 인메모리 키밸류 스토어

큐를 사용하기 위해 여러가지 방법이 있지만 제일 무난하다고 생각이 드는(만만하게 느껴진다라는 뜻) 레디스를 선택했다. 그리고 어차피 레디스로 캐싱도 구현해야 했다…

레디스로 BRPUSH, BLPOP 을 써서 큐를 구현하려고 했는데

Bee-Queue라는 므찐 라이브러리를 추천받아 사용하게 되었다.

푸시 서버는 나름 힙하고 타입에 익숙해지기 위해서 TS를 사용하게 되었는데, 생각보다 불편하다.

레디스가 뭔데?

레디스는 Key-Value 스토어로서 리스트, 해시, 셋 등의 자료구조를 지원하는 NoSQL 이다.

일반적으로 In-Memory 란 장점을 살려 RDBMS의 캐시 용도로 많이 사용한다.

이 외에도 Message Queue, Shared Memory, Remote Dictionary 용도로 자주 사용된다.

인메모리 디비기 때문에 휘발성이라는걸 감안해야한다. (그렇다고해서 디스크에 영구 저장 못한다는건 아니다.)

TMI

Node.js 에는 정말 많은 라이브러리 들이 있다. 노드 진영에선 강하게 어필할 수 있는 장점이지만 너무 많은 라이브러리들 때문에 무엇을 선택해야할지 잘 모를때가 있다.

레디스 클라이언트 라이브러리만해도 redis(node_redis), ioredis, redis-cluster 등등 엄청 많이 나온다. 본인은 이 3가지 대표 라이브러리중에 redis(node_redis)를 선택했다.

이런 경우에 일반적으로 Star의 수 또는 업데이트 날짜 그리고 컨트리뷰터의 수 를 결정하게 된다.

Star의 수나 업데이트 날짜를 봤을때 ioredis가 좀더 최신의 코드를 유지하고 있는것처럼 보였지만

node_redis와 큰 차이가 없었고 결정적으로 Bee-Queue 라이브러리 내부에선 node_redis를 사용하고 있어서 레디스 클라이언트 라이브러리를 redis(node_redis)로 선택했다.

구현

// provider
import Queue from 'bee-queue';
const queue = new Queue('push', redisConfig);
const createJob = async ({target, body}) => {
  try {

    const job = queue.createJob({target, body}); // target = mh, body = hi mh
    const result = await job.save();
    console.log(result.id);

  } catch (e) {
    throw e;
  }
}
// consumer
import Queue from 'bee-queue';
const queue = new Queue('push', redisConfig);

queue.on('ready', () => {
  console.log('Q is ready')
})

queue.on('error', (error) => {
  console.log('Q is error', error)
})

queue.on('succeeded', (job, result) => {
  console.log('Q is succeeded', job, result)
})

큐는 소켓처럼 이벤트 단위?로 실행되게 된다. 즉 on 메소드 안에 이벤트를 지정해줘야 한다.

크게 3개의 이벤트가 존재한다. ready, error, succeeded 각 이벤트에 알맞는 코드를 내부에 구현해주면 된다.

그리고 큐에는 process 라는 메소드를 통해 잡이 있는경우에 잡을 처리할 수 있다.

queue.on('ready', () => {
  console.log('Q is ready');
  queue.process((job) => {
    console.log('Processing job ', job.id, job.data);
    // job data에 target, body 등 provider에서 보낸 데이터들이 위치한다.
  })
})

이런식으로 처리하면 되겠다. (정답은 아니다!!)

예외 처리

나는 푸시를 위한 큐를 사용하는것이기 때문에 실패할 경우에 대해서 민감하게 대응할 필요는 없지만 한번 더 푸시 요청을 보내는 것으로 내 자신과 합의 봤다!

푸시 요청은 axios를 사용해서 보내고 있었는데 요청에 실패할경우 retry를 직접 구현해 줘도 좋지만 미들웨어로 axios-retry라는 좋은 라이브러리도 있기 때문에 이걸 사용하는것도 좋은것 같다.

첫번째 삽질

암튼 테스트는 싱글 인스턴스로 구성된 레디스 망에다가 테스트했는데

라이브망은 클러스터로 구성되어 있기 때문에 오류가 났따!!!!!

클러스터로 구성되어 있기 때문에 키가 여러곳에 분산 저장되어있기 때문인데

프리픽스를 {} 로 감싸주면 간단하게 해결된다

https://redis.io/topics/cluster-spec

여기 보면 자세히 나옴ㅎ

결론

큐가 뭔지 알기만했지 왜 써야하는지 몰랐는데, 이번 기회에 써보니까 좋은것 같다.

요즘 나는 정답은 알지만 문제가 뭔지 몰랐던 경우가 너무 많은것 같다.

더 길게 쓰고싶지만 뭘 써야할지 모르겠어서 기억나면 더 추가해야겠다.

그럼 안녕!

참고 / https://engkimbs.tistory.com/869