본문 바로가기

기타 개발관련

[펌] 큐 시스템을 이용한 NPUSH-GW 개선

최근에는 SBC쪽에 관심을 갖고있어서 개발쪽은 거의 안 보고있었는데 오랜만에 NAVER 개발자블로그에 방문하니 재미난 글이 올라왔군요.

관심있는 분들은 직접 출처로 이동하셔서 보시기 바랍니다.

출처 : http://helloworld.naver.com/helloworld/1022966


큐 시스템을 이용한 NPUSH-GW 개선 개발자Tip

 

네이버 Naver Labs 배상용, 임영완, 조항수, 김민곤, 김종현

Apple의 APNS(Apple Push Notification Service)나 Google의 GCM(Google Cloud Messaging) 같은 푸시 메시지 플랫폼은 모바일 서비스를 개발하면서 대부분 한 번씩은 사용해 봤을 만한 핵심 기능입니다. 최근에는 운영체제와 플랫폼 사업자별로 푸시 메시지 플랫폼이 끊임없이 개발되고 있습니다. 다양한 플랫폼을 이용해 메시지를 전송하려면 서비스 개발자가 모든 플랫폼의 명세를 이해하고 구현해야 합니다. 이런 불편 사항을 해결하려 네이버는 다양한 푸시 메시지 플랫폼을 하나의 인터페이스로 사용할 수 있도록 푸시 게이트웨이 시스템(이하 NPUSH-GW)을 개발해서 운영하고 있습니다.

이 글에서는 고성능 오픈소스 큐 시스템인 Luxun을 이용해 자체 개발한 NQueue 시스템으로 NPUSH-GW의 안정성을 향상시킨 방법을 공유하겠습니다.

NPUSH-GW란?

NPUSH-GW는 네이버의 모바일 푸시 메시지 플랫폼인 NPUSH의 컴포넌트 중 하나로, 서비스에서 요청한 푸시 메시지를 다양한 외부 푸시 플랫폼으로 전송하는 시스템이다.

3d464cc2e5a4a19ec09c9bd2b73ed8d0.png

그림 1 큐 시스템 도입 전 NPUSH-GW 구성도

2011년 11월에 네이버톡과 연동한 것을 시작으로 LINE, 네이버 앱 등을 포함해 네이버 계열사에서 개발하는 모바일 앱 대부분이 NPUSH-GW를 이용하여 푸시 메시지를 전송한다. 3년 동안 운영하면서 Apple과 Google은 물론 다양한 외부 오픈 푸시 플랫폼에 메시지를 전송하도록 개발했다.

NPUSH-GW 초기 개발 요구 사항 및 구조

초기의 NPUSH-GW는 네이버 서비스에서 요청한 푸시 메시지를 외부 푸시 플랫폼으로 전달할 때까지 소요되는 체류 시간을 최소화하도록 설계하고 구현했다. 되도록 시스템을 단순하게 하고 서버 구성을 간소하게 하도록 외부의 별도 큐 시스템을 사용하지 않고 Java에서 제공하는 LinkedBlockingQueue와 ThreadPoolExecutor의 내부 Worker Queue를 이용해 비동기 메시지를 처리하도록 구현했다. NPUSH-GW 내부의 모든 발송 모듈은 서비스별, 푸시 플랫폼별로 분리되어 있고, 각 모듈이 하나의 서비스 또는 푸시 플랫폼의 발송을 전담하는 구조다.

d3b5a164b4fbed28498e5e4083602d6e.png

그림 2 NPUSH-GW 기본 아키텍처

초기의 NPUSH-GW의 구조적 문제점

그림 2와 같은 메시지 발송 구조는 구현이 쉽고, 서버 구성이 간단하고, 체류 시간을 최소화해 메시지를 빠르게 전달할 수 있다는 이점이 있다. 하지만 운영 중 다음과 같은 경우에는 NPUSH-GW의 발송 처리량이 수신 처리량을 초과하는 문제가 발생할 수 있다.

  • NPUSH-GW를 사용하는 서비스에서 평소보다 많은 메시지 전송을 요청하는 경우
  • 전체 서버 중 일부 서버로 메시지 요청이 집중되는 경우
  • 외부 푸시 플랫폼의 응답 시간이 갑자기 느려지거나 장애 상황인 경우
  • 내부 IDC 네트워크에서 외부 IDC로 나가는 네트워크에 문제가 발생하는 경우

