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

LinuxTag 2013 - "Cache = Cash"

LinuxTag 2013 - "Cache = Cash"

Stefan Wintermeyer

May 24, 2013
Tweet

More Decks by Stefan Wintermeyer

Other Decks in Programming

Transcript

  1. Cache = Cash
    [email protected] @wintermeyer
    Stefan Wintermeyer

    View Slide

  2. 1 2 3
    Why? How? Autobahn!

    View Slide

  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.

    View Slide

  4. 1 Why?

    View Slide

  5. a) Save money!

    View Slide

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

    View Slide

  7. b) Snappiness

    View Slide

  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!

    View Slide

  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%

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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.

    View Slide

  15. 2 How?

    View Slide

  16. Raspberry Pi

    View Slide

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

    View Slide

  18. Example:
    Online Shop

    View Slide

  19. The Models

    View Slide

  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

    View Slide

  21. The UI

    View Slide

  22. View Slide

  23. current_user

    View Slide

  24. View Slide

  25. View Slide

  26. View Slide

  27. Bob‘s cart

    View Slide

  28. View Slide

  29. Discounted price

    View Slide

  30. Rate this product

    View Slide

  31. Measure the
    Success

    View Slide

  32. Watir script
    http://watir.com

    View Slide

  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

    View Slide

  34. View Slide

  35. View Slide

  36. 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 Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  41. Total: 43 webpages

    View Slide

  42. 30 sec. localhost demo
    run through the script

    View Slide

  43. View Slide

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

    View Slide

  45. 0
    29
    58
    87
    116
    Vanilla
    116 s

    View Slide

  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)

    View Slide

  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

    View Slide

  48. Fragment
    Caching
    aka Easy Money

    View Slide

  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

    View Slide

  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

    View Slide

  51. complete table
    single row

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  56. 5 LOC result in
    50% fewer
    servers!

    View Slide

  57. It‘s all about
    the details.

    View Slide

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

    View Slide

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

    View Slide

  60. Cluster of 10
    Cluster of 10

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  64. Important:
    You need a clean
    database structure!

    View Slide

  65. HTTP Caching

    View Slide

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

    View Slide

  67. The idea of Last-Modified

    View Slide

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

    View Slide

  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

    View Slide

  70. The curl version for Etag

    View Slide

  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"

    View Slide

  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"

    View Slide

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

    View Slide

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

    View Slide

  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.

    View Slide

  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!

    View Slide

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

    View Slide

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

    View Slide

  79. Not good enough?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  84. 3 Autobahn!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  93. caches_page
    vs.
    !current_user.nil?
    ???

    View Slide

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

    View Slide

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

    View Slide

  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 ᐅ

    View Slide

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

    View Slide

  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.

    View Slide

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

    View Slide

  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.

    View Slide

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

    View Slide

  102. Add Ember.js to your software stack!
    http://emberjs.com
    rails new testapp -m http://emberjs.com/edge_template.rb

    View Slide

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

    View Slide

  104. What to do with
    your application?

    View Slide

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

    View Slide

  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.

    View Slide

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

    View Slide