Redis Pub/Sub을 사용해 대규모 사용자에게 고속으로 설정 정보를 배포한 사례를 보고 작성한 글입니다.
LINE LIVE에는 수십만 규모의 동시 접속자가 발생하는 라이브 방송이 있습니다. 이를 위해 채팅 시스템의 경우 다양한 시스템과 사용자 설정을 한 번에 동기화할 수 있어야 합니다. 모든 사용자에게 가능한 한 실시간으로 동시에 정보를 반영해야 하는 타이밍이 중요한 기능이 많고 이런 기능의 정확도는 사용성에 상당한 영향을 미칩니다. 이전에도 필요에 따라 정보를 취득해서 캐시하는 구조가 있었는데요. 캐시가 만료될 때만 정보가 업데이트됐고 업데이트 빈도를 높이기 위해 유효 기간을 짧게 설정하면 캐시하는 의미가 없어져서 실시간도 아니었고 한 번에 동기화하려는 목적 또한 달성할 수 없었습니다. 이 문제를 해결하기 위해 Redis의 Pub/Sub을 이용해 채팅 서버 120대에 거의 실시간으로 정보를 배포한 뒤 각 서버 내에 캐시하는 시스템을 도입했습니다.
유명인이나 저명인 기업인 그 외 일반 유저들이 자유롭게 라이브 방송을 할 수 있는 라이브 스트리밍 서비스입니다. 또한 영상을 시청하는 유저들은 응원 아이템이나 하트를 보낼 수 있으며 7세 코멘트 기능을 사용해 방송자와 시청자가 대화하거나 시청자끼리 서로 이야기를 나눌 수 있습니다.
먼저 Line 라이브에서 사용되고 있는 채팅 서버 아키텍처에 대해서 살펴보겠습니다.
클라이언트와 서버는 웹 소켓을 통하여 통신하고 있으며 서버간 통신은 레디스의 Pub/Sub을 사용하고 있습니다. 레디스를 앞에 둬서 MySQL이 직접 저장하지 않는 것은 성능을 최우선으로 하기 때문입니다. 다만 레디스에 저장한 코멘트는 뒷단에서 배치 처리를 통한 정규화 작업을 거쳐 주기적으로 MySQL에 저장하고 있습니다.
마지막으로 유저가 시스템에서 설정한 정보를 사용해 코멘트를 제어하기도 합니다. 핵심 주제는 이 설정 정보를 120대나 되는 채팅 서버간에 어떻게 동기화하며 코멘트 제어에 반영되고 있는지입니다.
방송자가 시청자를 차단하는 경우를 예시로 들겠습니다. 방송자가 시청자를 차단하는 경우 차단된 시청자의 코멘트를 해당 방송에 표시되지 않도록 하는 기능이 활성화됩니다.
방송자가 시청자를 차단할 경우 차단된 시청자의 정보를 LINE LIVE API 서버로 송신합니다. LIVE LIVE API 서버에서는 수신받은 정보를 MySQL에 저장하고 Chat Internal API 서버로 다시 정보를 전송합니다. 이를 Chat Internal API 서버에서도 전달받은 정보를 MySQL에 저장합니다.
그 후 라이브 뷰어 A를 차단된 사용자라고 가정할 때 라이브 뷰어 A가 코멘트를 보내면 챗의 웹 소켓 서버는 로컬 메모리에 차단 정보가 존재한다면 이를 사용하고 존재하지 않는다면 MySQL로부터 데이터를 꺼내와서 기한부로 로컬 메모리에 캐싱하면서 사용합니다.
이 패턴의 경우 라이브 뷰어 A는 차단된 것으로 인식하기 때문에 다른 사용자는 라이브 뷰어 A의 코멘트를 읽지 못합니다. 다만 누군가가 코멘트할 때마다 차단 정보를 일일이 MySQL에 참조하게 되면 성능 저하로 이어질 수 있고 아무리 Redis의 캐시를 사용한다고 해도 시청자들의 코멘트가 급증하게 되면 스파이크 현상이 발생해 네트워크 대역이 고갈될 위험성이 있으므로 전체 챗 서버에 로컬 메모리 정보를 기한부로 저장하도록 하고 있습니다.
이와 같은 차단 처리를 비롯한 기존 기능은 전체 챗 서버에 대해 유저가 설정한 정보를 어느 정도 빨리 반영할 수 있으면 충분했고 각 서버에 설정이 반영되는 타이밍 또한 그렇게까지 엄수할 필요가 없었기 때문에 심플하게 기한부 로컬 메모리를 사용해 기한이 만료되면 새로운 정보로 교체하는 방식으로 각 서버에서 개별적으로 정보를 취득했습니다.
그것은 라인에서 제공하는 Chat decoration이라는 기능 때문입니다. 이 기능은 자신의 코멘트글의 배경이나 글씨체가 다른 사람과는 다르게 독특하게 꾸밀 수 있는 기능입니다. 만약 이 기능을 사용함에도 불구하고 다른 사람의 화면에서의 내 코멘트가 데코레이션 없이 화면에 노출된다면 과연 이 기능을 사용할 필요가 있을까요 ?
따라서 동시성과 실시간성이 매우 중요해진 것입니다. 그래서 라인에서는 기존 시스템에서 동시성과 실시간성에 최적화된 새로운 아키텍처를 구성하게 된 것입니다.
시청자가 데코레이션 기능을 설정하면 LINE LIVE API 서버에 리퀘스트가 전송되어 라인 라이브 본체 MySQL에 설정을 저장합니다. 그와 동시에 Chat Internal API 서버에 설정 정보를 넘겨 줍니다. 여기까지는 기존 방식과 동일합니다.
설정 정보를 받은 Chat Internal API 서버는 Redis에 퍼블리시하게 됩니다. 그렇게 되면 리슨 상태인 챗의 웹 소켓 서버는 published된 해당 정보를 subscribe해서 로컬 메모리에 캐싱합니다. 이때 subscribe process는 전체 웹 소켓 서버에서 동시에 진행되기 때문에 결과적으로 동일한 타이밍에 전체 서버에 설정이 반영됩니다.
데코레이션 설정을 마친 시청자가 코멘트를 전송하면 챗의 웹 소켓 서버는 로컬 메모리의 캐시에 저장되어 있는 설정을 참조하기만 하면 다른 시청자에게 데코레이션 정보가 반영된 코멘트를 전송할 수 있습니다.
Redis pub(publish)/sub(subscribe) 기능을 사용해서 모든 서버에 동시에 설정 정보를 동기화함으로써 사용자가 어떤 웹소켓 서버에 접속해있던지 똑같이 코멘트를 표시할 수 있게 된것입니다.
다음 예시는 라인 라이브가 현재 방송을 시청하고 있는 모든 시청자에게 어떤 알림을 동시에 전송하는 상황을 가정한 상황입니다.
라인 라이브에는 내부 운영자를 위한 Internal CMS가 존재하는데 이를 사용하여 내부 운영자가 사용자에게 일괄적으로 알림을 전송한다면 해당 정보를 받은 LINE LIVE API 서버는 MySQL의 조작 로그로서 알림 정보를 저장하는 한편 챗의 API 서버에 알림 정보를 보냅니다.
해당 알림 정보를 받은 Chat Internal API 서버는 Redis에 퍼블리시합니다. 리스닝 상태인 챗의 웹 소켓 서버는 퍼블리시드된 알림 정보를 취득해 현재 방송을 시청하고 있는 모든 시청자를 대상으로 알림 정보를 전송합니다.
이렇게 해서 알림 정보를 보냄으로써 시청자 모두에게 거의 지원없이 동시에 알림을 전송할 수 있습니다. 이러한 아키텍처를 사용함으로써 확장성을 보장하여 다양한 서비스를 구축하는데 있어 큰 어려움이 없습니다.