BE-commerce 프로젝트
약 5분 분량 성능·용량

캐시 값을 몇 바이트부터 압축할까: 교차점은 쟀고, 임계값은 거기에 두지 않았다

목차

Redis 캐시 값을 압축하면 187B에서는 원본보다 7% 커지고 1KB에서는 66% 줄었습니다. 손익이 갈리는 교차점은 187B와 279B 사이였지만 압축을 시작하는 임계값은 1KB로 정했습니다. 코덱은 통념과 달리 SNAPPY가 LZ4보다 CPU를 덜 썼고 그 이유는 확인하지 못했습니다. 그리고 값은 정해 두고 압축을 켜지 않았습니다. 지금 압축할 수 있는 캐시가 없기 때문입니다.

결론부터

  • 압축은 187B에서는 오히려 7% 커졌고 1KB에서는 66% 줄었습니다.
  • 교차점은 187B와 279B 사이였지만, 운영 임계값은 여유를 둔 1KB로 정했습니다.
  • 현재 캐시는 대부분 1KB보다 작거나 압축할 수 없는 구조라 코드를 켜지 않았습니다.

압축 임계값은 “작을수록 좋다”가 아니라 저장 공간·네트워크·CPU 중 실제 병목이 무엇인지 측정한 뒤 정하는 운영 정책입니다.

지금 캐시에 무엇이 있나

캐시는 DB 조회를 없애 주지만 네트워크와 메모리 비용까지 없애 주지는 않습니다. 값이 커지면 Redis 왕복이 꼬리에서 튀고 메모리도 그만큼 씁니다. 압축은 둘을 줄이는 대신 CPU를 쓰고 값이 작으면 오히려 커집니다. 정할 것은 하나였습니다. 몇 바이트를 넘을 때부터 압축할 것인가.

측정은 개인화 컨텍스트와 같은 모양의 JSON 값으로, 로컬 Redis에 한 요청씩 보내며 했습니다.

우리 캐시에는 무엇이 있나

먼저 밝혀 둘 것이 있습니다. 지금 BE-commerce에는 1KB를 넘는 값을 담는 Redis 캐시가 없습니다.

캐시어디에담는 값압축 대상인가
로그인 토큰(갱신·폐기)Redis사용자 id와 역할 문자열, 수십 바이트아니다. 1KB에 한참 못 미친다
대기열 통과 표식Redis"1"아니다
추천 컨텍스트Redis사용자 활동 JSON넣을 수 없다. Redis 안의 Lua 스크립트가 값을 읽어 병합한다
패싯 집계앱 메모리집계 객체해당 없다. 직렬화하지 않는다

그래서 아래 표의 187B~500KB는 추천 컨텍스트와 모양이 같게 만든 값입니다. 실제 캐시에 들어 있는 값의 크기를 잰 것은 아닙니다. 이 글은 “큰 값을 캐싱하게 되면 몇 바이트부터 압축할 것인가”를 미리 정해 둔 기록입니다.

DoorDash는 무엇을 캐싱하다 압축했나

DoorDash는 식당 메뉴를 JSON으로 직렬화해 Redis에 캐싱했습니다. Chick-fil-A 메뉴가 64,220바이트, Cheesecake Factory 메뉴가 350,333바이트였습니다. LZ4로 각각 10,199바이트와 67,863바이트, Snappy로 11,414바이트와 77,048바이트까지 줄었습니다. zlib, zstd, brotli는 압축 해제 속도와 CPU 부담 때문에 빼고 LZ4를 골랐습니다.

압축이 이득을 보는 자리는 이렇게 수십~수백 KB짜리 값을 자주 읽는 곳입니다. 아래 측정에서도 50KB와 500KB 구간의 이득이 가장 컸습니다. 기본 임계값도 비슷한 곳에 있습니다. memcached PHP 확장은 2,000바이트를 넘는 값만 압축합니다. 우리 캐시는 모두 1KB 아래이거나 압축할 수 없는 자리라 지금 켜면 이득 없이 설정만 늘어납니다.

압축은 작은 값에서 손해다. 187B에서 LZ4와 SNAPPY 모두 저장/원본 1.070, 279B부터 이득, 1.1KB에서 0.338. 교차점은 사실이고 임계값 1KB는 결정이다

작은 값에서는 압축이 손해였다

원본 크기LZ4 저장/원본SNAPPY 저장/원본
187B1.0701.070
279B0.8170.846
553B0.4990.521
1.1KB0.3380.352
50KB0.1680.163
500KB0.1660.170

187B에서 두 코덱 모두 원본보다 7% 커졌습니다. 압축 결과에 붙는 표식과, 이 저장소가 Redis에 문자열만 넣기 때문에 생기는 base64 팽창(33%)이 작은 값에서는 줄인 양보다 큽니다.

큰 값에서 얻는 것은 저장량만이 아니었습니다. 500KB 값의 get p99가 압축 없이 2.02ms, LZ4로 0.27ms였습니다. CPU를 쓰더라도 바이트가 줄어 왕복이 짧아지기 때문입니다. 캐시 값이 커질 때 문제가 되는 것은 평균보다 이 꼬리입니다.

