redis Sentinel

redis Sentinel 설정

redis Sentinel redis를 관리하는 시스템이다.
Monitoring, Notification, Automatic failover 이것들을 수행해 주는데 일단 필자가 하고 싶은건 Automatic failover 이다.
slave가 죽으면 그닥 상관없지만 master가 죽으면 큰일!이다. sentinel은 slave 하는 선출하여 master로 승격시키는 failover process를 시작할 수 있다.
우리는 1개의 master, 3개의 slave, 3개의 sentinel으로 진행할 것이다.

우리는 마스터로 승격 시키기 위해 몇가지 작업을 해야한다.
테스트를 하기 위해 레디스 한개를 설치 한후 여러 Config 파일을 만들어서 진행 할 예정이다.
일단 마스터 노트의 config파일이다.
딱히 해줄껀 없고 원하는 포트만 지정해주면 된다. 필자는 기본포트인 6379로 지정했다.
다음은 slave 파일은 3개를 준비하면 된다.
서버가 다르다면 상관없지만 필자를 한컴퓨터에서 테스트를 진행 하므로 포트는 나누어 진행했다.

slave config 파일 세군대에 다음과 같이 넣는다. 필자는 {포트}.conf 로 진행했다.

port 6380

...

slaveof 127.0.0.1 6379

세개의 각각이 다른 포트로 지정하고 slaveof 127.0.0.1 6379 는 고정한다. 마스터 포트가 필자와 다른경우에는 수정해서 넣는다.
그런다음 실행 시켜보자.

master 노드 실행

redis-server 6379.conf

slave 노드 실행

redis-server 6380.conf
redis-server 6381.conf
redis-server 6382.conf

이렇게 실행 하면 마스터 노드에 다음과 같이 로그가 출력 될 것이다.

2921:M 14 Apr 22:47:12.650 * The server is now ready to accept connections on port 6379
2921:M 14 Apr 22:47:14.130 * Slave 127.0.0.1:6381 asks for synchronization
2921:M 14 Apr 22:47:14.130 * Full resync requested by slave 127.0.0.1:6381
2921:M 14 Apr 22:47:14.130 * Starting BGSAVE for SYNC with target: disk
2921:M 14 Apr 22:47:14.131 * Background saving started by pid 2923
2923:C 14 Apr 22:47:14.228 * DB saved on disk
2921:M 14 Apr 22:47:14.285 * Background saving terminated with success
2921:M 14 Apr 22:47:14.286 * Synchronization with slave 127.0.0.1:6381 succeeded
2921:M 14 Apr 22:47:15.953 * Slave 127.0.0.1:6380 asks for synchronization
2921:M 14 Apr 22:47:15.953 * Full resync requested by slave 127.0.0.1:6380
2921:M 14 Apr 22:47:15.953 * Starting BGSAVE for SYNC with target: disk
2921:M 14 Apr 22:47:15.953 * Background saving started by pid 2925
2925:C 14 Apr 22:47:15.954 * DB saved on disk
2921:M 14 Apr 22:47:16.026 * Background saving terminated with success
2921:M 14 Apr 22:47:16.027 * Synchronization with slave 127.0.0.1:6380 succeeded
2921:M 14 Apr 22:47:17.593 * Slave 127.0.0.1:6382 asks for synchronization
2921:M 14 Apr 22:47:17.593 * Full resync requested by slave 127.0.0.1:6382
2921:M 14 Apr 22:47:17.593 * Starting BGSAVE for SYNC with target: disk
2921:M 14 Apr 22:47:17.593 * Background saving started by pid 2927
2927:C 14 Apr 22:47:17.594 * DB saved on disk
2921:M 14 Apr 22:47:17.661 * Background saving terminated with success
2921:M 14 Apr 22:47:17.661 * Synchronization with slave 127.0.0.1:6382 succeeded

대충 내용은 연결이 잘 됐다는 내용 같다.ㅎㅎㅎ

다음으론 sentinel 설정을 해보자!
이것도 역시 간단하다.
레디스를 설치면 sentinel.conf 가 존재 한다.
이걸 복사해서 사용하자!

port 26379

...

sentinel monitor mymaster 127.0.0.1 6379 2

sentinel down-after-milliseconds mymaster 5000

