HTTP 200 OK

Memento mori & Carpe diem

Redis

Redis 정의 및 사용법 && Docker 배포시 이슈

sjoongh 2023. 7. 21. 17:25

Redis란?

  • Key, Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터 베이스 관리 시스템 (DBMS)입니다.
  • 데이터베이스, 캐시, 메세지 브로커로 사용되며 인메모리 데이터 구조를 가진 저장소입니다.
  • db-engines.com 에서 key, value 저장소 중 가장 순위가 높습니다.
  • Redis는 Memcached와 비슷한 캐시 시스템으로서 동일한 기능을 제공하면서 영속성, 다양한 데이터 구조와 같은 부가적인 기능을 지원합니다. Redis는 Inmemory DataBase(빠르다)로서 모든 데이터를 메모리에 저장하고 조회합니다.
  • DBMS를 이용한다면 DB에 데이터를 저장하고 저장된 데이터를 정렬하여 다시 읽어오는 과정은 디스크에 직접 접근을 해야하기 때문에 시간이 더 걸린다는 단점이 존재합니다.
  • 하지만 Redis를 이용하고 Redis에서 제공하는 Sorted-Set이라는 자료구조를 사용하면 더 빠르고 간단하게 데이터를 정렬할 수 있습니다.

Redis에 대해서 더 자세하게 알아보기 전 캐시 서버(Cache Server)에 대해서

DB가 있는데도 Redis라는 인메모리 데이터 구조 저장소를 사용하는 이유

DB는 데이터를 물리 디스크에 직접 쓰기 때문에 서버에 문제가 발생하여 다운되더라도 데이터가 손실되지 않습니다. 하지만 매번 디스크에 접근해야 하기 때문에 사용자가 많아질수록 부하가 많아져서 느려질 수 있습니다.

일반적으로 서비스 운영 초반이거나 규모가 작은, 사용자가 많지 않은 서비스의 경우에는 WEB - WAS - DB 의 구조로도 DB에 무리가 가지 않습니다.

하지만 사용자가 늘어난다면 데이터 베이스가 과부하 될 수 있기 때문에 이때 캐시 서버를 도입하여 사용합니다.

  • 매 트랜잭션마다 디스크에 접근해야하므로 부하가 많아지면 성능이 떨어지기 때문입니다.

그리고 이 캐시 서버로 이용할 수 있는 것이 바로 Redis 입니다.

캐시는 한번 읽어온 데이터를 임의의 공간에 저장하여 다음에 읽을 때는 빠르게 결괏값을 받을 수 있도록 도와주는 공간입니다.

같은 요청이 여러 번 들어오는 경우 매번 데이터 베이스를 거치는 것이 아니라 캐시 서버에서 첫 번째 요청 이후 저장된 결괏값을 바로 내려주기 때문에 DB의 부하를 줄이고 서비스의 속도도 느려지지 않는 장점이 있습니다.

캐시 서버는 Look aside cache 패턴과 Write Back 패턴이 존재합니다.

 

Cache란?
  • Cache란 나중에 요청할 결과를 미리 저장해둔 후 빠르게 서비스해 주는 것을 의미합니다. 즉, 미리 결과를 저장하고 나중에 요청이 오면 그 요청에 대해서 DB 또는 API를 참조하지 않고 Cache를 접근하여 요청을 처리하는 기법
  • 캐시가 나온 배경으로는 파레토 법칙이 있습니다. 해당 법칙을 기반으로 캐시는 모든 결과를 캐싱할 필요가 없으며 서비스를 할 때 많이 사용되는 20%만 캐싱함으로써 전체적으로 효율을 끌어올릴 수 있습니다.

 

Look aside cache

  1. 클라이언트가 데이터를 요청
  2. 웹서버는 데이터가 존재하는지 Cache 서버에 먼저 확인
  3. Cache 서버에 데이터가 있으면 DB에 데이터를 조회하지 않고 Cache 서버에 있는 결과값을 클라이언트에게 바로 반환 (Cache Hit)
  4. Cache 서버에 데이터가 없으면 DB에 데이터를 조회하여 Cache 서버에 저장하고 결과값을 클라이언트에게 반환 (Cache Miss)
  5. DB에서 가져온 데이터를 캐시에 저장

 

