쿠버네티스에 레디스 캐시 클러스터 구축기

안녕하세요, 저는 콘텐츠서비스개발팀에서 카카오 TV 백엔드 서버를 개발하고 있는 헤오(Heo.d)입니다.

점점 클라우드 기반 개발이 일상화되고 있습니다. 이에 따라 쿠버네티스에 Redis를 도입할지 여부를 고민하는 분들이 많을 것 같습니다. 

 

Redis란?

Redis(REmote DIctionary Server)는 메모리 캐시 방식의 인메모리(In-memory) No-SQL 데이터베이스(키-값 구조)입니다. 즉, 데이터를 RAM에 저장하기 때문에, IO의 부담이 줄어 데이터 조회 속도가 빠른 것이 특징입니다.

 

저희 팀도 쿠버네티스에 Redis를 올리는 결정을 하게 되었습니다. 왜 Redis 클러스터 구축을 결심했는지와 어떤 방식으로 Redis 클러스터를 구축했는지에 대해 이야기하려고 합니다.

 


 

쿠버네티스에 Redis를 올리게 된 이유 :
Redis도 스케일인&아웃이 쉬워야 한다!

 

기존에 저희는 PM 장비 위에 Redis를 올리고, 이들 Redis를 클러스터로 엮어서 별문제 없이 사용하고 있었습니다.

그러던 어느 날 Redis 호출량이 높다는 모니터링 알람을 받게 되었습니다. 왜 이렇게 많은 Redis 호출이 발생을 하고 있는지 확인을 해 보았더니, 다음의 두 가지 문제가 시너지를 내고 있었습니다. 

첫 번째는 저희 코드에 문제가 있어서 Redis를 과도하게 호출하고 있었고, 두 번째는 대형 스포츠경기였습니다.

카카오tv 서비스의 좀 특별한 점은 이벤트가 있을때 트래픽이 폭등하는 패턴을 보인다는것입니다. 대형 스포츠 경기가 있을때도 동일하게 순간적으로 트래픽이 증가하는 패턴을 보입니다.

문제를 해결하기 위해서는 클라이언트 배포가 필요한 상황이어서 즉각적으로 코드 수정으로 대응을 할 수 없는 상황이었습니다. 다행히 그상태로 경기가 끝나면서 장애 상황 없이 지나갈 수 있었습니다.

그렇게 평온하게 또 며칠을 지내고 있던 어느 날, 특별한 이벤트 받게 되었습니다. 바로 인기 가수가 카카오TV 단독 송출을 한다는 이벤트였습니다. 인기가 있는 가수가 카카오 TV로 단독 송출하면 많은 팬들이 시청을 할 것이라고 생각이 되어서 미리 Redis 서버를 증설을 해두었습니다.

하지만, 안타깝게도 유튜브 동시 송출로 정책이 변경되었고, 미리 증설한 Redis 서버는 굉장히 미미한 수준의 트래픽만 처리하고 반납하게 되었습니다… 

대부분의 트래픽이 폭등하는 이벤트가 있을때 msa기반으로 구성된 서버들은 kubernetes 위에서 스케일 인/아웃이 가능하여 별 문제 없이 대응이 가능합니다. 하지만 캐시레이어로 사용하고 있는 레디스 클러스터는 온프레미스 환경으로 되어 있어서 스케일아웃이 힘든 구성이었습니다.

그렇기때문에 이벤트 트래픽을 대비해서 미리 여유롭게 레디스클러스터를 구축해 두었습니다. 조금 여유롭게 구성이 되어 있어서 평소에는 서버들의 리소스중에 일부분만 사용을하여 돌아가고 있어 리소스도 비효율적으로 사용이 되고 있었습니다.

이러한 경험들로 인해, “Redis 클러스터도 스케일인&아웃이 좀 쉽게 됐으면 좋겠다!”라는 생각을 하게 됐고, 어떻게 하면 가능할지를 고민하게 되었습니다.

