니들이 caffeine 맛을 알아?

로컬 캐시 ehcache vs caffeine cache

Kyung Rak Park
NAVER Pay Dev Blog
17 min readApr 24, 2023

--

게 맛을 잊지 못해 롯데리아에서 오징어 버거도 나온 사실을 아는가

안녕하세요. NAVER FINANCIAL 네이버페이 결제 백엔드 개발자 박경락입니다.

백엔드 개발을 하다 보면 상황에 따라 캐시 서버를 두기도 로컬 캐시를 사용하기도 할 텐데요.

이번 포스트에서는 로컬 캐시의 정석이라고 부를 수도 있는 ehcache와 떠오르는 강자 caffeine cache를 간략 비교해 보고자 합니다.
Ehcache에 대한 설명은 워낙 유명하기에 구체적인 설명보다는 비교점 및 caffeine cache의 차별점을 위주로 설명하도록 하겠습니다.

배경

결제 관련 레거시 프로젝트를 신규로 구축한 프로젝트로 이관을 진행하고 있으며 이중 로컬 캐시 적용을 위해 기존에 익숙한 ehcache를 최근에 떠오르는 caching library인 caffeine과 비교 검토했습니다.

Ehcache

  • 가장 널리 사용되는 JAVA 기반 캐시
  • 직렬화된 데이터 객체를 저장하는 메모리 블럭
  • 3개의 스토리지 저장 가능 (메모리 / off Heap(GC 적용하지 않아 매우 큰 캐시 생성 가능)/ 디스크)
https://www.ehcache.org/documentation/3.10/tiering.html
  • LRU / LFU / FIFO 제거 알고리즘 제공

ehcache에 대한 자세한 설명은 다른 블로그에 충분히 나와있으니,
여기까지만 간략히 작성해 보겠습니다.

Caffeine cache

  • high performance + 최적의 캐싱 라이브러리라고 소개 (어떤 곳은 극단적으로 local cache의 king이라고 표현 과연?)
  • Google 오픈 소스 Guava Cache / ConcurrentLinkedHashMap 을 바탕으로 만들어짐
  • 캐시 제거 전략에 우선순위를 부여 가능
  • 최적의 적중률을 제공하는 Window TinyLfu 제거 정책 사용

Caffeine cache를 보다 잘 이해하기 위해서는 eviction 정책을 좀 더 이해를 해야 하는데요.

아래의 eviction 동작 원리를 간략히 보면

Window TinyLFU 캐시 승인 정책 내용 발췌
https://www.sobyte.net/post/2022-04/caffeine/

Main Cache (전체 용량 99%)

  • Probation Cache(공간 80%)-자주 사용하는 데이터(제거 시 LRU rule 적용)
  • Protected Cache(공간 20%)-자주 사용하지 않는 데이터 (제거되지 않음)

Window Cache (전체 용량 1%)

  • 새로운 데이터가 Cache에 쓰일 때 가장 먼저 Window Cache에 쓰임
  • 공간에 가득 찰 경우 LRU 식으로 Window Cache 밖으로 제거 (LRU)
  • — Tiny LFU 알고리즘에 의해 제거되거나 Probation Cache 영역에 저장됨
  • Probation 영역 데이터에 일정한 횟수 이상 접근되면 Protected Cache 역으로 승격됨
  • — Protected Cache 영역이 Full 될 경우 오래된 데이터 밖으로 옮겨짐
  • — — TinyLFU 알고리즘에 의해 제거되거나 Probation Cache 영역에 저장됨

TinyLFU 제거 메커니즘

  • Window Cache / Protected Cache로부터 제거되는 데이터 = Candidate
  • Probation Cache에서 제거되는 데이터 = Victim
  • Candidate Cache 접근 > Victim Cache 접근 : Victim 데이터 제거
  • Candidate Cache 접근 < Victim Cache 접근 && Candidate 접근 횟수 5번 이하 : Candidate 데이터 제거
  • 둘 중 하나 랜덤하게 제거

Caffeine 캐시 내부 알고리즘은 LFU와 LRU의 장점을 통합

  • 서로 다른 캐시 영역에 다른 특성을 가진 캐시 항목을 저장하여 최근에 생성된 캐시 데이터가 Window Cache로 들어가 삭제되지 않음
  • 자주 호출되는 데이터 (LFU)은 Protected 영역에 들어가며 LRU에 의해 제거되지 않음
  • 호출 횟수 / 호출 시간 두 개의 자원에 대해 밸런스가 잘 되어 있음
  • — 자주 호출되고 최근에 생성된 데이터 들은 가능한 캐시에 유지 시킬수 있음
  • 전통적인 LRU/LFU 로 처리 할 수 없던 케이스를 보다 잘 처리함