Write Back - 주로 쓰기 작업

  1. 웹서버는 모든 데이터를 Cache 서버에 저장
  2. Cache 서버에 특정 시간 동안(일정 주기마다) 데이터가 저장됨(배치)
  3. Cache 서버에 있는 데이터를 DB에 저장
  4. DB에 저장된 Cache 서버의 데이터를 삭제
  • insert 쿼리를 한 번씩 500번 날리는 것보다 insert 쿼리 500개를 붙여서 한 번에 날리는 것이 더 효율적이라는 원리입니다.
  • 이 방식은 들어오는 데이터들이 저장되기 전에 메모리 공간에 머무르는데 이때 서버에 장애가 발생하여 다운된다면 데이터가 손실될 수 있다는 단점이 있습니다.

Redis 특징

  • Key, Value 구조이기 때문에 쿼리를 사용할 필요가 없습니다. → 속도가 빠름
  • 데이터를 디스크에 쓰는 구조가 아니라 메모리에서 데이터를 처리하기 때문에 속도가 빠릅니다.
  • String, Lists, Sets, Sorted Sets, Hashes 자료 구조를 지원합니다. → 다양한 자료구조
  • 영속성을 지원하는 인메모리 데이터 저장소
  • 읽기 성능 증대를 위한 서버측 복제를 지원
  • 쓰기 성능 증대를 위한 클라이언트측 샤딩(Sharding)지원
  • 다양한 서비스에서 사용되며 검증된 기술
  • 문자열, 리스트, 해시, 셋, 정렬된 셋과 같은 다양한 데이터형을 지원, 메모리 저장소임에도 불구하고 많은 데이터형을 지원하므로 다양한 기능을 구현

 

String : 가장 일반적인 key - value 구조의 형태입니다.

Sets : String의 집합입니다. 여러 개의 값을 하나의 value에 넣을 수 있습니다. 포스트의 태깅 같은 곳에 사용될 수 있습니다.

Sorted Sets : 중복된 데이터를 담지 않는 Set 구조에 정렬 Sort를 적용한 구조로 랭킹 보드 서버 같은 구현에 사용할 수 있습니다.

Lists : Array 형식의 데이터 구조입니다. List를 사용하면 처음과 끝에 데이터를 넣고 빼는 건 빠르지만 중간에 데이터를 삽입하거나 삭제하는 것은 어렵습니다.

  • Single Threaded 입니다.: 한 번에 하나의 명령만 처리할 수 있습니다. 그렇기 때문에 중간에 처리 시간이 긴 명령어가 들어오면 그 뒤에 명령어들은 모두 앞에 있는 명령어가 처리될 때까지 대기가 필요합니다.(하지만 get, set 명령어의 경우 초당 10만 개 이상 처리할 수 있을 만큼 빠릅니다.)

결론 : 레디스는 고성능 key-value 저장소로서 문자열, 리스트, 해시, 셋, 정렬된 셋 형식의 데이터를 지원하는 NoSQL이다.


Redis 영속성

  • 레디스는 지속성을 보장하기 위해 데이터를 DISK에 저장할 수 있다. 서버가 내려가더라도 DISK에 저장된 데이터를 읽어서 메모리에 로딩합니다.

데이터를 DISK에 저장하는 방식은 두 가지 방식이 존재합니다.

  • RDB(Snapshotting) 방식
    • 순간적으로 메모리에 있는 내용을 DISK에 전체를 옮겨 담는 방식
  • AOF(Append On File) 방식
    • Redis의 모든 write/update 연산 자체를 모두 log파일에 기록하는 형태