그래서 기존에 PM 장비에 올라가 있던 Redis 클러스터를 VM으로 이전하는 것을 고려하게 됐습니다. 이미 저희 파트를 포함해 사내의 많은 서비스들이 쿠버네티스 상에서 운영되고 있었기 때문에, 스케일 인,아웃과 운영이 쉽고 리소스 효율이 좋은 쿠버네티스에 Redis 클러스터를  올려보기로 결정했습니다.

 

쿠버네티스에 Redis를 올려보자!

결론부터 말씀드리면, 쿠버네티스 오퍼레이터 패턴(Kubernetes Operator Pattern)을 사용했고, 쿠버네티스환경에서 권장되지 않는 호스트 네트워크(Host Network)를 사용했습니다. 또한, HA를 위해서 파드 어피니티(Pod Affinity)를 적용하고, 모니터링을 위해 Redis Exporter와 Prometheus, 그리고 Grafana를 사용했습니다. 

  • Kubernetes Operator Pattern
  • Host Network
  • Pod Affinity
  • Redis Exporter + Prometheus + Grafana

 

Redis 클러스터를 구성한 방법

CRD(Custom Resource Definition) 생성

먼저 클러스터를 정의할 CRD(Custom Resource Definition)를 생성합니다.
CRD는 크게 다음과 같이 구성되어 있습니다.

 

Redis 컨트롤러 개발

위에서 정의된 kredis 커스텀리소스의 생성, 삭제, 변경 등의 이벤트들을 받아서 처리할 수 있는 Redis 컨트롤러를 개발했습니다.

Redis 컨트롤러의 동작 방식은 다음과 같습니다. 

Redis 컨트롤러가 사용자의 커스텀리소스(이름: Test1) 생성(추가)과 같은 이벤트를 수신하면, 해당 컨트롤러는 해당 커스텀리소스용 이벤트 큐를 생성하고 적재합니다. 이후, 별도의 쓰레드를 이용해 커스텀리소스 값에 맞게 Redis 클러스터를 생성합니다.

(Test1 값에 맞게 마스터 2개, 각 마스터 당 레플리카 1개씩 2개의 레플리카에 대한 4개의 파드를 띄우고 클러스터 meet, 슬롯을 할당, 레플리카 등록 등의 작업 수행)

그런 다음, HA처리를 위해 Redis 파드의 삭제에 대한 알림을 수신할 수 있도록 별도의 Watch 처리를 추가하였습니다.

 

Redis 컨트롤러의 동작 방법: “리컨사일(reconcile)”

 

Redis 컨트롤러는 커스텀리소스와 Redis클러스터의 현재 상태가 동일한 지 확인하는 리컨사일(reconcile)이라는 작업을 통해 작동합니다.

리컨사일은 다음과 같은 것들을 확인합니다. 커스텀 리소스의 마스터, 레플리카의 숫자와 Redis클러스터의 상태가 맞는지? 레디스 클러스터의 슬롯들은 균등하게 배분이 되어 있는지? POD와 커스텀리소스, redis 클러스터의 상태가 모두 동일한지? 등등

 

커스텀리소스가 하나 더 추가된 경우에는?

사용자가 Test2커스텀 리소스를 추가 했다고 하면 Redis 컨트롤러는 내부에 Test2용 이벤트 큐를 생성한 다음, 이를 기반으로 리컨사일을 진행해 Redis클러스터를 생성합니다. 

이처럼 커스텀리소스마다 이벤트 큐를 생성하는 이유는 Redis 클러스터가 서로 독립적으로 동작하도록 하기 위해서입니다. 즉, 예를 들어 Test1 클러스터가 스케일아웃을 하는 사이에 Test2 클러스터의 HA 처리가 지연되면 안 되기 때문에 각각의 쓰레드와 이벤트큐에서 별도 처리하도록 구성했습니다. 

 

커스텀리소스에 변경이 있다면? : 스케일 인/아웃과 삭제

