Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RailsConf 2013 - "Cache = Cash"

RailsConf 2013 - "Cache = Cash"

A Railsconf 2013 talk.

Stefan Wintermeyer

April 30, 2013
Tweet

More Decks by Stefan Wintermeyer

Other Decks in Programming

Transcript

  1. Cache = Cash
    Stefan Wintermeyer - @wintermeyer

    View full-size slide

  2. 1 2 3
    Why? How? Autobahn!

    View full-size slide

  3. Today we only
    discuss dynamic
    HTML which gets
    rendered views!
    The asset pipeline
    does a decent job.
    But never the less
    there is a lot of
    room for
    improvement.

    View full-size slide

  4. a) Save money!

    View full-size slide

  5. If you can deliver a
    webpage in half the
    time you only need half
    the amount of servers.

    View full-size slide

  6. b) Snappiness

    View full-size slide

  7. 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!

    View full-size slide

  8. 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%

    View full-size slide

  9. 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

    View full-size slide

  10. Yo ho ho and a few billion pages of RUM
    How speed affects bounce rate
    @igrigorik

    View full-size slide

  11. 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

    View full-size slide

  12. 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

    View full-size slide

  13. 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.

    View full-size slide

  14. Raspberry Pi (35 USD)

    View full-size slide

  15. Software Stack
    • Ruby on Rails 3.2 (no need to wait till 4.0)
    • Ruby 1.9.3
    • Nginx
    • Unicorn
    • MySQL
    • Raspbian Wheezy

    View full-size slide

  16. Example:
    Online Shop

    View full-size slide

  17. 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

    View full-size slide

  18. current_user

    View full-size slide

  19. Bob‘s cart

    View full-size slide

  20. Discounted price

    View full-size slide

  21. Rate this product

    View full-size slide

  22. Measure the
    Success

    View full-size slide

  23. Watir script
    http://watir.com

    View full-size slide

  24. 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

    View full-size slide

  25. Login Bob
    b.goto url
    b.goto "#{url}/users/sign_in"
    b.text_field(:id,
    "user_email").set("[email protected]")
    b.text_field(:id, "user_password").set("123")
    b.button(:name, "commit").click

    View full-size slide

  26. 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

    View full-size slide

  27. 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

    View full-size slide

  28. 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

    View full-size slide

  29. Logout
    b.link(:text, 'Logout').click

    View full-size slide

  30. Total: 43 webpages

    View full-size slide

  31. 30 sec. localhost demo
    run through the script

    View full-size slide

  32. How fast or slow is on
    the Raspberry Pi?

    View full-size slide

  33. 0
    29
    58
    87
    116
    Vanilla
    116 s

    View full-size slide

  34. /
    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)

    View full-size slide

  35. 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

    View full-size slide

  36. Fragment
    Caching
    aka Easy Money

    View full-size slide

  37. 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

    View full-size slide

  38. 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

    View full-size slide

  39. complete table
    single row

    View full-size slide

  40. Add these line to your Gemfile:
    gem 'dalli'
    gem 'cache_digests'
    Don‘t forget to run bundle install.

    View full-size slide

  41. production.rb:
    config.cache_store = :dalli_store

    View full-size slide

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

    View full-size slide

  43. 0
    30
    60
    90
    120
    Vanilla Fragment Caching (4 LOC)
    116 s 60 s

    View full-size slide

  44. 5 LOC result in
    50% fewer
    servers!

    View full-size slide

  45. It‘s all about
    the details.

    View full-size slide

  46. •Each row and each table (#index).
    •Each #show content.
    •The shopping cart.
    •The navigation bar and the footer.

    View full-size slide

  47. Don‘t just use a plain
    russian doll cache.
    Cluster the little dolls!

    View full-size slide

  48. Cluster of 10
    Cluster of 10

    View full-size slide

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

    View full-size slide

  50. 0
    30
    60
    90
    120
    Vanilla Fragment Caching
    116 s 45 s

    View full-size slide

  51. Remember DHH‘s
    Keynote which showed
    how to use JavaScript
    to further optimize
    fragment caching.

    View full-size slide

  52. Important:
    You need a clean
    database structure!

    View full-size slide

  53. HTTP Caching

    View full-size slide

  54. Web browsers and proxies
    don‘t want to fetch webpages
    twice.
    They use Last-Modified and Etag
    to avoid that.

    View full-size slide

  55. The idea of Last-Modified

    View full-size slide

  56. 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?“

    View full-size slide

  57. Web server:
    „Hi! Good to see you again!
    xyz.html hasn‘t changed since
    last week.
    Have a nice day!“
    aka 304 Not Modified

    View full-size slide

  58. The curl version for Etag

    View full-size slide

  59. > 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"

    View full-size slide

  60. > 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"

    View full-size slide

  61. 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
    [...]

    View full-size slide

  62. 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
    [...]

    View full-size slide

  63. > 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.

    View full-size slide

  64. > 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!

    View full-size slide

  65. The beauty of HTTP
    caching is that the
    view isn‘t rendered.

    View full-size slide

  66. 0
    30
    60
    90
    120
    Vanilla Fragment Caching HTTP Cache
    45 s 35 s

    View full-size slide

  67. 35 s / 43 pages = 0,8 s/p

    View full-size slide

  68. Not good enough?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  71. 0
    30
    60
    90
    120
    Vanilla Fragment Caching HTTP Cache Preheater
    26 s

    View full-size slide

  72. 26 s / 43 pages = 0,6 s/p
    WHOA!

    View full-size slide

  73. Use the night to
    preheat your cache.
    And don‘t be afraid
    of brute force!

    View full-size slide

  74. The fastest page is
    delivered by Nginx
    without ever asking
    Rails.

    View full-size slide

  75. ᵓᴷᴷ Gemfile
    ᵓᴷᴷ [...]
    ᵓᴷᴷ public
    ᴹ ᵓᴷᴷ 404.html
    ᴹ ᵓᴷᴷ 422.html
    ᴹ ᵓᴷᴷ 500.html
    ᴹ ᵓᴷᴷ favicon.ico
    ᴹ ᵋᴷᴷ robots.txt
    ᵓᴷᴷ [...]
    That‘s already
    done for the
    files in the
    public directory.

    View full-size slide

  76. Is there an easy way to
    save complete pages
    which are rendered by
    the Rails framework?

    View full-size slide

  77. Add caches_page to your
    controller to save views as static
    gz files in your public directory:
    caches_page :index, :show,
    :gzip => :true

    View full-size slide

  78. Again: Brute Force is your
    friend. The server has a
    hard time to keep awake at
    night any way.

    View full-size slide

  79. Tricky part:
    How to delete out of date
    gz files?

    View full-size slide

  80. 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

    View full-size slide

  81. 0
    30
    60
    90
    120
    Vanilla Fragment Caching HTTP Cache Preheater Page Caching
    19 s
    6.1 x faster!

    View full-size slide

  82. caches_page
    vs.
    !current_user.nil?
    ???

    View full-size slide

  83. caches_page is good to cache
    customized user content too.
    It just takes more thinking.

    View full-size slide

  84. Let us assume a user base
    of 10,000,000 people.

    View full-size slide

  85. /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 ᐅ

    View full-size slide

  86. /tmp ᐅ du -hs talks.gz
    28K talks.gz
    28K * 10,000,000 = 0,26 TB

    View full-size slide

  87. 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.

    View full-size slide

  88. Nginx will happily read a
    cookie and find the pre-
    rendered page in a given
    directory structure.

    View full-size slide

  89. 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.

    View full-size slide

  90. 0
    30
    60
    90
    120
    Vanilla Fragment Caching HTTP Cache Preheater Page Caching
    19 s
    Any chance to
    get a single
    digit here?

    View full-size slide

  91. Add Ember.js to your software stack!
    http://emberjs.com
    rails new testapp -m http://emberjs.com/edge_template.rb
    Find Tom Dale and
    Yahuda Katz at
    the conference for
    more Ember.js info.

    View full-size slide

  92. 0
    37,5
    75
    112,5
    150
    Vanilla Fragment CachingHTTP Cache Preheater Page Caching Ember.js
    8 s
    14.5 x faster!

    View full-size slide

  93. What to do with
    your application?

    View full-size slide

  94. Existing applications:
    Go for the low hanging
    fruits fragment caching
    and HTTP caching.

    View full-size slide

  95. New applications:
    Learn Ember.js and
    combine it with Rails.
    But don‘t forget HTTP
    caching.

    View full-size slide

  96. Thank you!
    [email protected]
    @wintermeyer
    Contact me if
    you need an
    performance
    audit of your
    existing Web
    application.

    View full-size slide