Redis 사용에 주의할 점

  • 서버에 장애가 발생했을 경우 그에 대한 운영 플랜이 꼭 필요합니다. 인메모리 데이터 저장소의 특성상, 서버에 장애가 발생했을 경우 데이터 유실이 발생할 수 있기 때문입니다.
  • 메모리 관리가 중요합니다.
  • 싱글 스레드(Thread Safe → 여러 스레드가 동시에 접근해도 실행에 문제가 없음, 연산을 하나씩 처리하므로)의 특성상, 한 번에 하나의 명령만 처리할 수 있습니다. 처리하는데 시간이 오래 걸리는 요청, 명령은 피해야 합니다.

이 외에도 Master-Slave 형식의 데이터 이중화 구조에 대한 Redis Replication, 분산 처리를 위한 Redis cluster, 장애 복구 시스템 Redis SentinelRedis TopologyRedis ShardingRedis Failover 등의 Redis를 더 효율적으로 사용하기 위한 개념들이 존재합니다.


Redis vs MongoDB

  • 두 DB를 비교하는 것에는 의미가 크지 않습니다. 그 이유는 보통 Redis와 MongoDB를 함께 사용하기 때문입니다.
  • 둘의 사용목적은 다르며 목적에 따라 각각의 장점을 지니고 있습니다.
    구분 Redis MongoDB
    공통점1 둘 다 서버의 데이터베이스로 사용됨 둘 다 서버의 데이터베이스로 사용됨
    공통점2 NoSQL(Structured가 아니므로 NoSQL임) 가장 대중화된 NoSQL 중 하나
    저장소 In Memory 물리 디스크
    사용목적 성능 목적의 캐시에 사용됨 메인 저장소나 로그 목적 용도
  • MongoDB는 주 데이터베이스로 사용되거나 특별한 목적(로그 수집 등의 목적으로 부가적인 DB형태로 쓰인다.)
  • Redis는 In-Memory 구조의 빠른 성능이 장점이기 때문에 주데이터베이스로 사용하기 보다 캐시(Cache)를 통한 빠른 성능의 목적으로 더 많이 사용
    1. 검색에 필요한 엔진에 사용되기에도 Redis의 장점이 큽니다
    2. 또한 상대적으로 MongoDB와 비교해서는 복잡하지 않고 단순합니다.

Redis 선택 이유

  • 로깅을 기록하기 위한 목적으로 db를 사용한다면 ~ mongodb
  • 하지만 현재 프로젝트는 짧은 주기로 로깅 데이터를 영구저장하기 않고 삭제시킬예정이기 때문에 캐시 용도로 적합한 redis를 활용

 

Redis 설정

 

yaml 설정
spring:
  config:
    activate:
      on-profile: local
  redis:
    host: localhost
    port: 6379
  • 만약 docker로 redis를 설치했다면 위와 같이 사용 가능
  • docker-compose를 사용하면 모든 컨테이너가 동일한 네트워크 영역대를 사용하기 때문에 컨테이너간의 통신이 가능하며 서비스 이름이 해당 컨테이너를 지칭하는 DNS가 됩니다.
  • 하지만 docker-compose가 종료 된다면 다시 띄울때 영향이 가서 실행이 안 될수도 있습니다.
  • 때문에 먼저 docker-compose 파일을 수정해줘야 합니다.
extra_hosts:- "host.docker.internal:host-gateway"
  • 다음과 같이 추가해줘서 host.docker.internal을 열어줍니다.
  • 이후 스프링의 redis host 설정에 spring.redis.host=host.docker.internal를 입력해주게 되면 도커 컨테이너에서 외부 호스트에 접근할 수 있게 됩니다.
  • 컨테이너마다 독립적인 ip를 가지므로 위와같은 설정이 필요합니다.

 

gradle 설정
implementation*("org.springframework.boot:spring-boot-starter-data-redis")

 

 

 

Entity 설정
  • TTL설정과 logging을 위한 entity
@RedisHash(timeToLive = 2592000)
data class DsiHistory(
    @Id
    val id: String = UUID.randomUUID().toString(),
    val methodName: String,
    val payload: Any,
    val solutionType: DsiType,
    var isFailed: Boolean = false,
    val createdAt: LocalDateTime = LocalDateTime.now(),
)
  • RedisHash : Redis에 hash포맷으로 저장

