💾

Solid Cache (Rails)

Redis 대신 SSD — 데이터베이스 기반 Rails 캐시 스토어

GitHub: rails/solid_cache

Solid Cache는 37시그널스가 만든 DB 기반 캐시 스토어입니다. "메모리(RAM)는 비싸고 SSD는 싸다"는 단순한 관찰에서 출발합니다. Redis의 메모리 제한(수 GB) 대신 SSD(수 TB)를 활용하면, 훨씬 큰 캐시를 훨씬 저렴하게 운영할 수 있습니다.


핵심 아이디어

캐시 항목을 DB 테이블의 행(row)으로 저장합니다.

solid_cache_entries
├── key        # 캐시 키 (문자열)
├── key_hash   # SHA256 → signed int64 (인덱스용)
├── value      # 직렬화된 캐시 값 (blob)
├── byte_size  # 값의 바이트 크기
└── created_at # 생성 시각 (별도 인덱스 없음, ID 순서 = 시간 순서)


아키텍처

app/models/solid_cache/
└── entry.rb                    # AR 모델 (캐시 항목)
    ├── entry/expiration.rb     # 만료 로직 (FIFO 삭제)
    └── entry/size/             # 크기 추정

lib/solid_cache/
├── store.rb                    # ActiveSupport::Cache::Store 서브클래스
├── store/
│   ├── api.rb                  # read/write/delete 구현
│   ├── connections.rb          # DB 연결 관리
│   ├── entries.rb              # Entry 모델 호출
│   ├── expiry.rb              # 쓰기 기반 만료 트리거
│   ├── failsafe.rb            # 에러 핸들링
│   └── stats.rb               # 통계
├── connections/
│   ├── single.rb              # 단일 DB
│   ├── sharded.rb             # 샤딩 (Maglev 해싱)
│   └── unmanaged.rb           # 외부 관리
└── maglev_hash.rb             # Google Maglev 일관 해싱


캐시 읽기/쓰기

쓰기 (write)

# Rails.cache.write('key', value)
Entry.write_multi(entries)
# → upsert_all로 일괄 삽입/업데이트 (1000건 배치)
# → 쓰기 후 확률적으로 만료 트리거

읽기 (read)

# Rails.cache.read('key')
Entry.read_multi(keys)
# → key_hash IN 절로 조회 (SHA256 해시 인덱스)
# → uncached(dirties: false)로 쿼리 캐시 비활성화


만료 메커니즘 (핵심!)

Redis의 TTL 방식이 아닌, 쓰기 빈도에 비례한 확률적 만료를 사용합니다:

쓰기 발생
  │
  ├─ track_writes(count) — 카운터 증가
  │
  ├─ 카운터가 batch_size의 50% 도달?
  │   ├─ Yes → expire_later 실행
  │   └─ No  → 다음 쓰기까지 대기
  │
  └─ 만료 실행 (비동기 스레드 또는 Job)
      ├─ max_age 초과 항목 삭제
      ├─ max_entries 초과 시 가장 오래된 항목 삭제 (FIFO)
      └─ max_size 초과 시 크기 기반 삭제

EXPIRY_MULTIPLIER = 2: 쓰기 1건당 2건분의 만료 압력을 가합니다. 캐시가 무한히 커지지 않도록 쓰기보다 빠르게 정리합니다.

만료 시 동시 작업 충돌 방지: 후보의 3배를 가져온 뒤 랜덤 샘플링으로 선택합니다.


샤딩 (Maglev Consistent Hashing)

Google Maglev 논문 기반의 일관 해싱으로 여러 DB에 캐시를 분산합니다:

  • 테이블 크기 2053 (소수), CRC32로 키 해싱

  • 샤드 추가/제거 시 최소한의 키만 재분배

  • database.yml에서 여러 DB를 설정하면 자동 샤딩


설정 옵션

옵션 기본값 설명
max_age 2주 최대 캐시 수명
max_entries 무제한 최대 항목 수
max_size 무제한 최대 캐시 크기
expiry_batch_size 100 한 번에 만료시킬 항목 수
expiry_method :thread 만료 방식 (:thread 또는 :job)

Redis vs Solid Cache

Redis Solid Cache
저장소 RAM (비쌈) SSD (저렴)
캐시 크기 ~수 GB ~수 TB
속도 ~0.1ms ~1-5ms
만료 TTL 기반 쓰기 비례 확률적
운영 Redis 서버 필요 DB만 필요
복잡성 인프라 추가 제로