교차점과 임계값은 다르다

교차점은 187B와 279B 사이에 있습니다. 그런데 임계값을 그 근처에 붙이지 않았습니다. 교차점 근처의 이득은 몇 %뿐이라서 값의 압축률이 조금만 달라져도 손해로 뒤집힙니다. 압축률은 값의 성질이지 우리가 정하는 값이 아닙니다. 1KB에서는 66%가 줄어 이득이 값의 성질에 크게 기대지 않고 CPU 차이도 500회에 2ms로 작았습니다.

버린 안이유
임계값을 교차점(약 240B)에근처 이득이 몇 %라 값의 성질에 기대는 결정이 된다
항상 압축(임계값 0)187B에서 저장량이 7% 늘어난다. 아끼는 것 없이 CPU만 쓴다
50KB(목업 화면에 있던 값)실측 교차점보다 200배 높다. 279B~50KB 구간에서 줄일 수 있는 50~83%를 그냥 버린다

교차점은 측정한 사실이고 임계값은 그 사실 위에서 여유를 두고 고른 결정입니다. 둘을 같은 값으로 두면 측정 오차가 곧 결정의 오차가 됩니다.

코덱 선택을 실측이 뒤집었다

코덱 선택 기준은 측정 전에 정해 두었습니다. 저장량이 비슷하면 CPU와 지연이 낮은 쪽을 고르기로 했습니다. 교과서적으로는 LZ4가 더 빠르다고 알려져 있습니다. 500KB 값으로 500번 왕복했을 때 CPU 시간은 SNAPPY 242ms, LZ4 359ms였습니다. 저장량은 50KB 이하에서 LZ4가 몇 % 낫고 50KB에서는 SNAPPY가 조금 나았으므로 사실상 같았습니다. 기준에 따라 SNAPPY를 골랐습니다.

이유는 나중에 확인했습니다(#346). 앱을 실행하는 jar 안에서 LZ4는 네이티브(JNI)가 아닌 Java 구현으로 돌고 있었습니다. lz4-java는 시스템 클래스로더가 올린 경우에만 네이티브를 쓰는데 스프링 부트 실행 jar는 자체 클래스로더로 올립니다. Snappy는 앱 안에서도 네이티브였습니다. 그래서 처음 측정은 네이티브 Snappy와 Java LZ4의 비교였습니다.

같은 라이브러리를 단독으로 재면 LZ4도 네이티브로 돌았고 그래도 Snappy가 500KB 왕복에서 8.7% 쌌습니다. 코덱 결정은 그대로 두고 코덱이 어떤 구현으로 도는지 기동 로그에 남기게 했습니다. 같은 코덱 이름이라도 실행 방식에 따라 순서가 바뀌므로 DoorDash의 결론(LZ4)을 그대로 가져오지 않고 우리 실행 환경에서 잰 값을 따랐습니다.

값은 정했고 켜지 않았다

압축은 기본으로 꺼 둡니다(NONE). 켤 때 쓸 값(SNAPPY, 1,024바이트)만 설정에 준비했습니다. 근거는 생겼지만 적용할 캐시가 없었습니다.

  • 가장 먼저 떠오르는 자리는 사용자 컨텍스트 캐시입니다. 그런데 이 캐시는 Redis Lua 스크립트가 값을 읽어 늦게 온 이벤트를 병합합니다. 값을 압축하면 스크립트의 JSON 파싱이 깨집니다. 병합을 원자적으로 만들기 위해 고친 구조를 되돌리지 않고는 압축을 넣을 수 없습니다.
  • 켜 봐야 아무 값도 압축되지 않는다면 “압축을 켰다”는 설정이 거짓이 됩니다.

첫 적용 후보는 홈 화면 조립 결과 캐시처럼 크고 스크립트가 들여다보지 않는 값입니다. 켤 때는 p95와 메모리를 다시 재고 임계값을 확정합니다.

이 측정이 말하지 않는 것

  • 실제 캐시 값의 크기 분포를 잰 것이 아닙니다. 1KB를 넘는 캐시가 생기면 그 값으로 다시 잽니다.
  • Redis가 로컬이라 전송량 감소가 루프백 위의 값입니다. 원격 Redis라면 줄어든 바이트가 지연으로 더 크게 돌아와 임계값이 더 내려갈 것입니다. 그래서 1KB는 상한으로 읽습니다.
  • 컨텍스트 JSON 모양만 쟀습니다. 이미지나 임베딩처럼 엔트로피가 높은 값은 교차점이 훨씬 오른쪽에 있거나 아예 이득이 없을 수 있습니다.
  • 동시 부하를 주지 않았습니다. 압축은 CPU를 쓰므로 요청이 몰리면 압축 자체가 대기열을 만들 수 있습니다.
  • 바이너리 직렬화로 바꾸면 base64 팽창이 사라져 임계값이 내려갑니다.

참고

프로필 사진
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.