위와같이 설정해준다.
sentinel 기본 포트는 26379이다.
그리고 sentinel monitor mymaster 는 주석 으로 처리 되어있는데 주석을 제거하고 다음으로 작성하는 것은 아이피 포트이다.
2라는건 quorum 값으로, 레디스는 마스터를 선출하기 위해 다수결을 사용한다. 한개의 sentinel가 마스터를 체크하는 동안 죽었다고 판단(네트워크 장애등? 여러가지 이유로) 하였으나 나머지 두개는 무슨소리냐고 잘된다고 하면 그것은 주관적인 다운 상태인 것이다. 그래서 진짜로 죽었는지 판단을 하기 위해서 다수결을 통해 2의 값을 넣었다.
만약 서버의 개수보다 많다면 제대로 동작 하지 않을 듯하다.
그리고 다음으로 나오는건 몇초동안 마스터노드에 실패 하였을 경우 진짜 다운 되었는지 간주하는 초이다.
디폴트는 30초인가? 그런데 그럼 장애가 너무 긴시간이니까 5초로 줄였다.

이것 또한 3개의 sentinel서버를 띄울 예정이라 3개의 conf 파일을 작성하자!
포트만 변경 시키고 sentinel monitor mymaster 는 동일하게 작성하자.

그리고 다음과 같이 실행해보자

redis-server sentinel.conf --sentinel
redis-server sentinel_2.conf --sentinel
redis-server sentinel_3.conf --sentinel

파일명이 영 시원찮네..

우리가 해야 할 일은 마스터를 죽이는 일이다.
그래야 마스터를 다시 선출 해야 하니까.
마스터를 죽여보자.
kill로 죽이던 뭐든 죽이자!

그럼 몇초 있다가 sentinel의 서버의 로그들이 쭉 작성 될 것이다.

2946:X 14 Apr 23:05:58.626 # +sdown master mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:58.708 # +odown master mymaster 127.0.0.1 6379 #quorum 2/2
2946:X 14 Apr 23:05:58.708 # +new-epoch 6
2946:X 14 Apr 23:05:58.708 # +try-failover master mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:58.711 # +vote-for-leader ac472a3fd5d51e933f862253d3824928711ce6f8 6
2946:X 14 Apr 23:05:58.713 # 127.0.0.1:26379 voted for ac472a3fd5d51e933f862253d3824928711ce6f8 6
2946:X 14 Apr 23:05:58.713 # 127.0.0.1:26381 voted for ac472a3fd5d51e933f862253d3824928711ce6f8 6
2946:X 14 Apr 23:05:58.770 # +elected-leader master mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:58.770 # +failover-state-select-slave master mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:58.829 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:58.829 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:58.901 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:59.776 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:59.776 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6379
2946:X 14 Apr 23:05:59.863 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:00.789 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:00.845 # -odown master mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:01.833 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:01.892 * +slave-reconf-sent slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:02.864 * +slave-reconf-inprog slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:02.864 * +slave-reconf-done slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:02.945 # +failover-end master mymaster 127.0.0.1 6379
2946:X 14 Apr 23:06:02.945 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380
2946:X 14 Apr 23:06:02.947 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
2946:X 14 Apr 23:06:02.947 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
2946:X 14 Apr 23:06:02.947 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380

마스터 노드를 선출하는 작업들을 하는 모양이다.

근데 막 이것저것 죽였다 살렸다 막 하면 레디스가 오락가락한다.
그래서 두번이나 conf를 다시 만들어야 했다.(필자의 잘못일 수도)
레디스는 자기 conf 파일들을 수정하면서 작업을 한다.
그래서 꼬인듯 싶다.

spring data redis를 통해 아주 간단하게 테스트로 진행하였다.
물론 redis-cli로 테스틀 진행 해도 상관없지만 redis로 cache를 하려고 테스트를 했다.
소스는 github에 올라가 있다.

Spring Redis Cache

이번시간엔 redis를 이용한 저장소 캐싱을 해보겠다.
스프링 프로젝트에서 복붙한거라.. 그리고 딱히 어려운부분은 없기에..
일단 레디스를 설치하자.
레디스 윈도우 설치 파일
설치한 후에 레디스를 실행 시키자!

public class RedisCacheManager extends AbstractCacheManager {


    private final RedisConnectionFactory redisConnectionFactory;
    private final RedisTemplate defaultTemplate;
    private final Map<String, RedisTemplate> templates;

    private boolean usePrefix = true;
    private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();
    private boolean dynamic = false;

    // 0 - never expire
    private long defaultExpiration = 0;
    private final Map<String, Long> expires;