벤치마크 (정말 좋은가?)

동일/분산 key에서 Throughput도 다른 cache에 비해 압도적으로 좋고
Read 처리도 기본 ConcurrentLinkedHashMap/ehcache에 비해 압도적으로 좋고
Read/Write가 있어도 ConcurrentLinkedHashMap/ehcache에 비해 압도적으로 좋고
Write만 해도ConcurrentLinkedHashMap/ehcache에 비해 압도적으로 좋고

물론 caffeine cache에서 진행한 벤치마크이기 때문에 어느 정도 감안은 하고 봐야겠지만,

모든 부분에서 벤치마크 결과가 압도적으로 좋게 나오고 있습니다.

대부분의 경우 로컬 캐시를 매우 다양한 처리를 하기 위함이 아니라 단순히 값을 임시로 저장/조회하는 용도로 사용하기에도 벤치마크 결과만 보면 Caffeine Cache를 사용하지 않을 이유가 딱히 떠오르질 않는 거 같습니다.

여기까지만 보고 아묻따 caffeine cache로 결정할 사람도 있을 거 같다

그래도 Local cache의 정석인 ehcache와 뭐가 다른지 조금만 더 찾아보도록 하겠습니다.

Ehcache vs Caffeine 비교

  • Ehcache 가 Caffeine에 비해 제공되는 기능은 더 많음 (multi-level cache, distributed cache, cahce listener …)
  • 단순 메모리 캐시 사용하고 높은 퍼포먼스를 원하면 Caffeine이 우선순위가 높음 (Ehcache에 비해 제거 알고리즘이 우월함)
  • — Window TinyLfu eviciton policy로 near-optimal hit rate
  • — Caffeine이 ehcache에 비해 writing 퍼포먼스가 좋다고 함
  • Eviction strategy
  • — Caffeine (size-based, time-based, reference-based)
  • — Ehcache (LRU, LFU, FIFO) — 메모리 관련만 가능
  • ehcache는 메모리 + 디스크 용량까지 사용 가능
  • — caffeine 도 post-eviction strategy로 커스텀 하게 구출할 수 있음
  • — — 최근 삭제 캐시를 잡아서 로드 시점에 다시 살리도록 가능(DB가 죽는것 같은 경우)
  • Caffeine 은 캐시 사용 시 3가지 전략을 제공 (manual, synchronous, asynchronous loading)
  • — Ehcache는 asynchronous loading 불가
  • — Caffeine이 좀 더 사용하기 쉬움

간략 요약

Caffeine is a Java 8 rewrite of Guava’s cache that supersedes support for Guava. If Caffeine is present, a CaffeineCacheManager (provided by the spring-boot-starter-cache “Starter”) is auto-configured. Caches can be created on startup by setting the spring.cache.cache-names property and can be customized by one of the following (in the indicated order)

  • spring-boot-starter-cache에 auto-configured 되어있음
  • size-based (설정 사이즈만큼만)
  • time-based (정해진 시간만큼만)
  • — read based (cache를 마지막으로 읽은 시간 부터 특정 시간)
  • — write based (cache에 write 한 시점부터 특정 시간)
  • eviction 알고리즘이 좋음
  • cache 벤치마크 결과가 다른 라이브러리에 비해 좋음 (믿거나 말거나)
  • 간단한 로컬 캐시로 사용하기 적합

이제 caffeine 맛을 보았으니, 간단한 로컬 캐시 용도로 아묻따 ehcache 말고 caffeine cache 도 한번 고민해 보는 것에 도움이 되었길 바랍니다.

.

.

.

.

.

.

.

라고 끝냈어야 했는데…

결제 트랜잭션에 깊게 관여된 팀이다 보니 신규 라이브러리 사용에 따른 장애가 발생하면 피해가 막중하기에 Caffeine cache 사용 전 자체적으로 부하 테스트를 해보면서 실제 운영 시 이슈가 없는지 체크를 진행해 보았습니다.

부하 테스트

kotlin + spring boot + k8s 환경에서 테스트를 진행하였으며,

