bricker
2012-Sep-08 05:25 UTC
Discussion about the performance of "auto-expiring" cache fragments
This was sparked by this post by DHH: http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works At first I was excited to read about a new method I hadn''t seen before, ActiveRecord''s `cache_key`. It seemed like I was going to restructure our entire cache strategy to take advantage of this cool technique. However, I realized that, although much easier to maintain, it''s much less performant than manually expiring cache keys. Also, it seems to only work okay with a very specific data structure (the one DHH is using in his post, for example). I would very much like to use this technique and just be able to forget about manually expiring cache fragments for the most part. But there are a few things that are keeping me from moving in this direction. I want someone to read this and tell me why I''m wrong, and why auto expiring keys is definitely the best way to go. A little context: The website I work for gets an average of about 30,000 visits per day - not a ton but definitely enough that little things make a big difference in performance. **TL;DR** : This technique requires too many queries and too many renders. Manually expiring gives us the ability to cache larger chunks of data. I am looking for opinions, thoughts, and especially arguments on this. Consider this example, where I''d like to display a list of blogs and each blog''s 5 most recent posts: ** blogs/index.html.erb** 1. <% @blogs.each do |blog| %> 2. <% cache blog do %> 3. <%= render partial: "posts/post", collection: blog.posts.recent.limit(5) %> 4. <% end %> 5. <% end %> **posts/_post.html.erb** 1. <% cache post do %> 2. <h2><%= post.title %></h2> 3. <p><%= post.body %></p> 4. <% end %> Line 1 will perform a database query no matter what, on every page load. It also requires several hits to the cache database to check for every blog''s `cache_key`. If any post in a blog is updated, that block will be required to render the post partial 5 times, no matter what. It will also have to fire off a query to the database to retrieve those 5 posts. At this point - with the 5 posts loaded in to memory, and the partials being rendered anyways - what is really the performance difference between fetching the HTML fragment for that post from cache, or just rendering the partial as usual? My guess is that it''s negligible, but I hope that I am wrong. Consider this example. I want to simply render the 5 most recent posts made, regardless of which blog: ** posts/recent.html.erb** 1. <% @posts = Post.recent.limit(5) %> 2. <% cache @posts do %> 2. <%= render @posts %> 3. <% end %> Same situation here: By calling `cache @posts`, we''re firing off that query, therefore defeating one of the awesome advantages of an ActiveRecord::Relation - lazy queries. And then we have to render the `post` partial 5 times, and at that point, with the post ready to go, is caching really going to help that much? Auto-expiring keys doesn''t support arbitrary view fragments - i.e., fragments of HTML that aren''t tied to any model object: **posts/recent.html.erb** 1. <% cache "recent_posts" do %> 2. <% @posts = Post.recent.limit(5) %> 3. <%= render @posts %> 4. <% end %> This method (on cache hit): * Will not perform any database queries * Doesn''t need to instantiate an ActiveRecord::Relation object * Doesn''t render any partials * Only needs to check the cache for a single key The only downside, of course, is that the cache needs to be manually expired - but that''s, what, 5 lines in an observer? **post_observer.rb** 1. class PostObserver < ActiveRecord::Observer 2. def after_save(post) 3. ActionController::Base.new.expire_fragment "views/recent_posts" 4. end 5. end Of course, if you have a lot of places where this object is being represented, you''d have to expire several fragments. But, with redis, you can take advantage of `sets` and `smembers` to do that. auto expiring keys also require extra writing to the database to update associated objects (such as a Blog) when a Post is saved. So - thoughts? -- You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To view this discussion on the web visit https://groups.google.com/d/msg/rubyonrails-talk/-/xNLSsQY_QUgJ. For more options, visit https://groups.google.com/groups/opt_out.
bricker
2012-Sep-08 06:16 UTC
Re: Discussion about the performance of "auto-expiring" cache fragments
As a side note, I realize that you wouldn''t be instantiating an ActiveRecord::Relation object in a view, but I used that as an example of how you can defer loading almost anything until there is a cache miss. On Friday, September 7, 2012 10:25:30 PM UTC-7, bricker wrote:> > This was sparked by this post by DHH: > http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works > > At first I was excited to read about a new method I hadn''t seen before, > ActiveRecord''s `cache_key`. It seemed like I was going to restructure our > entire cache strategy to take advantage of this cool technique. However, I > realized that, although much easier to maintain, it''s much less performant > than manually expiring cache keys. Also, it seems to only work okay with a > very specific data structure (the one DHH is using in his post, for > example). > > I would very much like to use this technique and just be able to forget > about manually expiring cache fragments for the most part. But there are a > few things that are keeping me from moving in this direction. I want > someone to read this and tell me why I''m wrong, and why auto expiring keys > is definitely the best way to go. > > A little context: The website I work for gets an average of about 30,000 > visits per day - not a ton but definitely enough that little things make a > big difference in performance. > > **TL;DR** : This technique requires too many queries and too many renders. > Manually expiring gives us the ability to cache larger chunks of data. I am > looking for opinions, thoughts, and especially arguments on this. > > Consider this example, where I''d like to display a list of blogs and each > blog''s 5 most recent posts: > > ** blogs/index.html.erb** > 1. <% @blogs.each do |blog| %> > 2. <% cache blog do %> > 3. <%= render partial: "posts/post", collection: > blog.posts.recent.limit(5) %> > 4. <% end %> > 5. <% end %> > > **posts/_post.html.erb** > 1. <% cache post do %> > 2. <h2><%= post.title %></h2> > 3. <p><%= post.body %></p> > 4. <% end %> > > Line 1 will perform a database query no matter what, on every page load. > It also requires several hits to the cache database to check for every > blog''s `cache_key`. > If any post in a blog is updated, that block will be required to render > the post partial 5 times, no matter what. It will also have to fire off a > query to the database to retrieve those 5 posts. At this point - with the 5 > posts loaded in to memory, and the partials being rendered anyways - what > is really the performance difference between fetching the HTML fragment for > that post from cache, or just rendering the partial as usual? My guess is > that it''s negligible, but I hope that I am wrong. > > Consider this example. I want to simply render the 5 most recent posts > made, regardless of which blog: > > ** posts/recent.html.erb** > 1. <% @posts = Post.recent.limit(5) %> > 2. <% cache @posts do %> > 2. <%= render @posts %> > 3. <% end %> > > Same situation here: By calling `cache @posts`, we''re firing off that > query, therefore defeating one of the awesome advantages of an > ActiveRecord::Relation - lazy queries. And then we have to render the > `post` partial 5 times, and at that point, with the post ready to go, is > caching really going to help that much? > > Auto-expiring keys doesn''t support arbitrary view fragments - i.e., > fragments of HTML that aren''t tied to any model object: > > **posts/recent.html.erb** > 1. <% cache "recent_posts" do %> > 2. <% @posts = Post.recent.limit(5) %> > 3. <%= render @posts %> > 4. <% end %> > > This method (on cache hit): > * Will not perform any database queries > * Doesn''t need to instantiate an ActiveRecord::Relation object > * Doesn''t render any partials > * Only needs to check the cache for a single key > > The only downside, of course, is that the cache needs to be manually > expired - but that''s, what, 5 lines in an observer? > > **post_observer.rb** > 1. class PostObserver < ActiveRecord::Observer > 2. def after_save(post) > 3. ActionController::Base.new.expire_fragment "views/recent_posts" > 4. end > 5. end > > Of course, if you have a lot of places where this object is being > represented, you''d have to expire several fragments. But, with redis, you > can take advantage of `sets` and `smembers` to do that. > > auto expiring keys also require extra writing to the database to update > associated objects (such as a Blog) when a Post is saved. > > So - thoughts? >-- You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To view this discussion on the web visit https://groups.google.com/d/msg/rubyonrails-talk/-/2kG3z64pVjUJ. For more options, visit https://groups.google.com/groups/opt_out.
bricker
2012-Sep-08 06:17 UTC
Re: Discussion about the performance of "auto-expiring" cache fragments
As a side note, I realize that you wouldn''t be instantiating an ActiveRecord::Relation object in a view, but I used that as an example of how you can defer loading almost anything until there is a cache miss. -- You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To view this discussion on the web visit https://groups.google.com/d/msg/rubyonrails-talk/-/kxIlJEZd4EEJ. For more options, visit https://groups.google.com/groups/opt_out.
Frederick Cheung
2012-Sep-08 08:30 UTC
Re: Discussion about the performance of "auto-expiring" cache fragments
On Saturday, September 8, 2012 6:25:30 AM UTC+1, bricker wrote:> > [snip] > Line 1 will perform a database query no matter what, on every page load. > It also requires several hits to the cache database to check for every > blog''s `cache_key`. > If any post in a blog is updated, that block will be required to render > the post partial 5 times, no matter what. It will also have to fire off a > query to the database to retrieve those 5 posts. At this point - with the 5 > posts loaded in to memory, and the partials being rendered anyways - what > is really the performance difference between fetching the HTML fragment for > that post from cache, or just rendering the partial as usual? My guess is > that it''s negligible, but I hope that I am wrong. > > Why guess - benchmark it. It will certainly depend on the complexity ofwhat''s going on inside your partials [snip]> This method (on cache hit): > * Will not perform any database queries > * Doesn''t need to instantiate an ActiveRecord::Relation object > * Doesn''t render any partials > * Only needs to check the cache for a single key > > The only downside, of course, is that the cache needs to be manually > expired - but that''s, what, 5 lines in an observer? > > **post_observer.rb** > 1. class PostObserver < ActiveRecord::Observer > 2. def after_save(post) > 3. ActionController::Base.new.expire_fragment "views/recent_posts" > 4. end > 5. end > > Of course, if you have a lot of places where this object is being > represented, you''d have to expire several fragments. But, with redis, you > can take advantage of `sets` and `smembers` to do that. > > auto expiring keys also require extra writing to the database to update > associated objects (such as a Blog) when a Post is saved. > >I think the main thrust of DHH''s post wasn''t "this is the absolutely fastest way of implementing caching" but "this is the easy way to cache stuff without screwing up cache expiration", which I think is hard to perceive with a simple blog example. Sweeper and so on also get harder when you want to deal with fine grained caching. Say for example that in addition to what you''ve outlined above, there''s also a tags page for each post (which caches its content), a way for users to have a page of their favourite posts, a ''featured posts section''. Now your 5 line post observer is getting quite complicated and has to know an awful lot about the structure of your site. Fred -- You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To view this discussion on the web visit https://groups.google.com/d/msg/rubyonrails-talk/-/ivAQmVg3-GsJ. For more options, visit https://groups.google.com/groups/opt_out.