티스토리 뷰
개요
이번 포스팅에서는 Redis를 효과적으로 구축/운영하기 위한 설계방법에 대해 알아보도록 하자. Redis는 대표적인 In-memory DB로 세션, 캐시, 큐 등으로 활용된다. 단일 환경으로 가볍게 구성이 가능하지만, 고 가용성을 위한 클러스터 환경이나 Replication 환경을 구성할 수 있다.
캐시 설계
먼저 살펴볼 내용은 캐시 서버를 설계하기 위한 공통적인 지침사항이다. 총 5가지의 설계 지침을 제시하며, 각각은 정답이 아닌 고려사항임을 유념하고 포스팅을 읽어 주셨으면 한다.
1) Cache & Data Store 배치 전략
대표적인 Cache Server 배치 전략으로는 Cache Aside 패턴과 Write Back 패턴이 있다.
a. Cache Aside 패턴
특징 : 읽기에 적합. 캐시 장애 대비 구성. 정합성 문제 발생. 반복적인 호출에 적합
Cache Asdie 패턴은 캐시로 부터 빠르게 데이터를 조회해 올 수 있도록 설계하는 방식이다. 읽기가 많은 경우 적합하며, 가용성 측면에서 Cache 서버에 장애가 발생해도 Data Store를 통해 지속 서비스를 할 수 있다. 다만, Cache Store와 Data Store간 정합성 유지 문제가 발생할 수 있으며, 초기 조회 시 무조건 Data Store를 호출 해야 하므로 단건 호출 빈도가 높은 서비스에 적합하지 않다. 반복적으로 동일 쿼리를 수행하는 서비스에 적합한 아키텍처이다.
b. Write Back 패턴
특징 : 큐 역할. 쓰기에 적합. 캐시 장애 시 데이터 유실. 정합성 확보. 불필요한 리소스 저장
Write Back의 경우 Cache Store가 데이터 저장소 역할을 하면서 동시에 Data Store에 Write 부하를 줄이기 위한 Queue 역할을 겸하게 된다. 이로인해 Database의 부하를 경감시킬 수 있다는 장점이 있다. 대체로 쓰기 작업이 많은 경우 적용을 권고하며, Cache Store에서 Data Store로 데이터를 전송하기 전에 장애가 발생할 경우 데이터 분실이 발생할 수 있다.
장점으로는 데이터베이스의 일시적인 다운타임을 허용하거나, 장애에 대응할 수 있으며, Cache Store와 Data Store 간의 데이터 정합성을 유지하기에도 유용하다. 반면에 사용되지 않는 데이터가 저장되어 리소스 낭비와 Write 작업에 부하가 발생할 수 있다. 이를 해결하기 위해 TTL을 꼭 사용하여 사용되지 않는 데이터를 반드시 삭제해야 한다.
Cache Aside와 Write Back 이외에도 몇가지 추가적인 패턴이 존재한다.
c. Read Through
Read Through 방식은 Cache Aside 방식과 비슷하지만, Cache Store에 저장하는 주체가 Server이냐 또는 Data Store 자체이냐에서 차이점이 있다. 초기 데이터는 cache miss가 항상 발생한다는 단점을 커버할 수 있도록 애플리케이션 측면에서 캐시 대상을 판단하여 Data Store 저장(Commit) 시 Cache Store에 Query를 동일하게 수행해 주는 것도 한가지 방안이 될 수 있다.
d. Write Through
Write Back의 경우 Queue의 역할을 하여 여러 요청을 Cache Store에서 조절하면서 Data Store에 Write 하는 작업을 관리하지만, Wrtie Through의 경우 Cache Store에도 반영하고 Data Store에도 반영하는 방식을 의미한다.
이는 항상 동기화가 되어 있다는 장점이 있지만, 캐시에 저장할 필요가 없는 데이터까지 Cache Store에 저장되어 리소스 낭비 및 Write 시간이 오래 걸린다는 문제가 있다. 다만 사용되지 않는 데이터가 저장되어 리소스 낭비와 Write 작업에 부하가 발생할 수 있다. 이를 해결하기 위해 TTL을 꼭 사용하여 사용되지 않는 데이터를 반드시 삭제해야 한다.
2) 캐시 검색 방식
캐싱은 성능, 확장성 및 가용성을 크게 향상할 수 있는 유용한 아키텍처 설계 방식이다. 특히 분산 애플리케이션 환경에서 보다 그 효과를 발휘할 수 있도록 설계되어 있다. 전통적인 데이터 저장 방식은 데이터베이스와 디스크와 같은 비 휘발성 Persistent Volume을 사용해 왔으나, 보다 많은 사용자가 동시에 접근하는 환경을 구성하기를 원하고, 더 많은 데이터를 동시에 처리하기 원하는 디지털 시대의 흐름에 따라 기존 영구 저장소는 경합 문제가 발생하고, 성능 저하 현상이 발생하면서 이를 대체하는 환경이 요구되어져 왔다.
캐시 솔루션은 자주 사용되면서 자주 변경되지 않는 데이터의 경우 캐시 서버에 적용하여 반영할 경우 높은 성능 향상을 이뤄낼 수 있다. 이를 Cache Hit Rating이라고 한다.
일반적으로 캐시는 메모리에 저장되는 형태를 선호한다. 디스크에 저장되는 방식은 기존 PV와 크게 다르지 않기 때문에 메모리에 저장하여 성능 향상을 극단적으로 뽑아내는 환경을 구성하기를 원한다. 대표적으로 Redis와 MemCached가 있다. 이와 같은 IMDG 솔루션은 메모리를 1차 저장소로 사용하기 때문에 디스크와 달리 제약적인 저장 공간을 사용하게 된다. 많아야 수십기가 정도의 저장소를 보유하게 되며, 이는 결국 자주 사용되는 데이터를 어떻게 뽑아 캐시에 저장하고 자주 사용하지 않는 데이터를 어떻게 제거해 나갈 것이냐를 지속적으로 고민해야 한다.
따라서 캐시를 저장하는 시점은 자주 사용되며 자주 변경되지 않는 데이터를 기준으로 하는 것이 좋다.
또한 한가지 고민해야 할 사항은 캐시 솔루션은 언제든지 데이터가 날라갈 수 있는 휘발성을 기본으로 한다는 점이다. 이는 데이터를 주기적으로 디스크에 저장할 수는 있지만, 실시간으로 모든 데이터를 디스크에 저장할 경우 성능 저하를 일으킬 수 있어 어느 정도 데이터 수집과 저장 주기를 갖게 된다. 즉 데이터의 유실 또는 정합성이 일정 부분 깨질 수 있다는 점을 항상 고려해야 한다. 따라서 캐시에 저장되는 데이터는 중요한 정보, 민감 정보 등은 저장하지 않는 것이 좋으며, 캐시 솔루션이 장애가 발생했을 경우 적절한 대응방안을 모색해 두는 것이 바람직하다. (TimeOut, 데이터베이스 조회 병행 등)
3) 캐시 제거
캐시 데이터의 경우 캐시 서버에만 단독으로 저장되는 경우도 있지만, 대부분 영구 저장소에 저장된 데이터의 복사본으로 동작하는 경우가 많다. 이는 2차 저장소(영구 저장소)에 저장되어 있는 데이터와 캐시 솔루션의 데이터를 동기화 하는 작업이 반드시 필요함을 의미하며, 개발 과정에 고려사항이 늘어난다는 점을 반드시 기억해야 한다. 예를 들어 캐시 서버와 데이터베이스에 저장되는 데이터의 커밋 시점에 대한 고려 등이 예가 될 수 있다. 캐시의 만료 정책이 제대로 구현되지 않은 경우 클라이언트는 데이터가 변경되었음에도 캐시된 오래된 정보를 사용할 수 있다.
캐시를 구성할 때 기본 만료 정책을 설정할 수 있다. 캐시된 데이터가 만료 되면 캐시에서 제거 되고 응용 프로그램은 원래 데이터 저장소에서 데이터를 검색 해야 한다. 캐시 만료 주기가 너무 짧으면 데이터는 너무 빨리 제거되고 캐시를 사용하는 이점은 줄어든다. 반대로 너무 기간이 길면 데이터가 변경될 가능성과 메모리 부족 현상이 발생하거나, 자주 사용되어야 하는 데이터가 제거되는 등의 역효과를 나타낼 수도 있다.
캐시 서비스는 LRU 알고리즘 기반으로 데이터를 제거하지만 일반적으로 이 정책을 재정의하고 항목을 제거하지 않도록 방지할 수 있다.
4) 캐시 공유
캐시는 애플리케이션의 여러 인스턴스에서 공유하도록 설계된다. 각 애플리케이션 인스턴스가 캐시에서 데이터를 읽고 수정할 수 있다. 따라서 모든 공유 데이터 저장소와 함께 발생하는 동일한 동시성 문제는 캐시에도 적용된다. 애플리케이션이 캐시에 보유하는 데이터를 수정해야 하는 경우 애플리케이션의 한 인스턴스가 만드는 업데이트가 다른 인스턴스가 만든 변경을 덮어쓰지 않도록 해야 한다.
데이터의 충돌을 방지하기 위해 다음과 같은 어플리케이션 개발 방식을 취해야 한다.
먼저, 캐시 데이터를 변경하기 직전에 데이터가 검색된 이후 변경되지 않았는지 확인하는 방법이다. 변경되지 않았다면 즉시 업데이트하고 변경되었다면 업데이트 여부를 애플리케이션 레벨에서 결정하도록 수정해야 한다. 이와 같은 방식은 업데이트가 드물고 충돌이 발생하지 않는 상황에 적용하기 용이하다.
두번째로, 캐시 데이터를 업데이트 하기 전에 Lock을 잡는 방식이다. 이와 같은 경우 조회성 업무를 처리하는 서비스에 Lock으로 인한 대기현상이 발생한다. 이와 같은 방식은 데이터의 사이즈가 작아 빠르게 업데이트가 가능한 업무와 빈번한 업데이트가 발생하는 상황에 적용하기 용이하다.
5) 가용성 및 성능 확보
캐시를 구성하는 목적은 빠른 성능 확보와 데이터 전달에 있으며, 데이터의 영속성을 보장하기 위함이 아니라는 점을 기억하고 설계해야 한다. 데이터의 영속성은 기존 데이터 스토어에 위임하고, 캐시는 데이터 읽기에 집중하는 것이 성능 확보 측면에서 핵심 고려사항이 될 수 있다.
또한, 캐시 서버가 장애 또는 특별한 사유로 인해 다운되었을 경우나 서비스가 불가능할 경우에도 지속적인 서비스가 가능해야 한다. 이는 캐시에 저장되는 데이터는 결국 기존 영구 데이터 스토어에 동일하게 저장되고 유지된다는 점을 뒷바침하는 설계방식이다. 캐시 서버가 장애로 부터 복구되는 동안 성능상의 지연은 발생할 수 있지만, 서비스가 불가능한 상태가 되지 않도록 고려해야 한다.
Redis 설계
Redis는 NoSQL 데이터베이스로 대부분 유휴 상태인 대량 데이터를 저장하지 않도록(즉, 자주 변경되고, 이동되는 데이터가 저장되도록) 설계되었다. 또한 Redis 데이터베이스는 설치 공간이 작고 최소한의 리소스로도 엄청난 처리량을 제공 할 수 있다.
마이크로서비스 아키텍처에서 각 서비스는 비즈니스의 모든 것을 실행하는 것이 아니라 서비스별 특정 용도에 맞게 설계되어있다. 즉, Redis 데이터베이스는 NoSQL 데이터베이스로 비용효율적으로 동작하여 마이크로 서비스 환경에서 적합하게 활용할 수 있다.
각 서비스는 비즈니스 프로세스 측면에서 독립된 역할을 수행하도록 설계되었으므로 데이터는 비 관계형이며 NoSQL 데이터 모델에 적합하다. Redis는 마이크로서비스 아키텍처에 저장된 데이터에 대해 모든 측면에서 적합하지는 않지만, 많은 요구사항에 부합한다.
기존 마이크로 서비스 환경에서 서비스 간 통신은 REST 또는 유사한 규칙을 사용하는 HTTP 엔드 포인트를 적용해 왔다. 이때 Redis는 스트림(Stream)처리를 지원하는 Redis Stream을 적용할 수 있다. Redis Stream은 스트림 데이터 또는 로그 데이터를 처리하기 위해서 5.0에서 새로 도입된 데이터 타입이다. 대부분의 프로젝트에서는 데이터가 지속적으로 누적되어 쌓이는 경우가 발생할 수 있다.
스트림(Stream) 또는 로그(Log)는 사람이 아니고 기계(machine)가 발생시키며 연속적이고 대량이라는 특징이 있다. 또한 기존 데이터를 수정하지 않고 오직 추가로 발생한다는 점이다.
이 경우 Redis Streams는 모든 서비스가 해당 스트림에서 이벤트를 알리고 관심있는 서비스에 속하는 스트림만 수신하는 완전 비동기 패턴을 허용한다.이 시점에서 양방향 통신은 서로의 스트림을 관찰하는 두 서비스에 의해 달성된다.
또한 Redis는 항상 데이터베이스에서 직접 데이터를 검색해야하는 것은 아닌 Redis에서 훨씬 더 빠르게 데이터를 가져올 수 있는 캐시 역할을 한다. 마찬가지로 API를 통해 액세스 해야 하는 외부 데이터 서비스도 너무 느릴 수 있으며 여기에서 Redis를 사용하여 불필요하고 긴 호출이 시스템의 전체 성능에 영향을 미치는 것을 방지 할 수 있다.
지금부터는 Redis 관련 세부 설계 요소를 살펴보도록 하자.
1) 단일 쓰레드 환경
Redis는 단일 쓰레드로 동작하는 인메모리 DB라는 특징을 갖고 있다. 단일 쓰레드로 동작한다는 의미는 한번에 1개의 명령어만 실행할 수 있다는 의미이며, 이와 같은 특성에 의해 개발 및 구성 시 고려해야 할 부분들이 있다. (물론 Async로 동작하는 Background 프로세스가 있지만, 서비스를 처리하는 동작은 기본 1 Redis 1 Worker를 기준으로 동작한다.)
a. Long Time Query 처리
단일 쓰레드로 동작하는 Redis는 오래 걸리는 명령어를 사용할 경우 해당 시간동안 다른 명령어 또는 서비스를 처리할 수 없다. 따라서 운영 환경에서는 다음과 같은 경우를 주의하여 처리해야 한다.
- keys * 명령어
keys는 해당 노드에 저장된 모든 키를 보여주는 명령어이다. 대량 데이터가 저장되어 있는 시스템의 경우 keys를 수행하는데 수초 또는 수분의 조회 시간이 발생할 수 있어, 운영환경에서 사용하는 것은 권고하지 않는다. 운영 환경에서 keys를 조회하고자 할 경우에는 keys 명령어 대신 scan 명령어를 사용하는 것을 권고한다.
[keys]
[root@ip-192-168-106-237 utils]# redis-cli -c -p 8000 127.0.0.1:8000> keys pattern ====================================================== 127.0.0.1:8000> keys * 1) "test4" 2) "11" 3) "7" 4) "j" 5) "ee" 6) "3" 7) "s" 8) "f" 9) "test5" 10) "10" 11) "b" 12) "n" 127.0.0.1:8000>
[scan]
key 수가 많을 수록 처리시간이 증가하는 keys 명령어를 대체하기 위해 scan 명령어를 활용할 수 있다.
keys는 한번에 모든 key를 full scan하여 조회하는 반면, scan은 한번에 약 10개씩 정도씩 key를 조회한다. key가 많으면 다음 커서를 지정해서 반복해서 조회한다. 모두 조회했을 경우 next cursor가 0이 된다.
[root@ip-192-168-106-237 utils]# redis-cli -c -p 8000 127.0.0.1:8000> scan cursor [MATCH pattern] [COUNT count] ====================================================== 127.0.0.1:8000> scan 0 1) "15" 2) 1) "7" 2) "test4" 3) "test5" 4) "10" 5) "f" 6) "j" 7) "b" 8) "11" 9) "ee" 10) "3" 11) "s" 127.0.0.1:8000> scan 0 count 5 1) "5" 2) 1) "7" 2) "test4" 3) "test5" 4) "10" 5) "f" 127.0.0.1:8000> scan 5 count 5 1) "15" 2) 1) "j" 2) "b" 3) "11" 4) "ee" 5) "3" 6) "s" 127.0.0.1:8000> scan 15 count 5 1) "0" 2) 1) "n" 127.0.0.1:8000> scan 0 count 5 match test* 1) "5" 2) 1) "test4" 2) "test5" 127.0.0.1:8000>
scan의 count는 조회 대상 key의 수이다. 기본으로 10개를 조회하지만, default 값 또는 지정한 수가 정확히 조회되지는 않는다. 조회되는 수는 처리 시간을 고려하여 Redis에서 자동으로 산정한다.
또한 match는 조회 대상 key들의 패턴을 검색 조건으로 지정하는 방법이다. 이와 같이 scan 명령어를 사용하면 keys의 전체 조회에 따른 성능 저하 현상을 방지할 수 있다.
- flushall 명령어
flushall은 현재 지정된 Master Node의 key와 data를 모두 제거하는 명령어이다. 대량 데이터가 저장되어 있는 환경에서 flushall을 사용할 경우 심각한 성능 지연 현상을 초래할 수 있다.
따라서 flushall은 가급적 운영환경에서 사용하는 것은 권고하지 않는다. 다만, 부득이하게 사용해야 할 경우 async 옵션을 적용하여 반영한다. Redis Server 4부터 async 옵션을 사용할 수 있으며, flushall 수행 시 별도의 쓰레드를 생성하여 background로 삭제하기 때문에 응답 속도가 매우 빠르고, 지연을 방지할 수 있다. 측정된 데이터를 기준으로 String key 백만개를 flush할 경우 Sync는 약 1초, Async는 1ms 미만이내에 명령이 수행된다.
이와같이 두 명령어는 개발/테스트 환경이나 소량의 데이터를 관리하는 환경에서는 적극 활용할 수 있으나, 실행 대상을 전수처리하기 때문에 데이터가 누적되는 시스템 또는 대량 데이터를 저장하는 시스템의 경우 운영에 영향을 줄 정도로 속도가 느려질 수 있다. 따라서 운영환경에서 해당 명령어를 적용하기 위해서는 반드시 유의사항에 따라 옵션을 검토해 보는 것이 좋다.
# 참조
Redis Server 4 이상에서 지원하는 Lazy Freeing 기능을 활용하여 UNLINK 옵션을 적용할 경우 비동기로 처리할 수 있다.
그 밖에 flushdb, collection 관련 삭제, 조회 등의 명령어는 가급적 운영환경에서 사용하지 않는 것을 권고한다.
b. Collection 활용
단일 쓰레드 환경에서 Long Time Query는 서비스 지연의 원인이 된다. Redis와 같은 캐시 솔루션은 단일 데이터 조회시 hash key를 기반으로 조회하기 때문에 대부분 조회 성능은 빠르게 처리된다. 다만, Redis에는 Memcached와 달리 Collection Type을 제공한다. Collection은 대량 데이터 조회에 보다 유용하게 활용될 수 있지만, 단일 Collection에 데이터 100만건을 넣으면 10초, 1천만건을 넣으면 100초씩 걸리는 식으로 시간이 늘어나기 때문에 Collection 당 저장해야 하는 데이터 사이즈는 1만건 미만으로 관리하는 것이 좋다.
또한, Hash, Sorted Set, Set과 같은 Collection은 메모리를 많이 사용점유하므로 필요에 의한 적용이 필요하다.
c. Multi Instance 구성
Redis는 데이터 저장 및 조회에 단일 쓰레드를 사용한다. 따라서 멀티코어 시스템에서 성능을 최대한 끌어내기 위해서는 인스턴스 여러 개를 실행하여 성능을 향상시킬 수 있다.
대체로 멀티코어 환경에서는 전체 코어의 절반이하의 Redis 인스턴스를 기동하여 운영하는 것이 적당하다. (ex - 4Core System은 max 2개의 Instance, 8Core System은 max 4개까지 인스턴스 기동을 권고한다.)
물론 코어만 충분하다고 해서 여러개의 인스턴스를 늘려 나갈 수 있는 것은 아니다. 바로 메모리 대여폭에 대한 임계점 문제가 발생할 수 있다. 메모리 임계점에 대한 내용은 하단을 참고한다. Memory 대여폭이 충분하다는 가정하여 위와 같은 기준치를 세우고 구성해 나가는 것이 바람직하다.
Multi Instance 환경에서 특히 고려해야 할 부분은 SMS나 APM 모니터링 환경 구성시 노드 전체 사용률을 기반으로 모니터링을 해서는 안된다는 점이다. 단일 쓰레드로 처리되기 때문에 하나의 Redis는 하나의 Core를 점유하게 되고, 하나의 Core만 100%를 차지하게 된다. 즉, 4Core VM 환경을 가정할 경우 Redis Server 하나가 100% CPU를 점유할 경우 노드 기준으로는 25%만 사용중이라는 의미이다. 따라서, SMS 알람을 노드 기준 75% 수준으로 지정하는 것은 바람직한 모니터링 구성이 아닐 수 있다. 이때는 노드 기준의 모니터링이 아닌 프로세스 기준으로 모니터링을 실행해야 한다.
2) 리소스 활용
a. Memory 구성 고려사항
Redis는 데이터를 메모리에 저장하는 대표적인 인메모리 DB이다. 따라서 Redis가 사용할 메모리 크기를 지정하는 것은 성능과 가장 밀접한 관계가 있다.
- memory 사이즈
별도로 Redis의 설정파일인 redis.conf의 maxmemory 설정을 하지 않을 경우 0으로 구성되며, OS에서 사용 가능한 Max Memory까지 확장하여 사용하게 된다. 이는 특정 인스턴스의 장애가 동일 노드의 다른 Redis 인스턴스로의 전파를 일으킬 수 있다. 특히 물리 메모리를 모두 사용하는 상황이 발생할 경우 스왑 메모리 영역을 사용하게 되는데 이때 Redis의 성능은 상황에 따라 수백배까지 느려질 수 있다. 따라서 반드시 maxmemory를 설정하여 메모리 사용계획과 저장되는 데이터 LifeCycle을 관리하는 정책을 세워야 한다.
- 메모리 대여폭
메모리 대여폭은 동작 속도를 의미하며, 대여폭이 높을 수록 빠르다고 볼 수 있다. (물론 대여폭이 높다고 항상 성능이 좋아지는 것은 아니다. 일정 수준의 대여폭이 확보되면 이후에는 대여폭의 영향은 성능에 영향을 거의 주지 않는다.) 메모리의 속도는 메모리 자체 속도와 CPU와의 데이터 전송폭을 모두 고려한다. 메모리 대여폭은 초당 바이트 전송률이라 볼 수 있으며, 흔히 ‘Bandwidth’이라고 한다.
Redis에서 동일 노드에 여러 개의 인스턴스를 운영시 메모리 대여폭의 임계점에 문제가 발생할 수 있다. 여러 개의 인스턴스들이 동시에 명령을 처리할 경우 Redis는 명령 처리를 위해 메모리에 접근하고 조회된 데이터는 메모리 대여폭을 사용하여 코어로 전송한다. 즉 Redis 인스턴스가 메모리 대여폭을 나누어 사용하므로 병목현상이 발생할 수 있다. 따라서 충분한 메모리 대여폭이 확보된 상태에서 멀티 인스턴스를 구성하는 것을 고려해야 한다.
b. Network 임계점 고려
Master와 Slave 간 데이터 전송과 동기화로 인해 네트워크에 부하가 발생할 수 있다. Master Node에 Write가 발생하면 연결된 Slave Node에 Redis 프로토콜을 사용하여 데이터를 전송한다. Key와 데이터의 크기에 해당하는 네트워크 대여폭을 사용하게 된다. 따라서 각 마스터는 Write가 발생할 때마다 복제를 위한 Slave 갯수 만큼의 추가 트래픽이 발생하게 된다. 가용성만을 중시하는 아키텍처는 네트워크 부하를 발생시킬 수 있음을 인지해야 한다.
c. Persistence 볼륨 관리
Redis는 인메모리 DB와 디스크 활용 측면에서 검토가 필요하다. Redis의 인메모리 DB는 빠른 속도가 강점이지만 큰 용량의 데이터를 담기엔 사이즈 제약이 크다.
따라서 실시간 처리는 인메모리, 자주 사용되지 않는 데이터의 저장 관리는 디스크 기반 스토리지로 하는 구조가 성능과 효율을 함께 달성할 수 있다.
먼저 메모리 상태를 그대로 디스크에 저장하는 RDB(Snapshot) 기능이 있다. 이 기능은 Redis 서버 장애 요인의 대부분을 차지하기도 한다. 따라서 해당 기능은 반드시 필요한 경우를 제외하고 꺼두거나 구성을 민감하게 관리하는 것이 좋다.
Redis의 SnapShot은 매우 짧은 시간에 매우 많은 디스크 입출력이 발생하는데, 외부 저장장치는 로컬 디스크에 비하여 스냅샷 파일을 기록하는데 많은 시간이 소요된다. 이와 같은 이유로 스냅샷이 완료되기 전까지 Redis의 전체적인 성능도 같이 떨어지게 된다.
Master/Slave 복제 구성에서 Master Node의 스냅샷 설정이 꺼져 있더라도, 새로운 슬레이브 노드가 마스터에 추가되면 마스터 노드는 스냅샷을 생성한다. redis.conf의 client-output-buffer-limit은 Redis 복제를 위해 스냅샷 데이터를 전송할 버퍼를 생성하는데, 이 버퍼의 크기가 지정된 크기보다 커지면 슬레이브의 연결을 강제로 끊게 된다. 이때 슬레이브는 마스터 노드와 연결이 끊어졌으므로 다시 Master Node로 접속을 시도하고, 접속 후에는 Master Node의 스냅샷이 생성되는 현상이 반복되게 된다.
대략 60GB 메모리 서버의 스냅샷을 만드는데 대략 10분 정도가 소요된다. Redis의 싱글쓰레드 문제를 겪지 않기 위해 Fork()를 사용하여 분기 처리할 수 있지만, 이 경우 메모리를 2배로 잡아 먹어 리소스가 부족현상을 발생시킬 수 있다.
다음으로 메모리 상의 정보를 디스크에 저장한다는 점은 RDB와 비슷하나, Redis 프로토콜로 통신한 내용들을 명령어, 키, 이름 등 형식 그대로 저장하는 AOF 기능이 있다. RDB 사용시 주의사항은 AOF에도 동일하게 적용된다.
RDB와 AOF 방식의 장단점을 상쇄하기 위해서 두가지 방식을 혼용해서 사용하는 것이 바람직하다. 주기적으로 RDB(SnapShot)으로 백업하고, 다음 스냅샷 생성 전까지의 저장은 AOF 방식으로 수행한다. 이렇게 하면 서버가 Restart 될때 백업된 스냅샷을 Reload하고, 소량의 AOF 로그만 Replace하면 되기 때문에, Restart 시간을 절약하고 데이터 유실을 방지할 수 있다.
3) 복제
Master/Slave Replication이란, Redis의 Master Node에 Write 된 내용을 Slave Node에 복제하는 것을 의미한다. 1개의 Master Node는 N개의 Slave Node를 가질 수 있으며, 각 Slave Node도 그에 대한 Slave Node를 또 가질 수 있다.
이 노드 간 복제는 Non-Blocking 상태로 이루어진다. 즉 Master Node에서 Write나 Query 연산을 하고 있을 때도 Background로 Slave Node에 데이터를 복사하고 있다는 이야기이며, 이는 순간적으로 Master/Slave가 데이터 불일치성을 유발할 수도 있다는 이야기이기도 하다. 즉 Master Node에 Write한 데이터가 Slave Node에 복제 중이라면 Slave Node에서는 이전의 데이터가 조회될 수 있다.
Redis 시스템에서 Slave는 이름처럼 마스터의 데이터 저장을 보조한다. 마스터가 죽었다가 되살아날 때 자신의 정보를 모두 없애고 그 데이터를 그대로 복제한다. 별도 조치 없이 아무 데이터가 없는 Master를 시스템에 연결하면 Slave에 남은 데이터를 Master로 되살릴 수 없다. 이를 피하기위해서는 복구할 데이터를 가진 시스템에 "Slave of no one(슬레이브오브노원)"이라는 명령어를 줘서 Slave를 Master로 승격시켜야 한다.
또한 Redis의 복제를 구성할 때 하나의 Master에 너무 많은 Slave를 구성하지 않도록 한다. 너무 많은 Slave는 가용성을 높이지만 데이터를 복제하기 위한 네트워크 사용률이 데이터 서비스를 위한 네트워크 사용률보다 커지게 되어 매우 느린 응답시간을 보이게 된다. 이를 위해 복제를 위한 별도의 네트워크 카드를 설정하는 방법과 복제를 위한 별도의 스위치 허브를 구성하는 방법을 고려할 수 있다.
Master/Slave간 동시접속자수나 처리 속도를 높이기 위해 Query Off Loading이라는 기법을 사용하는데 Master는 Write Only, Slave는 Read Only로 사용하는 방법이다.
a. Redis SENTINEL
장애시 복구를 위한 복제 작업을 Redis SENTINEL이라는 기술로 자동화할 수 있다. Redis SENTINEL은 Redis 시스템에서 마스터가 죽었다고 판단 시 다른 슬레이브를 마스터로 승격하고 사용자측에 마스터가 바뀌었다는 알림을 보내는 도구이다.
Redis SENTINEL을 사용해 Redis 환경에서 마스터와 슬레이브를 지정하는 방식, 우선순위 결정 원리, 실제 서비스 운영환경에서 Fail-over를 수행하기 위한 적정 설정값, 레디스 프로젝트에서 진행되고 있는 Redis SENTINEL 기술에 대한 개선 작업 현황 등을 함께 고려해야 한다.
4) 확장성 및 고 가용성
Redis는 확장성과 고 가용성을 달성하기 위한 다양한 방법을 제공한다.
데이터를 여러 Redis 노드로 분할하여 Redis Sharding을 사용하여 수평 확장 구성 할 수 있다. Sharding은 단일 인스턴스의 부담을 덜어주고 멀티 코어환경에서 이점을 누릴 수 있다. 그러나 Multi Key 작업 및 트랜잭션을 지원할 수 없기 때문에 샤딩의 한계를 알고 있어야한다.
복제를 사용하여 고 가용성을 얻을 수 있다. 마스터 노드는 동기식으로 복제되며 노드 장애, 데이터 센터 장애 및 Redis 프로세스 장애로부터 보호되도록 구성할 수 있다. 마스터가 실패하면 복제본이 대신 마스터를 수행한다. 다른 AZ에 복제본이있을 수도 있다. 이렇게하면 전체 AZ가 실패하는 일부 치명적인 이벤트로부터 보호될 수 있다.
# 참조
https://medium.com/@octoz/high-availability-and-scalability-with-redis-enterprise-54a48edcce17
5) 기타
a. Fork 활용
Redis는 AOF와 RDB(SnapShot)을 위해서 fork() 함수와 COW(Copy On Write : 서로 다른 프로세스 간의 리소스 공유)를 사용하여 백그라운드 저장 프로세스를 생성한다.
b. 저장 만료 주기 관리
max-memory만큼 메모리를 사용하게 되면, 메모리 정책에 따라 과거에 만들어진 키들이 삭제된다.
Redis 서버는 In-Memory 기반의 데이터 저장 관리기술을 제공하지만 시스템 메모리 크기는 제한적일 수 밖에 없고 상대적으로 사용자 데이터는 이보다 훨씬 더 클 수 밖에 없기 때문에 모든 데이터를 100% 메모리에서 저장 관리할 수는 없다. 이와 같은 문제점을 개선하기 위해 Redis 서버는 4.0 버전부터 LRU(Least Recently Used: 가장 최근에 사용되지 않은 것) 알고리즘과 LFU(Least-Frequently-Used: 가장 적게 사용된 것) 알고리즘을 제공하고 있다.
Redis가 Export 된 데이터를 삭제하는 정책은 내부적으로 active와 passive 두가지 방법을 사용한다. active는 요청된 key가 호출된 시점에 expired 여부를 검증하여 삭제하는 방법이며, passive는 random으로 key 100개만 반복적으로 스캔해서 지우는 방식이다.
expired time이 지난 후 클라이언트에 의해서 접근되지 않는 데이터는 active 방식으로 인해서 지워지지 않고 passive 방식으로 지워져야 하는데, 마찬가지로 전체를 scan하는 것이 아니기 때문에 Redis에는 항상 Expired 되었으나 지워지지 않는 Garbage 데이터가 존재할 수 있다는 점을 명심해야 한다.
[root@ip-192-168-106-237 ~]# redis-cli -c -p 8000 127.0.0.1:8000> set a 100 -> Redirected to slot [15495] located at 127.0.0.1:8002 OK 127.0.0.1:8002> expire a 100 (integer) 1 127.0.0.1:8002> ttl a (integer) 97 127.0.0.1:8002> ttl a (integer) 93 127.0.0.1:8002> ttl a (integer) 60 127.0.0.1:8002> ttl a (integer) -2 127.0.0.1:8002> get a (nil) 127.0.0.1:8002>
Redis는 결국 메모리에 저장되는 데이터 구조를 갖고 있기에 메모리를 어떻게 관리할 것인지가 성능과 가용성에 큰 영향을 끼치게 된다. Redis가 Max Memory에 도달할 경우 다음 Policy 중에서 선택하여 키와 데이터를 관리할 수 있다.
a. noeviction 캐시를 지우지 않는 정책이다. 메모리가 maxmemory 이상을 사용하게 되면 error를 발생시킨다. b. allkey 각 정책에 따라 모든 키를 대상으로 정리한다. allkeys-lru LRU 알고리즘 기반으로 키를 삭제한다. allkey-random 랜덤하게 키를 삭제한다. allkeys-lfu LFU 알고리즘 기반으로 키를 삭제한다. c. volatile 각 정책에 따라 EXPIRE SET에 있는 키들을 대상으로 정리한다. volatile-lru LRU 알고리즘 기반으로 키를 삭제한다. volatile-random 랜덤하게 키를 삭제한다. volatile-ttl TTL이 짧은 순으로 삭제한다. volatile-lfu LFU 알고리즘 기반으로 키를 삭제한다. |
# LRU는 가장 최근에 사용한 것을 의미
# LFU는 가장 적게 사용됨을 의미
# LRU, LFU 및 최소 TTL 알고리즘은 정밀한 알고리즘이 아니라 근사치
c. Memory Fragment
maxmemory 대비 사용된 메모리 비율이 낮더라도 메모리 조각화로 인해 Redis 인스턴스의 메모리가 부족해질 수 있다. 메모리 조각화는 운영체제가 반복적인 쓰기 및 삭제 작업 후 Redis가 완전히 활용할 수 없는 메모리 페이지를 할당할 때 발생한다. 이러한 페이지가 누적되면 시스템 메모리가 부족해져 Redis 서버가 다운될 수 있다.
Redis 버전 4.0 이상에서는 activedefrag 구성을 제공한다. activedefrag를 yes로 설정하면 CPU 사용량이 증가하지만 메모리 조각화를 완화하여 메모리 부족 문제를 해결할 수 있다.
결론
Redis와 같은 인메모리 DB는 마이크로서비스와 같이 확대되는 분산 아키텍처 구조에서 유용하게 활용되고 있다. 단순히 세션 서버의 용도를 벗어나 자주 사용되는 데이터 캐시 용도, 때로는 데이터 전송을 위한 Queue 용도로도 활용할 수 있다. 이와 같은 IMDG의 경우 또 하나의 SPOF가 될 수도 있기 때문에 반드시 각 설계 요소들을 검토하고, 보다 주의 깊게 설정을 검토하는 것이 반드시 필요할 것이다.
'⑦ Open Source Software' 카테고리의 다른 글
Redis 운영관리 (Redisinsight & RMA) (1) | 2021.07.02 |
---|---|
Redis5 vs Memcached 선택기준 (0) | 2021.06.18 |
Redis5 Cluster 구성하기 (0) | 2021.05.05 |
Embedded JupyterHub Content-Security-Policy Issue (0) | 2020.01.08 |
[Monitoring] Prometheus & Grafana를 활용한 자원 사용률 모니터링 (0) | 2019.12.09 |
- Total
- Today
- Yesterday
- node.js
- nodejs
- k8s
- openstack tenant
- 아키텍처
- git
- TA
- SA
- wildfly
- 오픈스택
- SWA
- aws
- aa
- 마이크로서비스 아키텍처
- JEUS6
- jeus
- 쿠버네티스
- JBoss
- OpenStack
- kubernetes
- JEUS7
- openstack token issue
- Docker
- MSA
- API Gateway
- apache
- 마이크로서비스
- Architecture
- webtob
- Da
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |