Incident Library

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 안까지 들어가 봐야 했다.

출처

관련 사례

내 클러스터에도 있는지 확인하기

kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이 사례와 같은 상태를 점검합니다. 이 사례의 구성 요소: 인메모리 캐시. 점검 항목의 근거는 이 사례 같은 공개 보고서입니다.

AI 도구와 스크립트용

API 는 키가 필요 없습니다. MCP 서버는 search_incidents, get_incident, list_incidents, get_topic, vocab 도구를 내놓습니다. 사용법은 API 와 MCP.