이번 글은 [우아한테크세미나] 191121 우아한레디스 by 강대명님 영상을 정리하였습니다.
함께보면 좋은 영상 [NHN FORWARD 2021] Redis 야무지게 사용하기
(기본적으로 Redis 는 Snapshot이 적용되어 dump.rdb 파일이 생성된다. 참고 Redis)
목차
- Redis 소개
- 왜 Collection 이 중요한가?
- Redis Collections
- Redis 운영
- Redis 데이터 분산
- Redis Failover
- 발표에서 다루지 않는 것
- Redis Persistence (RDB, AOF) -> 메모리에 있는 모든 내용 백업하는기능인데 이렇게 디스크에 저장한것을 서비스에 실시간으로 쓸 수 없기에 제외함
- Redis Pub / Sub ->
- Redis Stream -> Redis 버전 5부터 들어온 것
- 확률적 자료구조 - Hyperloglog ->
- Redis Module ->
Redis 소개
- In-Memory Data Structure Store
- Open Source ( BSD 3 License -> Redis 쓴고 있다는 것을 밝힌다면 , 코드를 고쳐써도 상관없음 , 그러나 Redis Module 은 License 가 조금 다르다 Redis Module 은 수정한다면 코드를 전부 공개해야 함 )
- Support data structures - String , Set , Sorted-Set , Hashes ,List / Hyperloglog , Bitmap , Geopatial Index / Stream
- Only 1 Committer -> Redis 소스 코드를 고쳐서 커밋할 수 있는 사람은 전세계 단 한명이다.
Redis 소개 전에 Cashe 먼저 ~~
캐시를 왜 쓰는가 ?
- Cache 는 나중에 요청올 결과를 미리 저장해두었다가 빠르게 서비스를 해주는 것을 의미
예를 들면 dynamic 프로그램의 핵심이 앞의 연산을 미리 저장해 놓고 다음번에 똑같은 연산을 하지말자가 핵심이다.
-> Factorial 를 예로 들면
10! = 10 * 9! = 10 * 9 * 8! = 10 * 9 * 8 * 7!
19! = 19 * 18 * 17 * 16 * 15 * 14 * 13 * 12 * 11 * 10!
팩토리얼 숫자가 크다면 ?
-> 20880! , 20881! 를 매번 계산해야 한다면?
---> 20880! 를 계산해 두고 어딘가에 저장해 뒀다면 20881! 계산은 어려운것이 아니다.
용량은 위로갈수록 커지고 속도는 밑으로 갈수록 빨라진다. 즉 용량과 속도는 반비례한다. 보통 우리가 쓰는 Registry 는 Memory 에 있다. 우리가 SSD 를 쓰고있지만 Disk 의 접근 속도가 Memory 의 접근속도에 비해 큰 차이가 나기때문에 Memory 에 올려놓고 쓰는것이 Disk 에 올려놓고 쓰는것보다 빠르다. 대신에 용량은 Disk 가 많다.
일반적인 웹서비스의 경우
- Client 요청 -> Web Server -> DB
순으로 접근을 하게된다. DB 의 경우 내부적으로 캐시를 사용하기는 하지만 요청을 많이하다보면 결국 캐시를 날리고 디스크에서 다시 읽어와야한다.
파레토의 법칙처럼 일반적으로 전체요청의 80 % 는 20 % 사용자에 의해 처리된다.
그렇기때문에 서비스를 할 때 사용하는 20%를 캐싱한다면 효율을 극대화 할 수 있다.
Cache 구조
- look aside cache (Lazy Loading) - 일반적으로 많이쓰는 방법
- Cache에 Data 존재 유무 확인
- Data가 있다면 cache의 Data 사용
- Data가 없다면 cache의 실제 DB Data 사용
- DB에서 가져온 Data를 Cache에 저장
- write back - 쓰기가 굉장히 빈번한 경우(batch 작업)
- Data를 Cache에 저장
- Cache에 있는 Data를 일정 기간동안 Check
- 모여있는 Data를 DB에 저장
- Cache에 있는 Data 삭제
write back 의 단점은 처음에 cache 즉 메모리에 저장하기에 장애가 생기면 데이터가 사라질 가능성이 있다.
그러나 보통 write back 을 많이 쓰는 이유는 write 가 헤비하거나 비중이 큰경우 많이 쓴다.
Collection 이 중요한가?
- 개발의 편이성 ↑
- 개발의 난이도 ↓
개발의 편의성이 왜 편해지는지 살펴보자.
먼저 랭킹서버를 구현해야한다고 가정하자. 만약 DB 에 랭킹을 저장하고 Order By 로 정렬해 가져온다했을때, 사용자가 많아져 개수가 많아지면 결국은 디스크에서 읽어오기에 속도에 문제가 생길 수 있다. 그렇기에 In-Memory 기준으로 개발이 필요하게된다. 이때 Redis 의 Sorted-Set 을 사용하면 편하게 구현할 수 있다. 덤으로 Replication도 가능하다.
두번째 예시로 친구-리스트를 관리한다고 가정하자. 친구-리스트를 kye/value 형태로 저장해야한다고 한다면? 만약 두개의 친구를 동시에 저장한다면, 이때 DB 는 ACID 해야한다. 그런데 여기서 동시성 문제가 발생할 수 있다. 그런데 Redis 의 경우 자료구조가 Atomic 하기에 동시성 문제가 발생할 가능성은 낮다.
-> 외부의 컬렉션을 잘 이용하는 것으로 비즈니스 로직에 집중할 수 있고, 개발 시간을 단축시킬 수 있다.
그 외 Redis 사용처
- Remote Data Store - A서버 , B서버 , C서버에서 데이터를 공유하고 싶을때
- 한대에서만 필요하다면, 전역 변수를 쓰면되지 않을까? - Redis 자체가 Atomic 을 보장해준다.(싱글 쓰레드이기에)
- 주로 많이 쓰는 곳 - 인증 토큰 등을 저장(String 또는 hash) , Ranking 보드로 사용 (Sorted-Set) , 유저 API Limit , 잡 큐(List)
Redis Collection
- Strings - key/value 로 저장
- List
- Set
- Sorted-Set
- Hash
Strings - 단일 Key
- 기본사용법 - Set<key><value> , Get<key>
Strings - 멀티 Key
- 기본사용법 - mset<key1><value1> <key2><value2> ... , mget<key1><key2> ...
Strings - 사용예
DB 에 사용하는 간단한 SQL 문을 Redis 로 바꿔보면
- Inset into users(name, email) values('kim','kim@naver.com');
Redis 에서는 아래와 같이 두가지 방법이 있다.
- Set name:kim , Set email:kim@naver.com
- mset name:kim email:kim@naver.com
List : inset
- 기본 사용법 :
- Lpush<key><A> -> 데이터는 Key:(A)
- Rpush<key><B> -> 데이터는 Key:(A,B)
- Lpush<key><C> -> 데이터는 Key:(C,A,B)
- Rpush<key><D,A> -> 데이터는 Key:(C,A,B,D,A)
===> 보통 잡 큐로 많이씀
List: pop
- 기본 사용법 :
- Key: (C , A , B , D , A)
- Lpop <key> -> Pop C, key : (A,B,D,A)
- Rpop <key> -> Pop A, key : (A,B,D)
- Rpop <key> -> Pop D, key : (A,B)
List: lpop , blpop , rpop , brpop
- 기본 사용법 :
- Key : ()
LPOP <key> -> No Data
BLPOP <key> -> 누군가 Data 를 Push 하기 전까지 대기
Set : 데이터가 있는지 없는지만 체크하는 용도
- 기본 사용법 :
- SADD <key> <value>
--> value 가 이미 key 에 있으면 추가되지 않는다.
- SMEMBERS <key>
--> 모든 value 를 돌려줌 ( 데이터가 많을수록 모든것을 가져오는 것은 주의하자 )
- SISMEMBER <key> <value>
--> value 가 존재하면 1 , 없으면 0
- 특정 유저를 Follow 하는 목록(유니크해야하는)을 저장할때 많이 사용함
Sorted Set : 랭킹에 따라서 순서가 바뀌길 바란다면
- ZADD <key> <Score> <value>
--> value 가 이미 key 에 있으면 해당 score 로 변경된다.
----> score 값으로 오름차순으로 정렬된다.
- ZRANGE <key> <StartIndex> <EndIndex>
--> 해당 Index 범위 값을 모두 돌려줌
--> Zrange testkey -> startIndex : 0 , endIndex : - 1
----> 모든 범위를 가져옴.
--> Zrange testkey -> startIndex : 0 , endIndex : 2
----> 0 , 1 인덱스를 가져옴
- 유저 랭킹 보드로 사용할 수 있음
- Sorted Set 의 score 는 double 타입이기 때문에, 값이 정확하지 않을 수 있다.
--> 컴퓨터에서는 실수가 표현할 수 없는 정수값들이 존재하기에.
ex)
select * from rank order by score limit 50 , 20;
-> zrange rank 50 70
select * from rank order by score desc limit 50 , 20;
-> zrevrange rank 50 70
select * from rank where score >= 70 and scroe < 100;
-> zrangebyscore rank 70 100
select * from rank where score > 70;
-> zrangebyscore rank (70 +inf
Hash : Key 밑에 sub key 가 존재
- 기본 사용법 :
- Hmset <key> <subkey1> <value1> <subkey2> <value2>
- Hgetall <key>
--> 해당 key의 모든 subkey 와 value 를 가져옴
- Hget <key> <subkey>
- Hmget <key> <subkey1> <subkey2> ... <subkeyN>
ex)
Insert into users(name, email) values('kim', 'kim@naver.com');
-> hmset kim name kim email kim@naver.com
Collection 주의 사항
- 하나의 컬렉션에 너무 많은 아이템을 담으면 좋지 않다.
--> 10000개 이하 몇천개 수준으로 유지하는 것이 좋다.
- Expire 는 Collection 의 item 개별로 걸리지 않고, 전체 Collection 에 대해서만 걸린다.
--> 즉 해당 10000개의 아이템을 가진 Collection 에 expire 가 걸려있다면 그 시간 후에 10000 개의 아이템이 모두 삭제됨
Redis 운영
- 메모리 관리를 잘하자
- O(N) 관련 명령어는 주의하자
- Replication
- 권장 설정 Tip
메모리 관리를 잘하자
- Redis 는 In-Memory Data Store 로 Physical Memory 이상을 사용하면 문제가 발생한다.
---> Swap 이 있다면 Swap 을 사용하게되고, Swap 사용(메모리 Page 를 디스크에 저장해놓고 필요시 loading 하고 처리하는 것으로 Swap 이 한번이라도 발생한 메모리 Page 는 계속 Swap 을 사용하게 되고 디스크를 읽고 쓰게된다.)으로 해당 메모리 Page 접근시마다 늦어짐 --> Redis 를 쓰는 이유가 사라짐.
----> Swap 이 없다면? 바로 Redis 가 죽을 수 있다.
- Maxmemory 를 설정하더라도 이보다 더 사용할 가능성이 큼.
- RSS 값을 모니터링 해야한다.
- 제일 큰 문제는 많은 서비스들이 현재 메모리를 사용해서 Swap 을 쓰고 있다는 것조차 모를때가 많다.
--> 즉 , Redis Memory 관리를 하는 곳은 별로없다.(해야하는데 놓치는 곳이 많음)
--> Redis 는 write 가 일어날때 fork 해서 쓰기에 최대 2배까지 쓰게될수도 있다. 때문에 작은 메모리를 여러개 사용하는것이 안정적이다.
--> Redis 는 메모리 파편화가 발생할 수 있다. 4.x 버전 부터 메모리 파편화를 줄이도록 jemlloc 에 힌트를 주는 기능이 들어갔으나, jemlloc 버전에 따라서 다르게 동작할 수 있다.
--> 3.x 버전의 경우 실제 used memory 는 2GB 로 보고가 되지만 11GB 의 RSS 를 사용하고 있는 곳이 많다.
--> 다양한 사이즈를 가지는 데이터 보다는 유사한 크기의 데이터를 가지는 경우가 메모리 파편화를 덜 일어나게한다.
메모리가 부족할 때는?
- Cache is Cash!!
--> 좀 더 메모리 많은 장비로 Migration. 하지만 이또한 메모리가 너무 빡빡하면 Migration 중에 문제가 발생할 수도 있다. 때문에 75% 정도부터는 더 큰 메모리 장비로 Migration 할 것을 고려하자.
- 있는 데이터 줄이기
--> 데이터를 일정 수준에서만 사용하도록 특정 데이터를 줄인다.
--> 다만 이미 Swap 을 사용중이라면, 프로세스를 재시작 해야한다.
- 기본적으로 Collection 들은 다음과 같은 자료구조를 사용한다.
-> Hash - HashTable 를 하나 더 사용
-> Sorted Set - Skiplist 와 Hash Table 를 사용
-> Set - HashTable 사용
===> 해당 자료구조들은 메모리를 많이 사용함.
======> Ziplist 를 이용하자.
Ziplist 구조
- In-Memory 특성 상, 적은 개수라면 선형 탐색을 하더라도 빠르다. ziplist 는 이러한 점을 이용해 데이터를 선형으로 저장해버린다.(ziplist 를 사용시 약 30% 속도를 높일 수 있으나 메모리를 많이 잡아먹을 수도 있다.)
- List , hash , sorted-set 등을 ziplist 로 대체해서 처리를 하는 설정이 존재한다.
--> hash-max-ziplist-entries , hash-max-ziplist-value
--> list-max-ziplist-size , list-max-ziplist-value
--> set-max-ziplist-entries , zset-max-ziplist-value
O(N) 관련 명령어는 주의하자
- Redis 는 Single Threaded 이다.
--> 그러면 Redis 가 동시에 여러개의 명령을 처리할 수 있을까? 아니다 한번에 한개밖에 안된다. 그러나 단순한 get/set 의 경우 , 초당 10만 TPS 이상 가능하다.(CPU 속도에 영향을 받는다.) 그렇다하더라도 한개의 처리가 1초가 걸리면 나머지는 1초를 기다려야한다. 그렇기에 긴 시간을 요하는 명령은 쓰면안된다.
다음의 명령은 되도록 쓰지말자( 대표적인 O(N) 명령들 )
- KEYS
- FLUSHALL , FLUSHDB (필요시에는 써도 되지만 되도록 쓰지말자)
- Delete Collections (item 이 몇백개있는 것은 괜찮지만 , 백만개 정도 있는 Collections 면 오래걸린다.)
- Get All Collections
----> Key 가 백만개 이상인데 확인을 위해 Keys 명령을 사용하는 경우
----> 모니터링 스크립트가 일초에 한번씩 keys를 호출하는 경우
-----> 아이템이 몇만개 든 hash , sorted set , set 에서 모든 데이터를 가져오는 경우
-----> 예전의 Spring security oauth Redis Token Store(최신버전은 해결됨)
Keys 는 어떻게 대체할 것인가?
- scan 명령을 사용하는 것으로 하나의 긴 명령을 짧은 여러번의 명령으로 바꿀 수 있다.
Collection 의 모든 item 을 가져와야 할 때?
- Collection 의 일부만 가져온다. --> Sorted Set 에 있는 기능 사용.
- 큰 Collection 을 작은 여러개의 Collection 으로 나눠서 저장한다.
--> Userranks 를 Userranks1 , Userranks2 , Userranks3 으로 나눠 저장
---> 하나당 몇천개 안쪽으로 저장하는게 좋음.
Spring Security Oauth RedisTokenStore(구버전) 이슈
- Access Token 의 저장을 List(O(N)) 자료구조를 통해서 이루어짐
--> 검색 , 삭제시에 모든 item 을 매번 찾아봐야 함.
-----> 100만개쯤 되면 전체 성능에 영향을 줌
--> 현재는 Set(O(1)) 을 이용해서 검색 / 삭제를 하도록 수정되었다.(현재버전은 해결됨)
Redis Replication
- Async Replication --> Replication Lag 가 발생할 수 있다.
- 'Replicaof' (>=5.0.0) or 'slaveof' 명령으로 설정 가능 --> Replicaof hostname port
- DBMS 로 보면 statement replication 이랑 유사하다.
Redis Replication 설정 과정
- Secondary 에 'replicaof' Or 'slaveof' 명령을 전달
- Secondary 에 Primary 에 sync 명령 전달
- Primary 는 현재 메모리 상태를 저장하기 위해 FORK 한다.(FORK 를 하기에 disk 에 dump 하고 메모리많이쓰게됨)
- FORK 한 프로세서는 현재 메모리 정보를 disk 에 dump (disk-less-replication 도 있다)
- 해당 정보를 Secondary 에 전달
- FORK 이휴의 데이터를 Secondary 에 계속 전달
Redis Replication 시 주의할 점
- Replication 과정에서 fork 가 발생하므로 메모리 부족이 발생할 수 있다.
- Redis-cli--rdb 명령은 현재 상태의 메모리 스냅샷을 가져오므로 같은 문제를 발생시킨다.
- AWS 나 클라우드의 Redis 는 좀 다르게 구현되어서 좀 더 해당 부분이 안정적이다. (Fork 없이 Replication 데이터를 전달하는 기능을 가진 것들이 있다. 그러나 느릴수 있다.)
- 많은 대수의 Redis 서버가 Replica 를 두고 있다면 네트웍 이슈나, 사람의 작업으로 동시에 replication 이 재시도 되도록 하면 문제가 발생할 수 있다. ex) 같은 네트웍안에서 30GB 를 쓰는 Redis Master 100대 정도가 레플리케이션을 동시에 재시작하면 어떤 일이 벌어질 수 있을까? -> 네트웍이 마비가될 수 있기에 한대씩 수동으로 해줘야 할 수도 있다.
권장 설정 Tip ( redis.conf )
- Maxclient 설정 50000 (값을 많이 높이자)
- RDB/AOF 설정 off (성능상 유리하고 안정성도 높다 -> main 은 무조건 끄고 , 필요시 replica 정도에만 킨다.)
- 특정 commands disable
---> Keys (disable 시키자)
-----> AWS 의 ElasticCache 는 이미 하고 있음
- 전체 장애의 90% 이상이 Keys 와 SAVE 설정을 사용해서 발생한다.
- 적절한 ziplist 설정
Redis Data 분산
- 데이터의 특성에 따라서 선택할 수 있는 방법이 달라진다.
--> 데이터 특성이 Cache 일때는 우아한 Redis
--> 데이터 특성이 Persistent 해야하면 안 우아한 Redis --> Open the hellgate --> Data 분산을 해야함
데이터 분산 방법
- Application
--> Consistent Hashing --> twemproxy 를 사용하는 방법으로 쉽게 사용가능
--> Sharding
- Redis Cluster
Consistent Hashing
- Consistent Hashing 방법으로 데이터를 적절한 서버에 분산하기에 한쪽의 서버장애시에도 Rebalancing 이 적게 일어난다.
- ex) 서버에 특정 해싱값을 할당하고 key 값을 해싱해 값이 나오면 그 값보다 큰 제일 근접한 값을 가진 서버에 저장 -> 장애 발생시에도 해당서버에 포함된 데이터만 Rebalancing 하면 되기에, 전체적인 Rebalancing 이 적게 일어남
Sharding
- 데이터를 어떻게 나눌것인가? == 데이터를 어떻게 찾을것인가?
- 하나의 데이터를 모든 서버에서 찾아야 하면?
===> 때문에 상황마다 Sharding 전략이 달라진다.
Range
- 그냥 특정 Range 를 정의하고 해당 Range 에 속하면 거기에 저장.
- 예를 들어 서버1(1~1000) , 서버2(1001~2000) 이 존재한다고 하면 Key500 은 서버1에 저장된다. -> 장점은 Range 범위가 필요하면 확장하면되서 확장이 쉽지만, 단점은 서버에 할당되는 값들이 한쪽으로 몰리는 현상이 일어날 수 있다.
Redis Cluster
- 장점
-> 자체적인 Primary , Secondary 의 Failover
-> Slot 단위의 데이터 관리
- 단점
-> 메모리 사용량이 더 많음
-> Migration 자체는 관리자가 시점을 결정해야 함
-> Library 구현이 필요함
Redis Failover
- Coordinator 기반 Failover
- VIP / DNS 기반 Failover
- Redis Cluster 의 사용
Monitoring Factor
- Redis Info 를 통한 정보
--> RSS (가장 최우선 모니터링 해야한다. - Os 가 보는 쓰고있는 Memory)
--> Used Memory (Redis 가 생각하는 쓰고있는 Memory
--> Connection 수 (맺고 끊음이 잦으면 문제가 있는것이다.)
--> 초당 처리 요청 수 (CPU 의 영향을 받는다.)
- System
--> CPU
--> Disk
--> Network rx / fx
===> Cache 로 쓸때는 죽더라도 Cache 에러이다. Redis 가 죽어 Cache 성능이 떨어져 DB 로 부하가 얼마나 가느냐, DB가 못버틸 정도냐에 달려있어서 큰 문제는 되지 않는다.
===> Persistent Store 로 쓰면 지워지면 안되는 데이터이기에 사실 답이 별로없다. 최대한 넉넉하게 서버를 잡고, 돈을 넉넉하게 투자해야하는게 좋다.
때문에 되도록 Cache 로 사용하는것이 좋은것 같아보인다.