실제 메모리 사용량이 어느 정도 되는지, time based eviction 이 시간 지나면 정말 evict 되는지를 확인해 보고자 아래 4가지를 적용하며 테스트했습니다.

  • request/response 랜덤하게 생성
  • eviction 시간이 지나고 정상 expire 되는지
  • evict 되고 heap 메모리 release가 되는지
  • 메모리 사용량이 어느 정도 되는지

request/response 랜덤하게 생성

cache 저장/조회할 시 동일한 값을 지속 사용하는 것이 아닌 랜덤 한 값을 사용하여 실제 메모리 사용량에 대한 측정을 잡아보고자 했습니다.

테스트했던 파라미터는 결제용 파라미터로 보안 이슈가 있을 수 있어 변경하였습니다.

부하테스트 시 put 위주로 요청
임의의 랜덤 문자열

eviction 시간이 지나고 정상 expire 되는지

cache의 경우 팀 내에서 micrometer를 활용하여 grafana로 모니터링을 진행하고 있습니다.

위에 있던 코드를 기반으로 50,000 개의 cache entry를 가지고 부하 테스트를 진행해 보았습니다.

cache size와 eviction weight grafana 그래프

cache size가 eviciton 시간(105초)가 지나도 evict가 안되는 듯한 이슈가 있는 게 확인이 됩니다.

마지막에 local cache 1건 조회 이후 그래프

하지만, 실제 1건의 cache 값을 조회해 보면 local cache 데이터는 존재하지 않으며 eviction이 발생되는 걸 확인했습니다.

time expire 설정이 동작 안 한다!!!

time based eviction을 위해 caffeine cache를 사용한 건데 시간이 지나도 안된다는 부분은 지나칠 수 없는 크리티컬 한 이슈였으며 관련하여 디깅을 해보았습니다.

time eviction의 진실을 캐겠다는 다짐

cache 조회가 없더라도 time expire 설정으로 관련 cache가 날아가는 것이 기대하는 동작이나 초기화되지 않았습니다.
문제는 scheduler 부분에 있었습니다.

caffeine cache 라이브러리 코드

Caffeine cache 라이브러리에서는 default로 scheduler로 disabledScheduler를 쓰고 있었습니다.

빈 껍데기 scheduler

disabledScheduler는 아무 동작도 하지 않게 completed future만 항상 반환

또한,

caffeine clean up 내용에 따르면 자동으로 evict를 하지 않는다 명시

caffeine cache wiki에 있는 내용

maintenance 실행 조건 (ft. high-throughput 이면 관리 신경 쓰지마라)

  • write가 일어날 때 작은 부분만큼 maintenance 실행
    or
  • write가 드물게 발생하면 read 실행하고 난 뒤 상황에 따라 실행

Scheduler를 사용해서 즉각적인 expire 작업을 수행할 수 있음

  • java 9 이상은 Scheduler.systemScheduler() 권장
  • systemScheduler 수행 주기는 시스템 상황(os, 시스템 워크 로드)에 따라 달라짐
  • — queue에 있는 작업을 주기적으로 수행하며, priority에 따라 cache eviction 보다 다른 동작이 우선시 될 수 있음 (=주기가 일정하지 않을 수 있다)
  • 최선의 노력을 하지만 정확히 언제 expire 되는지에 대한 보장은 하지 않는다

즉,

caffeine cache에서는 time based eviction도 default로 event driven으로 정해진 규칙에 따라 정리를 하게 되며,

원한다면 Scheduler를 임의로 설정해서 기대하는 시간에 따른 eviction을 발생시킬 수 있다로 정리가 될듯합니다.

caffeine 이슈 댓글에서도 timely expiration을 원하면 scheduler를 사용하라고 작성되어 있습니다.

해당 가이드 대로 Scheduler 연동 이후에는 자동으로 eviction 수행되고 있음

Scheduler 적용 이후로 시간에 따라 eviction이 되는 부분은 확인을 하였으나,

권장한 대로 SystemScheduler를 사용할 경우 system의 우선순위에 따라 eviction이 정확한 시간에 일어나지 않는다는 점이 단점으로 생각됩니다.

개인적으로 받는 느낌은 caffeine cache는 high throughput을 전제하에 개발되어 time based eviction을 위한 Scheduler는 보완책의 개념으로 들어가 있는 듯합니다.