이런 경우 NPUSH-GW로 유입된 메시지를 임시로 저장하기 위해 사용한 ThreadPool executor 내의 Runnable Worker Queue(그림 2의 Thread Worker Queue)가 점점 증가한다. 짧은 시간 동안 Runnable Worker Queue가 증가하는 것은 메시지 전달이 지연되는 것 외에 시스템에 문제를 일으키지 않는다. 하지만 만약 오랜 시간 동안 Runnable Worker Queue가 지속적으로 증가하면 힙 메모리 사용량이 증가하고, 힙 메모리 사용량이 일정 수준을 넘어서면 서버 긴급 투입이나 신규 메시지 차단 등의 별도 조치가 필요하다. 만약 적절한 조치를 제때에 하지 못하면 시스템이 정상적인 서비스를 제공하지 못하게 된다.

따라서 성능은 뛰어나면서도, 외부 푸시 플랫폼에 장애가 발생하거나 서비스에서 발송하는 공지 메시지 등 대량의 푸시 메시지를 처리할 수 있도록 큐 시스템 적용을 검토하게 되었다. 100,000TPS로 들어오는 1,024바이트의 푸시 메시지를 두 시간 동안은 문제없이 NPUSH-GW에서 저장해 처리하는 것이 목표였다.

NPUSH-GW 개선

Producer/Consumer 구조로 변경

메시지 큐 도입을 검토하면서 하나의 서버에서 푸시 수신과 푸시 발송이 이루어지는 기존 구조를 변경해 푸시 수신 서버군(Producer)과 푸시 발송 서버군(Consumer)을 분리했다. 모든 푸시 메시지는 Producer 서버를 통해 Queue Broker 서버에 저장되고, Consumer 서버는 지속적으로 Queue Broker 서버에 저장된 메시지를 가져와서 외부 푸시 플랫폼으로 발송하는 구조로 변경했다. Producer 서버를 분리했기 때문에, 일부 서버로 메시지 요청이 집중되어 특정 서버만 Worker Queue가 증가하는 문제가 발생하지 않는다.

1e9dfcbd61930557eb4a98f492562d68.png

그림 3 NQueue 적용 후 NPUSH-GW의 구조

Producer와 Broker, Consumer는 오픈소스 큐 시스템 Luxun을 이용해 자체 개발한 NQueue라는 라이브러리로 사용할 수 있다. NPUSH-GW에서는 동일한 NPUSH-AGENT 내에서 역할에 따라 Producer 객체와 Consumer 객체를 생성해 NQueue의 기능을 이용하도록 개선했다. 현재 NPUSH-GW에 큐 시스템이 적용된 외부 푸시 플랫폼은 GCM 등이며 2014년 말까지 모든 외부 푸시 플랫폼으로 확대할 예정이다.

0ef84efccbe2f765f44de8033bf0ad58.png

그림 4 NQueue Producer와 Consumer 구조

NQueue를 도입해 실제 GCM에 적용한 후 다음과 같은 조건으로 Broker의 성능을 측정했다.

  • HP DL360 G7(표준 웹 서버, 메모리 24GB, HDD SAS 300GB)
  • 메시지 크기: 1024 바이트

위와 같은 조건에서 성능을 측정한 결과는 다음과 같다.

  • 메시지 체류 시간 100ms 이내: 약 20,000TPS
  • 메시지 체류 시간 100ms 초과: 약 40,000TPS(공지 메시지 등)

ThreadPool Executor 사용 방법 개선

NQueue를 도입하기 전에는 FixedThreadPoolExecutor의 Worker Queue 크기에 제한을 두지 않고, 서비스에서 요청한 메시지 전송을 최대한 보장하기 위해 메시지를 힙 메모리가 허용하는 한계까지 Worker Queue에 저장했다. NQueue를 도입하면서 FixedThreadPoolExecutor의 Worker Queue의 크기를 고정하고, 더 이상 내부 큐에 Runnable을 생성하지 못할 경우 Queue Broker에서 더 이상 메시지를 가져오지 않도록 해서 NQueue Broker에만 메시지가 쌓이도록 구조를 변경했다.

7f8c26cc2e21fc29a8a9c11097fea21f.png

