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