    public RedisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
        this.defaultTemplate = new RedisTemplate();
        this.defaultTemplate.setConnectionFactory(this.redisConnectionFactory);
        this.defaultTemplate.afterPropertiesSet();
        this.templates = new ConcurrentHashMap<>();
        this.expires = new ConcurrentHashMap<>();
    }

    public void setUsePrefix(boolean usePrefix) {
        this.usePrefix = usePrefix;
    }

    public void setCachePrefix(RedisCachePrefix cachePrefix) {
        this.cachePrefix = cachePrefix;
    }

    public void setDynamic(boolean dynamic) {
        this.dynamic = dynamic;
    }

    public void setDefaultExpiration(long defaultExpiration) {
        this.defaultExpiration = defaultExpiration;
    }

    public RedisCacheManager withCache(String cacheName, long expiration) {
        return withCache(cacheName, this.defaultTemplate, expiration);
    }

    public RedisCacheManager withCache(String cacheName, RedisTemplate template, long expiration) {
        this.templates.put(cacheName, template);
        this.expires.put(cacheName, expiration);
        RedisCache cache = createCache(cacheName, template, expiration);
        addCache(cache);
        return this;
    }

    @Override
    public Cache getCache(String name) {
        Cache cache = super.getCache(name);
        if (cache == null && this.dynamic) {
            return createCache(name, this.defaultTemplate, this.defaultExpiration);
        }
        return cache;
    }

    protected RedisCache createCache(String cacheName, RedisTemplate template, long expiration) {
        return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), template, expiration);
    }

    @Override
    protected Collection<? extends Cache> loadCaches() {
        Assert.notNull(this.defaultTemplate, "A redis template is required in order to interact with data store");
        return this.getCacheNames().stream().map(name -> getCache(name)).collect(Collectors.toList());
    }
}

원래 org.springframework.data.redis.cache에도 RedisCacheManager 가 있기는 한데 불편한듯 싶다.

public class JsonRedisTemplate<V> extends RedisTemplate<String, V> {

    public JsonRedisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper, Class valueType) {
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        super.setKeySerializer(stringSerializer);
        super.setHashKeySerializer(stringSerializer);
        super.setHashValueSerializer(stringSerializer);
        Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(valueType);
        jsonRedisSerializer.setObjectMapper(objectMapper);
        super.setValueSerializer(jsonRedisSerializer);
        super.setConnectionFactory(connectionFactory);
        super.afterPropertiesSet();
    }

}

RedisTemplate 도 상속 받아 구현했다.
StringSerializer와 jsonSerializer를 셋팅했다.

@Configuration
@EnableCaching(proxyTargetClass = true)
public class RedisCacheConfig {

    private static final Class CACHE_TYPE = Hello.class;
    private static final String CACHE_NAME = "cache.hello";
    private static final String CACHE_TTL = "${cache.hello.timetolive:60}";
    private static final String NETWORK_CACHE_NAME = "cache.network";

    @Value(CACHE_TTL)
    private Long cacheNetworkTimeToLive;

    @Value(CACHE_TTL)
    private Long cacheBlogTimeToLive;

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,
                                          ObjectMapper objectMapper) {

        RedisCacheManager cacheManager = new RedisCacheManager(redisConnectionFactory);

        cacheManager.withCache(NETWORK_CACHE_NAME, this.cacheNetworkTimeToLive);

        JsonRedisTemplate helloTemplate = new JsonRedisTemplate<>(redisConnectionFactory, objectMapper, CACHE_TYPE);
        cacheManager.withCache(CACHE_NAME, helloTemplate, this.cacheBlogTimeToLive);

        return cacheManager;
    }
}

마지막으로 CacheManager 를 빈으로 등록한다.
chche_name은 cache.hello 정하고 캐시 시간은 60초, Type은 Hello로 정했다. Hello 는 일반 자바빈 객체다.
캐시할 목록을 늘려가도 된다.
한번 테스트를 해보자

@Autowired
private CacheManager cacheManager;

@Autowired
private HelloRepository helloRepository;

@Test
public void putRedisCache() {
    Cache cache = cacheManager.getCache("cache.hello");
    cache.put("hello", helloRepository.findOne(1L));
}

@Test
public void getRedisCache() {
    Cache cache = cacheManager.getCache("cache.hello");
    Hello hello = cache.get("hello", Hello.class);
    assertThat(hello.getId(), is(1L));
    assertThat(hello.getName(), is("wonwoo"));
    System.out.println(hello.getName());
}

hello라는 키로 Hello 객체를 한개 넣어 두었다.
그리고 나서 그 다음 테스트는 hello라는 키로 객체를 뽑아 냈다.
성공적으로 테스트가 통과 했다면 캐싱이 된것이다.
테스트가 통과한후 다시 getRedisCache() 메서드를 호출해보자. 그럼 아직도 캐싱되어 있다.
하지만 60초가 지나서 getRedisCache() 테스트만 다시 돌리면 nullpointexception 혹은 테스트를 실패 할 것이다.

이것으로 레디스로 저장소 캐싱을 하는 법을 알아봤다.