삭제나 업데이트(스케일아웃 포함)의 경우도 비슷합니다. 커스텀리소스에 삭제 또는 업데이트된다는 이벤트를 Redis 컨트롤러가 수신하게 되면, Redis 클러스터가 파드를 삭제하거나 업데이트된 정보에 맞춰 클러스터를 업데이트하고 클러스터 내의 슬롯을 리밸런싱 합니다. 

즉, Redis 컨트롤러가 커스텀리소스와 클러스터의 현재 상태가 동일한 지 확인하는 리컨사일(reconcile) 작업을 진행합니다.

 

클러스터 HA처리 : 마스터 파드가 죽었다면?

마스터 2번 파드가 죽은 상황을 가정하겠습니다. 즉, 마스터 2번 파드가 죽으면 Redis 클러스터에서는 자체적으로 Redis간 통신을 통해 마스터 2번이 죽었음을 확인합니다. 그러면, 클러스터 자체의 선출 과정을 통해 레플리카 2번이 마스터 2번으로 승격하게 되고, 그 사이 파드가 삭제되었다는 이벤트를 Redis 컨트롤러가 수신합니다. 

이후, Redis 컨트롤러는 파드가 삭제되었다는 이벤트 값을 확인하여 신규 파드를 생성하고, 이렇게 생성된 파드를 새로 선출된 마스터 2번의 레플리카로 등록하는 작업을 진행합니다.

 

Redis 파드의 내부 구성

 

Redis 파드는, 크게 Redis 컨테이너Exporter 컨테이너(메트릭 수집용)의 2개의 컨테이너로 구성되어 있습니다.

 

Redis 파드의 내부 구성 (1) : 호스트 네트워크 사용

파드 구성을 보시면, 호스트 네트워크를 사용하는 걸 알 수 있습니다. 

Redis 클러스터에 어떻게 접근할 것인가에 대한 고민을 많이 했습니다. 쿠버네티스가 권장하는 인그레스를 통하는 방식의 경우에는, Redis 파드가 뜰 때마다 컨트롤러에서 컨피그맵의 tcp 설정을 수정하고 업데이트하는 작업을 매번 해야 합니다. 또한, 직접 접근하는 것보다는 성능이 떨어질 거라는 염려도 있었습니다. 그래서 개발 복잡도를 낮추고 캐시 서버의 성능을 최대한 확보하기 위해 호스트 네트워크를 사용하기로 했습니다. 

