redis를 그냥 띄우게 되면 데이터를 무한정 넣을 수 있다. 음? 인메모리 데이터 그리드에 데이터를 무한정 넣을 수 있다고? 그런 말도 안 되는 일이 어떻게 일어나는 걸까. 그 비밀은 바로 maxmemory라는 설정에 있다.
레디스는 maxmemory를 별도로 설정하지 않으면 메모리 제한을 걸지 않는다. 그렇다면 모든 메모리를 잡아먹게 되는 경우라면 어떨까? 그럴 경우에는 디스크의 스왑 영역까지 사용하게 되며, 이 때부터 레디스의 성능이 급격하게 저하된다. 떠올려보면 이러한 케이스가 몇 번 있었던 것 같다. 레디스를 단순히 적당한 수준에서의 캐싱이나 TTL을 걸어 토큰을 저장하는 경우에는 크게 문제 될 일이 없었다. 하지만 학교 공지사항을 스크래핑하는 크론잡을 만들어서 공지사항에 대한 내용을 전부 레디스에 꽂았을 때에 성능 저하와 레디스가 이상하게 작동했던 것 같다.
그렇다면 위와 같은 문제를 해결하기 위해서는 maxmemory를 설정해야하지 않겠는가? 그렇다면 메모리 설정이 뭔지, 어떻게 해야 하는지 더 나아가 왜 해야 하는지에 서술하여 포스팅을 마무하려고 한다.
본론
redis의 maxmemory
Redis를 이용해서 클러스터를 운영하다가 레디스가 죽는 것을 봤다면 대부분의 경우는 OOM(Out of Memory)일 것이다. 직접 호스트에 올려서 redis를 운영하는 경우에도 OOM_Killer라는 리눅스의 프로세스에 의해서 죽을 수도 있고, 쿠버네티스를 쓰게 된다면 limits에 걸린 메모리 제한으로 인해서 죽을 수도 있을 것이다.
이러한 불상사를 막기 위해서 레디스에서 메모리 설정이 있다. 레디스에서는 redis.conf를 통해서 다양한 설정을 해줄 수 있는데, 대표적인 것이 maxmemory와 memory-policy 부분이다.
메모리 제한을 걸 수 있으며, 제한에 걸린다면 어떤 알고리즘을 통해서 레디스의 메모리를 조절할 것인가가 된다. 그렇다면 먼저 redis.conf에 대해서 살펴보자. 관련된 스크립트는 아래의 깃허브에서 확인할 수 있다.
- [github] redis/redis.conf : https://github.com/redis/redis/blob/unstable/redis.conf
위 부분에서 maxmemory와 policy에 대한 부분을 미리 살펴보면 아래와 같다.
# Set a memory usage limit to the specified amount of bytes.
# When the memory limit is reached Redis will try to remove keys
# according to the eviction policy selected (see maxmemory-policy).
#
# If Redis can't remove keys according to the policy, or if the policy is
# set to 'noeviction', Redis will start to reply with errors to commands
# that would use more memory, like SET, LPUSH, and so on, and will continue
# to reply to read-only commands like GET.
#
# This option is usually useful when using Redis as an LRU or LFU cache, or to
# set a hard memory limit for an instance (using the 'noeviction' policy).
#
# WARNING: If you have replicas attached to an instance with maxmemory on,
# the size of the output buffers needed to feed the replicas are subtracted
# from the used memory count, so that network problems / resyncs will
# not trigger a loop where keys are evicted, and in turn the output
# buffer of replicas is full with DELs of keys evicted triggering the deletion
# of more keys, and so forth until the database is completely emptied.
#
# In short... if you have replicas attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for replica
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select one from the following behaviors:
#
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# Note: with any of the above policies, when there are no suitable keys for
# eviction, Redis will return an error on write operations that require
# more memory. These are usually commands that create new keys, add data or
# modify existing keys. A few examples are: SET, INCR, HSET, LPUSH, SUNIONSTORE,
# SORT (due to the STORE argument), and EXEC (if the transaction includes any
# command that requires memory).
#
# The default is:
#
# maxmemory-policy noeviction
아무것도 설정하지 않으면 maxmemory는 없고, maxmemory-policy는 noeviction으로 설정되며, 이는 한계를 넘어서는 데이터를 추가로 쓰지 않으며 모든 요청에 대해 error를 반환하는 정책이다.
자신만의 시나리오에 따라서 적절한 policy를 사용하는 것이 좋겠다. 하지만 maxmemory는 설정해 주는 것이 좋다. 이유 아래와 같은 레퍼런스에서 찾을 수 있었다.
- [Moss] Redis 서버 설정 관리 : https://moss.tistory.com/entry/Redis-서버-설정-정리#maxmemory-policy
위 레퍼런스에서 maxmemory에 대한 설명을 찾아보면 다음과 같은 설명이 나와있다.
주의: maxmoery 설정 된 instance에 slave가 존재할 때, slave에게 data를 제공하기 위해서 사용되는 output buffer의 size는 used memory count에서 제외된다. 왜냐하면 network 문제나 재동기화가 keys들이 제거된 loop에 대한 trigger를 발생시키지 않게 하기 위해서이다. loop가 발생하면 slave의 output buffer가 제거된 key들의 DEL 명령으로 가득 찰 것이다. 그리고 이것은 database가 완전히 빌 때까지 지속된다. (좀 더 정확히 알아볼 필요가 있다. 확실히 이해가 안 됨) 간단히 말하면, 만약 slave를 가진다면, slave output buffer를 위해서 system에 약간의 free RAM이 존재시키기 위해서, maxmoery에 약간 낮은 limit를 설정하는 것이 추천된다. (하지만 policy가 'noeviction'이면 필요하지 않다.)
레디스 클러스터 운영으로 인해 master와 slave가 분리되어 있는 상황에서는 output buffer를 위해서 시스템에 약간의 여유 메모리를 두는 것이 좋다는 내용이다. 레디스는 주로 하나의 레디스만을 사용하지 않고, 클러스터를 만들어 사용하거나 혹은 statefulset을 이용하여 여러 개의 레디스를 같은 볼륨을 물어 배포하는데, 이러한 점을 생각하면 진작에 maxmemory를 설정하는 것이 좋다.
다음으로는 정책에 대해서 살펴보자.
redis의 maxmemory-policy
그렇다면 MAXMEMORY POLICY는 어떠한 것들이 있을까? 아래와 같은 종류가 있으며 기본값은 noeviction이다.
- volatile-lru : expire가 설정된 키 들 중 LRU 알고리즘에 의해서 선택된 키를 제거한다.
- allkey-lru : 모든 키들 중 LRU 알고리즘에 의해서 선택된 키를 제거한다.
- volatile-lfu : expire가 설정된 키 들 중 LFU 알고리즘에 의해서 선택된 키를 제거한다.
- allkey-lfu : 모든 키들 중 LFU 알고리즘에 의해서 선택된 키를 제거한다.
- volatile-random : expire가 설정된 키들 중 랜덤으로 키를 제거한다.
- allkey-random : 모든 키들 중 랜덤으로 키를 제거한다.
- noeviction : 어떤 키도 제거하지 않는다. 쓰기 동작이 들어온 경우 error를 반환한다. ( 기본값 )
위와 같이 다양한 정책들이 있으며 시나리오에 따라서 맞는 동작을 사용하면 된다.
redis의 config 설정
그렇다면 위와 같은 정보에 대해서는 알았으니 어떻게 redis를 설정할 수 있을까? 일단 호스트에서 직접 띄우는 경우에는 redis.conf와 같은 파일을 수정한 후에 직접 redis-server 명령어로 띄우면 될 것이다.
이 외에도, 우리는 docker를 사용하는 경우나 쿠버네티스를 사용하는 경우로 나뉘어서 살펴보자.
docker를 사용하는 경우
docker를 사용하는 경우에 대한 레퍼런스는 아래와 같다. dockerfile을 이용하거나 docker run 명령어에 -v 파라미터로 볼륨을 물어서 컨테이너를 띄우는 방법이 있다.
- [dockerhub] redis : https://hub.docker.com/_/redis
다음과 같이 dockerfile을 작성할 수 있다.
FROM redis
COPY redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]
혹은 호스트에 설정된 redis.conf가 있다면 다음과 같이 실행할 수 있다.
$ docker run -v /myredis/conf:/usr/local/etc/redis \
--name myredis redis redis-server /usr/local/etc/redis/redis.conf
어느 쪽이던 정답은 없으니, docker를 사용하는 경우는 적절한 시나리오에 따라 선택하면 될 것이다.
Kubernetes를 사용하는 경우
쿠버네티스를 사용하는 경우에도 공식 레퍼런스를 통해 어떻게 config를 적용할 수 있는 지 확인할 수 있었다.
- [Kubernetes] 컨피그맵을 통해서 Redis 설정하기 : https://kubernetes.io/ko/docs/tutorials/configuration/configure-redis-using-configmap/
쿠버네티스 매니패스트만을 가져오자면 아래와 같이 ConfigMap과 Pod을 띄울 수 있겠다.
apiVersion: v1
kind: ConfigMap
metadata:
name: example-redis-config
data:
redis-config: |
maxmemory 2mb
maxmemory-policy allkeys-lru
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
containers:
- name: redis
image: redis:5.0.4
command:
- redis-server
- "/redis-master/redis.conf"
env:
- name: MASTER
value: "true"
ports:
- containerPort: 6379
resources:
limits:
cpu: "0.1"
volumeMounts:
- mountPath: /redis-master-data
name: data
- mountPath: /redis-master
name: config
volumes:
- name: data
emptyDir: {}
- name: config
configMap:
name: example-redis-config
items:
- key: redis-config
path: redis.conf
위와 같이 volumeMounts를 통해서 /redis-master에 config 파일을 마운트 한 후에, redis를 띄울 때, 해당 /redis-master/redis.conf와 함께 서버를 띄움으로써 다양한 설정을 적용할 수 있게 된다.
redis.conf를 설정하지 않는다면?
일반적으로 무리해서 redis를 사용하지 않는다면 레디스가 크게 문제될문제 될 일은 없을 것이다. 적당한 수준의 캐싱과 약간의 세션 저장 정도라면 말이다. 어떻게 짜느냐에 따라 다르지만 간단한 ID값 같이 짧은 문자열 데이터를 레디스에 저장하는 정도라면 거의 문제 될 일은 없다. 실제로 많은 답변이 달린 내용은 아니지만 스택오버플로우에서 다음과 같은 이야기를 볼 수 있었다.
- [stackoverflow] How much memory / CPU to allocate to Redis instance? : https://stackoverflow.com/questions/65727287/how-much-memory-cpu-to-allocate-to-redis-instance
편의를 위해 업보팅된 댓글의 conclustion을 번역하여 가져오면 아래와 같다.
결론 Redis 파드는 이러한 요청과 함께 배포할 수 있습니다: 100m 및 100Mi. 제한이 설정되어 있지 않기 때문에, Redis 파드가 점점 더 많은 리소스를 사용하여 다른 파드가 종료될 수 있습니다. 이 노드가 Redis 포드 전용인 경우, 사용 가능한 최대 리소스인 용량 - 할당 가능 값을 사용할 수 있습니다. 이제 노드 리소스의 절반으로 제한을 설정하고 나중에 결과에 따라 변경할 수 있습니다. 파드가 제한에 도달하면 더 높은 값으로 변경할 수 있고, 반대의 경우 더 낮은 값으로 변경할 수 있습니다.
쿠버네티스의 경우에는 CPU와 다르게 메모리는 Limits를 걸어주는 것이 중요하다. 그 이유는 쿠버네티스의 requests와 limits 정책이 CPU와 메모리에게 적용되는 것이 조금 차이가 있다는 점인데, 메모리는 넘어가게 되면 치명적인 운영체제 이슈가 생길 수 있기 때문에 OOM(Out of Memory)에 대한 문제를 아예 제거하는 것이 좋다. 반대로 CPU의 경우에는 초과하는 경우에 서버의 성능이 엄청나게 느려지겠지만, 상황에 따라서 대부분은 CPU를 크게 잡아먹지 않는다. ( 물론 CPU가 100%를 찍게 되면 난리가 나겠지만 )
이러한 부분에 대해서는 추후에 포스팅하겠지만 결론적으로는 메모리를 관리하는 부분에 있어서는 제한을 꽤나 세게 걸어둬야 한다는 점이다.
그렇다면 베스트 프랙티스는?
사실, 가장 안전한 방법은 redis.conf와 함께, 그리고 K8s의 resources.limits를 통해서 메모리에 제한을 거는 것이다. 쿠버네티스 클러스터를 운영하는 입장에서는 혹시나의 경우를 예방하는 것이 좋기 때문에 둘 다 걸어주는 편이 좋다.
또한, Redis의 경우에는 단일 Pod를 Pod 혹은 Deployment 워크로드를 통해서 배포할 수 있겠지만 일반적으로는 StatefulSets를 사용한다. 이는 Redis나 Kafka와 같은 상태가 있는 응용 프로그램을 관리하기 위한 워크로드로써 사용하는데, 결국에 단일 redis가 아니라 가용성이 있는 redis를 사용하기 위해서는 클러스터를 사용하게 된다.
쿠버네티스의 master-slave 구성을 생각하면, 다시 앞서 이야기 했던 output beffur 문제로 인하여 여분의 memory를 남기는 것이 중요하다. 따라서 클러스터를 구성하게 될 것을 상정하면 꼭 메모리 설정을 하는 것이 좋고, 그렇지 않더라도 혹시 나의 케이스에 대비해서 OOM으로 인해 redis의 모든 데이터가 유실되지 않게끔 설정을 해두는 것이 좋다.
마치며
사실 레디스는 정말 대부분의 기술적으로 한 가지 아쉬운 부분을 이어주는 역할을 한다. 상태 저장이나 속도 문제 등 한 끗차이로 아쉬운 부분을 메워주는 역할을 하지만, 왠지 모르겠지만 이따금씩 사고 치는 건 항상 레디스였다. ( 사실 전부 내 탓이다 레디스는 잘못이 없다 )
이러한 레디스를 잘 다루기 위해서 정말 엄청난 노력을 들여 redis를 공부하고 실험해봐야 할 것 같다. 아직 내 수준이 redis를 극한으로 사룰 수준까지는 이르지 못했기도 하고 그런 환경이 아니겠지만 직접 테스팅을 해보면서 감을 익혀보려고 한다.
참고
- [Moss] Redis 서버 설정 관리 : https://moss.tistory.com/entry/Redis-서버-설정-정리#maxmemory-policy
- [github] redis/redis.conf : https://github.com/redis/redis/blob/unstable/redis.conf
- [dockerhub] redis : https://hub.docker.com/_/redis
- [Kubernetes] 컨피그맵을 통해서 Redis 설정하기 : https://kubernetes.io/ko/docs/tutorials/configuration/configure-redis-using-configmap/
- [stackoverflow] How much memory / CPU to allocate to Redis instance? : https://stackoverflow.com/questions/65727287/how-much-memory-cpu-to-allocate-to-redis-instance
감사합니다.
'Database' 카테고리의 다른 글
Redis(레디스)란? 그리고 기본적인 사용법 (1) | 2024.12.13 |
---|