개발 중 예상치 못한 Redis 메모리 초과 문제로 서버가 다운되는 경험을 했다.
테스트용으로 임시 서버를 두고 작업을 해두었던 터라, 잠깐의 시간 동안 Redis Memory가 가득 찰 일은 없겠다는 안일한 생각을 했다. 그래서 Redis의 maxmemory와 maxmemory-policy 설정을 따로 해두지 않았었다.
그러다보니 무제한으로 데이터를 저장할 수 있는 상태가 되었다. 그리고 서버가 꽝 터졌다.
이번 기회를 통해 Redis 설정을 최적화하는 과정을 공유해보고자 한다.
문제 상황
개발 중이던 프로젝트에서, Redis를 사용해 방에 참가한 사용자 닉네임을 list 자료구조에 저장했다.
테스트 중, 클라이언트에서 소켓 이벤트를 잘못 처리해 엄청난 양의 데이터가 Redis로 흘러들어왔고, 순식간에 Redis 메모리가 초과되었다. 그리고 서버(Nest.js)도 다운되어버렸다. (프론트에서 소켓 이벤트 무한리필 문제)
서버 로그
<--- Last few GCs --->
[74:0x7f5177d99000] 926795 ms: Scavenge (interleaved) 1952.0 (1990.5) -> 1951.6 (1995.5) MB, pooled: 0 MB, 21.36 / 0.00 ms (average mu = 0.262, current mu = 0.196) allocation failure;
[74:0x7f5177d99000] 928965 ms: Mark-Compact (reduce) 1954.8 (1995.5) -> 1952.7 (1990.5) MB, pooled: 0 MB, 1378.40 / 0.00 ms (+ 418.4 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 1851 ms) (average mu = 0.234
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----
Redis 컨테이너 로그
Redis 컨테이너도 종료되었는데, inspect로 확인해보니 아래의 OOM 에러 로그를 확인할 수 있었다.
ExitCode 137
OOMKilled: true
이는 운영체제의 OOM(Out of Memory) Killer가 시스템 메모리를 보호하기 위해 프로세스를 종료한 것이다.
해결 방법과 개선
1. Max-memory : 시스템 메모리의 절반으로 제한하는 이유
레디스를 사용할 때, max-memory-policy 설정은 필수적이다. 그리고 max-memory는 전체 시스템 메모리의 절반 정도로 설정하는 것이 권장된다.
왜 절반으로 제한을 두어야할까? 이유는 크게 두 가지로 정리해볼 수 있다.
RDB 스냅샷 생성 중의 메모리 사용 : fork()
- RDB란?
Redis는 인메모리 데이터 저장소다. 서버 재시작 시 모든 데이터가 유실된다. 그렇기에 적절한 데이터 백업이 필요하다.
Redis는 이를 위해 AOF, RDB의 두 가지 Presistence Option을 제공한다.
아주 간단히 비교하자면, RDB는 key1 : "duck" 이라는 정보의 스냅샷을 보관하는 것이고,
AOF는 set key1 "a", set key1 "duck" 이라는 명령어의 모음을 보관한다.
레디스에는 복구를 위한 RDB 스냅샷 기능을 제공한다. RDB 기능은 어떻게 레디스가 동작하면서 동시에 작업이 진행될 수 있을까?
이것은 Redis가 RDB 스냅샷을 생성할 때 OS의 fork()를 호출해 새로운 자식 프로세스를 생성하기 때문이다. 백그라운드에서는 자식 프로세스로 RDB를 저장하고, 원래의 프로세스는 일반적인 요청을 처리할 수 있게된다.
여기서 fork()는 프로세스를 복제하는데, 이 과정에서 Copy-On-Write 방식을 사용하여 부모 프로세스의 메모리를 자식 프로세스와 공유하게 된다. 이렇게 자식 프로세스는 해당 메모리를 복사하면서 추가적인 메모리 사용을 유발한다.
따라서 Redis가 이미 시스템 메모리를 거의 다 사용하고 있다면, fork() 호출로 인해 메모리 부족(OOM) 상태가 발생할 가능성이 높아진다.
이를 방지하기 위해, Redis의 maxmemory는 전체 메모리의 절반 이하로 설정하는 것이 권장된다.
Jemalloc과 메모리 단편화
하나의 이유가 더 있다면, 레디스는 내부적으로 메모리 할당 시, jemalloc를 기본 할당자로 사용하기 때문이다.
jemalloc는 효율적인 메모리 관리를 위해 설계되었다. 하지만 그렇다 할지라도 여전히 메모리 단편화 문제가 발생할 수 있다.
단편화로 인해 실제 사용 중인 메모리보다 더 많은 물리적 메모리를 점유하게 되고, 시스템 메모리의 절반으로 maxmemory를 제한하면, 단편화나 추가 메모리 사용으로 인해 Redis가 과도한 메모리를 사용하는 상황을 방지할 수 있게 된다.
2. Max-memory-policy 설정
maxmemory가 설정되었다면, Redis는 메모리 제한에 도달했을 때의 동작을 정의하기 위해 maxmemory-policy 설정이 필요하다.
메모리가 가득차면, 어떻게 키를 삭제할 것인가에 대한 알고리즘을 설정해주는 것이다.
대표적으로 아래의 정책이 있다.
1. noeviction: 메모리가 가득 차면 새 데이터를 받지 않고 에러를 반환.
2. allkeys-lru: 모든 키에서 가장 적게 사용된 데이터(LRU)를 제거.
3. volatile-lru: 만료 시간이 설정된 키에서 LRU 정책으로 제거.
기본 설정은 noeviction이다. (위에서 레디스가 터진 이유..) 내가 참여하는 서비스에서는 allkeys-lru를 설정했다.
3. ziplist 적용
ziplist란
ziplist는 Redis에서 사용되는 압축된 연속 메모리 구조로, 작은 데이터를 효율적으로 저장하기 위한 구조이다.
ziplist를 사용하면 메모리가 줄어드는 이유
그럼 ziplist를 사용하면, 기존의 방식과 어떻게 다르길래 메모리를 줄일 수 있을까?
이는 ziplist가 연속된 메모리 공간에 데이터를 저장하여 오버헤드를 줄이고, 메모리를 최소화하도록 설계되었기 때문이다.
일반적으로 Redis의 표준 리스트나 해시는 포인터 기반 데이터 구조로, 각 요소를 저장할 때 포인터 오버헤드가 추가된다. 반면 ziplist는 데이터를 하나의 연속된 메모리 블록에 저장하므로 포인터와 관련된 추가적인 메모리 사용을 없앨 수 있다.
이게 무슨 말인지 간단히 살펴보자.
일반 리스트
[포인터1 | 데이터1] -> [포인터2 | 데이터2] -> ...
각 요소가 독립적으로 저장되고 포인터를 사용하여 연결된다.
즉, 여기서는 연속된 데이터로 저장되지 않기 때문에, 포인터 오버헤드가 포함된다.
ziplist 일반 리스트
[헤더 | 데이터1 길이 | 데이터1 | 데이터2 길이 | 데이터2 | ... | 테일]
모든 요소가 연속된 메모리 공간에 저장되기 때문에, 포인터 오버헤드가 필요없다.
추가로 ziplist는 요소의 데이터 타입과 길이, 값을 인코딩해서 저장한다. 예를 들어, 작은 정수는 최소한의 바이트(1바이트 또는 2바이트)로 저장할 수 있다.
물론 단점도 존재한다.
데이터 크기/수 제한이 있다. 특정 크기 이상(디폴트: 64바이트 또는 512개 요소)으로 커지면 더 이상 ziplist를 사용할 수 없게 되어서, 일반적인 자료 구조로 전환이 된다. 그래서대규모 데이터에서는 비효율적일 수 있다.
그리고 데이터가 연속적으로 저장되기 때문에, 중간 삽입/삭제 시 재배치가 필요하여 업데이트 시간이 더 걸리는 단점이 있다.
3-1. Redis 5.0부터는 점차 Listpack이 도입
Ziplist의 단점을 보완하기 위해 Redis 5.0부터 Ziplist 대신 Listpack이 도입되기 시작했다.
그러다 Redis 7.0는 Listpack이 대부분의 데이터 타입에 적용되었다.
[헤더 | 데이터1 길이 + 데이터1 | 데이터2 길이 + 데이터2 | ... | 테일]
데이터 길이와 데이터를 함께 저장하여 데이터 접근 속도가 개선되었고, 동적으로 길이를 계산하지 않아도 되어 메모리 효율이 증가했다.
참고자료
[우아한테크세미나] 191121 우아한레디스 by 강대명님
https://www.youtube.com/watch?v=mPB2CZiAkKM
[NHN FORWARD 2021] Redis 야무지게 사용하기
https://www.youtube.com/watch?v=92NizoBL4uA
Redis Copy-on-Write 분석
http://redisgate.kr/redis/configuration/copy-on-write.php
'Backend > Redis' 카테고리의 다른 글
Redis HINCRBY로 동시성 문제 해결: 실시간 베팅 (0) | 2024.11.19 |
---|---|
레디스에서 O(N) 관련 커맨드는 주의하기 (0) | 2024.11.19 |