Spring boot와 Redis Master-Slave 구조의 만남

박준수·2023년 12월 20일
0

이것저것

목록 보기
3/9
post-thumbnail

이전 게시글 - Jwt 인증 + Redis BlackList

🤔Redis Master-Slave를 프로젝트에 적용하려는 이유

  • JWT 인증 방식으로 로그인/로그아웃을 할 때 Redis를 블랙리스트로 사용을 하였다. Redis의 In-Memory DB를 사용하여 RDBMS보다 더 빠르게 조회가 가능하고, 토큰들의 만료시간을 설정하여 저장할 수 있기 때문이다.
  • 세션/쿠키 방식을 사용하지 않은 이유는 인증을 요청마다 매번 해야하고 Session Strorage에 부하가 쌓이면 터져버릴 위험이 있으니깐 JWT를 사용한 것이다. 그런데 하나 이상한 점이 있다. 우린 Redis로 BlackList를 구현하여 토큰의 만료시간을 RDBMS보다 효율적으로 저장을 했지만, 결국 Redis도 Session Storage 역할을 하는 셈이긴 하다. 그럼 토큰의 상태를 저장하고 있는 Redis가 터져버린다면? 서비스는 아무 역할을 하지 못하게 된다.
  • 따라서 나는 Redis가 터져버렸을 때의 상황을 해결하기 위해 Redis의 Master-Slave 구조를 설계할 필요성을 느꼈다.

Redis Master-Slave 구조의 핵심 특징

  • Master
    • 데이터를 쓰기(write) 및 읽기(read)에 대한 모든 요청을 처리하는 주 서버입니다.
    • 데이터의 쓰기 작업이 발생하면 해당 데이터를 자체적으로 업데이트하고, 동시에 하나 이상의 Slave 서버에게 복제(replicate)합니다.
  • Slave
    • 주로 읽기 작업(read)을 처리하는데 사용됩니다. 복제 서버가 데이터를 제공하므로 Master 서버에 가해지는 부하를 분산시킬 수 있습니다.
    • Slave 서버는 Master 서버로부터 주기적으로 데이터를 동기화하며, Master의 데이터 변경 사항을 지속적으로 반영합니다.
  • Replication
    • Master와 Slave 간의 데이터 복제는 비동기적으로 이루어집니다. Master가 쓰기 작업을 수행할 때마다 해당 변경 사항이 Slave로 전달됩니다.

redis.conf 설정

replicaof master 6379
  • ./redis 디렉토리 안에 redi.conf 파일 생성
  • 이 설정은 스냅샷 방식(RDB)이나 AOF 방식을 직접 지정하는 것은 아니며, 마스터 서버로부터의 데이터 복제만 활성화합니다.

docker-compose.yml 설정

redis_master:
    container_name: master
    image: redis
    ports:
      - 6379:6379
    networks:
      - aiary
  redis_slave-a:
    container_name: slave-a
    image: redis
    ports:
      - 7000:6379
    volumes:
      - ./redis:/usr/local/etc/redis/
    command: redis-server /usr/local/etc/redis/redis.conf
    networks:
      - aiary
  redis_slave-b:
    container_name: slave-b
    image: redis
    ports:
      - 7001:6379
    volumes:
      - ./redis:/usr/local/etc/redis/
    command: redis-server /usr/local/etc/redis/redis.conf
    networks:
      - aiary

networks:
  aiary:
    driver: bridge

Master Log 확인

→ Replica 성공

redis-cli로 접속하고 데이터 저장

Slave redis 확인

→ get key1 확인하니깐 slave redis에서도 hello redis가 나옴

→ 만약 replication에서 insert를 한다면 replica 옵션으로 master를 복제하고 있는 redis에서는 READONLY 옵션이 걸려있으므로 insert할 수 없다!

🧐Spring boot에 적용을 해보자

redis:
    master:
      host: localhost
      port: 6379
    slaves:
      - host: localhost
        port: 7000
      - host: localhost
        port: 7001
  • applcation.yml에 redis 설정을 다음과 같이 한다.
@Getter
@Setter
@NoArgsConstructor
@ConfigurationProperties(prefix = "spring.redis")
@Configuration
public class RedisInfo {

    private String host;
    private int port;
    private RedisInfo master;
    private List<RedisInfo> slaves;
}
  • RedisInfo 객체를 만든다.
  • @ConfigurationProperties(prefix = "spring.redis") 을 통해 yml 에서 설정한 redis 설정 값들을 가져온다.
@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {

    private final RedisInfo redisInfo;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .readFrom(ReadFrom.REPLICA_PREFERRED)    // replica에서 우선적으로 읽지만 replica에서 읽어오지 못할 경우 Master에서 읽어옴
            .build();
        // replica 설정
        RedisStaticMasterReplicaConfiguration slaveConfig = new RedisStaticMasterReplicaConfiguration(
            redisInfo.getMaster().getHost(), redisInfo.getMaster().getPort());
        // 설정에 slave 설정 값 추가
        redisInfo.getSlaves().forEach(slave -> slaveConfig.addNode(slave.getHost(), slave.getPort()));
        return new LettuceConnectionFactory(slaveConfig, clientConfig);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}
  • 이때 replica에서 우선적으로 읽지만 replica에서 읽어오지 못할 경우 Master에서 읽어오게 설정을 한다.

테스트 확인

  • redis를 RefreshToken을 저장하는 용도이기에 로그인을 하니깐 master, slave-a, slave-b에서 모두 같은 토큰을 저장하였다.
  • RedisRepositoryConfig 에서는 replica에서 우선적으로 읽지만 replica에서 읽어오지 못할 경우 Master에서 읽어 오도록 설정을 했는데 과연 설정한대로 될까?
public void logout(UserTokenReq userTokenReq) {
      // Access 토큰에서 User 정보를 가져온다
      Authentication authentication = jwtTokenProvider.getAuthentication(
          userTokenReq.getAccessToken());

      redisTemplate.opsForValue().get("RT:" + authentication.getName());
}
  • 로그아웃 Service 코드를 간단하게 redis에서 get만 하도록 수정을 하였다. (실제로는 get만 하지 않은데 테스트를 확인해볼려고 잠깐 수정을 해본 것이다.)

  • master 도커 컨테이너를 중지시키고 get만 있는 로그아웃 코드를 돌려보면 정상적으로 replica에서 값을 읽어서 로그아웃이 되는 것을 확인했다. 만약에 get이 아닌 set 도 있다면 1분 정도 딜레이 된 다음 타임아웃이 발생되는 것을 알 수 있었다.

  • 이번엔 master만 살리고 replica를 중단시켰는데 get 메서드만 있는 로그아웃이 잘 실행되었다.

결론

  • Redis Master-Slave 구조를 통해 Slave는 읽기 전용으로 Master의 부하를 줄일 수 있다.
  • Master에 저장된 데이터가 비동기적으로 Slave에 복제가 된다.
  • 그러나 Master 노드가 죽었을 때 Slave 노드는 읽기만 가능하므로 고가용성을 확보했다고 볼 수 없다. 즉, 시스템에서 Master노드의 장애시 근본적인 문제를 해결할 수는 없다.

참고자료

Redis Master Slave 구성

Redis Master Slave Configuration and Tested in Spring Boot

profile
방구석개발자

0개의 댓글