accelerating rails with edge caching
DESCRIPTION
In this talk, we'll cover all the great built-in rails caching options and best practices for getting the most out of these. Then we'll talk about dynamic content, why it's traditionally not cached, and how you can can cache it using this thing called "edge caching". Welcome to the future, where you can cache the uncacheable.TRANSCRIPT
Accelerating Rails with edge cachingMichael May | @ohaimmay | SFRails | 10/16/2014
Topic
• Rails caching best practices
• Dynamic content/caching
• Edge caching
• Edge caching dynamic content with Rails
Rails caching
• Query/SQL
• Page/Action (removed from Rails 4 core)
• Asset
• Fragment
config.action_controller.perform_caching = true
Rails caching
Query caching
• Automagically done by rails when perform_caching = true
• Not cached between requests!
• Could just store the query result in a variable
class Product < MyModel
def self.out_of_stock Rails.cache.fetch("out_of_stock", expires_in: 1.hour) do Product.where("inventory.quantity = 0") end end
end
Manual Query Caching
Asset Caching• Serve static assets from a proxy
• config.serve_static_assets = false
• Enable Compression*
• config.assets.compress = true
• # In Rails 4
• config.assets.css_compressor = :yui
• config.assets.js_compressor = :uglifier
• Asset Digests
• config.assets.digest = true
https://fast.mmay.rocks/assets/catzlol-75408509152249b79b818b252da51bc4.png
Enable Compression*
* http://robots.thoughtbot.com/content-compression-with-rack-deflater
module FastestAppEver
class Application < Rails::Application config.middleware.use Rack::Deflater end
end
Compress HTML, JSON responses at runtime
Asset Caching
• Configure an asset host if needed
• config.action_controller.asset_host = ENV[‘FASTLY_CDN_URL']
• Cache-Control like a pro
• config.static_cache_control = 'public, s-maxage=15552000, maxage=2592000'
Cache-Controlpublic, s-maxage=15552000, maxage=2592000
public“please cache me”
maxage=2592000“keep me for 30 days”
s-maxage=15552000“PROXIES ONLY! - Keep me for 180 days”
Keepin’ it fresh• stale-while-revalidate
• Serve the current (stale) version for n seconds while it re-fetches the latest version in the background
• Cache-Control: max-age=604800, stale-while-revalidate=86400
• stale-if-error
• If the re-fetch fails within n seconds of the response becoming stale, serve the cached response
• Cache-Control: max-age=604800, stale-while-revalidate=86400, stale-if-error=259200
The Vary HTTP Header
• In general, never Vary on anything other than Content-Encoding
• Varying makes it impossible to serve the same response more than once and limits caching benefits
• NEVER Vary on User-Agent!
• There are THOUSANDS of these!
Dynamic Content
Dynamic Content
• Changes are unpredictable!
• user driven events
• Can’t just set a Time To Live (TTL)
Dynamic Content
• Changes are unpredictable!
• user driven events
• Can’t just set a Time To Live (TTL)
• Frequently, but not continuously changing
• Actually static for short periods of time (we can cache static things)!
Dynamic Content Caching
• Usually don’t
• Edge Side Includes (ESI)
• Dynamic Site Acceleration (DSA)
Fragment CachingThe rails answer to caching dynamic
HTML# products/index.html.erb<% cache(cache_key_for_products) do %> <% Product.all.each do |p| %> <%= link_to p.name, product_url(p) %> <% end %> <% end %>
# products_controller.rbdef update … expire_fragment(cache_key_for_products) …end
Nested Fragment Caching
<% cache(cache_key_for_products) do %> All available products: <% Product.all.each do |p| %>
<% cache(p) do %> <%= link_to p.name, product_url(p) %> <% end %>
<% end %><% end %>
Nested Fragment Issues
• Tedious
• Comb through (probably terrible) view code
• Cache keys are weird
• “A given key should always return the same content.” - DHH
• products/15-20110218104500
• “A given key should always return the most up-to-date content.” - Me
• products/15
• Hacks around cache limitations
• Memcache has no wildcard purging!
Nested Fragment Issues
• Garbage left in the cache
• Defaults writing to disk
• Memcached, Redis, etc
• Probably lives in the same DC as your app server
• Distributing, replication takes effort
• What about dynamic API caching?
• “The caching itself happens in the views based on partials rendering the objects in question”
• Take control over your cached data!
Edge Caching
Edge Cachingaka content delivery network
aka CDN
Edge Cache
• Geographically distributed
• Highly optimized storage and network (nanoseconds count)
• Move content physically closer to the end-users
• End goal - DECREASE LATENCY!
The more content we can offload, the better performance
we get
#cachemoney• App servers cost real cash money (not
cache money)
• Less requests back to your application server
• Avoid complex or less efficient strategies
• Edge Side Includes (ESI)
• Fragment caching
Edge caching the dynamic content
Our approach to dynamic content
• Tag content with Surrogate-Key HTTP headers
• Programmatically purge (~150ms globally)
• By Surrogate-Key
• By resource path
• Real-time analytics and log streaming
• Optimize the hell out of the pieces of the network we can control
Tagging responses with Surrogate-Key
class ProductsController < ApplicationController # set Cache-Control, strip Set-Cookie before_filter :set_cache_control_headers,only [:index,:show] def index @products = Product.last(10) # set Surrogate-Key: products set_surrogate_key_header @products.table_key respond_with @products end def show @product = Products.find(params[:id]) # set Surrogate-Key: product/666 set_surrogate_key_header @product.record_key respond_with @product endend
class ProductsController < ApplicationController # set Cache-Control, strip Set-Cookie before_filter :set_cache_control_headers,only [:index,:show] def index @products = Product.last(10) # set Surrogate-Key: products set_surrogate_key_header @products.table_key respond_with @products end def show @product = Products.find(params[:id]) # set Surrogate-Key: product/666 set_surrogate_key_header @product.record_key respond_with @product endend
class ProductsController < ApplicationController # set Cache-Control, strip Set-Cookie before_filter :set_cache_control_headers,only [:index,:show] def index @products = Product.last(10) # set Surrogate-Key: products set_surrogate_key_header @products.table_key respond_with @products end def show @product = Products.find(params[:id]) # set Surrogate-Key: product/666 set_surrogate_key_header @product.record_key respond_with @product endend
Purge on updates
class ProductsController < ApplicationController def create @product = Product.new(params) if @product.save # purge Surrogate-Key: products @product.purge_all render @product end end ...
def update @product = Product.find(params[:id]) if @product.update(params) # purge Surrogate-Key: product/666 @product.purge render @product endend
fastly-railsgithub.com/fastly/fastly-
rails
Edge caching in practice
Be aware of cookies
• Nothing with a Set-Cookie header is cached (by default)
• Authentication frameworks/middleware might inject Set-Cookie after the rails stack removes it
• Avoid caching pains by knowing when, where, and how you use Set-Cookie
edge scripting
URL Rewriting
• Apache, nginx, etc support URL Rewriting
• Filter bad requests
• Normalize paths
URL Rewrite at the edge
• Varnish HTTP cache
• VCL
• Requests never hit origin!
#winning
meh
yay
What can we do better?
• Add better caching defaults?
• Cache-Control, stale-while-revalidate, stale-if-error
• Re-use existing rails cache interfaces for edge caching?
• ActiveSupport::Cache::EdgeStore
• More fine-grained integration with HTTP accelerators like Varnish?
Takeaways• Rails has tons of built-in caching options
• Get fancy with Cache-Control directives
• Use Google PageSpeed Insights (chrome plugin adds it to dev tools)
• Dynamic edge caching is all about the power of purge!
• Similar school of thought to rails action caching
Questions?
Contact: Michael May | @ohaimmay | [email protected]
cool links:fastly-rails - github.com/fastly/fastly-railssurrogate keys - fastly.com/blog/surrogate-keys-part-1cache-control tutorial - docs.fastly.com/guides/tutorials/cache-control-tutorialserve stale cache-control - fastly.com/blog/stale-while-revalidatevary header best practices - fastly.com/blog/best-practices-for-using-the-vary-headercaching like & share buttons - fastly.com/blog/caching-like-and-share-buttonspagespeed insights - developers.google.com/speed/pagespeed