글의 목적
- Redis를 이해하기
- 직접 사용해본적은 없지만 앞으로 사용해야 할 스텍임이 분명함으로 미리 정리해두려고 함.
본론
Redis 캐시로 사용
What is Caching?
- Cache란 사용자의 입장에서 데이터의 원래 소스보다 더 빠르고 효율적으로 액세스할 수 있는 임시 데이터 저장소이다.
- 대부분의 애플리케이션에서 속도 향상을 위해 cache를 사용.
cache가 유용하게 사용되려면?
- cache에 접근하는 게 원본에 접근하는 것보다는 쉽고 빨라야 함.
- 동일한 데이터에 대해 반복적으로 액세스하는 상황이 많을 때(데이터의 재사용 횟수가 한 번 이상)
- 잘 변하지 않는 데이터일수록 cache를 사용할 때 더 효율적
=> 그런면에서 Redis는 이상적인 솔루션임.
Redis가 이상적인 솔루션인 이유
- 단순하게 key-value 형태로 저장할 수 있기 때문에 어떤 데이터라도 쉽게 저장할 수 있음. (사용이 간편)
- 레디스는 모든 데이터를 메모리에 올려두는 인메모리 데이터 스토어임. (매우 빠름을 의미함)
- 평균 읽기 및 쓰기 작업 속도가 1밀리 세컨드 미만
- 초당 수백만 건의 작업이 가능하다는 것을 의미함.
- 빠른 처리속도는 곧 지연시간의 감소와 처리량의 증가로 연결됨.
캐싱 전략(Caching Strategies)
- 레디스를 cache로 사용할 때 어떻게 배치하는지가 시스템의 성능에 큰 영향을 끼치기도 함.
- 이를 caching 전략이라고 함.
- caching 전략은 데이터의 유형과 해당 데이터에 대한 액세스 패턴을 잘 고려해서 선택해야 함.
읽기 전략 (Look-Aside)
- 이 구조는 레디스를 cache로 쓸 때 가장 일반적으로 사용하는 방법 임.
- 애플리케이션은 데이터를 찾을 때 cache에 먼저 확인 함.
- cache에 데이터가 있으면 cache에서 데이터를 가지고 오는 작업을 반복 함.
- 만약 레디스에 찾는 키가 없다면 애플리케이션은 DB에 접근해서 데이터를 직접 가지고 온 뒤 다시 레디스에 저장하는 과정을 거침.
- 결국 cache는 찾는 데이터가 없을 때에만 입력되기 때문에 이를 lazy loading이라고도 부름.
- 이 구조는 레디스가 다운되더라도 바로 장애로 이어지지 않고 DB에서 데이터를 가지고 올 수 있음.
- 대신 cache로 붙어있던 커넥션이 많이 있다면 그 커넥션이 모두 데이터베이스로 붙기 때문에 DB에 갑자기 많은 부하가 몰릴 수 있음.
- 이런 경우에 cache를 새로 투입하거나 DB에만 새로운 데이터를 저장했다면 처음에 캐시 미스가 엄청 발생해서 성능에 저하가 올 수 있음.
- 이럴 때에는 미리 DB에서 cache로 데이터를 밀어 넣어주는 작업을 해서 해결함. (cache warming)
쓰기전략 (write around, write-through)
- write around 방식은 DB에만 데이터를 저장 함.
- 일단 모든 데이터는 DB에 저장되고 cache miss가 발생한 경우에만 cache에 데이터를 끌어오게 됨.
- 이 경우엔 cahce 내의 데이터와 DB 내의 데이터가 다를 수 있다는 단점이 있음.
- write-through 전략은 DB에 데이터를 저장할 때 cache에도 함께 저장하는 방법임.
- cache는 항상 최신 정보를 가지고 있다는 장점이 있지만, 저장할 때마다 두 단계 스텝을 거쳐야 하기 때문에 상대적으로 느리다고 볼 수 있음.
- 또한 저장하는 데이터가 재사용되지 않을 수도 있는데 무조건 캐시에 넣어버리기 때문에 일종의 리소스 낭비라고도 볼 수 있음.
- 따라서 이렇게 데이터를 저장할 때는 expire time을 설정해 주는 것이 좋음. (이 값의 관리가 곧 장애 포인트)
Redis의 데이터 타입
Redis Data Types
- 레디스는 자체적으로 많은 자료구조를 제공하고 있음.
- Strings
- string은 제일 기본적인 데이터 타입.
- set 커맨드를 이용해 저장되는 데이터는 모두 string 형태로 들어감.
- Bitmaps
- bitmap은 string의 변형이라고 볼 수 있고 bit 단위 연산이 가능함.
- Lists
- list는 데이터를 순서대로 저장하기 때문에 큐로 사용하기 적절함.
- Hashes
- Hash는 하나의 키 안에 또다시 여러 개의 필드와 밸류 쌍으로 데이터를 저장함.
- Sets
- set은 중복되지 않은 문자열의 집합임.
- Sorted Sets
- sorted set은 set처럼 중복되지 않은 값을 저장하지만, 모든 값은 score라는 숫자 값으로 정렬됨.
- 데이터가 저장될 때부터 score 순으로 정렬되며 만약 score가 같을 때에는 사전 순으로 정렬되어 저장됨.
- HyperLogLogs
- hyperloglogs는 굉장히 많은 데이터를 다룰 때 주로 쓰며 중복되지 않는 값의 개수를 카운트할 때 사용함.
- Streams
- Stream은 log를 저장하기 가장 좋은 자료구조임.
- Strings
Counting
- 레디스에서 카운팅을 하기에 가장 쉬운 방법은 키 하나를 만들어서 카운팅할 상황마다 하나씩 증가시키는 것임.
- Strings
- increment 함수를 사용하면 간단하게 구현할 수 있음.
- score:a라는 key에 10을 저장한 뒤 INCR 함수를 쓰면 1이 증가된 11이됨.
- INCREMNETY BY 함수를 사용해서 증가시킬 값을 4로 직접 지정하면 키는 15로 증가됨.
- Bitmaps
- bit 연산을 사용하면 저장 공간을 많이 절약할 수 있음.
- 오늘 접속한 유저 수를 세고 싶다 할 때, 날짜 key 하나를 만들어놓고 유저 ID에 해당하는 bit를 1로 올려주면 됨.
- 한 개의 bit가 한 명을 의미하므로 천만 명의 유저는 천만 개의 bit로 표현할 수 있고 이는 1.2 메가 바이트 정도밖에 차지하지 않음을 의미함.
- SETBIT으로 BIT를 설정할 수 있고 BITCOUNT를 통해 1로 설정된 값을 모두 카운팅할 수 있음.
- 하지만 이 방법을 이용하려면 모든 데이터를 정수로 표현할 수 있어야 함.
- 즉 user id 같은 값이 0 이상의 정수값일 때에만 카운팅이 가능하며 그런 시퀀셜한 값이 없을 때에는 이 방법을 사용할 수 없음.
- HyperLogLogs
- hyperloglogs는 모든 string 데이터 값을 유니크하게 구분할 수 있음.
- set과 비슷하지만 대량의 데이터를 카운팅 할 때 훨씬 더 적절함.
- 왜냐하면 HyperLogLogs는 저장되는 데이터 개수에 상관없이 모든 값이 12KB로 고정되어 저장되기 때문.
- 저장되는 값이 몇백만, 몇천만 건이던 상관 없이 모두 12KB임.
- 대신 한번 저장된 값은 다시 불러올 수 없는데 경우에 따라 데이터를 보호하기 위한 목적으로도 적절하게 사용할 수 있음.
- 예를 들어 우리 웹사이트에 방문한 IP가 유니크하게 몇 개가 되는지 혹은 하루 종일 크롤링한 URL의 개수가 몇 개인지 우리 검색 엔진에서 검색된 유니크한 단어가 몇 개가 되는지 등등 엄청 크고 unique한 값을 계산할 때 아주 적절함.
- PFADD 커맨드로 데이터를 저장하고 PFCOUNT 커맨드로 유니크하게 저장된 값을 조회할 수 있음.
- 만약 내가 일별로 데이터를 저장했는데 일주일 치를 취합해서 보고 싶다면 PFMERGE 커맨드로 key들을 머지해서 확인할 수 있음.
Messaging
- Lists
- 레디스의 List는 메시지 큐로 사용하기 적절함.
- 특히 자체적으로 blocking 기능을 제공하기 때문에 이를 적절히 사용하면 불필요한 polling을 막을 수 있음.
- 만약 Client A가 BRPOP 커맨드를 통해 myqueue에서 데이터를 꺼내오려 하는데 현재 리스트 안에 데이터가 없어 대기를 하고 있는 상황이라면, Client B가 hi라는 값을 넣어주면 client A에서 바로 이 값을 확인할 수 있음.
- LPUSHX나 RPUSHX 같은 커맨드를 사용하면 키가 있을 때에만 그 리스트에 데이터를 추가하는데 이것도 잘 사용하면 굉장히 유용한 기능임.
- key가 이미 있다는 건 예전에 사용했던 큐라는 거고 사용했던 queue에만 메시지를 넣어줄 수 있기 때문에 비효율적인 데이터의 이동을 막을 수 있음.
- 인스타그램, 페이스북이나 트위터같은 SNS에는 각 유저별로 타임라인이 존재하고 그 타임라인에 내가 팔로우한 사람들의 데이터가 뜨는데 이게 RPUSHX를 활용한 것임.
- 트위터에서는 각 유저의 타임라인에 보일 트윗을 캐싱하기 위해 레디스의 리스트를 사용하는데 이때 RPUSHX 커맨드를 사용함.
- 이를 이용해서 트위터를 자주 이용하던 유저의 타임라인에만 새로운 데이터를 미리 캐시해 놓을 수 있으며 자주 사용하지 않는 유저는 caching key 자체가 존재하지 않기 때문에 그런 유저들을 위해 데이터를 미리 쌓아놓는 것과 같은 비효율적인 작업을 방지할 수 있게 됨.
- Streams
- Streams은 로그를 저장하기 가장 적절한 자료구조라고 볼 수 있음.
- 실제 서버에 로그가 쌓이는 것처럼 모든 데이터는 append - only 방식으로 저장되며 중간에 데이터가 바뀌지 않음.
- XADD 커맨드를 이용해 키에 데이터를 저장함.
- 키 이름 옆에 *은 ID를 의미하며 ID를 직접 저장할 수도 있지만 일반적으로는 이렇게 *로 입력하면 레디스가 알아서 저장하고 ID 값을 반환시켜줌.
- 이 반환되는 ID 값은 데이터가 저장된 시간을 의미함.
- 이 값 뒤로는 해시처럼 key-value 쌍으로 데이터가 저장됨.
- Streams의 데이터를 읽어오는 방법은 다양한데 ID 값을 이용해 시간 대역대로 저장된 값을 검색할 수도 있고 실제 서버에서 로그를 읽을 때 tail -f를 사용하는 것처럼 새로 들어오는 데이터만 리스닝할 수도 있음.
- 또 카프카처럼 소비자 그룹이라는 개념이 존재하기 때문에 원하는 소비자만 특정 데이터를 읽게 할 수도 있음.
- 레디스 공식 문서에서는 Streams을 메세징 브로커가 필요할 때 카프카를 대체해서 간단하게 사용할 수 있는 자료구조라고 소개하고 있음.
Redis에서 데이터를 영구 저장하려면? (RDB vs AOF)
Redis의 치명적인 단점
- 레디스는 인메모리 데이터 스토어임.
- 따라서 모든 데이터가 메모리에 저장되어 있기 때문에 서버나 레디스 인스턴스가 재시작되면 모든 데이터가 유실됨.
- 복제 구조를 사용하고 있더라도 데이터 유실에 있어 안전하다고 볼 수는 없음.
- 코드상에 버그가 있었거나, 혹는 사람의 실수로 데이터를 날린 경우에는 바로 복제본에도 똑같이 적용되기 때문에 이런 경우에는 데이터를 복원할 수 없게 됨.
- 따라서 레디스를 cahce 이외의 용도로 사용한다면 적절한 데이터의 백업이 필요함.
AOF
- AOF는 Append Only File의 약자로 데이터를 변경하는 커맨드가 들어오면 커맨드를 그대로 모두 저장함.
- AOF 파일은 Append Only하게 동작하기 때문에 데이터가 추가되기만 해서 대부분 RDB 파일보다 커지게 됨.
- 따라서 AOF 파일은 주기적으로 압축해서 재작성되는 과정을 거쳐야 함.
- AOF 파일은 레디스 프로토콜 형태로 저장됨.
- AOF는 파일의 크기를 기준으로 해서 압축되는 시점을 지정할 수 있음.
RDB
- RDB는 스냅샷 방식으로 동작하기 때문에 저장 당시의 메모리에 있는 데이터 그대로를 사진 찍듯 찍어서 파일로 저장함.
- RDB는 바이너리 파일 형태로 저장됨.
- RDB의 경우, 시간 단위로 파일을 저장할 수 있음.
AOF, RDB 공통
- RDB와 AOF 파일 모두 커맨드를 이용해 직접 파일을 생성할 수 있으며 원하는 시점에 파일이 자동 생성되도록 설정할 수도 있음.
AOF, RDB 백업
- 만약 레디스를 cache로만 사용한다면 굳이 이 기능을 사용할 필요가 없음.
- 우리는 백업이 필요한데 어느 정도 데이터 손실은 감수할 수 있어 하시는 분들은 RDB만 사용, 대신에 redis.conf에 'SAVE' 옵션을 적절히 사용해야 함.
- 예를 들어 SAVE 900 1이라고 지정한다면, 900초 동안 한 개 이상의 키가 변경되었을 때, RDB 파일을 재작성하라는 의미임.
- 만약 장애 상황 직전까지의 모든 데이터가 보장되어야 할 경우라면 AOF를 사용하면 됨.
- 이 때 APPENDFSYNC 옵션이 기본값인 에브리세컨드인 경우에는 최대 1초 사이의 데이터는 유실될 가능성이 있음.
- 마지막으로 가장 강력한 내구성이 필요한 경우 RDB, AOF를 동시에 사용하라고 레디스 공식 문서에 가이드 되어 있음.
Redis 아키텍처 선택 노하우 (Replication vs Sentinel vs Cluster)
- 간단히 말하자면 Redis의 아키텍처는 세 가지로 나눌 수 있음. Replication, Sentinel, Cluster
- 레디스의 구조에서 복제는 비동기식으로 동작함. 즉, 마스터에서 복제본에 데이터가 잘 전달됐는지 매번 확인하고 기다리진 않음.
Replication
- 리플리케이션 구성, 즉 복제 구성은 마스터와 리플리카 이렇게만 존재하는 간단한 구조임.
- 리플리케이션 구성은 단순히 복제만 연결된 상태를 말함.
- 이 구조는 HA(High Availability) 기능이 없기 때문에 마스터에 장애가 발생하면 수동으로 변경해 줘야 할 작업들이 많음.
- 우선적으로 리플리카 노드에 직접 접속해서 복제를 끊어야 하고 애플리케이션에서도 연결 설정을 변경해서 배포하는 작업이 필요함.
Sentinel
- 센티널 구성에서는 마스터와 리플리카 노드 외에 추가로 센티널 노드가 필요함.
- 센티널은 일반 노드들을 모니터링하는 역할을 함.
- 센티널 노드는 일반적인 다른 노드를 계속 모니터링하는 역할을 담당함.
- 그러다 마스터가 죽으면 자동으로 페일오버를 발생시켜 기존의 리플리카 노드가 마스터가 됨.
- 이때 애플리케이션에서는 연결 정보를 변경할 필요가 없음.
- 애플리케이션에서는 센티널 노드만 알고 있으면 되고 센티널이 변경된 마스터 정보로 바로 연결시켜줌.
- 이 구조를 사용하려면 센티널 프로세스를 추가로 띄워야 하는데 센티널은 항상 세대 이상의 홀수로 존재해야 함.
- 보통 두 대의 서버에는 일반 레디스와 센티널을 함께 띄우고 최저 사양의 다른 서버에는 센티널 노드만 올려 사용함.
Cluster
- 클러스터 구성에서는 최소 세대의 마스터가 필요하며 샤딩 기능을 제공함.
- 클러스터 구성에서는 데이터가 여러 마스터 노드에 자동으로 분할되어 저장됨.
- 이 구성에서는 모든 노드가 서로를 감시하고 있다가 마스터가 비정상 상태일 때 자동으로 페일오버를 진행함.
- 최소 세대 이상의 마스터 노드가 필요하며 리플리카 노드를 하나씩 추가하는 게 일반적인 구조임.
Replication vs Sentinel vs Cluster 선택 팁
- HA기능이 필요할까? 즉, 자동으로 페일오버가 되어야 할까?
- Yes
- 우리 서비스에 샤딩 기능이 꼭 필요할까?
- Yse
- 만약 서비스의 확장을 위해 scale out이 필요하다면 클러스터 구조
- No
- HA만 필요할 것 같으면 센티널
- Yse
- 우리 서비스에 샤딩 기능이 꼭 필요할까?
- No
- 복제 기능은 필요할까?
- Yes
- 복제가 필요하다면 리플리케이션 구조
- No
- 필요 없다면 마스터 하나를 띄운 스탠드 얼론 구조
- Yes
- 복제 기능은 필요할까?
- Yes
정리 AND 팁
- 레디스는 싱글 스레드로 동작함. 따라서 한 사용자가 오래 걸리는 커맨드를 실행한다면 나머지 모든 요청들은 수행할 수 없고 대기하게 됨. 이로 인해 장애도 빈번하게 발생함.
- keys 는 모든 키를 보여주는 커맨드인데 개발할 때 자주 사용하다가 운영 환경에서 실수로 손이 먼저 나가는 경우가 있을 수 있음. 그래서 저는 그냥 아예 사용하지 않는 것을 권장 드림. 이 커맨드는 scan으로 대체가 가능. scan을 사용하면 재귀적으로 key들을 호출할 수 있음.
- hash나 sorted set 같은 자료구조는 내부에 여러 개의 아이템을 저장할 수 있는데 키 내부에 아이템이 많아질수록 성능이 저하되게 됨. 만약 좋은 성능을 원한다면 하나의 키에 최대 100만 개 이상은 저장하지 않도록 키를 적절하게 나누는 것이 좋음.
- key에 많은 데이터가 들어있을 때 del 로 데이터를 지우면 그 key를 지우는 동안 아무런 동작을 할 수 없음. 이때 unlink 커맨드를 사용한다면 key를 백그라운드로 지워주기 때문에 이것을 사용하는 걸 권장함.
- STOP-WRITES-ON-BGSAVE-ERROR 옵션은 기본으로 YES로 되어 있는데, 이 의미는 RDB 파일이 정상적으로 저장되지 않았을 때 레디스로 들어오는 모든 write를 차단한다는 기능임.만약 레디스 서버에 대한 모니터링을 적절히 하고 있다면 이 기능을 꺼두는 게 오히려 불필요한 장애를 막을 수 있는 방법임.
- MAXMEMORY-POLICY = ALLKEYS-LRU 옵션은 메모리가 한정되어 있기 때문에 MAXMEMORY-POLICY 값을 설정하지 않는다면 금세 레디스의 MAXMEMORY까지 데이터가 가득 차게 됨. 데이터가 가득 찼을 때는 MAXMEMORY-POLICY 정책에 의해 데이터가 삭제됨. 기본 값은 noeviction인데 이건 메모리가 가득 차면 더 이상 새로운 키를 저장하지 않는다는 것을 의미함. 즉, 메모리가 가득 찼을 때 레디스에 새로운 데이터를 입력하는 게 불가능하기 때문에 이는 장애로 발생할 수 있음. volatile lru는 가장 최근에 사용하지 않았던 key부터 삭제한다는 것을 의미하는데 이때 expire 설정이 있는 key값만 삭제한다는 것을 의미함. 만약에 메모리에 expire 설정값이 없는 key만 남아 있다면 이 설정에서 위와 똑같이 장애 상황이 발생할 수도 있음. 그래서 추천 설정은 ALLKEYS-LRU 임. 이 값은 모든 key에 대해 lru 방식으로 key를 삭제하겠다는 것을 의미하고 이 설정에서는 적어도 데이터가 가득 참으로 인해 장애가 발생할 가능성은 없음. 하지만 대규모의 트래픽 환경에서 TTL 값을 너무 작게 설정한 경우 cache stampede 현상이 발생할 가능성이 존재함.
결론
- node와 redis는 매우 잘어울릴것 같다고 생각한다.
- redis를 그냥 사용하기보다 핵심적인 동작원리를 알고 사용하게 될 수 있어서 이번 정리가 맘에 든다.
'데일리' 카테고리의 다른 글
[2023.11.24] 내가 놓치고 있던 부분을 발견하여 정리2 (PM2, NGINX, gzip) (2) | 2023.11.24 |
---|---|
[2023.11.24] 내가 놓치고 있던 부분을 발견하여 정리1 (cookie, session, JWT) (2) | 2023.11.24 |
[23.11.04~05] 주말동안 프로그래머스 MYSQL 코테 전 문제 풀고 난 후기 (2) | 2023.11.05 |
[23.11.04] LEFT JOIN과 WHERE IS NULL을 활용한 그룹 별 최상단행 뽑기 (2) | 2023.11.04 |
[23.10.31] 테스트 자동화 nestjs (66) | 2023.10.31 |