트레이드오프: 약간 느리지만(ms 수준), 훨씬 큰 캐시를 훨씬 저렴하게 운영할 수 있습니다. 대부분의 Rails 앱에서 이 차이는 체감되지 않습니다.

구조 다이어그램

캐시 저장 구조

💾 solid_cache_entries entry.rb ↗
key string
캐시 키
key_hash int64
SHA256 → signed int
value blob
직렬화된 값
byte_size integer
값 크기 (만료용)
INDEX: key_hash (검색) / ID 순서 = 시간 순서 (만료)
핵심: <strong>key_hash</strong>로 빠른 검색 + <strong>ID 순서</strong>로 FIFO 만료 (created_at 인덱스 불필요)

캐시 읽기/쓰기 플로우

쓰기 (Write)
1. Rails.cache.write('key', value)
2. Entry.write_multi → upsert_all code ↗
3. track_writes → 카운터 증가
확률적 만료 트리거? expiry.rb ↗
읽기 (Read)
1. Rails.cache.read('key')
2. LocalCache 확인 (메모리 1차 캐시)
3. Entry.read_multi → key_hash IN (?)
uncached(dirties: false)
쿼리 캐시 OFF → 항상 최신

만료 메커니즘 (TTL 아닌 확률적 만료)

write
write
write
write
50%!
↓ batch_size의 50% 도달
expire_later (비동기) expiration.rb ↗
max_age
2주 초과 삭제
max_entries
ID 순 FIFO 삭제
max_size
크기 기반 삭제
EXPIRY_MULTIPLIER = 2 → 쓰기 1건당 2건분 만료 압력

Redis vs Solid Cache

Redis
Solid Cache
저장소
RAM (비쌈)
SSD (저렴)
캐시 크기
~수 GB
~수 TB
속도
~0.1ms
~1-5ms
만료
TTL 기반
확률적 FIFO
운영
Redis 서버 필요
DB만 필요
샤딩
Redis Cluster
Maglev 해싱

샤딩 (Maglev Consistent Hashing)

Rails.cache.write('user:123', data)
MaglevHash
CRC32('user:123') → 테이블[2053] → shard 결정
maglev_hash.rb ↗
Shard 1
cache_db_1
Shard 2
cache_db_2
Shard 3
cache_db_3
핵심: 샤드 추가/제거 시 <strong>최소한의 키만 재분배</strong> (일관 해싱)

핵심 포인트

1

GitHub에서 rails/solid_cache 저장소 열기

2

app/models/solid_cache/entry.rb → 캐시 항목 모델 분석

3

app/models/solid_cache/entry/expiration.rb → 만료 로직 분석

4

lib/solid_cache/store.rb → ActiveSupport::Cache::Store 서브클래스 구조

5

lib/solid_cache/store/expiry.rb → 쓰기 기반 확률적 만료 트리거

6

lib/solid_cache/maglev_hash.rb → 일관 해싱 구현 분석

7

lib/solid_cache/connections/ → 단일 DB vs 샤딩 연결 관리

8

db/migrate/ → solid_cache_entries 테이블 스키마 확인

장점

  • Redis 불필요 — 운영 인프라 단순화
  • SSD 활용 — RAM보다 훨씬 큰 캐시 가능
  • ActiveSupport 호환 — Rails.cache API 그대로 사용
  • 별도 설정 없이 Rails 8에서 기본 동작
  • Maglev 해싱 — 우아한 샤딩 지원
  • 암호화 지원 — ActiveRecord Encryption 활용

단점

  • Redis보다 느림 (RAM ~0.1ms vs SSD ~1-5ms)
  • DB 부하 증가 — 별도 캐시 DB 권장
  • 확률적 만료 — TTL처럼 정확한 만료 시점 보장 안 됨
  • 고성능 실시간 시스템에는 Redis가 여전히 유리
  • 쿼리 캐시 비활성화로 항상 DB 히트

사용 사례

Redis 제거: 인프라 단순화 (DB만으로 캐시) 대용량 캐시: SSD 기반으로 수 TB 캐시 가능 설치형 앱: ONCE 같은 단일 서버 앱에서 Redis 없이 운영 Rails.cache.fetch 패턴: 기존 코드 변경 없이 백엔드만 교체 샤딩: 여러 DB에 캐시를 분산하여 성능 확보