Pro Yearly is on sale from $80 to $50! »

LinuxTag 2013 - "Cache = Cash"

LinuxTag 2013 - "Cache = Cash"

Ad005fac83baa60843ddf2bc3bc8fe93?s=128

Stefan Wintermeyer

May 24, 2013
Tweet

Transcript

  1. Cache = Cash stefan.wintermeyer@amooma.de @wintermeyer Stefan Wintermeyer

  2. 1 2 3 Why? How? Autobahn!

  3. Today we only discuss dynamic HTML which gets rendered views!

    This talk is about Rails stuff. But you can use the same ideas with PHP, etc.
  4. 1 Why?

  5. a) Save money!

  6. If you can deliver a webpage in half the time

    you only need half the amount of servers.
  7. b) Snappiness

  8. What's the impact of slow sites? Lower conversions and engagement,

    higher bounce rates... Ilya Grigorik @igrigorik Make The Web Faster, Google Have a look at Ilya‘s book!
  9. Performance Related Changes and their User Impact Web Search Delay

    Experiment @igrigorik • The cost of delay increases over time and persists • Delays under half a second impact business metrics • "Speed matters" is not just lip service Type of Delay Delay (ms) Duration (weeks) Impact on Avg. Daily Searches Pre-header 50 4 Not measurable Pre-header 100 4 -0.20% Post-header 200 6 -0.59% Post-header 400 6 -0.59% Post-ads 200 4 -0.30%
  10. Performance Related Changes and their User Impact Server Delays Experiment

    • Strong negative impacts • Roughly linear changes with increasing delay • Time to Click changed by roughly double the delay @igrigorik
  11. Yo ho ho and a few billion pages of RUM

    How speed affects bounce rate @igrigorik
  12. Usability Engineering 101 Delay User reaction 0 - 100 ms

    Instant 100 - 300 ms Feels sluggish 300 - 1000 ms Machine is working... 1 s+ Mental context switch 10 s+ I'll come back later... Stay under 250 ms to feel "fast". Stay under 1000 ms to keep users attention. @igrigorik
  13. For many, mobile is the one and only internet device!

    Country Mobile-only users Egypt 70% India 59% South Africa 57% Indonesia 44% United States 25% onDevice Research @igrigorik
  14. The (short) life of our 1000 ms budget 3G (200

    ms RTT) 4G(80 ms RTT) Control plane (200-2500 ms) (50-100 ms) DNS lookup 200 ms 80 ms TCP Connection 200 ms 80 ms TLS handshake (200-400 ms) (80-160 ms) HTTP request 200 ms 80 ms Leftover budget 0-400 ms 500-760 ms Network overhead of one HTTP request! @igrigorik The broswer needs 100 - 150ms to render the page.
  15. 2 How?

  16. Raspberry Pi

  17. Software Stack • Ruby on Rails 3.2 (no need to

    wait till 4.0) • Ruby 1.9.3 • Nginx • Unicorn • MySQL • Raspbian Wheezy
  18. Example: Online Shop

  19. The Models

  20. Cart state string LineItem price decimal (8,2) quantity integer Category

    name string Product description text name string price decimal (8,2) DiscountGroup discount integer name string User email string ∗ encrypted_password string ∗ first_name string last_name string Rating value integer
  21. The UI

  22. None
  23. current_user

  24. None
  25. None
  26. None
  27. Bob‘s cart

  28. None
  29. Discounted price

  30. Rate this product

  31. Measure the Success

  32. Watir script http://watir.com

  33. require 'watir-webdriver' url = 'http://ip.address:3000' b = Watir::Browser.new # An

    anonymous user looks around. # ['AA', 'AB', 'AH'].each do |product_name| b.goto url Watir::Wait.until { b.link(:text, product_name).exist? } b.link(:text, product_name).click Watir::Wait.until { b.link(:text, 'Webshop').exist? } b.link(:text, 'Webshop').click end 3 different products
  34. None
  35. bob@aol.com

  36. Login Bob b.goto url b.goto "#{url}/users/sign_in" b.text_field(:id, "user_email").set("bob@aol.com") b.text_field(:id, "user_password").set("123")

    b.button(:name, "commit").click
  37. Bob looks around too ['AA', 'AB', 'AH'].each do |product_name| b.goto

    url Watir::Wait.until { b.link(:text, product_name).exist? } b.link(:text, product_name).click Watir::Wait.until { b.link(:text, 'Webshop').exist? } b.link(:text, 'Webshop').click end
  38. Bob rates products ['AA', 'AD', 'AI'].each do |product_name| b.goto url

    b.link(:text, product_name).click star_id = 'product_' + product_name + '_rating_2' b.link(:id, star_id).click end
  39. Bob fills his cart [1, 3, 5].each do |product_id| b.goto

    url add_button_id = 'add_' + product_id.to_s b.link(:id, add_button_id).click end
  40. Logout b.link(:text, 'Logout').click

  41. Total: 43 webpages

  42. 30 sec. localhost demo run through the script

  43. None
  44. How fast or slow is on the Raspberry Pi?

  45. 0 29 58 87 116 Vanilla 116 s

  46. / Started GET "/" for x.x.x.x at 2013-02-28 21:05:34 +0000

    Processing by ProductsController#index as HTML Rendered products/index.html.haml within layouts/application (3022.3ms) Rendered layouts/_navbar.html.haml (12.5ms) Rendered layouts/_footer.html.haml (2.9ms) Completed 200 OK in 3097ms (Views: 3013.3ms | ActiveRecord: 72.6ms)
  47. Started GET "/" for x.x.x.x at 2013-02-28 21:05:34 +0000 Processing

    by ProductsController#index as HTML Rendered products/index.html.haml within layouts/application (3022.3ms) Rendered layouts/_navbar.html.haml (12.5ms) Rendered layouts/_footer.html.haml (2.9ms) Completed 200 OK in 3097ms (Views: 3013.3ms | ActiveRecord: 72.6ms) 3013.3ms
  48. Fragment Caching aka Easy Money

  49. Vanilla HAML Version %table.table.table-striped{:id => 'products'} %thead %tr %th Name

    %th Category [...] %tbody - @products.each do |product| %tr %td= link_to product.name, [...] %td= product.description
  50. Russian Doll - cache [current_user, @products] do %table.table.table-striped{:id => 'products'}

    %thead %tr %th Name %th Category [...] %tbody - @products.each do |product| - cache [current_user, product] do %tr %td= link_to product.name, [...] %td= product.description complete table single row
  51. complete table single row

  52. Add these line to your Gemfile: gem 'dalli' gem 'cache_digests'

    Don‘t forget to run bundle install.
  53. production.rb: config.cache_store = :dalli_store

  54. Seriously, how much difference can 5 LOC make?!

  55. 0 30 60 90 120 Vanilla Fragment Caching (4 LOC)

    116 s 60 s
  56. 5 LOC result in 50% fewer servers!

  57. It‘s all about the details.

  58. •Each row and each table (#index). •Each #show content. •The

    shopping cart. •The navigation bar and the footer.
  59. Don‘t just use a plain russian doll cache. Cluster the

    little dolls!
  60. Cluster of 10 Cluster of 10

  61. Just 2 hours of optimizing save another 15 ms.

  62. 0 30 60 90 120 Vanilla Fragment Caching 116 s

    45 s
  63. Remember DHH‘s Keynote which showed how to use JavaScript to

    further optimize fragment caching.
  64. Important: You need a clean database structure!

  65. HTTP Caching

  66. Web browsers and proxies don‘t want to fetch webpages twice.

    They use Last-Modified and Etag to avoid that.
  67. The idea of Last-Modified

  68. Web browser: „Hello web server. How‘s life? My user wants

    to have a look at xyz.html. I cached a copy last week. Is that still good?“
  69. Web server: „Hi! Good to see you again! xyz.html hasn‘t

    changed since last week. Have a nice day!“ aka 304 Not Modified
  70. The curl version for Etag

  71. > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8

    X-Ua-Compatible: IE=Edge Etag: "9a779b80e4b0ac3c60d29807e302deb7" [...] > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  72. > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8

    X-Ua-Compatible: IE=Edge Etag: "9a779b80e4b0ac3c60d29807e302deb7" [...] > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  73. Set the Etag class ProductsController < ApplicationController # GET /products

    def index @products = Product.limit(25) fresh_when :etag => [current_user, @products.order(:updated_at).last] end [...]
  74. Set the Etag class ProductsController < ApplicationController # GET /products

    def index @products = Product.limit(25) fresh_when :etag => [current_user, @products.order(:updated_at).last] end [...]
  75. > curl -I http://0.0.0.0:3000/ -c cookies.txt HTTP/1.1 200 OK Etag:

    "4d348810e69400799e2ab684c0ef4777" > curl -I http://0.0.0.0:3000/ -b cookies.txt HTTP/1.1 200 OK Etag: "4d348810e69400799e2ab684c0ef4777" The cookie is needed for the CSRF-Token.
  76. > curl -I http://0.0.0.0:3000/ -b cookies.txt --header 'If-None-Match: "4d348810e69400799e2ab684c0ef4777"' HTTP/1.1

    304 Not Modified Etag: "4d348810e69400799e2ab684c0ef4777" 304!
  77. The beauty of HTTP caching is that the view isn‘t

    rendered.
  78. 0 30 60 90 120 Vanilla Fragment Caching HTTP Cache

    45 s 35 s
  79. Not good enough?

  80. We waste a lot of time writing the initial cache!

  81. We could preheat the cache in off business hours!

  82. 0 30 60 90 120 Vanilla Fragment Caching HTTP Cache

    Preheater 26 s
  83. Use the night to preheat your cache. And don‘t be

    afraid of brute force!
  84. 3 Autobahn!

  85. The fastest page is delivered by Nginx without ever asking

    Rails.
  86. ᵓᴷᴷ Gemfile ᵓᴷᴷ [...] ᵓᴷᴷ public ᴹ ᵓᴷᴷ 404.html ᴹ

    ᵓᴷᴷ 422.html ᴹ ᵓᴷᴷ 500.html ᴹ ᵓᴷᴷ favicon.ico ᴹ ᵋᴷᴷ robots.txt ᵓᴷᴷ [...] That‘s already done for the files in the public directory.
  87. Is there an easy way to save complete pages which

    are rendered by the Rails framework?
  88. Add caches_page to your controller to save views as static

    gz files in your public directory: caches_page :index, :show, :gzip => :true
  89. Again: Brute Force is your friend. The server has a

    hard time to keep awake at night any way.
  90. Tricky part: How to delete out of date gz files?

  91. after_update :expire_cache before_destroy :expire_cache private def expire_cache ActionController::Base.expire_page(Rails.application.routes.url_h elpers.company_path(self)) ActionController::Base.expire_page(Rails.application.routes.url_h

    elpers.companies_path) end app/models/product.rb
  92. 0 30 60 90 120 Vanilla Fragment Caching HTTP Cache

    Preheater Page Caching 19 s 6.1 x faster!
  93. caches_page vs. !current_user.nil? ???

  94. caches_page is good to cache customized user content too. It

    just takes more thinking.
  95. Let us assume a user base of 10,000,000 people.

  96. /tmp ᐅ wget http://www.railsconf.com/2013/talks --2013-04-27 21:04:24-- http://www.railsconf.com/2013/talks Resolving www.railsconf.com... 107.20.162.205

    Connecting to www.railsconf.com|107.20.162.205|:80... connected. HTTP request sent, awaiting response... 200 OK Length: unspecified [text/html] Saving to: ‘talks’ [ <=> ] 74,321 258KB/ s in 0.3s 2013-04-27 21:04:25 (258 KB/s) - ‘talks’ saved [74321] /tmp ᐅ du -hs talks 76K talks /tmp ᐅ gzip talks /tmp ᐅ du -hs talks.gz 28K talks.gz /tmp ᐅ
  97. /tmp ᐅ du -hs talks.gz 28K talks.gz 28K * 10,000,000

    = 0,26 TB
  98. 28K * 10,000,000 = 0,26 TB Harddrive space is cheap.

    By saving the files non-gz and using a data deduplication file system you just need 5-10% of the 0,26 TB. Nginx can gzip the files on the fly.
  99. Nginx will happily read a cookie and find the pre-

    rendered page in a given directory structure.
  100. Warning: To setup a complex page_cache system is a lot

    of work. You have to tackle not only Rails but nginx too. It does increase the snappiness of your application but might not be worth the effort for small systems.
  101. 0 30 60 90 120 Vanilla Fragment Caching HTTP Cache

    Preheater Page Caching 19 s Any chance to get a single digit here?
  102. Add Ember.js to your software stack! http://emberjs.com rails new testapp

    -m http://emberjs.com/edge_template.rb
  103. 0 37,5 75 112,5 150 Vanilla Fragment CachingHTTP Cache Preheater

    Page Caching Ember.js 8 s 14.5 x faster!
  104. What to do with your application?

  105. Existing applications: Go for the low hanging fruits fragment caching

    and HTTP caching.
  106. New applications: Learn Ember.js and combine it with Rails. But

    don‘t forget HTTP caching. To learn more about Ember: Come to my Ember.js talk at 17:00 in room New York I.
  107. Thank you! stefan.wintermeyer@amooma.de @wintermeyer Contact me if you need an

    performance audit of your existing Web application.