IL-0432 Hello Inc. · 2026-10-27 (게시일)databasegcp
Memorystore for Redis 가 메모리 단편화로 쓰기를 거부해 AutoReserve 예약 알림 일부가 조용히 빠졌다
Hello Inc. 의 AutoReserve 와 Respo 는 같은 Ruby on Rails 애플리케이션이고, Google Cloud 위에서 캐시로 Memorystore for Redis 를 함께 썼다. 한 종류의 예약 알림은 보내는 경로가 여럿이라 같은 알림이 두 번 나가지 않도록 보낸 상태를 Redis 에 기록했다. 코드는 Rails.cache.write 에 unless_exist: true 를 넘겨 키가 없을 때만 쓰고, 키가 있으면 false 를 받아 보내지 않았다. Redis 장애로 쓰기 자체가 실패하면 결과가 nil 인데 코드가 이 경우를 다루지 않았다. Rails 는 캐시 스토어 장애로 앱이 죽지 않도록 쓰기 실패에 예외를 던지지 않는다. 그래서 Redis 가 쓰기를 거부하는 동안 알림은 나가지 않았는데 보낸 것으로 판정되었고, 로그에는 Notification already scheduled 가 찍혔다.
Redis 는 버전 3.2, 8GiB 인스턴스였다. Memorystore 에서는 두 곳이 따로 쓰기 거부를 판정한다. Redis 자체는 값의 합계인 논리 메모리 사용량으로 판정하고, Memorystore 는 Redis 프로세스의 물리 메모리에 OS 와 관리 프로세스 몫을 더한 인스턴스 전체 사용량으로 판정한다. 메모리 할당자 jemalloc 은 페이지 단위로만 OS 에 메모리를 반환할 수 있어서, 크기가 다른 값이 섞여 쓰이면 빈 자리가 메워지지 않은 채 페이지만 늘어난다. 이 단편화로 물리 메모리 사용량이 논리 메모리 사용량의 1.31배가 되었다. Redis 가 보기에는 여유가 있어 오래된 키를 버리지 않았고, 인스턴스 전체로는 임계값을 넘나들어 쓰기가 띄엄띄엄 거부되었다.
팀은 처음에 메모리가 부족하다고 보고 8GiB 를 16GiB 로 늘렸지만, 캐시 데이터와 단편화도 함께 늘어 쓰기 거부가 멈추지 않았다. Redis 3.2 를 7.2 로 업그레이드하자 새 프로세스가 데이터를 다시 배치하면서 물리 메모리 사용량이 17.45GiB 에서 10.94GiB 로 줄었고, 비율은 1.40배에서 1.01배가 되었다. 글을 쓴 시점까지 업그레이드 뒤 4개월쯤 동안 쓰기 거부는 한 번도 기록되지 않았다. 팀은 Redis 가 언제 다시 쓰기를 거부할지 통제할 수 없다고 보고, 보낸 상태를 RDB 에서 관리하도록 옮겼다.
- 심각도
- –
- 영향 범위
- One kind of AutoReserve reservation notification was intermittently not sent to users
- 영향 사용자
- –
- 지역
- –
- 감지까지
- –
- 복구까지
- –
증상
레스토랑 예약 서비스 AutoReserve 에서 예약 관련 알림 일부가 도착하지 않는다는 버그 보고가 들어왔다. 여러 종류의 알림 가운데 한 종류만 갔다 안 갔다 했고, 로그에는 정상으로 보이는 Notification already scheduled 가 찍혔다.
팀은 먼저 알림 발송 인프라의 장애를 의심했지만, 같은 예약의 다른 알림은 도착했으므로 이 가능성을 뺐다. 다음으로 매장의 계약 플랜 차이를 의심했지만 같은 플랜에서도 결과가 제각각이었다. 배포 때 재시작으로 알림 작업이 사라졌다는 가설도 확인했지만 맞지 않았다.
도착하지 않는 알림의 처리에만 Redis 캐시가 쓰인다는 것을 알아챈 뒤 Redis 에 직접 써 보자 OOM command not allowed under OOM prevention 오류로 쓰기가 거부되었다. 앱 쪽에서는 Rails.cache.write 가 예외를 던지지 않고 nil 을 반환했고, 코드는 이를 이미 보낸 것으로 판정했다.
인스턴스 화면의 메모리 사용량 메트릭은 8GiB 상한에 못 미치는 6.71GiB 였다. 이 값은 논리 메모리 사용량이었고, 인스턴스 화면 메트릭에 나오지 않는 물리 메모리 사용량을 INFO memory 로 확인하니 8.81GiB 였다. 물리 메모리 사용량이 임계값 앞뒤를 오가면서 쓰기 거부가 띄엄띄엄 났다.
원인
- 촉발
- –
감지
AutoReserve 에서 예약 관련 알림 일부가 도착하지 않는다는 버그 보고가 들어왔다.
조치
- 메모리가 부족하다고 보고 Redis 인스턴스를 8GiB 에서 16GiB 로 늘렸지만, 쓰기 거부는 멈추지 않았다.
- Redis 3.2 를 7.2 로 업그레이드했고, 그때 쓰기 거부가 멈췄다.
- Redis 가 다시 쓰기를 거부해도 알림이 빠지지 않도록 보낸 상태를 Redis 가 아니라 RDB 에서 관리하게 바꿨다.
후속 조치
- 물리 메모리 사용량이 논리 메모리 사용량의 1.3배를 넘기 시작하면, Redis 를 멈추지 않고 단편화를 해소하는 active defrag 를 켜는 것을 검토한다.
교훈
보낸 상태를 판정하는 데 중요한 것은 속도가 아니라 값이 사라지지 않는 것과 여러 처리가 동시에 와도 결과가 하나로 정해지는 것이었다. 캐시는 사라져도 되살릴 수 있는 값을 두는 곳이고, 처음부터 쓰기가 실패할 수 있다고 보고 만든 것이다. 그러므로 값의 성질에 맞춰 둘 곳과 다루는 방식을 정해야 한다. 로그와 인스턴스 화면 메트릭처럼 겉으로 보이는 정보만으로는 원인을 잡지 못했고, Redis 안까지 들어가 봐야 했다.
출처
관련 사례
IL-0290GitLab.com 의 Redis Sidekiq 클러스터에서 노드 여러 대가 메모리를 다 써 멈추고 쿼럼을 잃어, 웹과 Git 과 CI 전반에서 500 오류가 늘었다GitLab · 2026 · sev1같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
IL-0067Redis 마스터의 50Mi 영구 볼륨이 가득 차 AOF 가 깨졌고, Redis 파드가 크래시 루프에 빠져 Klamm 운영 서비스가 멈췄다BC Government · 2025 · sev1같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
IL-0202갑자기 몰린 쿼리와 데이터베이스 쓰기로 Honeycomb 수집 호스트가 메모리 부족으로 죽었고, 쿠버네티스 CrashLoopBackOff 가 용량 회복을 막아 US1 이벤트 수집이 한 시간 넘게 불안정했다Honeycomb · 2023 · sev1같은 원인 · 공통 인메모리 캐시
IL-0386Google Cloud 로 옮긴 뒤 GKE 노드에서 Postgres 연결 타임아웃과 memcached i/o timeout 이 간헐적으로 튀었다incident.io · 2023같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
같은 원인 · 공통 인메모리 캐시
내 클러스터에도 있는지 확인하기
AI 도구와 스크립트용
- 이 사례의 JSON: https://incidentlib-api.benchlet.com/cases/IL-0432
- 가장 비슷한 사례들: https://incidentlib-api.benchlet.com/cases/IL-0432/similar
- MCP 서버(Streamable HTTP, 키 없음):
https://incidentlib-mcp.benchlet.com/mcp
API 는 키가 필요 없습니다. MCP 서버는 search_incidents, get_incident, list_incidents, get_topic, vocab 도구를 내놓습니다. 사용법은 API 와 MCP.