그림 5 ThreadPool Executor 사용 방법 개선

  1. NQueue Consumer는 NQueue Broker에서 메시지를 가져온다.
  2. NQueue Consumer는 NPUSH-AGENT에 메시지를 전달한다.
  3. NPUSH-AGENT는 Runnable 객체를 만들어 Thread Worker Queue에 넣는다. 이때 Thread Worker Queue의 크기를 제한한다.
  4. 메시지를 전달하는 스레드는 Connection Pool에서 Connection을 가져온다.
  5. 정상적이라면 제대로 전송하고 자원을 해제한다.
  6. 만일 발송 스레드가 느려지거나 설정된 최대 발송량보다 많은 메시지가 유입되어 Worker Queue가 꽉 차고 더 이상 Worker Queue에 Runnable을 주입할 수 없다면 Queue Broker에서 더 이상 메시지를 가져오지 않는다.
  7. 발송 스레드가 모든 메시지를 처리하고, Thread Worker Queue에 Runnable 주입이 가능해지면 NQueue Consumer는 다시 메시지를 가져와서 NPUSH-AGENT에 전달하기 시작한다.

NQueue 소개

앞에서 설명했듯이 NPUSH-GW의 안정성을 높이는 구조 개선 작업을 위해 외부 오픈소스 큐 시스템을 기반으로 한 자체 큐 시스템을 구축해 NPUSH-GW에 적용했다.

오픈소스 큐 시스템을 선정하는 데 중요한 기준은 다음과 같다.

  • 한 번 저장된 메시지는 잃어버리지 않아야 한다(Persistent).
  • 별도의 운영 리소스를 투입해 운영해야 할 정도로 규모가 크거나 복잡하지 않아야 한다.
  • 큐 시스템 내에서의 메시지 체류 시간을 최소화할 수 있도록 높은 성능을 보장해야 한다.

위와 같은 기준에 부합할 수 있는 오픈소스 큐 시스템을 조사한 결과, Luxun이라는 오픈소스 큐 시스템을 이용하여 NQueue 시스템을 구성했다.

Queue Broker 선택 및 개선

Luxun은 하나의 라이브러리에 Producer, Broker, Consumer를 모두 제공하는 간단한 구조로 되어 있으며, Persistent 지원을 위해 Netflix의 Suro 프로젝트에도 사용했던 Big Queue 라이브러리를 기반으로 한다. 자체 성능 테스트 결과는 만족스러웠으나 프로젝트가 범용적으로 사용되지 않다 보니 안정성을 확보하기 위해 많은 테스트와 수정 작업을 거쳤다. 추후 Apache Kafka 등 다양한 큐를 지원할 예정이다.

Luxun 코드 수정 및 오픈소스 기여

Luxun의 안정성을 확보하고 모니터링을 강화하게 위해서 추가한 기능은 다음과 같다. 그리고 대부분의 수정 사항은 Luxun에 전달하여 Luxun 프로젝트에 반영되었다.

  • Broker와 연결이 비정상일 때 Producer를 종료하면 아무리 재시도해도 메시지를 모두 전송하지 못한다. 아직 전송하지 못한 메시지를 가진 Producer를 종료할 때 연결이 정상적인 다른 Producer로 재시도할 수 있는 기능을 추가했다.
  • Broker 장애 시 장애 서버를 감지하여 서비스에서 제거할 수 있는 기능을 추가했다.
  • Broker를 L4에 연결하여 사용할 때 하나의 Broker로만 메시지를 전달하는 것을 방지하기 위해 연결을 주기적으로 재연결한다. 그런데 L4를 사용하지 않을 때에는 성능 저하가 발생하므로 설정을 변경할 수 있도록 수정했다.
  • Consumer에서 Broker에 저장되어 있는 메시지를 요청하고 응답을 수신하기 전에 Consumer가 종료되면 메시지가 유실되는 현상을 수정했다.
  • 모니터링을 위해 Producer 내부적으로 아직 전송하지 못한 데이터의 수를 알려주는 기능과 Broker에서 오류가 발생하였을 때 로그를 저장하는 기능을 추가했다.

Big Queue 라이브러리안정성 강화

