RUBY ON RAILS · 17 MIN READ ·

Rails 8 Solid Cache: Productieopzet, TTL-strategieën en Migreren vanaf Redis

Rails 8 Solid Cache in productie: opzetgids met databasesizing, TTL- en evictiestrategieën, migreren vanaf Redis en monitoren van de cache hit rate.

Rails 8 Solid Cache: Productieopzet, TTL-strategieën en Migreren vanaf Redis

De Redis-instance van een van mijn klanten kostte $340 per maand op ElastiCache en bevatte ongeveer 900 MB aan cachedata. Toen Rails 8 Solid Cache als standaard cache-backend uitbracht, duurde het gesprek met hun CTO ongeveer vier minuten: “Dus we kunnen het Redis-cluster verwijderen en de cache op de Postgres-database zetten die we toch al draaien?” Ja. En twee weken later ging die ElastiCache-rekening naar nul.

Rails 8 Solid Cache is een database-backed cache store die in dezelfde relationele database leeft die je al gebruikt voor je applicatie. Het vervangt Redis of Memcached voor het merendeel van de Rails-applicaties die ze puur als Rails-cache gebruikten — niet als Sidekiq-queue, niet als Pub/Sub-bus, gewoon als Rails.cache. Als Redis nog zo’n stuk infrastructuur was dat je ops-team onderhield voor wat neerkwam op een fancy key-value tabel, dan kun je het met Solid Cache verwijderen.

Na negentien jaar Rails in productie draaien, deden de meeste Redis-clusters die ik in Rails-apps heb gezien exact dit: fragmenten cachen, dure queries memoizen en sessiedata bewaren. Niks daarvan had sub-milliseconde latency nodig. Niks daarvan had de operationele overhead van een tweede datastore nodig.

Wat Rails 8 Solid Cache Werkelijk Is

Solid Cache is een Rails.cache-implementatie gebaseerd op SQL. Het bewaart elke cache-entry als een rij in een tabel met een byte-array value-kolom, een expiration-timestamp en een hash-based key index. Reads zijn single-row lookups op primary key. Writes zijn inserts met ON CONFLICT DO UPDATE. Expiration en evictie gebeuren via een achtergrond-sweep die binnen je Rails-proces draait — geen cron job, geen aparte worker.

Drie dingen maken het levensvatbaar als Redis-vervanging in plaats van speelgoed:

  • Encryptie at rest — cache values kunnen versleuteld worden voor ze op disk komen via ActiveRecord::Encryption. Sessietokens en gecachte user data zijn beschermd, zelfs als iemand een database-dump te pakken krijgt.
  • LRU-achtige evictie met size targets — je geeft Solid Cache het maximale aantal rijen of de maximale byte-grootte, en het evicteert de minst recent aangeraakte entries wanneer het target wordt overschreden. Geen ongebreidelde groei.
  • Multi-database support — de cache-tabel kan in een andere database staan dan je applicatiedata. In productie zet ik hem altijd op een eigen Postgres-instance of eigen database op een gedeeld cluster.

Het performance-plafond ligt lager dan Redis. Een single-row Postgres lookup met een primary key hit duurt 0,5–2 ms end-to-end vanuit Rails; de equivalente Redis GET duurt 0,2–0,5 ms. Voor fragment caches, view caches en gememoizede query results is het verschil ruis. Voor iets dat atomic increments duizend keer per seconde vereist, gebruik Redis.

Rails 8 Solid Cache Opzetten op een Nieuwe App

Rails 8 komt standaard met Solid Cache geconfigureerd wanneer je een nieuwe app genereert. De solid_cache gem staat in de Gemfile, en config/cache.yml ziet er zo uit:

# config/cache.yml
default: &default
  store_options:
    max_age: <%= 60.days.to_i %>
    max_size: <%= 256.megabytes %>
    namespace: <%= Rails.env %>

development:
  <<: *default

test:
  <<: *default

production:
  database: cache
  <<: *default

De database: cache regel verwijst naar een entry in config/database.yml. Voeg een aparte cache-database configuratie toe:

# config/database.yml
production:
  primary:
    <<: *default
    database: myapp_production
  cache:
    <<: *default
    database: myapp_production_cache
    migrations_paths: db/cache_migrate

Maak het schema aan en draai de Solid Cache migrations:

bin/rails db:create
bin/rails solid_cache:install:migrations
bin/rails db:migrate:cache

Dat is de complete setup. Rails.cache.read("key") en Rails.cache.write("key", value, expires_in: 1.hour) raken nu de Postgres cache-database. Er draait geen Redis-proces.