Redis 포트 번호는  커스텀리소스의 basePort를 기반으로 순차적으로 증가합니다. Redis 클러스터에서 사용하는 버스 포트는 일반적인 커뮤니케이션 포트와 10000 차이가 납니다(참고: https://redis.io/topics/cluster-tutorial). 

모든 Redis 클러스터 노드는 2개의 TCP 연결이 개방되어 있어야 합니다. 하나는 일반적인 Redis TCP 포트로 클라이언트 서빙을 위해 사용하며, 여기에 10000을 더한 포트를 클러스터 버스 포트(노드 간 통신 채널)로 사용합니다. 

한편, 저희는 메트릭 수집에 사용되는 Exporter의 포트는 Redis 포트에 5000을 더한 값을 사용하고 있습니다. 

예를 들어, 커스텀리소스의 basePort가 10000이라고 가정하면, 파드 1번은 10000번, 파드 2번은 10001번, 파드 3번은 10002번 포트와 같이 순차적으로 하나씩 증가합니다. 이에 따라, 대략 5,000개 파드를 생성할 수 있습니다. 즉, 쿠버네티스 클러스터 당 5,000개의 Redis 파드를 운영할 수 있다고 생각하면 됩니다. 

파드 번호 Redis 포트 클러스터 버스 포트 Exporter 포트
1 10000 20000 15000
2 10001 20001 15001
3 10002 20002 15002
5000 14999 24999 19999
5001 15000 25000 20000

참고로, 정확하게는 5,000개 이상의 파드를 생성할 수 있습니다. 만약에 포트 번호가 겹친다고 해도 워커 노드가 겹치지만 않으면 배포와 운영이 정상적으로 이루어집니다. 따라서 5,000개보다 더 많이 파드를 생성할 수는 있습니다만 안정적인 운영을 위해 추천드리지 않습니다.

 

Redis 파드 내부 구성 (2) : 레이블과 어노테이션 추가 

레디스의 마스터노드와 레플리카 노드가 동일한 쿠버네티스 워커노드에 배포가 되면 해당 워커 노드가 죽는 순간 정상적인 HA처리가 되지 않기때문에 레이블에 현재레디스 노드의ID를 추가 하고 이 값을 이용해서 마스터와 레플리카가 같은 워커노드에 올라가지 않도록 파드어피니티를 적용하였습니다.

또한, Prometheus가 메트릭 값을 정상적으로 읽어 갈 수 있도록, 포트 번호도 어노테이션에 추가했습니다. 

 

Redis 컨트롤러 자체가 죽었다면?

Redis 컨트롤러 자체가 죽을 수도 있습니다. Redis 컨트롤러는 쿠버네티스의 디플로이먼트로 구성이 되어 파드가 자동으로 생성이 됩니다. 컨트롤러가 생성이되면 가장먼저 현재 적용되어 있는 커스텀리소스 리스트와 파드 리스트의 정보를 가져오게 됩니다. 이후 전체 커스텀리소스의 리컨사일 작업을 진행을 하는데요. 이유는 컨트롤러가 죽어있던 순간에 여러가지 이유로 커스텀 리소스와 Redis클러스터의 상태가 틀어질 수 있기 때문입니다.

리컨사일작업이 완료되면 컨트롤러는 이벤트큐를 생성하고 이벤트를 받아서 정상적인 처리할 수 있는 상태가 되어서 복구가 완료되게 됩니다

이렇게 해서 Redis 클러스터를 구성했습니다!!
추가적으로, 구축 시 했던 고민 내용과 이를 어떻게 해결했는지에 대해 간단히 말씀드리려고 합니다. 

 

구축 시 고민 사항들과 해결한 방법

 

PM -> VM으로 변경되면서 발생하는 성능 이슈는 어떻게 극복해야 하는가?

메모리의 경우, 클라우드에서 이용하는 워커 노드의 메모리 자체의 사이즈를 키우거나 Redis 마스터 노드의 개수를 늘려 대응하고 있습니다.

CPU성능의 경우, 레플리카 리드(Replica Read)를 통해서 부하 분산을 하고 있습니다. 기존에는 마스터에서만 Read를 했는데, 현재는 마스터와 레플리카 둘 다에서 동시에 Read를 사용하고 있습니다. 핫키나 다량의 트래픽이 발생했을 때 Redis 레플리카 노드를 스케일아웃하는 전략으로 대응하고 있습니다. 

 

Redis 노드 주소 접근은 어떻게 해야 하는가?

쿠버네티스에 올라가 있기 때문에 클러스터 내에서 파드가 죽고 살아나고 있을 수 있습니다. 따라서 어딘가에서는 Redis 주소를 제공해 주어야 합니다. 

저희는 Redis 컨트롤러에서 HTTP API 방식을 통해 주소를 제공하고 있습니다. 저희가 사용 중인 레터스(lettuce) 라이브러리의 경우, 서버가 뜰 때 딱 한 번만 Redis 컨트롤러에 주소를 넣어주게 되면 레터스 라이브러리 내부에서 해당 주소를 기반으로 레지스트 클러스터와 커넥션을 맺게 됩니다. 그 이후에는 맺어진 커넥션을 이용해서 클러스터 상태 변경 이벤트를 받습니다.

 

컨트롤러 동작중 발생한 오류에 따른 롤백처리

컨트롤러에서 어떤 처리를 할때 이전 상황으로 되돌릴 수 있도록 Action단위로 스택에 저장을 하고 문제가 발생하는 경우에 저장되어 있는 Action을 보고 이전 상태로 되돌리는 롤백 방법을 사용하였습니다.

예를 들어 레플리카노드 스케일아웃을 진행했다고 가정을 하겠습니다. 아래 보시는 것처럼 Action들이 스택에 쌓이게 됩니다. 이때 ReplicaOf를 하려려다 통신 오류가 발생을 했다고 가정을 해보겠습니다

스케일아웃이 진행되기 이전상태로 되돌리기위해 스택에 저장되어 있는 Action에 반대되는 액션을 생성해서 롤백처리하게 됩니다. 롤백 처리가 완료되면 스케일아웃을진행 하기 전상태로 돌어갈 수 있습니다. 이러한 방법으로 롤백 처리 하고 있습니다.

 

모니터링은 어떻게 해야 하는가?

Exporter와 Prometheus, Grafana를 사용해 구성했습니다. 여러 쿠버네티스 클러스터에서 수집되는 메트릭을 하나의 Grafana 화면에서 볼 수 있도록 구성했습니다.  

 

스케일인&아웃은 정말로 필요한가?

Redis 클러스터는 슬롯은 여러 단계를 거쳐서 이동해야 해서 불편합니다. 또한, 경험상 Redis의 메모리는 한 번에 확 차오르는 경우는 잘 없었습니다(완만). 따라서 메모리 스케일아웃이 필요한 경우에는, 대부분의 경우에는, 용량이 큰 신규 Redis 클러스터를 생성한 이후에 옮겨가는 전략을 사용해도 문제가 없습니다.

하지만 이벤트로 인해 트래픽이 갑자기 올라가거나 모니터링을 놓쳐 메모리가 이미 차 있는 경우에는 스케일아웃이 필요할 수 있습니다. 따라서 긴급한 상황에서의 스케일아웃 기능은 꼭 필요합니다

 

마치며

 

마지막으로, 쿠버네티스에 오퍼레이터패턴으로 Redis 클러스터를 도입한 후의 장단점을 정리해 보겠습니다.

장점 

  • 스케일 인/아웃을 편하게 할 수 있습니다.
  • 문제가 발생했을때 클러스터가 자동으로 복구가 가능합니다.
  • 트래픽에 따라서 리소스를 효율적으로 사용할 수 있습니다.
  • Redis 클러스터를 생성할때 정확한 용량산정이 필요 없습니다.(스케일 인/아웃이 으로 조절이 가능해서)

 

단점 

  • 컨트롤러 코드에 문제가 있어서 Redis 클러스터에 이상이 생기는 경우, 코드를 수정해서 배포하기 전까지는 정상적인 복구가 어렵습니다. 
  • 컨트롤러가 죽었을 때는 Redis 접근을 못하는 문제점이 있습니다. 
  • 레플리카 리드(Replica Read)의 경우, Redis 라이브러리에서 지원하지 않는 경우들이 있습니다(예: jedis)

 



해당 내용은 ifkakao(2021)에 발표했던 내용으로 데모영상은 링크를 참고 부탁드립니다.
if(kakao)2021 “쿠버네티스 레디스 클러스터 구축기”

쿠버네티스에 Redis를 도입할지에 대해 고민하는 분들께 도움이 되면 좋겠습니다.
긴 글 읽어주셔서 감사합니다.

카카오톡 공유 보내기 버튼

Latest Posts

제5회 Kakao Tech Meet에 초대합니다!

Kakao Tech Meet #5 트렌드와 경험 및 노하우를 자주, 지속적으로 공유하며 개발자 여러분과 함께 성장을 도모하고 긴밀한 네트워크를 형성하고자 합니다.  다섯 번째

테크밋 다시 달릴 준비!

(TMI: 이 글의 썸네일 이미지는 ChatGPT와 DALL・E로 제작했습니다. 🙂) 안녕하세요, Kakao Tech Meet(이하 테크밋)을 함께 만들어가는 슈크림입니다. 작년 5월에 테크밋을 처음 시작하고,