doc/development/caching.md
This document describes the various caching strategies in use at GitLab, how to implement them effectively, and various gotchas. This material was extracted from the excellent Caching Workshop.
A faster store for data, which is:
The goal for every web page should be to return in under 100 ms:
Despite downsides to Redis caching, you should still feel free to make good use of the caching setup inside the GitLab application and on GitLab.com. Our forecasting for cache utilization indicates we have plenty of headroom.
Is the cache being added "worthy"? This can be hard to measure, but you can consider:
?performance_bar=flamegraph to the URL to help find
the methods where time is being spent.jq:
tail -f log/development_json.log | jq ".duration_s"tail -f log/api_json.log | jq ".duration_s"development.log:
tail -f log/development.log | grep "cache hits"tail -f log/development.log | grep "Rendered "binding.pry to poke about in live requests. This requires a
foreground web process.?performance_bar=flamegraph to the pagepublic setting.@article ||= Article.find(params[:id])strong_memoize_attr :method_nameGitlab::SafeRequestStore.fetchThis is well-documentation in the Rails guides
cache in views, which is almost an alias for:Rails.cache.fetch, which you can use everywhere.cache includes a "template tree digest" which changes when you modify your view files.expires_inThis sets the Time To Live (TTL) for the cache entry, and is the single most useful (and most commonly used) cache option. This is supported in most Rails caching helpers.
The TTL, if not set with expires_in,
defaults to 8 hours.
Consider using an 8 hour TTL for general caching, as this matches a workday and would mean that a user would generally only have one cache-miss per day for the same content.
When writing large amounts of data, consider using a shorter expiry to decrease its impact on the memory usage.
race_condition_ttlThis option prevents multiple uncached hits for a key at the same time. The first process that finds the key expired bumps the TTL by this amount, and it then sets the new cache value.
Used when a cache key is under very heavy load to prevent multiple simultaneous writes, but should be set to a low value, such as 10 seconds.
Rails.cache uses Redis as the store.
GitLab instances, like GitLab.com, can configure Redis for key eviction.
See the Redis development guide.
Use conditional GET caching when the entire response is cacheable:
updated_at value for the etag.This is no longer very commonly used in the Rails world:
cache_action.All the time!
cached: true on them.StrongMemoize.Gitlab::Cache.fetch_once works.Gitlab::Cache::JsonCache
and Gitlab::SafeRequestStore, for example, can lead to stale data issues
where the cache data doesn't have the appropriate value for the new attribute
(see this past incident).Rails uses this automatically for identical queries in a request, so no action is needed for that use case.
identity_cache has a different purpose: caching queries
across multiple requests.Article.find(params[:id]).If you've exhausted other options, and must cache something that's really awkward, it's time to look at a custom solution:
RepositorySetCache, RepositoryHashCache and AvatarCache.merged_branch_names,
using RepositoryHashCache.In short: the oldest stuff is replaced with new stuff:
allkeys-lru, which is functionally similar to Memcached.UNLINK instead of DEL now, which allows Redis to
reclaim memory in its own time, rather than immediately.
cache_key_with_version and cache_key.
The first one is used by default in version 5.2 and later, and is the standard behavior from before;
it includes the updated_at timestamp in the key.Example found in the application.log:
cache(@project, :tag_list)
views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29/projects/16-2021031614242546945
2/tag_list
views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29updated_at values
projects/16-20210316142425469452tag_listupdated_at field to changeGrape::Entity makes effective caching extremely difficult in the API layer. More on this later.break or return inside the fragment cache helper in views - it never writes a cache entry.nil and swapping them around.{ project: nil } instead.#cache_key on members of an array to find the keys, but it doesn't call it on values of hashes.