De Cache-Database Sizen voor Productie

De enige vraag die je Solid Cache-opzet bepaalt is: hoe groot mag de cache-tabel groeien? Als je dit fout hebt, evicteer je constant productieve cache-entries of laat je de cache-tabel opzwellen en je database-backups vertragen.

De formule die ik gebruik voor Rails 8 Solid Cache-sizing is:

max_size = (gemiddelde entry-grootte) × (aantal entries in working set) × 1,3 safety factor

Voor een typische Rails-applicatie met page fragment caching, gecachte query results en Russian-doll caching komt de gemiddelde entry-grootte uit rond 4–12 KB en de working set op 20.000–100.000 live entries. Dat geeft je een cache-grootte tussen 100 MB en 1,5 GB. Bij de klant die ik eerder noemde stelden we max_size: 768.megabytes in en de cache stabiliseerde op 620 MB met een hit rate van 94%.

Het size-target wordt afgedwongen door een achtergrond-trimmer:

# config/cache.yml — volledige productieconfig
production:
  database: cache
  store_options:
    max_age: <%= 30.days.to_i %>
    max_size: <%= 768.megabytes %>
    namespace: <%= Rails.env %>
    expiry_batch_size: 100
    expiry_method: :thread
    trim_batch_size: 100
    encrypt: true
    shards:
      - cache_primary
      - cache_secondary

Twee knoppen zijn hier in de praktijk belangrijk:

  • expiry_method: :thread draait de trimmer in een achtergrond-thread binnen elke Puma-worker. Er is geen apart expiry-proces. Op een 4-worker Puma-setup coördineren vier threads via SQL row locks op de solid_cache_entries tabel.
  • expiry_batch_size en trim_batch_size bepalen hoeveel rijen per sweep-pas worden verwijderd. De defaults (100) zijn prima voor cache-tabellen onder 1 GB. Verhoog ze naar 1000 als je cache groter is en expiration achterloopt op writes.

Encryptie voegt 20–30% write-overhead toe en een vergelijkbare read-overhead. Op cache-workloads gedomineerd door hits verschijnt dit als een 5–10% latency-bump in de mediaan. Zet het aan als je iets cachet dat gebruikers identificeert, en zet het nooit lichtvaardig uit — encrypt achteraf omdraaien laat je met onleesbare historische entries zitten.

Migreren van Redis naar Solid Cache

Het migratiepad dat ik gebruik voor bestaande Rails-applicaties op Redis heeft vier fases. Sla er geen enkele over.

Fase 1: Solid Cache naast Redis toevoegen

Installeer de gem, draai migrations, maar houd Redis als config.cache_store in productie. Solid Cache is aanwezig maar inactief:

# config/environments/production.rb
config.cache_store = :redis_cache_store, {
  url: ENV["REDIS_URL"],
  expires_in: 1.day
}

Ship dit naar productie en verifieer dat migrations schoon zijn gedraaid.

Fase 2: Solid Cache in een shadow-write patroon inschakelen

Wrap Rails.cache in een class die naar zowel Redis als Solid Cache schrijft maar alleen van Redis leest. Zo kun je valideren dat Solid Cache writes ontvangt met de verwachte snelheid zonder gebruikerslatency te beïnvloeden:

# lib/dual_write_cache.rb
class DualWriteCache
  def initialize(primary:, shadow:)
    @primary = primary
    @shadow  = shadow
  end

  def write(key, value, options = nil)
    result = @primary.write(key, value, options)
    Thread.new { @shadow.write(key, value, options) rescue nil }
    result
  end

  def read(key, options = nil)
    @primary.read(key, options)
  end

  def fetch(key, options = nil, &block)
    @primary.fetch(key, options) do
      value = block.call
      Thread.new { @shadow.write(key, value, options) rescue nil }
      value
    end
  end

  # Delegeer alles anders naar primary
  def method_missing(method, *args, &block)
    @primary.public_send(method, *args, &block)
  end
end

Wire het aan in production.rb:

redis_cache = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
solid_cache = ActiveSupport::Cache::SolidCacheStore.new
config.cache_store = DualWriteCache.new(primary: redis_cache, shadow: solid_cache)

Draai dit een week. Kijk hoe de row count van solid_cache_entries groeit. Bevestig dat de dagelijkse write rate overeenkomt met wat je Redis-cluster rapporteert.

Fase 3: Het read-pad omdraaien

Wissel primary en shadow. Nu bedient Solid Cache reads en is Redis de fallback. Bij een cache miss in Solid Cache leest de class Redis en vult Solid Cache aan:

