티스토리 뷰

실무 개발

Redis 도입 회고

Jason of the Argos 2023. 5. 31. 23:08

Redis Sentinel 기반 고가용성 캐시 시스템 구축기

최근 시스템의 성능 향상 및 장애 대응력을 높이기 위해 Redis를 도입하였다. 단일 인스턴스 구조의 한계를 극복하고자, Redis Sentinel을 기반으로 한 고가용성(HA) 구조를 구성하였다.

1. Redis Sentinel 3대 구성: Master - Slave - Slave

Redis는 기본적으로 싱글 스레드 구조이며, 단일 장애 지점(SPOF: Single Point of Failure)을 갖는다는 단점이 있다. 이를 해결하기 위해 Sentinel을 활용하여 총 3대의 Redis 노드를 구성하였다. 구조는 다음과 같다.

  • 1대의 Master 노드
  • 2대의 Slave 노드
  • 모든 노드에서 Sentinel 프로세스를 별도로 실행하여 장애 감지 및 자동 페일오버를 지원

Sentinel은 Master 노드의 상태를 지속적으로 모니터링하며, 장애 발생 시 자동으로 새로운 Master를 선출하고 클라이언트 설정을 갱신할 수 있도록 구성하였다.

/configs
├── redis-1
│   ├── redis.conf
│   └── sentinel.conf
├── redis-2
│   ├── redis.conf
│   └── sentinel.conf
├── redis-3
│   ├── redis.conf
│   └── sentinel.conf

 

redis.conf

port 6379
dir /data
appendonly yes

 

slave 노드 설정 (redis-2, redis-3)

replicaof <MASTER_IP> 6379

 

sentinel.conf

port 26379
dir /tmp
sentinel monitor mymaster <MASTER_IP> 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000

 

  • mymaster는 Master 인스턴스 이름
  • 2는 quorum 수로, 최소 2대가 다운을 인지해야 failover 수행

2. Grafana 기반 모니터링 환경 구축

Redis의 상태 및 Sentinel의 동작을 실시간으로 확인할 수 있도록 Prometheus Exporter를 활용하여 메트릭을 수집하고, 이를 Grafana 대시보드에 연동하였다.

모니터링 항목은 다음과 같다:

  • 각 노드의 메모리 사용량 및 연결 수
  • keyspace 정보
  • Master-Slave 동기화 지연 시간
  • Sentinel의 현재 Master 인식 정보 및 failover 이력

이로 인해 장애 발생 시 원인 파악 및 대응이 훨씬 수월해졌다.

 

Redis Exporter 설치

docker run -d \
  --name redis_exporter \
  -p 9121:9121 \
  oliver006/redis_exporter \
  --redis.addr=redis://<REDIS_HOST>:6379

 

Prometheus prometheus.yml

scrape_configs:
  - job_name: 'redis'
    static_configs:
      - targets: ['redis-1:9121', 'redis-2:9121', 'redis-3:9121']

 

3. 내부용 Key-Value 조회 API 구현

Redis 운영 편의성을 높이기 위해 내부 개발자들이 손쉽게 캐시 데이터를 확인할 수 있도록 조회 전용 API를 구현하였다. 주요 기능은 다음과 같다:

  • 지정한 Key에 대한 Value 조회
  • Key 패턴 검색 (Wildcard 지원)
  • TTL 정보 확인

application.yml

spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - redis-1:26379
        - redis-2:26379
        - redis-3:26379
    password: your_password # 없다면 생략

 

RedisConfig.java

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory(RedisProperties properties) {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master(properties.getSentinel().getMaster());
        properties.getSentinel().getNodes().forEach(node -> {
            String[] parts = node.split(":");
            sentinelConfig.sentinel(parts[0], Integer.parseInt(parts[1]));
        });
        return new LettuceConnectionFactory(sentinelConfig);
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

 

RedisService.java

@Service
@RequiredArgsConstructor
public class RedisService {

    private final RedisTemplate<String, String> redisTemplate;

    public String getValue(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public Long getTTL(String key) {
        return redisTemplate.getExpire(key);
    }

    public Set<String> scanKeys(String pattern) {
        return redisTemplate.keys(pattern);
    }
}

 

RedisController.java

@RestController
@RequestMapping("/redis")
@RequiredArgsConstructor
public class RedisController {

    private final RedisService redisService;

    @GetMapping("/value/{key}")
    public ResponseEntity<String> getValue(@PathVariable String key) {
        String value = redisService.getValue(key);
        return ResponseEntity.ok(value);
    }

    @GetMapping("/ttl/{key}")
    public ResponseEntity<Long> getTTL(@PathVariable String key) {
        return ResponseEntity.ok(redisService.getTTL(key));
    }

    @GetMapping("/keys")
    public ResponseEntity<Set<String>> getKeys(@RequestParam String pattern) {
        return ResponseEntity.ok(redisService.scanKeys(pattern));
    }
}