본문 바로가기

MY아이디어

[아이디어] 대용량 앱푸시 예약발송 기능 어떻게 만들까?

앱 푸시 메세지 발송 기능을 만들어야 한다면, 그리고 예약시간에 정확히 발송해야 한다면? 어떻게 설계를 해야할까?

 

물론 내가 말하는 모든 가설은 대용량/대량 트래픽을 기본으로 하고있다. 따라서 좀더 고민을 해야하는 포인트들이 많다.

우선은, 기본적인 것은 카프카 큐를 이용해, 발송대상 이벤트를 받고 해당 이벤트를 컨슘하며, 푸시를 보내는 설계는 기본일듯하다.

 

여기서 가장 큰 문제는 바로 예약 발송을 어떻게 구현을 할까이다..

이 부분이 상당히 어렵기도하고 까다롭기도 하다.

 

예약 발송 구현을 위한 방법은 다양하다. 가장 합리적이고, 괜찮은 방법을 몇개 생각해 봤다.

 

1) 레디스의 ZSET을 이용하는 방법이다.

레디스의 ZSET는 SORTED SET으로 키값을 정렬하여 리스트로 가지고있다.

이 키값을 범위조회로 가져올수있다. (복잡도 : O(log N))

따라서, 예약시간을 키로 ZSET에 넣어두고, 배치나 스케줄을 통해 현재 시간에 해당하는 이벤트를 조회하여 발송큐로 발행한다.

해당 발송큐에서 앱푸시 메세지를 발송하게 설계하는 방식이다.

 

ZSET은 자체적으로 키에 대해서 Map과 Sorted List 2개의 자료구조를 가지고 있기에, 빠르게 조회/관리가 가능하다.

따라서 대량 대용량 데이터를 넣고, 조회하는데 부담없이 사용할수있다.

주의할점은 ZSET으로 조회한 값을 ZREM으로 삭제하는 케이스만 레이스 컨디션이 일어나지만 않게 하면 된다.

이 부분은 Lua script를 통해서 조회와 조회된 데이터를 삭제하는 명령어를 아토믹하게 엮어주게 구성을 하면 문제가 없을듯하다.

 

2) 카프카의 토픽을 시간 단위로 여러개를 만들고, 이벤트를 시간단위 토픽으로 이동시킨다.

PUSH_D (날짜별 토픽)

PUSH_H (시간별 토픽)

PUSH_M (분별 토픽)

 

발송이벤트를 처음에는 날짜별 토픽(PUSH_D)으로 발행하고, 해당 토픽에서 현재 날짜를 비교하여 예약날짜가 아니라면, 리큐하도록 큐에 다시넣는다, 그리고 예약날짜의 이벤트라면, 해당 이벤트를 시간별 토픽(PUSH_H)으로 이동시킨다. 해당 토픽에서 현재 시간을 비교하여, 예약 시간이 아니라면, 리큐하도록 큐에 다시넣고, 예약시간이 맞다면, 다시 분별 토픽(PUSH_M)으로 이동시킨다.

해당 토픽에서 현재 분과 예약분이 맞다면, 메세지를 발송시킨다.

마치 시간의 단위에 따른 체이닝이 되는 느낌으로 설계하는게 포인트다.

 

이렇게 시간단위로 토픽을 만들고 지속적으로 이벤트를 이동시키면서 최종 토픽에서 발송을 시키는 방법이 있다.

카프카큐를 적극적으로 이용하기에 대량 트래픽에 장점이 많지만, 리큐에 따른 시스템 부하여부도 신경을 써야한다.

여기서 왜 굳이 여러 토픽으로 넘어가야하나?! 리큐의 횟수를 줄이려는 의도가 가장크다.

시간단위가 큰 단위부터 점점 작은 단위로 넘어가기에 그나마 리큐의 횟수가 많이 줄어들게된다. 완충작용이랄까?!!

 

3) ESPER를 이용하는 방법이다.

레디스에서 시간순서의 SET기능과 유사하게 ESPER에 이벤트를 발행하고, 쿼리를 통해 현재 시간을 구하게 만들수있다.

esper는 자체 쿼리를 지원하기에, 아마 아래와 비슷한 쿼리 하나면, 끝나지 않을까? ㅎㅎㅎ

select * from PushEvent where eventTime = current_timestamp

 

esper에 이벤트를 발행하고, 위 쿼리를 실행하면, 리스너로 현재시간에 발송해야하는 이벤트를 리스너로 보내준다.

해당 이벤트를 잡아서 카프카 큐에 넣고, 발송만 하면 된다.

 

esper는 정말 이벤트 cep중에는 가장 강력하긴하다.

이런 실시간 처리기능설계등에서 좀더 적극적으로 사용하고 싶은 욕구가 막 올라오긴한다.

 

이 방법은 상당히 깔끔하고 간단하다.

하지만, esper는 기본적으로 클러스터구성이 아닌 싱글노드로 동작하고 (엔터프라이즈...유료),

서버다운등에 따른 HA복구등이 없기에, 서버가 내려가거나 다운되면, epser안에 발행한 이벤트가 사라질수있다.

따라서, 복구 전략을 꼭 만들어두어야 한다.

 

복구 전략은 아마 카프카에 복구시점의 offset을 찾아서, 다시 컨슘해서 esper에 복구할 이벤트를 다시 적재하는 방법이 가장 심플하면서 강력할것같다.

 

 

4) 데이터베이스에 예약발송 내역을 저장하고 폴링하여, 큐로 발송하는 방법이다.

이 방법은... 기본적인 것이라 그냥 설명은 생략!!! 

장단점도 명확한 기본적인 방법이니..

 

여기서 가장 중요한 부분은 바로 하나의 발송내역 테이블을 여러 디비 커넥션들이 읽어가며 발송하고, 발송내역을 컬럼에 마킹하는 과정에서 다른 커넥션이 변경이전의 상태로 조회를 하게 되는 케이스가 발생하고 중복으로 데이터를 읽어 중복 발송하는 케이스가 발생한다.

이부분을 엄청 신경 써야한다. 한개의 디비 커넥션으로 순차적으로 읽어가면 해결이 되긴하지만,

이러면 성능이 안나오니..결국 여러개의 디비 커넥션이 경쟁적으로 조회하여, 발송을 하게 만들어야 하기때문이다.

 

이부분은 각각의 디비커넥션, 즉 쓰레드들이 샤딩된 별개의 범위를 읽게하여 같은 영역을 조회하는 것을 원천적으로 막는 방법이 있고,

다른 방법으로는 select for update 구문에 skip locked 옵션을 주어, 로우학을 걸어 동시성을 막고, 동시성이 걸려 락획득을 못한 다른 쓰레드들은 대기하지않고, 다음 로우를 자연스럽게 읽을수있게 옵션을 설정하면, 해결을 할수있다. (중요!!!)

 

웬만하면 이 방법은 대용량 트래픽상황에서는 안쓰고 싶다..

 

내가 앱 예약발송 기능을 만들어야 한다면, 레디스나 ESPER를 이용한 방법으로 구현을 할것같다..특히 레디스..ㅎㅎ