The Secret to Memcached

Posted by tobi — 11:50 AM May 22

Memcached has long been the answer to most questions containing the word scale. There are some spectacular memcached installations out there. Facebook is said to run a 200 server with 3TB of memory solely for servicing memcached; Shopify, twitter, digg, Slashdot and just about every other public facing application depends on it. Facebook’s installation is said to deliver a 99% cache hit rate while servicing tens of thousands of requests a second.

There are many ways to use this elaborate hash table and many ways which are more trouble then they are worth. In our experience the key to use memcached effectively is to ask it for the exact thing you want, but i’m getting ahead of myself.

A common pattern to using memcached is the following


class Product < AR:B

  def load(id)
    Cache.get(key, self) || Cache.set(key, find(id))
  end

  def after_save; Cache.expire(key); end
  def after_destroy; Cache.expire(key); end

  def key
   "#{table_name}/#{id}" 
  end
end

The issue is that this model only caches on a per object basis. But the real database load comes usually from loading collections. Storing a collection in memcached is harder because you have to start tracking the objects in the collection somewhere so that you can efficiently expire the collection once one of its items is changed. And that way, he knew, lay madness.

In Shopify’s case, what we really need, is to cache all the required data to render a given public URL. Two requests to the same URL should always yield a cache hit given all input parameters being equal. In code this could look something like this:


cache params.values.sort.to_s do
  ... load all data ...
end

Of course you have to keep track of all the keys you store in memcached now. A database table will do nicely here.


class CacheKey < AR:B
  def after_destroy; Cache.expire(key); end
end

cache key = params.values.sort.to_s do
  ... load all data ...
  CacheKey.create :key => key
end

CacheKey.destroy_all # Sweep cache

So far so good.

This has been the traditional approach and has worked somewhat. I’m here to offer a better solution here though:

Ask for the thing you need, be specific: The complexity to the above solution comes from the simple fact that we formulated our question to memcached too vague. Ask yourself what you really require from memcached and then ask it for exactly that. Consider this: When a product is updated all current urls should be invalidated because they are outdated. Shopify allows the designers to reference a product from any page in the system so we have to run a full sweep. Without informing memcached that its caches are stale it will continue to deliver this stale data and customers will continue to see the old version of the product. A clear miss-understanding between shopify and memcached.

The solution is simple: At the beginning of each request we load a shop object which we pick depending on the incoming host name. We use the fact that we always load this shop model anyways and add versioning to it. This version column is incremented every time we want to sweep all caches.

Now we add the version number to the cache keys:


cache shop.version + params.values.sort.to_s  do
  ... load all data ...
end

this means that we will never get an outdated version from the caches because we ask them for a very specific thing. After the version number is increased in the database all incoming requests will miss the caches but will be re-cached quickly.

Memcached will automatically get rid of the stale keys once space is needed, least recently used keys are discarded first so there is no need for manual cleanup.

In Shopify we use this technology as a way to do Page caching. We keep the rendered HTML, HTTP return status code and Content-Type in memcached and use all the differentiating input variables as keys such as content of the shopping cart. We keep the HTML because this saves our server cluster valuable bandwidth by avoiding loading and compiling the liquid templates from the NFS server. Requests for cached documents are now rendered in sub 10ms regions.

To summarize Shopify asks memcached politely to: “Hand over version 55 of the index html for www.snowdevil.com the way it would look like with one Draft 151cm snowboard in the cart”. A very specific question for which there is only one valid answer, the exact data we want, stale data can never be returned because everything which would make it stale will increase the version number.

Quick remark. When you use memcached in ruby make absolutly sure that you use memcache-client as it’s the fastest and most used ruby implementation of the protocol.

