{"id":"IL-0453","org":"Hyperconnect","date":"2022-12-12","docs":["https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/backups.html","https://redis.io/docs/reference/patterns/indexes/","https://redis.io/docs/manual/keyspace-notifications/","https://github.com/spring-projects/spring-data-redis/blob/main/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java#L810-L819","https://docs.spring.io/spring-data/redis/docs/current/reference/html/#redis.repositories","https://engineering.salesforce.com/lessons-learned-using-spring-data-redis-f3121f89bff9/"],"title":{"en":"Hakuna API latency spiked every day during the ElastiCache Redis backup as memory filled and the backup went forkless","ko":"하쿠나의 API latency 가 매일 ElastiCache Redis 백업 시간마다 치솟았다"},"domain":["database","aws"],"impact":{"scope":"Hakuna service API latency spiked during the daily ElastiCache backup","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"The team says its Redis operation and monitoring had gaps, and lists what to watch: use a relational database when data has relationships or must be searched by value; write data that must be durable to on-disk storage rather than Redis; keep available memory at 50% or more when ElastiCache snapshots must be taken; take care that O(n) commands such as DEL on a collection do not stall production Redis; and check regularly for unnecessary I/O between the application and the database. Spring Data Redis repositories, which manage secondary indexes in the framework, suit workloads that need no TTL and need fast queries.","ko":"백엔드 팀은 Redis 운영과 모니터링에 미흡한 부분이 있었다고 돌아봤다. 데이터 사이에 관계가 있거나 값으로 검색해야 하면 관계형 데이터베이스를 쓰고, 영속성이 필요한 데이터는 되도록 Redis 가 아닌 on-disk 기반 저장소에 둔다. ElastiCache Snapshot 을 만들어야 하면 가용 메모리를 50% 이상으로 유지한다. DEL collection 처럼 O(n) 인 명령 때문에 운영 Redis 가 멈추지 않게 조심한다. 애플리케이션과 데이터베이스 사이에 불필요한 I/O 가 없는지도 주기적으로 확인한다. Spring Data Redis Repository 는 secondary index 를 프레임워크가 관리해 주므로, TTL 이 필요 없고 쿼리 성능이 많이 요구되는 업무에서는 좋은 선택지가 될 수 있다."},"related":[],"sources":[{"url":"https://hyperconnect.github.io/2022/12/12/fix-increasing-memory-usage.html","kind":"blog","archived":"https://web.archive.org/web/20250620140706/https://hyperconnect.github.io/2022/12/12/fix-increasing-memory-usage.html"}],"summary":{"en":"Hakuna's ElastiCache Redis uses Auto-failover for availability. The primary node was busy with service requests as well as the backup, while the secondary node only replicated the primary and was mostly idle. Running the backup on the secondary can make the backup inconsistent because changes reach the secondary late, but a once-a-day backup is already inconsistent to some degree, so the team accepted that and moved the backup to the secondary node, which removed the bottleneck caused by the backup.\n\nThe memory growth came from how Hakuna used Spring Data Redis repositories through the CrudRepository interface. To support count and query derivation on a key-value store, Spring Data Redis keeps a set of all keys per domain class and, for fields annotated with @Indexed, a secondary index keyed domain_class:field_name:value. When data gets a TTL through @TimeToLive or @RedisHash(timeToLive = 1), the data itself expires, but its entry in the key set stays, so the set keeps growing and count returns wrong numbers. Keeping the sets consistent requires subscribing to Redis keyspace notifications with @EnableRedisRepositories(enableKeyspaceEvents = ON_STARTUP), in which case each save also writes a copy under domain_class:ID:phantom so the expired data can be restored into a RedisKeyExpiredEvent. The team decided that accessing data through RedisTemplate was better, because the business used few of the CrudRepository methods and each save carried hidden extra commands.\n\nThe team first changed the code so that data stored without a TTL is stored with one, then reimplemented the places that used the Redis Hash CrudRepository as a CustomRepository on RedisTemplate, and then ran a script to delete the unneeded data that had piled up. This freed 58 GB of memory in total on 3 databases, and memory usage stopped growing.","ko":"하쿠나 서비스의 API latency 가 매일 특정 시간에 치솟았다. APM 으로 보니 병목은 Redis 였다. 배포나 트래픽 변화는 없었지만 스파이크 시간이 AWS ElastiCache 백업 시간과 맞았다. ElastiCache 는 가용 메모리가 충분하지 않으면 forkless 기반의 백업을 쓰고, 이 백업은 요청의 latency 와 throughput 을 나쁘게 만든다. Redis 메모리 사용량이 꾸준히 늘어 가용 메모리가 부족해진 것이 이때 와서 문제가 된 이유였다. Auto-failover 를 쓰는 클러스터에서 secondary 노드는 서비스 요청을 받지 않고 복제만 하므로 대부분 idle 상태다. 백엔드 팀은 백업을 이 secondary 노드로 옮겼다. 복제 지연 때문에 백업 데이터가 조금 어긋날 수 있지만, 하루 한 번 하는 백업에는 원래 그런 어긋남이 있으므로 감수할 수 있다고 판단했다.\n\n메모리가 늘어난 원인을 찾으려고 팀은 scan 으로 모든 key 를 훑으며 key, TTL, 데이터 타입, 메모리 사용량을 파일에 적는 스크립트를 실행했다. 그 결과 domain_class 패턴의 set 과 domain_class:ID:phantom 패턴의 set 이 10GB 넘는 메모리를 쓰고 있었다. 하쿠나는 CrudRepository 를 쓰는 Spring Data Redis Repository 로 도메인 데이터를 다뤘다. 이 Repository 는 count 와 필드 값 조회를 빠르게 하려고, 데이터를 저장할 때마다 key 만 모은 set 과 @Indexed 필드의 index 를 따로 만든다. @TimeToLive 나 @RedisHash 의 timeToLive 로 TTL 을 걸면 데이터는 만료돼 지워지지만, key 를 모은 set 의 원소는 지워지지 않는다. 이 원소까지 지우려면 @EnableRedisRepositories(enableKeyspaceEvents = ON_STARTUP) 로 expired 이벤트를 받아야 한다. 이 설정을 켜면 저장할 때마다 phantom 사본도 함께 쓴다.\n\n팀은 쓰지도 않는 CrudRepository 기능 때문에 복잡도를 높일 필요가 없다고 보았다. save 연산에 의도하지 않은 오버헤드가 있다는 점도 들어, 데이터를 RedisTemplate 으로 직접 다루기로 했다. 먼저 TTL 없이 저장되던 데이터에 TTL 을 걸었다. 이어서 Redis Hash CrudRepository 를 쓰던 곳을 RedisTemplate 기반의 CustomRepository 로 다시 구현했고, 쌓여 있던 필요 없는 데이터를 지우는 스크립트를 실행했다. 데이터베이스 3대에서 모두 합쳐 58GB 의 가용 메모리를 되찾았고, 메모리 사용량은 더 늘지 않았다."},"trigger":{"en":"The daily ElastiCache backup on the primary node ran while available memory was insufficient, so ElastiCache used a forkless backup that slowed user requests.","ko":"가용 메모리가 부족한 상태에서, 하루 한 번 primary 노드에서 도는 ElastiCache 백업이 forkless 방식으로 실행됐다."},"severity":"unknown","symptoms":["high-latency","memory-pressure"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"A latency graph showed the service API latency spiking periodically, and the APM's per-dependency latency analysis pointed at Redis.","ko":"특정 시간에 주기적으로 치솟는 API latency 를 그래프에서 보았고, APM 의 Dependency 별 Latency 분석으로 병목이 Redis 임을 알아냈다."},"components":["cache","database-backup"],"root_cause":"capacity","remediation":[{"text":{"en":"Moved the ElastiCache backup from the primary node to the secondary node","ko":"ElastiCache 백업을 primary 노드가 아니라 secondary 노드에서 실행하도록 바꿨다."}},{"text":{"en":"Changed the code so data stored without a TTL is stored with one","ko":"TTL 없이 저장되던 데이터에 TTL 을 걸어 저장하도록 코드를 고쳤다."}},{"text":{"en":"Replaced the Redis Hash CrudRepository usages with a CustomRepository built on RedisTemplate","ko":"Redis Hash CrudRepository 를 쓰던 곳을 RedisTemplate 으로 다시 구현한 CustomRepository 로 바꿨다."}},{"text":{"en":"Ran a script that deleted the unneeded data already stored, freeing 58 GB on 3 databases","ko":"이미 쌓여 있던 필요 없는 데이터를 지우는 스크립트를 실행했다."}}],"action_items":[],"contributing":["no-alert-on-limit"],"recurrence_of":null,"symptoms_text":{"en":"The service API latency of Hyperconnect's Hakuna rose sharply at a certain time, periodically. The per-dependency latency analysis in the APM showed that a bottleneck in Redis caused it, but no deployment and no change in traffic pattern could have affected how Redis was used.\n\nThe team then found that the spikes coincided with the AWS ElastiCache backup time. Hakuna had run ElastiCache backups periodically for a long time, so the team looked into why they started to hurt only now. When available memory is not sufficient, ElastiCache chooses a forkless backup strategy, and a forkless backup degrades the latency and throughput of user requests. Further monitoring showed that Redis memory usage had been rising steadily, which had left too little available memory.\n\nTo find what was using the memory, the team suspected data stored without a TTL and kept forever. A script walked all keys with scan and wrote each key's name, TTL, data type and memory use to a file. Comparing that list with the Hakuna code turned up sets with the key pattern domain_class and sets with the key pattern domain_class:ID:phantom that together used more than 10 GB of memory.","ko":"하쿠나 서비스의 API latency 가 특정 시간에 주기적으로 치솟았다. APM 의 Dependency 별 Latency 분석에서 병목은 Redis 였다.\n\n그 무렵 Redis 사용에 영향을 줄 만한 배포도 없었고 트래픽 패턴도 바뀌지 않아, 백엔드 팀은 처음에 근본 원인을 찾기 어려웠다. 그러다 latency 스파이크가 나는 시간이 AWS ElastiCache 백업 시간과 일치한다는 것을 알아냈다. 백업은 이전부터 주기적으로 돌고 있었는데 이때 와서야 문제가 생기기 시작했다.\n\n추가로 모니터링해 보니 Redis 메모리 사용량이 이전부터 꾸준히 늘어 가용 메모리가 부족해져 있었다. ElastiCache 는 가용 메모리가 충분하지 않으면 forkless 기반의 백업을 고른다. 이 백업은 사용자 요청의 latency 와 throughput 을 나쁘게 만든다."},"updated_at":"2026-11-14T12:34:10.209117Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0453","org_slug":"hyperconnect"}