Luxun에서 사용하는 Big Queue 라이브러리는 Producer로부터 받은 데이터를 관리하는 큐 라이브러리다. 큐로 유입된 데이터는 메모리에서 관리하지 않고 디스크에 저장한다. 디스크에 저장된 데이터 파일이 일정 크기 이상이 되면 삭제하는 기능이 있다. 그러나 데이터 파일을 삭제하는 기준에서 오동작하는 부분이 있어 수정했다.

삭제할 데이터 파일을 선정하는 기준은 각 데이터 파일이 생성된 시간이었다. 하지만 빠르게 데이터 파일이 생성될 때 생성 시간이 역전되는 상황이 있었다. 잘못된 데이터 파일이 삭제되면 큐 데이터를 가리키고 있는 인덱스가 데이터 파일을 잘못 가리키고 있어 이미 Broker에 저장된 데이터 파일을 더 이상 꺼낼 수 없는 현상이 발생한다. 이러한 문제점을 해결하기 위해 생성 시간이 아니라 파일명에 있는 인덱스 번호를 기준으로 삭제할 데이터 파일을 선정하도록 변경했다.

NQueue-library 개발

NQueue 개발 시 가장 중점을 둔 부분은 일부 Broker 서버 장애 시 자연스럽게 서비스에서 제거되는지 여부와 그때의 메시지 유실을 최소화하는 것이다. 데이터 변경 시 동적으로 데이터를 수신하여 처리할 수 있도록 ZooKeeper를 사용해 개발했다.

큐 시스템을 사용하기 위해 사용하는 라이브러리가 바로 NQueue-library이고, 다음과 같은 기능을 가지고 있다.

  • Broker 장애 시 서비스에서 바로 Broker를 제거하고, 모든 Producer와 Consumer로 변경 사항을 전달해 메시지 유실을 최소화한다.
  • Broker 추가, 삭제, 변경 등을 서비스에 영향을 주지 않고 할 수 있어야 한다. 모든 Producer와 Consumer는 Broker 정보를 관리하는 ZooKeeper를 바라보고 있으며, 변경 사항이 발생하면 바로 Broker 목록을 교체한다.
  • 서비스당 트래픽이 일정량 이상이면 자동으로 Broker 서버를 증설하는 기능을 개발 중이다. 플랫폼 릴리스 시 운영 리소스를 최소화하기 위한 기능이다.

Producer와 Consumer에서 연결해야 할 Broker 정보를 관리하기 위한 ZooKeeper 데이터는 다음과 같다.

  • /NQueueCluster/클러스터/ Broker 정보
    • 상태: Standby, ProduceOnly, ConsumeOnly, Suspended, Running
  • /NQueue/계정/카테고리/파티션 정보

첫 번째는 Broker의 집합인 클러스터에 포함된 Broker에 대한 정보이며, 그 Broker의 현재 상태도 포함되어 있다. 상태를 요약해 보면 다음과 같다.

  • Standby: Broker 설치 후 아직 서비스에 투입하기 전 상태이다.
  • ProduceOnly: 해당 Broker는 메시지를 저장만 하는 상태이다.
  • ConsumeOnly: 해당 Broker는 메시지를 저장하지는 않고, 꺼내기만 하는 상태이다.
  • Suspended: Broker 장애로 판단되어 서비스에서 제거되어야 하는 상태이고, 모든 Producer에 전달되어 더 이상 이 상태의 Broker로 메시지를 전송하지 않는다.
  • Running: 정상적으로 서비스가 운영되고 있는 상태이다.

두 번째는 서비스별로 실제로 클러스터 중에 어떤 Broker가 투입되어 있는지에 대한 정보이다. 파티션은 클러스터 중 실제로 특정 서비스에 투입된 Broker의 집합이다. 특정 서비스를 운영할 때 클러스터의 모든 Broker가 투입되는 게 아니라 파티션에 포함된 Broker만 투입된다. 클러스터에 있지만 파티션에 포함되지 않은 Broker는 다른 서비스에서 사용하거나 트래픽이 증가할 때 사용한다.

Producer와 Consumer 관리를 위한 ZooKeeper 데이터는 다음과 같다.

  • /NQueueInfo/클러스터/계정/카테고리/con_호스트명
  • /NQueueInfo/클러스터/계정/카테고리/pro_호스트명

이 노드에서 관리하는 정보는 다음과 같다.

  • Producer와 Consumer 생성 시간
  • 접속한 서버의 호스트명
  • NQueue-library 버전
  • Producer와 Consumer 생성 개수