@RedisHash

  • RedisEntity라는 엔티티 데이터들을 이후에 무수히 많이 저장이 됩니다. 그래서 이 엔티티들만을 보관하는 하나의 해쉬키 값이 @RedisHash("person")이 되는 것입니다. 그리고 이 해쉬 공간에서 각 엔티티들이 person:hash_id 라는 아이디 값을 가지게 됩니다.(실제로 @Id에 매핑되는 것은 Hash_id)
  • 이것을 자바의 해쉬 타입으로 지정한다면 HashMap<String,HashMap<String,Person>>의 구조가 되는 것 입니다.

Redis with Docker 명령어

  • redis 최신 이미지 가져오기
    • docker pull redis
  • 이미지 확인
    • docker images
  • redis 서버 실행(기본 포트)
    • docker run -d -p 6379:6379 redis
    • -d : 백그라운드에서 실행
    • -p : 외부에서 해당 포트로 접속할 수 있게 열어둔다는 의미
  • docker 구동 확인
    • docker ps
  • docker 의 redis-cli로 접속
    • docker run -it --link exciting_noyce:redis --rm redis redis-cli -h redis -p 6379
      • 레디스 서버 실행 시 docker run --name myredis -d -p 6379:6379 redis 과 같이 name을 지정하지 않으면 exciting_noyce이 default name으로 자동 지정

 

  •  

기타 명령어

  • vi /etc/redis/redis.conf → redis config 파일 접근
  • ps -ef | grep redis → redis 실행 정보 확인
  • redis-server --version → redis 서버 정보
  • redis-cli -h 10.70.175.76 → redis-cli 접근
  • AUTH douzone1! → 비밀번호 입력으로 권한 해제

Redis 바인딩(외부 접속) 설정

  1. 레디스 설치 경로 내의 redis.windows.conf 파일을 찾습니다. (윈도우 기준)
  2. 설정을 하지 않았을 시 localhost인 127.0.0.1 만 등록이 되어있는 상태. 접속을 열어줄 ip를 뒤에 이어 입력하면 됩니다.
  3. !https://velog.velcdn.com/images/tnqlsdl1300/post/b1f8ce72-bcaa-48ce-a0b8-dfc47cd3ab4e/image.png
  • 모든 ip를 허용하고 싶을 시 0.0.0.0 으로 설정
  1. 서비스에서 redis 재시작 시 바인딩이 적용 되어 위의 ip에서 redis에 connect가 가능해집니다.

spring Boot + redis 사용법 참고 : https://basketdeveloper.tistory.com/77


이슈 : Redis with CRUD

  • repository를 사용하면 entity 그대로 사용할 수 있는 장점이 있지만 @Indexed로 속성을 만들면 만료키가 그대로 남아있는 상황이 발생
    • @RedisHash : Hash Collection 명시 -> Jpa의 Entity에 해당하는 애노테이션이라
      • value 값은 Key를 만들 때 사용하는 것으로 Hash의 Key는 value + @Id로 형성
    • @Id : key를 식별할 떄 사용하는 고유한 값으로 @RedisHash와 결합해서 key를 생성
      • 해당 애노테이션이 붙은 변수명은 반드시 id여야 합니다.
    • @Indexed : CRUD Repository를 사용할 때 jpa의 findBy필드명 처럼 사용하기 위해서 필요

 