Comments

  • Jonathan 22 May 12:05

    I assume you wanted to recomment memcache-client (http://seattlerb.rubyforge.org/memcache-client) and not Ruby-MemCache?

  • Tobi 22 May 12:15

    Oh thanks for catching this.

  • Martin 22 May 14:21

    Why not just make a service based webapplication so that you can scale horizontal and vertical and get rid of premature stuff like cache.

  • Michael B 22 May 14:32

    Connect to your RDBMS. Run

    SELECT * FROM account WHERE id = blah;

    Great. Now immediately run it again. The second time around it probably finished much faster. Why? Because your RDBMS does the same exact caching that memcached offers.

    The success of memcached has always baffled me because it’s offering redundant functionality. Perhaps it isn’t so much the function as it is the presentation: conceptually it’s much easier to understand and measure, so it’s more comfortable for developers.

    If you can properly tune MySQL’s caches and set up replication you should never need memcached.

  • tobi 22 May 14:38

    Using mysql’s querycache and memcached is not an either/or proposition. Querycache can do enough caching to get most successful web applications through their first year of operation. Its no replacement for memcached however for various reasons:

    1. It only caches data acquisition, no computation or disk accesses can be avoided by it
    2. Every update or insert to the database flushes the entire cache for the table. Do you think facebook would serve with 99% cache hits if all the caches would be flushed every time someone pokes someone else?

    When building a big web application you layer caches on caches. Ideally the top cache (browser local cache) can service the entire request such as it happens on google.com. if that fails the next best thing is blazingly fast memcached. Failing that we have to aquire and compute some data which is hopefully cached. If that fails the database gets the data from its cache or from disk. If that fails than the database files are probably cached by the operating system. And finally we move the physical read head of the disk to fetch the database off permanent storage.

  • Mars 22 May 15:25

    This is a very nice method of composing cache keys with a content version id.

    I recently found a similar answer with Rail’s fragment cache, that creating cache keys using parameters that affect the response permits very granular caching.

    Specifically, I used the the logged-in username & request hostname to increase specificity of cached fragments.

  • JGiles 22 May 17:11

    Interesting. What we really need is a layer over the DB to store serialized objects (at least for ruby) such that loading from the DB doesn’t take so damn long.

    That’s where 90% of my CPU overhead is going right now.

    Memcached is one way to do that, but I’d like to see something that can consume SQL and spit out objects, instantiating from the DB if needed, but caching otherwise. Using cache keys is moving somewhat backwards, compared to the power of SQL and RDBMS.

  • Nate 22 May 18:45

    Looking at shopify specifically, stuff like cart state isn’t carried along in params, as far as I can see. (Thank god; that’d make the shop behave strangely)

    So, wouldn’t caching on shop.version + params.values.sort.to_s not work for page caching with cart contents?

    Or would adding things to a cart increment shop.version?

  • tobi 22 May 20:53

    This is correct. In shopify’s case we use shop.version, request.host, request.path, request.format, cart.items.collect(&:item_id), cart.items.collect(&:qty) as namespace. Cart comes from the session.

  • Michael Koziarski 22 May 21:09

    Nice write up tobi. Have you considered extracting a plugin which gives a ‘cache’ method to handle both memcache caching, and etags? Seems like you could build something very interesting here…

  • Benjamin Curtis 22 May 23:08

    Michael,

    The action_cache plugin might be of interest to you:

    http://agilewebdevelopment.com/plugins/action_cache

  • Albert Ramstedt 23 May 02:46

    We use memcached a lot in our apps, and at our shop, a guy made a libmemcache wrapper c extension. It is faster than the memcache-client. It is listed on rubyforge under adocca-plugins and is called caffeine.

    Also, we use a somewhat other approach to cache to implicitly cache relationships through AR magic, and explicitly cache finders like Model.find_by_username_and_age() or so, and then only cache the id responses, and fetch them thru memcache with another call. Our approach builds on CachedModel that robot-coop built.

    A third note is that we use namespace caching a lot, so we can cache a lot of different data, and just sweep them with a namespace sweep. There is some memcache overhead involved, but it makes the cache invalidation a LOT simpler. We have implemented namespaces in our version of memcache-client (AdoccaMemcache) and in caffeine.

    See here: http://rubyforge.org/projects/adocca-plugins/

  • Carlos Bueno 23 May 09:41

    @JGiles: put a wrapper on your SQL layer that uses the SQL statements (or its SHA hash) as the key to memcache. That is roughly what MySQL’s query cache does. On a miss, run the query.

    But you will run into the problem of cache expiration. MySQL4 punts on it by invalidating all caches on a table when that table is changed.

  • Morten 23 May 12:20

    Tobi, is it correctly understood that you now serve the full responses directly from the controller using the stored HTTP return code + content type + HTML and return rather than using ‘render’ at all?

  • tobias Luetke 23 May 12:58

    This is correct morten. We save the overhead of loading a liquid file over NFS, compiling it and then rendering it. This is the principal reason and is pretty specific to shopify.

  • Michael B 23 May 13:43

    “Using mysql’s querycache and memcached is not an either/or proposition.”

    In my earlier message I never once said MySQL querycache, nor did I imply it. In fact, I run with it disabled.

    Repeated queries will still benefit from a number of caches within the RDBMS, even without an explicit querycache in effect.

  • Gaurav 23 May 14:25

    Has anyone used the C library for memcached. I tried it a couple of weeks ago and it seemed unripe. e.g. statistics structure between C libmemcached and memcached code base had a difference in one element. Some other weirdness also popped up e.g. no more than 13,000 inserts could succeed during bootup and those entries timed out after sometime.

  • Nathan Schmidt 23 May 14:59

    @Michael B, Memcached is particularly useful in two situations. First, when your cache footprint is larger than the physical RAM of your backing store (db, filesystem, whatever) so you need to spread the load across multiple physical machines. Facebook has multiple TB of network-addressable RAM using memcached, and there’s just not practical way to pool MySQL or whatever other RDBMS to exploit that kind of low-latency storage. The second scenario is when stale data is ok for the sake of efficiency. On Digg for example, it’s probably ok for the displayed number of diggs or comments to be +/- the actual value. No point doing a ‘select count(*) from comments where story_id=1234’ for every page load of every story on the front page. Better to just store a snapshot of ground truth for 30 seconds or whatever. That’s the kind of optimization a RDBMS simply can’t do, but is simple with memcached.

    @Gaurav, they’re supposed to time out and fall off the end of the pool, unless you specify otherwise when starting the daemon.

  • bob 23 May 15:17

    We are testing with the APR version of the C client (http://www.outoforder.cc/projects/libs/apr_memcache/ ) we have found it to work much better than the danga client.

    On the issue of letting the db cache it, we always start there and when stuff slows down we move it to the cache. Since we’re talking multiple terabyte databases on some pretty heavy iron (and fast disk) we recognize the limitations of db caches. This is mysql level stuff although we have that in various places too.

    Our db vendor (informix) even supports inmemory tables but you have to take into account the message overhead to the db and that fact that the db can do other stuff when you hit memcached.

    again, it works for us and is worth the effort.

  • PENIX 23 May 16:28

    Are there any major open source projects that rely on memcache? I know Drupal has a module for it.

  • irakli 23 May 17:03

    In our experience memcache client is bloody slow when used from PHP, serialization/deserialization speed is so horrific that you are way better off just hitting MySQL, granted that you have it reasonably tuned with query_cache on, reasonable denormalization and alike.

  • anon 23 May 21:06

    PENIX: Livejournal is an obvious example.

  • Indianpad 24 May 15:50

    If you have only one server and want to alleviate load on mysql then memcache will be slower than other solutions such as apc which also has user-cache similar to memcache…

  • Reuben 26 May 11:29

    If anyone is interested in another architecture that does this well, check out IBM’s DynaCache. It implements this concept fairly well for WebSphere based apps.

  • kypmeiyyus 28 May 15:39

    Hello! Good Site! Thanks you! dkgptwuhmgpyy

  • Artorios 28 May 21:48

    Excellent blog.

  • yjjzkxhajb 29 May 04:45

    auoyeqq

  • srbhacynjw 29 May 05:41

  • rljcjlcjlw 29 May 05:42

    http://vwgmejleqf.com

  • Jacob Marley 02 Jun 04:06

    This is a test

  • Stuart 06 Jun 12:41

    Thanks, thats by far the most friendly guide to memcached that i’ve read.

  • Tyler 18 Jun 14:58

    Thanks for your comments on our experimental Intelligent Fragment Cache Plugin Tobi (http://blog.overlay.tv/articles/2007/06/16/intelligent_fragment_cache). I actually stumbled across your post a few weeks ago and thought it was great but I couldn’t see how we could use that approach unless I’m missing something.

    I think your solution lends itself very nicely to your problem but doesn’t map as nicely to an application like ours where many pages are based on the latest information aggregated across multiple authors (I guess it would be across multiple stores in your case).

    Let’s say I had a page that allowed me query movie reviews in the system in different ways like the most recent, most popular, top rated, etc. and the result sets were paged.

    You recommend that I ask myself what I really want from memcache and then ask for exactly that. On that hypothetical page I would want the most recently added movie reviews added in the last 30 minutes by any user and a teaser on the side showing the top 10 review posters and another showing the 5 most popular movies. Correct me if I wrong, but I think your approach needs me to perform all three queries in the controller every time the page is accessed in order to get the version numbers for each of the objects to construct a key. Would the key look something like this?

    
    /reviews/recent/review12_1, review542_3, review123_1, review29_2, review50_6, review81_2, review85_2, review100_3, review71_5,user200_2,user,716_4,user552_2,user297_1,user98_2,user100_1,user,413_2,user225_5,user729_4,user89_1,review55_1,movie31_2,movie,58_1,movie120_5,movie166_3,movie492_6
    

    And if I add a new teaser, do I need to add the object id’s and versions for its contents to the cache key as well? Do I understand correctly?

    We want our pages to run as fast as possible and we’d rather not query the database if we don’t have to. Our solution is actually not that complicated at all once the plug-in is installed (although the plug-in itself is a little complicated). Once installed, all you have to do differently is to add

    
    acts_as_intelligent_fragment_cache_model 
    

    to a model and:

    
    <% cache("/reviews/#{@review.id}_right_panel", {:expiry => 86400} %>
       stuff
    <% end %>
    

    to a view and the model starts being monitored for changes. Of course you can get fancier but that handles the basics.

Commenting are now closed…