이 정보는 추후 버전 업데이트 요청이나 더 이상 특정 버전에서 업그레이드를 지원하지 않을 때 사용된다. 해당 노드는 ephemeral nodes로, Producer/Consumer와 ZooKeeper 사이의 연결이 유지되는 동안에만 유효하다. 즉, ZooKeeper와 연결이 끊어지면 더 이상 Producer와 Consumer는 존재하지 않는 것으로 판단하여 노드를 제거한다.

모니터링을 위한 Broker 정보를 위한 ZooKeeper 데이터는 다음과 같다.

  • /NQueueMonitor/Broker 정보

앞으로 설명할 모니터링을 위해서는 디스크 용량 등 다양한 정보가 필요하다. 모니터링에 필요한 정보는 이 노드에서 관리한다.

NQueue Manager

Luxun은 모니터링 도구를 제공하지 않는다. 따라서 Broker 상태를 실시간으로 감시하고 관리하기 위해 모니터링 도구를 개발했다.

모니터링 도구의 주요 특징은 다음과 같다.

  • 관리 화면에서는 Produce TPS와 Consume TPS를 게이지 그래프와 실시간 그래프로 효과적으로 보여 준다.
  • 큐에 저장되어 있는 메시지를 Left Messages 화면에서 실시간으로 보여 준다.
  • Broker 내 서비스별 디스크 사용량을 도넛 그래프로 보여 준다.
  • Broker의 각종 상태 정보를 관리 화면에서 보여 준다.
  • 관리 화면에서 Cluster와 Broker를 쉽게 추가할 수 있으며 Broker 상태도 변경할 수 있다.

4918f90c4d23937ba31e0accc965a2f6.png

그림 6 Broker별 Produce TPS와, Consume TPS: 게이지 그래프

631719d75971dff9a1984c64c2c978a8.png

그림 7 Broker별 Produce TPS와 Consume TPS: 실시간 그래프

NPUSH-GW의 큐 운영 방법

NQueue 운영 방법을 몇 가지만 간략하게 살펴보면 다음과 같다.

Broker 추가

Broker는 다음과 같은 방법으로 추가한다.

  1. 클러스터에 Broker를 추가한다. 상태는 자동으로 Standby로 설정된다.
  2. 특정 서비스에 클러스터 중 어떤 파티션을 사용할지 선택한다.

Broker 삭제

Broker는 다음과 같은 방법으로 삭제한다.

  1. 클러스터에 있는 Broker의 상태를 ConsumeOnly로 변경한다.
  2. 해당 Broker에는 더 이상 메시지 유입이 없으므로 이미 저장되어 있는 메시지가 모두 소진되면 Broker는 더 이상 사용되지 않는다.
  3. 저장되어 있는 메시지가 없는 것을 확인하고 Broker의 상태를 Standby로 변경한다.
  4. 모든 Producer와 Consumer에서 더 이상 해당 Broker와 연결을 유지하지 않으므로 제거하면 된다.

Broker 장애 시 대응

Broker에 장애가 발생했을 때는 다음과 같이 대응한다.

  1. 특정 Producer에서 장애를 감지하고 Broker의 상태를 Suspended로 변경한다.
  2. 모든 Producer가 Broker의 상태 변경을 감지하고, 해당 Broker를 자신의 Broker 목록에서 제거한다.
  3. 모든 Consumer가 메시지를 처리할 수 있는 데까지 최대한 많은 메시지를 처리한다.
  4. 장애 해결 후 해당 Broker의 상태를 Running으로 변경하면 다시 서비스에 투입된다.

마치며

NPUSH-GW는 빠르면서도 메모리의 한계를 벗어나 장시간 메시지를 저장해야 한다. 그래서 Big Queue 라이브러리 기반의 가벼운 메시지 큐 시스템을 도입했고, 많은 테스트를 통해 안정성을 향상시켜 플랫폼에 적용했다. NQueue를 개발하고 NPUSH-GW에 적용하면서 배운 개발 지식과 운영 노하우가 비슷한 요구 사항을 가진 시스템을 개발할 때 참고가 되었으면 한다.

네이버 Naver Labs 배상용, 임영완, 조항수, 김민곤, 김종현

Tag :
 ,   ,   ,   ,   ,   ,   ,