def read(key, options = nil)
  value = @primary.read(key, options)
  return value unless value.nil?

  redis_value = @shadow.read(key, options)
  @primary.write(key, redis_value, options) if redis_value
  redis_value
end

Kijk goed naar je cache hit rate-metric. Die zakt in het eerste uur terwijl Solid Cache zich vult uit Redis-reads. Als de steady-state hit rate meer dan 3 procentpunten onder je Redis-baseline ligt, is je max_size te klein — verhoog hem voor je verder gaat.

Fase 4: Redis verwijderen

Na een week op Solid Cache-reads met Redis als fallback en geen incidenten, vervang je de DualWriteCache door de gewone Solid Cache store en beëindig je het Redis-cluster:

config.cache_store = :solid_cache_store

Verwijder de ElastiCache-instance. Zeg de rekening op. Update config/database.yml om de Redis URL te verwijderen. Dit is het moment waarop ik meestal een screenshot maak voor het cost-dashboard van de klant.

Cache Hit Rate Monitoren in Productie

De enige productiemetric die je vertelt of je cache gezond is, is hit rate. Onder de 80% en je cache is waarschijnlijk te klein of je TTL’s zijn te agressief. Boven de 99% en je cachet mogelijk dingen die nooit veranderen en statisch zouden moeten zijn.

ActiveSupport::Notifications publiceert cache_read.active_support en cache_write.active_support events voor elke cache-operatie. Subscribe één keer in een initializer en push counters naar je metrics-backend:

# config/initializers/cache_metrics.rb
ActiveSupport::Notifications.subscribe("cache_read.active_support") do |event|
  hit = event.payload[:hit] ? "hit" : "miss"
  StatsD.increment("rails.cache.read", tags: ["result:#{hit}"])
  StatsD.timing("rails.cache.read_duration_ms", event.duration)
end

ActiveSupport::Notifications.subscribe("cache_write.active_support") do |event|
  StatsD.increment("rails.cache.write")
  StatsD.timing("rails.cache.write_duration_ms", event.duration)
end

Bereken hit rate als hits / (hits + misses) in je dashboard, gesegmenteerd per minuut. Alerteer wanneer die langer dan 15 minuten onder de 75% zakt — dat betekent meestal dat de cache te agressief wordt geëvicteerd of dat een deploy een verandering heeft geshipt die een hot key invalideerde.

Voor Solid Cache specifiek, tracken we ook de solid_cache_entries tabel row count en byte size:

# lib/tasks/solid_cache_metrics.rake
namespace :solid_cache do
  task metrics: :environment do
    stats = SolidCache::Entry.connection.execute(<<~SQL).first
      SELECT count(*) as rows, sum(byte_size) as bytes
      FROM solid_cache_entries
    SQL

    StatsD.gauge("solid_cache.entries.count", stats["rows"])
    StatsD.gauge("solid_cache.entries.bytes", stats["bytes"] || 0)
  end
end

Plan die elke minuut via Solid Queue recurring jobs. Als de byte count langere tijd binnen 10% van max_size blijft, werkt de trimmer correct. Als hij max_size overschrijdt en daar blijft, loopt de trimmer achter en moet je trim_batch_size verhogen.

Wanneer Solid Cache Niet te Gebruiken

Solid Cache vervangt Redis-als-cache. Het vervangt geen Redis-als-alles-anders. Houd Redis als je het gebruikt voor:

  • Sidekiq-queues — hoewel, als je Sidekiq ook puur als job queue gebruikt, kijk dan naar migreren naar Solid Queue tegelijkertijd.
  • Rate limiting met atomic counters — Solid Cache’s increment werkt, maar bij honderden ops per seconde wordt het een database hot row.
  • Pub/Sub voor Action Cable — Solid Cable bestaat ook in Rails 8, maar dat is een aparte migratie.
  • Cross-application shared cache — als twee Rails-apps een cache delen, houd ze dan op Redis. Solid Cache is scoped tot één database.

Ik raad Solid Cache ook af op cache-workloads boven ruwweg 5.000 writes per seconde op een gedeelde database. Onder die write rate is de impact op je primary database verwaarloosbaar; erboven zie je wait time op WAL flushes voor de cache-database die doorwerkt in je applicatiequeries. Verhuis de cache in dat geval naar een dedicated Postgres-instance — waar de database: cache config voor is ontworpen.

Het andere geval waarvoor ik Redis houd is intensief gebruik van Rails.cache.write(key, value, race_condition_ttl:). Solid Cache ondersteunt het, maar het write-plus-read-plus-refresh patroon vereist een database-transactie en meerdere round trips. Op Redis is dezelfde operatie één commando. Als de meeste van je cache-writes deze optie gebruiken, benchmark voor je migreert.

