Redis 메모리 90% 알림 — BGSAVE fork, 스왑, TTL 없는 키

데이터·관측성4분조회 –

Redis 서버 한 대에서 OS 메모리 90% 알림이 왔습니다.

Problem: Linux: High memory utilization (>90% for 5m)
Opdata: 90.06 %   Severity: Average
Trigger: The system is running out of free memory.

EC2 에 직접 올린 클러스터 모드 Redis 로, 웹 서비스의 세션 저장소(connect-redis)로 씁니다. 16GiB VM 세 대에 인스턴스를 두 개씩, 모두 여섯 노드로 돌립니다.

상한은 있었음 — 7G 둘이 16G 한 대에

알림이 온 VM 의 두 인스턴스 모두 maxmemory 와 eviction 정책은 제대로 걸려 있었습니다.

인스턴스 used_memory maxmemory RSS fragmentation ratio policy
6379 마스터 6.09G 7.00G 2.32G 0.38 allkeys-lru
6380 레플리카 6.06G 7.00G 2.80G 0.46 allkeys-lru

그런데 VM 기준으로 합치면 인스턴스마다 7G 라 둘이 14G 이고, OS 가 보는 전체 메모리는 15G 입니다. 두 인스턴스가 상한 근처까지 차 있으면 OS 와 다른 프로세스가 쓸 자리가 거의 남지 않습니다.

fragmentation ratio 가 1 아래 — 스왑으로 밀린 Redis

mem_fragmentation_ratio 는 RSS 를 used_memory 로 나눈 값입니다. 보통은 1 을 조금 넘고 단편화가 심하면 더 커지는데, 이 VM 의 두 인스턴스는 0.38 과 0.46 이었습니다. Redis 가 쓰고 있다고 생각하는 메모리보다 실제로 물리 메모리에 올라와 있는 양이 훨씬 적다는 뜻이고, 나머지는 스왑으로 밀려나 있습니다.

free -h 도 같은 얘기를 했습니다.

total 15G   free 172M   available 1.5G   Swap used 8.3G

알림 시점에 돌던 BGSAVE

알림이 온 그 시각에는 redis-rdb-bgsave 6379 자식 프로세스가 RSS 6.15G, CPU 95% 로 돌고 있었습니다.

RDB 스냅샷을 쓸 때 Redis 는 fork 로 자식 프로세스를 만들고, 자식은 부모의 메모리를 copy-on-write 로 나눠 쓰다가 부모가 키를 고칠 때마다 그 페이지를 복사하니 쓰기가 들어오는 동안 메모리가 계속 늘어납니다. 이미 상한 근처에서 버티던 VM 이라 그 증가분이 그대로 물리 메모리를 넘었고 커널이 스왑으로 밀어내면서 OS 메모리 알림이 난 것입니다.

같은 구성의 다른 VM 도 free 가 167M, 스왑이 5.1G 로 거의 같은 상태였습니다. 이 VM 하나의 문제가 아니라 세 대가 전부 상한 근처를 스왑으로 버티고 있었고, 알림은 그 시점에 BGSAVE 를 돌던 노드에서 먼저 난 것이었습니다. dmesg 에 OOM 킬러 흔적은 없어서 장애로 번지지 않고 스왑이 받아 내던 중이었습니다.

상한을 채운 건 세션이 아니었음

세션 저장소니까 세션이 쌓였을 거라고 보기 쉽지만 키 구성을 보면 아니었습니다. 키가 441만 개인데 TTL 이 걸린 건 7.8% 뿐이었고 evicted_keys 는 누적 140만 건이었습니다.

키 200개를 무작위로 뽑아 값은 열지 않고 TTL 과 크기, 타입만 봤습니다.

for i in $(seq 200); do
  k=$(redis-cli -p 6379 randomkey)
  echo "$(redis-cli -p 6379 ttl "$k")|$(redis-cli -p 6379 memory usage "$k")|$k"
done
구분 개수 메모리 비중 특징
TTL 없음 182 (91%) 98.3% 네임스페이스 없는 32자 랜덤 토큰, 대부분 string, 평균 1.1KB
TTL 있음 18 (9%) 1.7% 전부 sess: 세션 키, 평균 200B

세션 키는 작고 TTL 도 제대로 걸려 있었습니다. 메모리의 98% 를 차지한 건 TTL 도 네임스페이스도 없는 32자 토큰 키였고, 앱이 SET·SADD 로 만료 없이 써 넣은 키들입니다. 이 키들이 상한을 늘 채우고 있으니 allkeys-lru 가 쉬지 않고 키를 밀어내고 fork 가 쓸 여유도 남지 않습니다.

원인은 세 겹

근본    TTL 없는 토큰 키가 데이터의 98% → 인스턴스가 상한 7G 를 늘 채움
증폭    16GiB VM 한 대에 상한 7G 인스턴스 둘 → 상한 합 14G ≈ 물리 메모리
트리거  RDB BGSAVE fork → copy-on-write 로 메모리 증가 → 스왑 → OS 메모리 90%

알림만 없애려고 fork 를 줄이거나 VM 을 키우면 증상은 가라앉지만 상한을 채우는 키는 그대로 남습니다.

손댈 순서

가장 먼저 할 일은 TTL 없는 토큰 키를 누가 쓰는지 찾아 만료를 거는 것이고, 앱 코드에서 SET·SADD 를 부르는 자리를 찾을 때는 위 표본에서 뽑은 키 이름 패턴이 출발점이 됩니다.

fork 로 튀는 메모리는 레플리카에서 RDB 를 끄고 AOF 만 남기거나(CONFIG SET save "" 후 설정 파일에도 반영), repl-diskless-sync yes 로 줄일 수 있습니다. 한 VM 에 인스턴스가 둘이라 두 인스턴스의 저장 시각이 겹치지 않게 나누는 것도 같이 봅니다.

구조를 바꾸려면 인스턴스별 maxmemory 를 5G 로 내려 fork 와 OS 몫을 남기거나(대신 eviction 이 늘어남), VM 을 32GiB 로 올려 7G 둘에 fork 여유까지 담는 방법이 있습니다.

알림보다 먼저 잡는 신호

OS 메모리 하나로만 보면 이 상태를 BGSAVE 가 도는 순간에야 알게 됩니다. mem_fragmentation_ratio 가 1 아래로 떨어지는 것과 evicted_keys 가 늘어나는 속도를 따로 보면 스왑으로 밀리기 시작한 걸 알림보다 먼저 잡을 수 있습니다.

Redis 서버에서 OS 메모리 알림이 오면 used_memory 만 보지 말고 fragmentation ratio 와 스왑부터 봅니다. 1 아래면 Redis 페이지가 스왑에 나가 있다는 뜻이고, 그 시각에 BGSAVE 가 돌고 있었는지 같이 보면 왜 하필 그 노드에서 알림이 났는지까지 설명이 됩니다.

  1. 불러오는 중