Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

1 2 3 Why? How? Autobahn!

Slide 3

Slide 3 text

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.

Slide 4

Slide 4 text

1 Why?

Slide 5

Slide 5 text

a) Save money!

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

b) Snappiness

Slide 8

Slide 8 text

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!

Slide 9

Slide 9 text

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%

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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.

Slide 15

Slide 15 text

2 How?

Slide 16

Slide 16 text

Raspberry Pi

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Example: Online Shop

Slide 19

Slide 19 text

The Models

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

The UI

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

current_user

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

Bob‘s cart

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Discounted price

Slide 30

Slide 30 text

Rate this product

Slide 31

Slide 31 text

Measure the Success

Slide 32

Slide 32 text

Watir script http://watir.com

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

bob@aol.com

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Total: 43 webpages

Slide 42

Slide 42 text

30 sec. localhost demo run through the script

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

How fast or slow is on the Raspberry Pi?

Slide 45

Slide 45 text

0 29 58 87 116 Vanilla 116 s

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Fragment Caching aka Easy Money

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

complete table single row

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

production.rb: config.cache_store = :dalli_store

Slide 54

Slide 54 text

Seriously, how much difference can 5 LOC make?!

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

5 LOC result in 50% fewer servers!

Slide 57

Slide 57 text

It‘s all about the details.

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Cluster of 10 Cluster of 10

Slide 61

Slide 61 text

Just 2 hours of optimizing save another 15 ms.

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Important: You need a clean database structure!

Slide 65

Slide 65 text

HTTP Caching

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

The idea of Last-Modified

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

The curl version for Etag

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

Not good enough?

Slide 80

Slide 80 text

We waste a lot of time writing the initial cache!

Slide 81

Slide 81 text

We could preheat the cache in off business hours!

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

3 Autobahn!

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

caches_page vs. !current_user.nil? ???

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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.

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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.

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

What to do with your application?

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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.

Slide 107

Slide 107 text

Thank you! stefan.wintermeyer@amooma.de @wintermeyer Contact me if you need an performance audit of your existing Web application.