문제발생 원인
  • Spring Data Redis 리포지토리는 여러 Redis 기능을 사용하여 Redis에서 도메인 개체를 유지합니다.
  • 도메인 개체는 주로 해시( job:a6d6e491-5d75-4fd0-bd8e-71692f6d18be)에 저장됩니다. 모든 만료는 해시에 직접 적용되므로 Redis가 키를 만료할 수 있습니다.
  •  Spring Data Redis는 또한 특정 필드 값으로 조회를 제공하기 위해 보조 인덱스( job:campaignId:aa)를 유지합니다. job:recipient:dd세트 내의 개별 요소는 만료될 수 없습니다. 전체 데이터 구조만 만료될 수 있지만 만료되지 않은 모든 요소가 그렇게 되면 사라지기 때문에 원하는 작업이 아닙니다.
  • 따라서 Spring Data Redis는 원본 해시의 복사본을 job:a6d6e491-5d75-4fd0-bd8e-71692f6d18be:phantomTTL이 약간 더 긴 팬텀 해시( )로 유지합니다.
  • Spring Data Redis는 키 이벤트(설정 @EnableRedisRepositories(enableKeyspaceEvents = EnableKeyspaceEvents.ON_STARTUP포함)를 구독하여 만료 이벤트를 수신합니다. 원래 해시가 만료되는 즉시 Spring Data Redis는 팬텀 해시를 로드하여 정리(보조 인덱스에서 참조 제거)를 수행합니다.

 

데이터 정리가 수행되지 않는 여러 가지 이유

  1. 데이터를 삽입하고 종료하기 위해 콘솔 애플리케이션을 실행하는 경우 만료 시 해시가 제거되지만 애플리케이션이 더 이상 실행되지 않기 때문에 인덱스 정리를 수행하지 않습니다. Redis에서 게시한 모든 이벤트는 일시적이며 애플리케이션이 수신하지 않는 경우 이러한 이벤트는 손실됩니다.
  2. (keyspace-events를 활성화하지 않고) 리포지토리 지원을 활성화 @EnableRedisRepositories
  3. 한 경우 Keyspace 이벤트 리스너가 활성화되지 않고 Spring Data Redis가 만료 이벤트를 구독하지 않습니다.

 

해결
@Configuration
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
class RedisConfig
  • class 파일을 하나 만들어 위의 설정들을 추가하면 ttl이 만료될때 Spring Data Redis에서 해당 이벤트를 수신하여 제거시킨다.

 

Redis 배포시 실제 경험한 이슈

문제 상황
  • Redis를 k8s서버에 올린 후 서버를 시작하면 err unknown command config, with args beginning with: get, notify-keyspace-events 에러가 발생
해결 과정
  1. 처음에는 @EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.*ON_STARTUP*, keyspaceNotificationsConfigParameter = "") 를 config 설정에 추가해주어 notifications의 기본설정을 처리 했음 → 실패
  2. AUTH 관련 에러가 존재하기에 비밀번호 권한 문제라고 생각
@Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        val redisConfiguration = RedisStandaloneConfiguration()
        redisConfiguration.hostName = host
        redisConfiguration.port = port
        redisConfiguration.setPassword(password)
        return LettuceConnectionFactory(redisConfiguration)
    }
  • 위와같이 config에 설정을 추가, password만 설정했을때에는 Unable connection redis 에러가 발생했음 host와 port를 못찾는 상황이 발생해서 같이 추가함
  1. 위의 설정들을 마친 뒤에도 문제 상황과 동일한 에러가 발생
    1. /etc/redis/redis.conf 에 위치한 redis 설정에서 notify-keyspace-events "AKE” 로 변경하여 모든 이벤트에 대한 알림을 허용했음

로컬의 Redis(7버전)는 notify-keyspace-events 가 자동으로 변경되어 설정되는것으로 확인했는데 k8s의 redis는 6버전이라 그런지 몰라도 notifications에 대한 설정을 따로 해줬어야 했다.. AUTH password 에 관한 설정은 yaml에서 읽지 못하고 config에서는 읽는것 같다.

  • 덕분에 redis.conf에 대한 다양한 설정들을 정독할 수 있었고 직접 설정 또한 변경해보며 이것저것 테스트 할 수 있었다. 앞으로는 에러를 좀 더 꼼꼼히 보고 근본적인 원인을 파악하기 위해 노력해야겠다고 느꼈습니다.

'Redis' 카테고리의 다른 글

Redis를 k8s에 업로드할때 경험한 이슈들  (0) 2023.01.30