파레토의 법칙(Pareto principle, law of the vital few)
- 전체 결과의 80%가 전체 원인의 20%에서 일어나는 현상
- 이 법칙을 서버에 적용하면,
- 전체 요청의 80%는 20%의 사용자로부터 나온다는 의미로 볼 수 있고
- 혹은 전체 부하의 80%는 특정 20%의 요청에서 나온다는 의미이기도 하다.
- 이미 요청한 데이터를 다시 요청하는 경우가 많다는 의미이다.
- 그러니 캐싱을 활용하여 read 성능을 향상시켜보자!
캐시를 사용하기 적절한 데이터인지 판단하는 기준
- 데이터가 변경에 민감한지?
- 데이터의 연산에 드는 비용이 비싼지?
- 데이터의 변경이 전파가 되는지?
- 요약하자면, “잘 바뀌지 않으면서 접근할 일이 많은 데이터, 변경되더라도 다른 서비스에 큰 영향을 미치지 않는 데이터” 가 캐시에 저장하여 활용하기 적절하다.
2가지 Caching 전략
Look aside Cache
- Cache 에 원하는 Data 가 있는지 유무 확인
- Data 가 존재한다면 해당 Data 를 바로 사용
- Data 가 없다면 실제 DB 에 접근
- 해당 Data 를 캐시에 저장
Cache 를 사용한다고 하면 대부분 이 개념으로 사용한다고 보면 된다
Write Back
- 쓰기 요청시 해당 Data 를 DB가 아닌 Cache 에 먼저 저장
- 일정량 혹은 일정 시간 이상 Cache 에 Data 를 모은 후 실제 DB에 한꺼번에 저장
- Cache 에 쌓였던 Data 삭제
이 전략은 Cache 를 조금 다른 용도로 사용하는 것 → 쓰기 Cache 로 사용
DB 에 쓰기작업의 횟수를 최소화하여 퍼포먼스의 향상을 가져오겠다는 뜻
극단적으로 heavy 한 write 가 있을 경우 사용
하지만 Cache 에 저장되는 만큼 데이터의 유실 가능성이 있지 않을까?
Redis ?
- key-value 구조의 비정형 데이터(NoSQL)를 저장하고 관리하기 위한 Open Source 기반의 DBMS(BSD 3)
- In-Memory 데이터 저장소
- disk가 아닌 메모리를 데이터 저장소로 활용
- 지원하는 자료구조
- String, Set, Sorted-Set, Hashes, List
- Hyperloglog, bitmap, geospatial index
- Stream - 다양한 자료구조의 지원은 레디스의 큰 장점 중 하나
- 단 한명의 Committer → Redis 의 코드를 고칠 수 있는 사람은 단 한명
Redis Collections
- String → key : value 형태의 자료구조
- List
- Set → 중복된 값 X
- Sorted Set → 순서를 보장하는 Set
- Hash
String
- 기본 사용법
Set <key> <value>
Get <key> - mset, mget 한번에 여러개 처리
- Set Token:1234567 abcdefg
- Get Token:1234567
- Key 를 어떻게 잡을건지 고민해야한다.
- 보통 위의 예시처럼 key를 Token:~ 형식으로 prefix 를 많이 붙인다.
- 이를 통해 분산을 어떻게 할지 정할 수 있다.
List
- Lpush <key> <A>
- Rpush <key> <B>
- Lpop, Rpop <key>
- BLpop → 데이터가 push 되기까지 대기
- JobQueue 의 경우 List 를 많이 쓴다.
Set
- SADD <key> <value>
value 가 이미 해당 Key 에 있으면 추가되지 않는다. - SMEMBERS <key>
해당 key의 모든 value를 돌려줌 - SISMEMBER <key> <value>
value가 존재하면 1, 아니면 0
Sorted Set
- ZADD <key> <score> <member>
member 가 key, score 가 value 의 역할을 한다고 생각하면 된다.
score 값을 기준으로 각 원소들이 순서를 가지게 된다. - Set 이기 때문에 member 는 unique 하고, member 값을 통해 O(1) 로 원하는 원소에 바로 접근할 수 있다.
- Sorted Set 의 score는 실수형(double)임에 유의하자.
- ZSCORE <key> <member>
해당 member 의 score 조회 - ZRANK <key> <member>
해당 member 의 rank(몇 번째인지) 조회 - ZRANGE <key> <start> <stop>
정렬된 원소 중 내가 원하는 범위만큼만 출력한다.
첫번째 원소를 0이라 했을 때의 상대적인 위치값이고, 양수/음수 모두 가능하다. - ZRANGE <key> 0 -1
해당 key의 모든 member 출력 - 0 0 → 가장 낮은 멤버, 1 1 → 가장 높은 멤버
- 순서를 반대로 하고싶은 경우 ZREVRANGE 사용
- 만약 member의 score도 함께 출력하고 싶다면,
WITHSCORES
옵션을 추가하면 된다.
Hash
- Key 밑에 또다른 Key-Value set이 존재하는 자료구조
- Key 하나에 여러개의 field와 value로 구성되는 것
- RDB의 table과 비슷하다.
- key는 table의 PK, field는 column, value는 value로 보면 된다.
- 해당 key에 어떤 Collection 자체를 저장한다고 생각해도 된다.
- HMSET <key> <subkey1> <value1> <subkey2> …
- HGETALL <key>
해당 key의 모든 subkey 와 value 를 가져옴 - HGET <key> <subkey>
Redis에서 Collection 사용 시 주의사항
- 하나의 컬렉션에 너무 많은 아이템을 담지 말자.
- 10000개 이하로 유지하는게 좋다.
- Expire는 Collection의 item 개별로 걸리지 않고 전체 Collection에 대해서 걸린다.
- 10000개의 아이템을 가진 컬렉션에 expire가 걸려있다면 해당 시간후에 모든 아이템이 삭제됨
Redis 운영 시 주의사항
메모리 관리
- Redis가 메모리를 많이 사용하게되면 심각한 문제가 발생할 수 있다.
- Redis는 In-memory Data Store.
- 물리적 메모리 그 이상을 사용하게 되면 문제가 발생한다.
Swap이 한번이라도 발생한 메모리 페이지는 계속 Swap이 일어난다.
→ 심각한 성능 저하가 발생 - Maxmemory를 설정하더라도 이보다 더 사용할 가능성이 크다.
Maxmemory란 일정 메모리이상은 사용하지 말라고 설정하는 것인데,
해당 메모리보다 더 써야할 것 같은 상황이 오면 이미 존재하는 키중 몇개를 지우거나, expire 목록중에서 몇개를 지워서 메모리를 확보 - Redis는 자기 자신이 정확히 얼마만큼의 메모리를 사용하는지 모른다.
- Redis는 메모리 파편화가 발생할 가능성이 있다. 4 버전 이상부터는 파편화를 줄이도록 jemalloc에 힌트를 주는 기능이 들어갔으나, 버전에 따라 다르게 동작할 가능성이 있다.
- 3 버전대의 경우 실제 used memory는 2GB로 보고 되지만, 11GB의 RSS를 사용하는 경우가 발생
- RSS 값을 항상 모니터링 해야한다.
- 메모리가 부족할 때는?
좀 더 메모리가 많은 장비로 migration 하자.
하지만 현재 장비의 메모리가 빡빡한 경우에는 migration 중에 문제가 발생할 수도 있다. - 다음 Collection들은 내부 구현때문에 메모리를 많이 차지한다.
- Hash → Hash Table을 하나 더 사용
- Sorted Set → Skiplist와 Hash Table 둘 다 사용
- Set → Hash Table
- Ziplist 를 이용하도록 설정해보자.
Ziplist 구조
- In-memory 특성 상, 적은 개수라면 굳이 인덱스 탐색을 하지않고 선형 탐색을 하더라도 속도차이가 얼마 나지 않는다.
- 그래서 List, Hash, Sorted Set 등의 구현을 Ziplist로 대체할 수 있는 설정이 존재한다.
- 메모리 사용량이 20~30% 이상 차이가 난다.
- 개수가 많아지면 알아서 기존의 구현으로 돌아간다. → 속도를 유지하는 대신 메모리 사용량이 늘어날 것
O(N) 관련 명령어에 대한 주의
- Redis는 Single Threaded.
- 동시에 처리할 수 있는 명령의 개수가 하나라는 뜻
- 긴 시간을 요하는 명령을 실행한다면 뒤의 모든 명령은 대기하게 됨
- 대표적인 O(N) 명령등
- Keys → 모든 key를 순회
- FLUSHALL, FLUSHDB
- DELETE Collections → 적은 개수의 컬렉션이면 상관없지만 1만개가 넘어가면 1~2초 걸린다.
- GET ALL Collections → DELETE 와 마찬가지 - 위의 명령들은 되도록이면 피하자.
- 실수 사례
- Key가 백만개 이상인데 확인을 위해 Keys 명령을 사용하는 경우
- 아이템이 몇만개 들어있는 Hash, Sorted Set, Set 에서 모든 데이터를 가져오는 경우 - Key를 어떻게 대체하면 되나?
- scan 명령으로 하나의 큰 명령을 여러개의 작은 명령으로 쪼갤 수 있다.
- 쪼개진 작은 명령 사이에 몇만개의 get, set 명령이 수행될 수 있다. - Collection의 모든 아이템을 가져와야만 한다면?
- Collection의 일부만 가져오면 안되는건지 고민해보자.
- 큰 Collection을 작은 여러개의 Collection들로 나누어 저장하자.
- 하나당 몇천개 안쪽으로 유지
Redis 정리
- Redis 는 기본적으로 매우 강력하고 좋은 도구이다.
- 하지만 메모리를 빡빡하게 사용할 경우 안좋은 도구로 변한다.
- 메모리를 70–80% 이상 사용중이라면 장비 증설을 고려하자.
- migration 또한 주의해서 진행해야 한다.
Redis as Cache
- 특별한 경우가 아닌 이상 큰 문제가 발생할 여지는 적다.
- DB가 못버틸 정도의 부하가 아닌 이상 문제는 잘 없다. → Redis에 문제가 있다면 DB 부하를 살펴보자.
- Consistent Hashing 도 실제로 부하를 아주 균등하게 나누지는 않는다. → Adaptive Consistent Hashing 을 이용해 보자.
Redis as Persistent Store
- 반드시 Priamry/Secondary 구조로 구성하자.
- 데이터 유실을 최소화 해야한다.
- Persistent Store로 사용하는 경우 migration 이슈가 반드시 발생할텐데, 이 때 Primary/Secondary 구조가 아니면 힘들다.
- 정기적으로 Secondary로 백업이 필요하다. - 메모리를 절대 빡빡하게 사용하면 안된다.
- Persistent Store 니까 데이터 유실을 더욱더 최소화 해야한다.