Global Store - Spring Boot 연결 이슈

3/14/2024

AWS 제공의 Global Store 사용에 있어 어플리케이션(Spring Boot) 서버에서의 몇가지 이슈가 있어 정리하려한다.

배경

장애 내용의 공유를 위해서 공유하기 전 왜 장애가 발생하게 됐는지와 현재 상황과 환경에 대해 공유해야 한다.

Notion Image

AWS ElastiCache 를 이용한 일반적인 Redis 클러스터 구성이다. 기본적으로 클러스터 구성 시, 3개 노드까지 지원해주며 각 노드에 직접 접근하지 않아도 되도록 구성 엔드포인트(configuration endpoint)를 제공하여 클러스터 내에 노드중 일부에 장애가 발생하여도 문제가 되지 않도록 제공해준다. (추가적으로 샤딩 구성을 제공하지만 생략하여 구성을 보면 위와 같다)

클러스터 구성에 서 조금 더 얘기해보면 3개 중 하나의 노드만이 마스터 노드가 되고, 다른 노드들은 마스터 후보 노드 구성되어, 실질적인 읽기/쓰기 기능을 마스터 노드가 담당하여 제공해주다가, 일련의 이슈로 마스터 노드가 죽게 되면 다른 마스터 후보 노드가 마스터 노드로 승격되어 클러스터의 안전성 제공이 목적이다. 그래서 이 마스터 노드, 마스터 후보 노드에 대해 반영하면 다음과 같다.

Notion Image

이렇게 구성되게 되면 실제 사용하는 어플리케이션 서버에선 클러스터에 대해 제공받는 엔트포인트만 접근하면 되도록 되어있다.

보통은 위의 내용 처럼 클러스터 구성을 하나만 두고 사용하는 경우가 많을 것 같다. 하지만 현재 서비스하는 운영하는 서버에서는 리전(지역) 문제가 있다.

글로벌 서비스 제공

글로벌 서비스를 제공하는데, 글로벌 전반의 사용자 경험이 저하되는 일이 없도록 현재 하나의 도메인에 AWS 글로벌 액셀레이터 서비스를 제공받아, 접근 IP 에서 가까운 지역(리전)의 서버로 연결되도록 트래픽을 분기 처리하였다.

Notion Image

간소화하여 도메인에 들어오는 트래픽이 분리되는 모습을 어플리케이션 서버까지 보면 다음과 같다. 문제는 글로벌 서비스에서 이 트래픽에 대한 분기는 처리하였으나, 그 뒤에 데이터 흐름에서 또 문제가 생긴다. 결국엔 각 리전별로 분리되어 들어간 어플리케이션은 요청에 따라 데이터를 가져와 제공해주어야 하는데, 이 때 제공되어야할 데이터 때문에 ElastiCache 클러스터 구성에서 문제가 생긴다.

단일 클러스터 구성 문제

만약 한국 - 서울 리전에 클러스터를 구성한다고 해보자.

Notion Image

다음과 같은 구성을 하게 되면 그림에도 적어두었지만 미국 - 오하이오 에 구성한 서버로 접근한 사용자들은 ElastiCache 데이터에 접근하게 되면, 물리적으로 서버가 굉장히 멀리있기 때문에 응답이 느려 사용자 경험이 저하된다. 아무리 메모리 DB 라지만 물리적으로 서버가 멀리 있기 때문에 클라이언트 응답까지 5s 이상이 걸리게 된다.