De Kosten-en-Complexiteit Rekensom

Voor de meeste Rails-applicaties waar ik mee werk komen de getallen ergens hierop uit. Redis op ElastiCache met een cache.t3.small instance kost $18–25 per maand plus data transfer. Een productie-sized ElastiCache-cluster met een replica is $150–400 per maand. De cache-workload op Postgres voegt een meetbare maar kleine hoeveelheid toe aan je primary database-load — bij de klant die ik noemde waren cache-reads na migratie 8% van hun totale Postgres query count, maar slechts 0,4% van de query-tijd omdat elk een single-row primary-key lookup was.

De operationele simplificatie is groter dan de kostenbesparing. Eén service minder om te monitoren, één set security patches minder, één ding minder dat kan falen tijdens een deploy, één ding minder om over na te denken bij het debuggen van productie. In teams onder de vijf engineers is die reductie in oppervlak meer waard dan de bespaarde dollars.

Rails 8 Solid Cache is niet geschikt voor elke applicatie. Maar voor de standaard Rails-app die Sidekiq-in-Postgres en Redis-als-cache-only draait, is Redis verwijderen een tweeweeks project dat je infrastructuur-footprint permanent verkleint. De tooling is nu goed genoeg om dat een saaie beslissing te maken in plaats van een risicovolle.

FAQ

Is Rails 8 Solid Cache sneller dan Redis?

Nee. Een Redis GET retourneert typisch in 0,2–0,5 ms; een Solid Cache read raakt Postgres en retourneert in 0,5–2 ms. Voor de fragment caching, view caching en gememoizede query results die 95% van Rails cache-workloads uitmaken, is het verschil onzichtbaar in end-to-end response times. Als je applicatie afhankelijk is van sub-milliseconde cache latency voor een specifieke operatie, houd Redis voor die operatie en gebruik Solid Cache voor de rest.

Vereist Solid Cache een aparte database?

Nee, maar ik raad het sterk aan in productie. Je kunt Solid Cache naar je primary applicatiedatabase wijzen en het zal daar de solid_cache_entries tabel aanmaken. Op low-traffic apps is dat prima. Op elke app die meer dan een paar honderd cache-writes per seconde doet, zet de cache-tabel in zijn eigen database — de WAL-activiteit van cache-writes concurreert met je applicatietransacties om I/O. De database: cache config in config/cache.yml regelt dit netjes.

Hoe leeg ik de Solid Cache in productie zonder downtime?

Gebruik Rails.cache.clear vanuit een Rails console — het truncate de solid_cache_entries tabel. Dit is veiliger dan de equivalente Redis FLUSHALL omdat de operatie transactioneel is en andere database-operaties niet blokkeert. Voor een partiële invalidatie, gebruik namespace scoping in Rails.cache.delete_matched (ondersteund in Solid Cache) of verhoog een cache key version constant om misses op de betrokken keys te forceren.

Kan ik Solid Cache gebruiken met een read replica om reads te schalen?

Niet direct. Solid Cache doet zijn eigen routing via Rails multi-database configuratie, maar read replicas van de cache-database verslaan het doel — een cache miss op een replica gevolgd door een write naar de primary nodigt uit tot replication-lag bugs. Als je cache-reads moet schalen, ofwel shard je de cache-database over meerdere writers (Solid Cache’s shards: optie regelt dit), of accepteer je dat één Postgres-instance tienduizenden cache-reads per seconde kan bedienen wanneer de working set in RAM past.

Redis uit een Rails 8-app slopen? TTB Software helpt teams cache-infrastructuur te migreren, hun databases juist te dimensioneren en hun productiestack te simplificeren. Negentien jaar Rails, en ik help je met plezier een service te verwijderen in plaats van er een toe te voegen.

#rails-8-solid-cache #solid-cache-production #rails-solid-cache-vs-redis #solid-cache-migrate-from-redis #rails-8-cache-store-configuration #solid-cache-database-sizing

Related Articles

Laatste sectie. Bel dan alsjeblieft.

Het is een telefoongesprek. Erger dan dat kan het niet worden.

Geen discovery-deck. Geen 45-minuten "kwalificatiegesprek." 30 minuten, jouw probleem, mijn mening. Als we een fit zijn weet je dat in minuut 12.

Directe lijn — Roger neemt zelf op
+31 6 5123 6132
Ma–vr, 09:00–18:00 CET · Nu beschikbaar

OF
info@ttb.software