evict 되고 heap 메모리 release가 되는지

로컬 캐시는 heap 메모리 영역을 사용하므로, 로컬 환경에서 대량으로 주입 후 heap 메모리 늘어나는 부분 확인하였습니다.

30만 건 local cache 저장을 통한 메모리 테스트

spring boot actuator micrometer 지표
로컬 캐시 저장 중 메모리 상태

local cache evict 이후 메모리 확인

spring boot actuator micrometer 지표
로컬 캐시 evict 후 메모리 상태

eviction이 일어나면 heap 메모리는 정상적으로 모두 release 되는 것으로 확인되었습니다.

메모리 사용량이 어느 정도 되는지

메모리 임시 하향 조정

-Xms512m -Xmx512m
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m
container limit : 1Gb

k8s를 사용하여 테스트하기에 매번 서버 가동 후 메모리 상태가 다를 수 있어,

서버 가동 후 임의의 api로 충분한 부하를 주어 warm up 을 시킨 뒤 3차 테스트를 하여 최댓값을 기반으로 확인했습니다. (메모리 최댓값을 확인한 이유는 실제 운영 시 최악의 케이스를 대비하기 위한 기준)

max cache entry : 10 000

1차
메모리 : 761 Mb -> 795 Mb
증가분 : 34 Mb
2차
메모리 : 768 Mb -> 792 Mb
증가분 : 24 Mb
3차
메모리 : 754 Mb -> 786 Mb
증가분 : 32 Mb

증가분 최대 : 34Mb

cache entry : 50 000

1차
메모리 : 773 Mb -> 865 Mb
증가분 : 92 Mb
2차
메모리 : 752 Mb -> 855 Mb
증가분 : 103 Mb
3차
메모리 : 742 Mb -> 853 Mb
증가분 : 111 Mb

증가분 최대 : 111 Mb

요약

최댓값 기반 계산이라 로컬 캐시 사용량이 정확히 떨어지지는 않지만 대략적인 추세를 확인 가능
10 000 = 34 Mb
50 000 = 111 Mb

부하 테스트 결론

1. eviction 관련 scheduler 적용

evict 관련 처리는 event driven을 메인으로 진행하고 있음
상황에 따라 추가로 필요할 경우 scheduler 적용이 필요하며,
자바 9+ 이상 권장 systemScheduler 사용 시

  • 수행 주기가 보장되지 않음
  • 정확한 expire가 보장되지 않음

systemScheduler 사용이라 동작이 예측이 되지 않아
오히려 주기적으로 들어오는 l7 health check에 local cache 더미 데이터 조회를 넣는 방안 검토 필요

참고) evict 발생 조건

  • write/read 발생
  • scheduler
  • gc

2. 다른 용도의 로컬 캐시 사용 시 저장 객체의 크기가 압도적으로 커질 경우

사용 중인 서버 및 프로세스의 메모리 사용 임계치에 따라 다르며, 그에 맞는 부하 테스트 및 메모리 사용량을 검토할 필요합니다.

3. Caffeine Cache 적용 시 유의사항

Caffeine 설정 옵션으로
maximumSize : cache entry 크기
maximumWeight : 저장된 전체 크기
softValues : GC 발생 시 처리되도록 하는 옵션

두 설정(maximumWeight/softValue)을 추가하더라도 이슈 없이 처리가 되지만,
실제 documentation을 보면 옵션이 무효화된다는 설명이 있음

빌드 오류가 나도록 해야 하는 게 아닌가 의문이 들지만,
caffeine 관련 설정은 친절하지 않아 옵션 넣을 시 documentation 꼼꼼히 읽어보길 권장합니다.

마치며

로컬 캐시 Caffeine에 대해 기본적인 개념과 적용하기 전 테스트 및 관련 유의 사항에 대해 적어보았습니다. 글 제목과 같이 Caffeine 맛을 좀 느끼셨길 바라며, 신규로 로컬 캐시 적용 시 조금이나마 도움이 되길 바랍니다.

끝.

관련 자료

https://www.sobyte.net/post/2022-04/caffeine/
https://erjuer.tistory.com/m/127
https://gosunaina.medium.com/cache-redis-ehcache-or-caffeine-45b383ae85ee
https://github.com/ben-manes/caffeine/wiki/Benchmarks
https://blog.yevgnenll.me/posts/spring-boot-with-caffeine-cache

--

--