위 내용을 파악한 후, 팀 내부적으로 다음과 같은 방향을 고려하게 되었다.

  • 각 리전별 ElastiCache 구성하여 개별 DB 관리
  • 리전별 ElastiCache 동기화 서비스인 Global Store 서비스 사용
  • 처음 1안에 대해 얘기했을 때 문제로, 현재 프로젝트 내부적으로 ElasctiCache 에 대해 캐시 용도로 사용하며 이 캐시에 담는 내용 중 유저들의 세션 정보를 포함하고 있는데, 글로벌 엑셀레이터가 클라이언트 IP 에 대해 분기 처리 시 애매한 지역에 걸쳐있는 사용자의 경우 한국 - 서울, 미국 - 오하이오 둘 다 접근할 가능성이 있다는 것이었다. 애매한 지역에 걸쳐있는 사용자의 경우 다음과 같은 일이 발생할 수 있다.

  • 글로벌 엑셀레이터에서 한국 - 서울 지역으로 분기 → 한국 - 서울 어플리케이션에서 로그인하여 유저 세션 저장
  • 글로벌 엑셀레이터에서 미국 - 오하이오 지역으로 분기 → 미국 - 오하이오 어플리케이션에서 유저 세션 정보를 찾지 못해 로그인이 끊겨 다시 로그인 처리
  • 흔히 로그인에서 이유를 알 수 없는 틩김 현상을 경험할 수 있다. 이 또한 서비스의 사용자 경험에 저해되는 내용이므로 이 보다 비용이 조금 더 나가더라도 Global Store 서비스를 고려하게 되었다.

    Global Store 서비스의 고질적인 문제

    사실 말은 좋게 해당 서비스를 사용하면 간단하게 해결될 것 처럼 얘기했지만, 사실 Global Store 를 사용하는 것도 고려해야할 문제들이 있다. Global Store 는 다음과 같이 서비스를 제공한다.

  • 다른 리전에 대해 개별 클러스터 구성과 각 클러스터 간 데이터 동기화 0.9s 내로 지원
  • 단일 리전 클러스터에 마스터(읽기/쓰기) 권한 구성, 다른 리전 클러스터는 해당 읽기 권한만 제공
  • 첫번째 안만 보면 굉장한 서비스라고 생각이 들지만, 두번째 안을 보면 뭔가 싶을 것이다. 저 두번째 안은 결국 어떤 말을 의미하느냐 하면 다음과 같다.

  • 두 리전 중 같은 리전 내에 마스터(읽기/쓰기) ElastiCache 가 없는 어플리케이션 서버는, 쓰기 기능에 대해 성능상 손해를 감수해야 한다.
  • 두 리전 중 같은 리전 내에 마스터(읽기/쓰기) ElastiCache 가 없는 어플리케이션 서버는, 쓰기 위해 마스터 ElastiCache 연결, 읽기에 대해 레플리카(읽기) ElastiCache 를 연결, 두 개의 클러스터를 연결하여야 한다.
  • 두 리전 중 같은 리전 내에 마스터(읽기/쓰기) ElastiCache 가 없는 어플리케이션 서버는, 위 언급한 두개의 클러스터 연결을 지원하며 동시에 쓰기에 사용되는 연결과 읽기에 사용되는 연결을 분리하여 역할에 맞게 사용하여야 최소 읽기에 대한 Latency 지연이 적은 이득을 취할 수 있다.
  • 정리하고 나니 단일 클러스터 구성과 다르게 고려해야할 내용들이 많다. 하지만 적어도 이 데이터 동기화로 사용자는 로그인 틩김 현상을 겪지 않아도 되고, 내부적으로 캐시는 만료시간을 관리하는데 이 만료시간이 만료되지 않는 동안은 읽기만 사용하여 성능 상 이점을 분명히 얻을 수는 있었다.

    테스트 환경 구성

    먼저 어플리케이션 서버의 설정도 설정이지만, 이를 확인하기 위한 테스트 환경의 확보가 우선된다. 테스트 환경은 다음과 같은 문제가 있다.

  • 현재 운영환경이 아닌 개발, 스테이징 환경의 경우 클러스터 구성이 아닌 Standalone 구성이다.
  • 로컬에서 테스트를 위해 해당 ElastiCache 에서 처럼 각 클러스터 구성을 하고 역할 분리까지 진행할 물리적인 시간 여유가 없다.
  • ElastiCache 는 내부 연결을 지원하고 외부 접근을 막는다.
  • 기존 로컬에서 테스트 진행 시 사용한 Redis 의 경우 개발환경(dev) 에 인스턴스 내에 직접 구성하여 사용하고 있다. 이 경우 Standalone 구성으로, 현재 ElastiCache 의 클러스터 구성과 조금 다르게 구성되어 있다.

    즉, 이 테스트를 위해서 어플리케이션 서버에 대해서만 고려하는 데에도 시간적 여유가 없는데, 운영이 아닌 다른 환경에 동일한 Redis 환경을 구성하는데에는 더 더욱 시간적 여유가 없다 판단했다. (인프라의 문제와 새로운 환경 구성은 시간을 많이 소요하고 이를 확인하기 위한 테스트도 별개로 진행해야 한다)

    결국 테스트는 배포 등에 시간이 들어가겠지만 그나마 현실적으로 빠르게 테스트 할 수 있도록, 운영과 같은 네트워크 대역에 운영과 다른 환경으로 어플리케이션 서버를 구성하고, Global Store 에 연결하는 방식으로 진행하였다.

    어플리케이션에서 두 개의 클러스터 연결

    Notion Image

    Notion Image

    (이상적으로 연결되길 희망하는 구성환경)

    그렇다 클러스터가 두 개다. 이 방향으로 가기로 결정은 되었으니, 이제부턴 어플리케이션 설정에서 이걸 지원해주느냐 마느냐의 문제이다. 길고 길었는데 대제목으로 언급한 Spring Boot 에서 연결에 대한 이슈가 이제부터 이다.

  • 어플리케이션 서버에서 두 개의 Redis 에 대한 커넥션 연결
  • 두 개의 Redis 커넥션을 연결하고 각 커넥션에 대해 읽기/쓰기에 대해 역할 분리하여 사용
  • 핵심 과제라고 하면 저 두 가지 문제를 해결해야한다. 현재 사용하는 어플리케이션 서버의 Spring Boot 프레임워크에서 이를 지원하는가? 이게 핵심 문제가 된다.

    Spring Boot 에서 Redis 연결에 대해 공식 지원하는 라이브러리는 Lettuce 이다.

    Redis 연결을 지원하는 라이브러리가 이전에는 Jedis 가 있었지만, 현재 Spring 공식 지원은 Lettuce 로 되어있다. 이에 LettuceConnectionFactory 객체를 지원하여 해당 ConnectionFactory 를 이용하여 template(커스텀 명령지원), cacheManager(캐시 관리) 등을 사용할 수 있다. 즉, 저 LettuceConnectionFactory 를 만드는 부분을 봐야 하는데, 해당 객체는 다음과 같은 정보가 필요하다.

  • RedisConfiguration: 레디스 구성 정보
    • RedisStandaloneConfiguration: Standalone Redis 연결에 사용한다.
    • RedisStaticMasterReplicaConfiguration: 정적으로 마스터, 레플리카를 설정한다.
    • RedisClusterConfiguration: 클러스터 Redis 연결에 사용한다.
    • RedisSentinelConfiguration: Redis Sentinel 구성 연결에 사용한다.
  • LettuceConfiguration: Lettuce 라이브러리를 이용한 연결 시 구성 정보
  • Redis 는 구성을 여러가지 할 수 있고, 거기에 맞게 연결할 수 있는 구현체도 여러가지가 있다. ElastiCache 가 클러스터 구성이기 때문에, 처음 구성할 때 팀 내부적으로도 클러스터 구성으로 가는게 맞는지, 마스터레플리카 구성으로 가는게 맞는지 얘기가 많았다. 문제가 당시 저 문제로 여러 테스트를 진행했지만, 급하게 진행하던 터라 남아 있는 히스토리가 없고, 현재 커밋 기록만 일부 남아있다.

    가장 이상적으로 연결되는 모양은 RedisStaticMasterReplicaConfiguration 로 구성하여 각 리전별 구성된 클러스터 엔드포인트에 연결되는 상단 이미지처럼 연결되는게 이상적이었으나, 당시 해당 설정으로 해결되지 않았던 것으로 기억한다. 그렇다고 RedisClusterConfiguration 구성으로 가니 읽기/쓰기 에 대해 역할 구분이 되지 않았던 것으로 기억하고 있다. 하여 내부적으로 당시 정리된 내용은 다음과 같이 진행되었다.

    Notion Image

    ElastiCache 는 구성 엔드포인트로(configuration endpoint)를 제공하지만, 각 노드들에 대한 엔드포인트도 제공한다. 연결 엔드포인트 설정은 읽기/쓰기를 분리해서 접근할 서울은 다음과 같이 각 클러스터 1번 노드에 접근하게 하고, LettuceConfiguration 설정에 ReadFrom 이라는 설정을 REPLICA_PREFERED 로 설정하여 읽기는 레플리카를 우선으로 보게 설정하였다.

    해당 설정으로 반영된게 2023-12-14 경이다. 이상적인 모습의 연결이 아니라 팀내 부채로 남았지만, 시간적 여유가 없는 만큼 연결에 문제가 없다고 판단하여 해당 내용으로 반영되었다.

    2024-03-03(일) 장애 발생

    03/03 CS 채널로 로그인, 결제가 되지 않는다는 얘기가 올라왔다.

    Notion Image

    Notion Image

    ElastiCache 에서 몇가지 의미있을 만한 지표로 가져왔다. 오하이오에 두었던 마스터 클러스터 지표이다. 내용을 보면 03/01 부터 일련의 이유로 001 노드에 대해 문제가 발생한 것으로 보인다.

    1번째 지표를 보면 1번 노드에 연결되어 사용되던 TTL 내용에 대해 3번으로 옮겨 간 것이 보인다. 1번 마스터 노드에 연결되어 여러 사용 지표에 대해 1번 노드에서 유지되던 내용들이 1번 노드에 대해 일련의 이유로 3번 노드로 마스터 노드가 변경된 것으로 보인다.

    2번 지표는 캐시 적중률 지표인데, 보면 실제 장애 발생이 03/01 쯤에 발생하였으나, 실제 대응했던 03/03 일자까지 전혀 관리 되지 않고 있다.

    03/01 일자 약 로컬 시간으로 16:00 경 쯤에 일련의 문제가 발생하여 001 노드가 죽고, 003 노드가 그 때부터 마스터 노드 역할을 수행한 것으로 보인다. 때문에 당시 팀장님께서 응급 조치로 03/03 중에 마스터 노드인, 003번 노드로 연결되도록 어플리케이션 설정을 변경하여 배포해주셨다.

    당시 공휴일로 인해 장애 상황을 인지 하지 못하고 있었고, 때문에 대응도 늦어지게 된 것으로 보인다. 하지만 향후 이런 상황이 발생하지 않도록, 장애 대응이 필요하다 생각되어 파트 내에서는 최우선적으로 이에 대응할 방안 생각해야 했다.

    원인 분석

    그럼 이와 같은 상황이 왜 발생했는지 원인을 살펴보자. 현재 우리의 클러스터 연결상태가 어떠한가.

    Notion Image

    위 그림을 그대로 가져오긴 했지만 001번 노드에 연결되어있던 내용이 응급조치로 이제 003번 노드에 연결되어있을 뿐, 거의 그대로의 설정이다. 큰 그림에서 제일 문제되는 부분은 각 클러스터의 마스터 노드에 직접 연결하여 사용하는 부분이다. 때문에 해당 마스터 노드가 문제가 발생하여 클러스터 내에 다른 노드로 마스터 노드가 변경되어도 우리가 설정하고 올린 어플리케이션 서버에서는 변경된 마스터 노드가 어딘지 알 수가 없다. 이 때문에 마스터 노드에 캐시 매니저로 쓰기 요청을 보내야 하는데, 적절한 쓰기 요청을 사용할 수 없어 발생한 장애였다.

    이런 상황이 발생하는 경우 우리가 적절한 장애 대응을 하지 않으면, 장애 발생 시 변경된 마스터 노드를 찾아 해당 노드에 대해 새로 설정을 변경하여 배포해주어야 한다. 이는 제대로 된 장애 대응이 될 수가 없고, 직접 문제가 발생하면 이에 대한 대응을 개발자가 직접 짊어지고 가게 된다. 언제 어디서나 장애 대응을 할 수 있도록 개발자를 스텐바이 상태에 두게 되어 개발자 정신건강에 좋지 않다.

    그럼 최우선적으로 이에 대응하기 위해 고려해야할 부분은 다음과 같다.

  • 마스터 클러스터 내에서 일련의 문제가 발생하여도, 마스터에 읽기/쓰기 등의 요청은 지속적으로 사용할 수 있어야 한다.
  • 이 한가지 최우선 대응을 위해 어플리케이션 설정에 대해 전면 재검토가 이뤄졌다.

    장애 대응 재검토

    해당 장애로 인해, 현재 Redis 연결에 대해 문제점이 있다는걸 알게 되었다. 위에서 짧게 얘기했지만 현재 어플리케이션 내에 설정은 RedisStaticMasterReplicaConfiguration 설정에 LettuceConfiguration 에 ReadFrom.REPLICA_PREFERED 설정이라고 잠시 설명한 적이 있긴한데 이를 쉽게 보기 위해 그림으로 보면 다음과 같다.

    Notion Image

    아마 어플리케이션 서버 내에 Redis 연결과 관련된 설정만 간략하게 본다면 다음과 같다. 일단 이전 테스트 내용에 대한 히스토리가 별도 관리된게 없고, 현재 클러스터에서 마스터 노드로 단일 연결만으로는 장애 대응이 불가능 하다는 점에서 현재 설정 외에 어떤 설정으로 가야할지 방향이 모두 열려 있다고 생각했다. 큰 방향성으로 두가지 안이 제시되었다.

  • Redis Configuration 을 변경하여 적용한다.
    1. 클러스터 엔드포인트로 설정이 되는가
    2. 클러스터 내에 전체 노드에 대해 설정하고 적용 되는가
  • 읽기/쓰기에 대해 ConnectionFactory 부터 분리하여 적용한다.
    1. 읽기/쓰기 분리된 Connection 처리에 대해 각 주입받는 단일 객체에서 나눠서 처리 되는가
    2. 읽기/쓰기 분리된 Connection 처리에 대해 주입받는 객체도 분리된다면 현재 적용되어있는 관련 로직에 대해 공수가 얼마나 들 것이며, 가장 공수를 안들이고 처리할 수 있는 방법은 무엇인가
  • 어플리케이션에서 봐야할 큰 맥락이 두가지 정도 나왔지만, 정말 어떤 처리가 맞고 적절한 것인지는 모르는 상황이라 모든 가능성이 열려있다고 봐야했다. 이 중에 현재 운영과 관련없는 Stansalone, Sentinel 설정은 제외하고 본다.

    테스트 사전준비

    ConnectionWatchdog 로깅 확인

    테스트를 진행하며 알게된 내용 중 나름 유용했던 설정이 있다.

    logging.level.io.lettuce.core.protocol.ConnectionWatchdog=DEBUG

    해당 객체는 lettuce 에서 제공하는 객체이며 커넥션 연결에 대해 확인할 수 있도록 로깅을 제공해준다.

    해당 객체가 확인해주는 로깅을 보면 다음과 같다.

    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x50d0bb6c, /10.0.42.59:55816 -> photowidget-redis-slave-v3.yl2nl5.clustercfg.apn2.cache.amazonaws.com/10.0.37.146:6379, last known addr=photowidget-redis-slave-v3.yl2nl5.clustercfg.apn2.cache.amazonaws.com/10.0.37.146:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xd23e595d, /10.0.42.59:39986 -> photowidget-redis-master-v3.sevvte.clustercfg.use2.cache.amazonaws.com/20.0.168.191:6379, last known addr=photowidget-redis-master-v3.sevvte.clustercfg.use2.cache.amazonaws.com/20.0.168.191:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x67c26040, /10.0.42.59:52558 -> /10.0.132.115:6379, last known addr=/10.0.132.115:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x9093a911, /10.0.42.59:48900 -> /10.0.30.39:6379, last known addr=/10.0.30.39:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xbfe929c2, /10.0.42.59:55822 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x38d2f875, /10.0.42.59:39990 -> /20.0.168.191:6379, last known addr=/20.0.168.191:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xb572c207, /10.0.42.59:55792 -> /20.0.157.174:6379, last known addr=/20.0.157.174:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xbee72739, /10.0.42.59:36300 -> /20.0.7.184:6379, last known addr=/20.0.7.184:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xd23e595d, /10.0.42.59:39986 -> photowidget-redis-master-v3.sevvte.clustercfg.use2.cache.amazonaws.com/20.0.168.191:6379, last known addr=photowidget-redis-master-v3.sevvte.clustercfg.use2.cache.amazonaws.com/20.0.168.191:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xd23e595d, /10.0.42.59:39986 -> photowidget-redis-master-v3.sevvte.clustercfg.use2.cache.amazonaws.com/20.0.168.191:6379, last known addr=photowidget-redis-master-v3.sevvte.clustercfg.use2.cache.amazonaws.com/20.0.168.191:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x50d0bb6c, /10.0.42.59:55816 -> photowidget-redis-slave-v3.yl2nl5.clustercfg.apn2.cache.amazonaws.com/10.0.37.146:6379, last known addr=photowidget-redis-slave-v3.yl2nl5.clustercfg.apn2.cache.amazonaws.com/10.0.37.146:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x50d0bb6c, /10.0.42.59:55816 -> photowidget-redis-slave-v3.yl2nl5.clustercfg.apn2.cache.amazonaws.com/10.0.37.146:6379, last known addr=photowidget-redis-slave-v3.yl2nl5.clustercfg.apn2.cache.amazonaws.com/10.0.37.146:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x67c26040, /10.0.42.59:52558 -> /10.0.132.115:6379, last known addr=/10.0.132.115:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x67c26040, /10.0.42.59:52558 -> /10.0.132.115:6379, last known addr=/10.0.132.115:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x9093a911, /10.0.42.59:48900 -> /10.0.30.39:6379, last known addr=/10.0.30.39:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x9093a911, /10.0.42.59:48900 -> /10.0.30.39:6379, last known addr=/10.0.30.39:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xbfe929c2, /10.0.42.59:55822 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xbfe929c2, /10.0.42.59:55822 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xb572c207, /10.0.42.59:55792 -> /20.0.157.174:6379, last known addr=/20.0.157.174:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xb572c207, /10.0.42.59:55792 -> /20.0.157.174:6379, last known addr=/20.0.157.174:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xbee72739, /10.0.42.59:36300 -> /20.0.7.184:6379, last known addr=/20.0.7.184:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xbee72739, /10.0.42.59:36300 -> /20.0.7.184:6379, last known addr=/20.0.7.184:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x38d2f875, /10.0.42.59:39990 -> /20.0.168.191:6379, last known addr=/20.0.168.191:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x38d2f875, /10.0.42.59:39990 -> /20.0.168.191:6379, last known addr=/20.0.168.191:6379] Reconnect scheduling disabled
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xd51bf7a8, /10.0.42.59:52560 -> /10.0.132.115:6379, last known addr=/10.0.132.115:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x3b697368, /10.0.42.59:52572 -> /10.0.132.115:6379, last known addr=/10.0.132.115:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0xace1fbde, /10.0.42.59:48904 -> /10.0.30.39:6379, last known addr=/10.0.30.39:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x5a5c10c1, /10.0.42.59:55834 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] channelActive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x5a5c10c1, /10.0.42.59:55834 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] channelInactive()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x5a5c10c1, /10.0.42.59:55834 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] scheduleReconnect()
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x5a5c10c1, /10.0.42.59:55834 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] Reconnect attempt 1, delay 1ms
    36mi.l.core.protocol.ConnectionWatchdog    Reconnecting, last destination was /10.0.37.146:6379
    36mi.l.core.protocol.ConnectionWatchdog    [channel=0x154d48fb, /10.0.42.59:55838 -> /10.0.37.146:6379, last known addr=/10.0.37.146:6379] channelActive()
    36mi.l.core.protocol.ReconnectionHandler   Reconnected to 10.0.37.146/<unresolved>:6379

    각 클러스터 엔드포인트 에 대해 설정했을 때의 로깅이다.

    redis health check 엔드포인트 추가

    기존에 캐시를 사용하는 엔드포인트를 호출하는 방식은 기존 캐시매니저에 관리되는 TTL 이 만료되기 전에는 삭제하는 별도 처리가 필요했다. 그리고 기존 운영되는 캐시를 삭제하는 부분은 혹시 모를 장애를 불러올 지 모르는 부분이기에 별도의 단순 확인용도의 health check 엔드포인트를 추가하였다.

    @Cacheable(value = ["health"])
    @CircuitBreaker(name="health", fallbackMethod = "getHealthCheck")
    fun healthCheck(key: String=""): String {
        return this.getHealthCheck()
    }
    
    fun getHealthCheck(t: Throwable?=null): String {
        return if(t != null) "failed" else "success"
    }

    프로젝트 내부적으로 별도의 circuitBreaker 를 사용하도록 되어있는데, cacheManager 를 통해 데이터를 조회해보고 없다면, fallbackMethod 를 통해 가져온 값을 cacheManager 의 캐시에 집어넣게 되어있다.

    여기서 실제 DB 등을 조회하는게 아닌, 단순한 문자열을 집어넣게 해두고, 만약 여기에 문제가 발생하면 failed 문자열을 밷도록 만들었다.

    테스트 진행 내역

    읽기/쓰기에 대한 커넥션 분리

    처음엔 현재 적용된 마스터 노드 엔드포인트 + MasterReplica + ReadFrom.REPLICA_PREFERED

    설정에 대해 실패했다고 생각하여, 반드시 클러스터 엔드포인트 로 구성해야 할거라 생각하였고, 단순하게 생각한다면 설정에 의존하지 않고, 직접 각 읽기/쓰기 에 대해 각각 커넥션 연결을 구성하여 사용하는 방법이 최선이라 생각하여 고려하게 된 부분이었다. 하지만 이 방법에 대해서는 조금만 고민해보면 굉장히 고려해야 할 부분이 많아진다.

    위 Redis 설정에 대한 그림에서 LettuceConnectionFactory 를 분리하여 생성하게 된다면, 당연히 그 하위에 ConnectionFactory 객체를 주입받아야 하는 CacheManager, RedisTemplate 두 객체도 그에 맞게 별도의 구성을 해주어야 한다.

    Notion Image

    아마 그림으로 보면 위와 같이 구성되게 될텐데, 애초에 RedisTemplate 에 대해서는 수동조작하는 방식으로 객체를 가져다 코드에서 조작하도록 사용하였으니, 해당 객체를 사용하는 부분에 들어가 읽기와 쓰기에 대해 사용하는 부분을 수정하면 되는 부분이긴 하다. 어쨌든 이 객체를 사용하는 부분들 모두 찾아가 수정해주어야 하는 부분이 해당 처리방법에 첫번째 우려되는 부분이다. 변경점에 모두 수작업으로 변경해주고, 각 처리되는 부분에 대해 모두 테스트를 재진행해주어야 한다.

    제일 문제되는 부분이 CacheManager 이다. Cache 사용에 있어 CacheManager 를 사용하여 얻는 이점은 당연히 Spring 에서 지원해주는 Aspect 기능의 어노테이션으로 지원되는 부분이다. CacheManager 를 사용하므로 기본적으로 코드는 @Cacheable @CacheEvict 등의 선언식의 코드를 작성하도록 되어있다. 이때 @CacheEvict 에 대해서는 사실 쓰기 담당하는 CacheManager 를 사용하면 그만이지만 @Cacheable 에 대해서는 사실 읽기/쓰기가 같이 있다고 봐야 한다. Redis 에 데이터가 있다면 읽어서 리턴하고, 없다면 코드를 수행하여 결과값을 캐시에 넣고 리턴하기 때문에 읽기/쓰기 두개의 동작을 해야한다. 하디만 @Cacheable 어노테이션 설정에 cacheManager 는 하나만 설정할 수 있다.

    이 문제를 해결하기 위해 기존 CacheManager 를 뜯어보고, 내부 사용 객체에 cacheWriter 라는 객체를 사용하는걸 확인하여, 혹시 해당 CacheManager(RedisCacheManager) 를 상속받아 cacheReader 라는 별도의 ConnectionFactory 를 주입받아 Routing 처리가 가능한지 확인해보는 방법도 고려해보았다.

    각 클러스터 엔드포인트 + MasterReplica + ReadFrom.REPLICA_PREFERED

    이 경우 Redis 설정에 넣은 각 클러스터 엔드포인트에 대해 어떤게 마스터인지 모르겠다고 얘기하며 Exception 이 떨어진다.

    Unsupported block type: quote

    위와 같은 exception 을 뱉어낸다. 그래도 위 내용으로 하나 알게된 내용은, Redis 연결에 대해 우리가 설정에 넣은 순서에 따라 역할이 부여되거나 하는 내용이 아니라는 점이다. 분명하게 에러 내용을 보면 내가 설정한 클러스터 엔드포인트에 대해 role 을 REPLICA 로 가져갔다.

    이 테스트로 알게 된 내용이다.

  • Redis 연결에 대해 우리가 설정으로 역할을 부여할 수 없다.
  • 각 클러스터 엔드포인트는 Replica 역할을 수행한다.
  • 각 클러스터 엔드포인트 + Cluster + ReadFrom.REPLICA_PREFERED

    이 경우 Redis 설정을 Cluster 로 올렸기 때문에 설정한 클러스터 엔드포인트에 대해 별도의 Master/Replica 의 역할을 구분하여 인식하지 않는다.

    그렇기 때문에 ReadFrom 설정을 하더라도 별다른 역할 구분이 없기 때문에 둘 중 어느쪽이든 커넥션 맺는대로 사용하게 된다. 대부분의 경우 테스트에서 마스터만 사용하게 되었다.

    각 노드 엔드포인트 + MasterReplica + ReadFrom.REPLICA_PREFERED

    처음으로 정상동작했던 설정이다. 해당 설정을 사용하면 커넥션에 대해 Role(역할) 체크까지 정상적으로 마치고 사용 시에도 문제 없이 동작했다.

    즉, 중요한건 실제 연결되는 Redis 에 대해 역할이 정확하게 확인되어야 한다는 것이다. 클러스터 엔드포인트도 다른 레플리카 되는 노드의 엔드포인트도 Redis 자체에 룰이 정확하게 마스터로 되어있지 않았고, 어플리케이션 서버에서 직접 커넥션을 맺어보고 확인했을 때 해당 Redis 가 본인의 역할(룰)을 알려주는걸 사용하는 것이기 때문에 우리가 임의로 다른 엔드포인트를 마스터라고 설정할 수는 없는 것이었다.

    (마스터 클러스터 노드 엔드포인트 + 레플리카 클러스터 엔드포인트) + MasterReplica + ReadFrom.REPLICA_PREFERED

    위 테스트로 알게 된 내용으로 마스터 역할을 하는 Redis만 식별되면 되는 것을 알게되었다. 그렇기 때문에 마스터 클러스터에 있는 노드들에 대해서는 실제 마스터 Redis 식별을 위해 다 넣어주어야 한다. 하지만 레플리카 되는 클러스터의 경우, 클러스터 엔드포인트의 역할이나 각 레플리카 노드들의 역할이 다르지 않을 것으로 보여 레플리카의 경우 클러스터 엔드포인트를 지정해주었다.

  • (마스터 클러스터 노드 엔드포인트 + 레플리카 클러스터 엔드포인트) + MasterReplica + ReadFrom.LOWEST_LATENCY

      최종 적용한 설정이다. ReadFrom 설정을 LOWEST_LATENCY 로 변경한 이유는 REPLICA_PREFERED 설정으로 적용할 경우 마스터 클러스터에 있는 다른 Replica 노드에 연결될 가능성도 있기 때문이다. 때문에 가장 latency 가 짧은 곳으로 설정하게 되면 자연스럽게 커넥션 설정에서 가장 가까운 서울을 바라보게 될 것이다.