views:

643

answers:

2

This question concerns two implementations of counters which are intended to scale without sharding (with a tradeoff that they might under-count in some situations):

  1. http://appengine-cookbook.appspot.com/recipe/high-concurrency-counters-without-sharding/ (the code in the comments)
  2. http://blog.notdot.net/2010/04/High-concurrency-counters-without-sharding

My questions:

  • With respect to #1: Running memcache.decr() in a deferred, transactional task seems like overkill. If memcache.decr() is done outside the transaction, I think the worst-case is the transaction fails and we miss counting whatever we decremented. Am I overlooking some other problem that could occur by doing this?
  • What are the significiant tradeoffs between the two implementations?

Here are the tradeoffs I see:

  • #2 does not require datastore transactions.
  • To get the counter's value, #2 requires a datastore fetch while with #1 typically only needs to do a memcache.get() and memcache.add().
  • When incrementing a counter, both call memcache.incr(). Periodically, #2 adds a task to the task queue while #1 transactionally performs a datastore get and put. #1 also always performs memcache.add() (to test whether it is time to persist the counter to the datastore).

Conclusions

(without actually running any performance tests):

  • #1 should typically be faster at retrieving a counter (#1 memcache vs #2 datastore). Though #1 has to perform an extra memcache.add() too.
  • However, #2 should be faster when updating counters (#1 datastore get+put vs #2 enqueue a task).
  • On the other hand, with #1 you have to be a bit more careful with the update interval since the task queue quota is almost 100x smaller than either the datastore or memcahce APIs.
A: 

Memcache gets flushed, you lose your counter. OUCH. Using a mysql database or a NOSQL solution will resolve that problem with a possible performance hit. (Redis, Tokyotyrant, MongoDB etc...) may not have that performance hit.

Keep in mind, you may want to do 2 actions:

  1. keep a memcache counter just for the high performance reasons.
  2. keep a log, and then get more accurate metrics from that.
Daniel
Note: this question is specific to GAE.Anyway, as you suggest, one could log countable events and then extract precise numbers from the logs, but it isn't clear what kind of performance overhead this will add - and GAE logs are in a ring buffer so they can be lost too.Both of these implementations use memcache for the reason you suggest - they trade-off accuracy (e.g., they may undercount) in order to achieve higher performance.
David Underhill
memcache is unlikely to be flushed in a production environment, because doing a "flush all" will push all the load of serving onto your main servers. If your main servers could keep up with the full load, then why use memcache? So the conclusion is you'd fall over if doing that.A counter will be updated very frequently, and thus you can set the expiry time such that never expires in practice. If you couple this with periodic read-and-clear operations to persist counter values, memcache is probably a fine solution if you can under-count some occurrences occasionally.
Jon Watte
+1  A: 

Going to datastore is likely to be more expensive than going through memcache. Else memcache wouldn't be all that useful in the first place :-)

I'd recommend the first option.

If you have a reasonable request rate, you can actually implement it even simpler:

1) update the value in memcache
2) if the returned updated value is evenly divisible by N
2.1) add N to the datastore counter
2.2) decrement memcache by N

This assumes you can set a long enough timeout on your memcache to live between successive events, but if events are so sparse that your memcache times out, chances are you wouldn't need a "high concurrency" counter :-)

For larger sites, relying on a single memcache to do things like count total page hits may get you in trouble; in that case, you really do want to shard your memcaches, and update a random counter instance; the aggregation of counters will happen by the database update.

When using memcache, though, beware that some client APIs will assume that a one second timeout means the value isn't there. If the TCP SYN packet to the memcache instance gets dropped, this means that your request will erroneously assume the data isn't there. (Similar problems can happen with UDP for memcache)

Jon Watte
Sharding the memcache counter for large sites is a great idea.I also like the idea for the simpler implementation since it gets rid an extra round-trip to memcache by avoiding the `memcache.add()` call, though if you choose too small of an `N` for your site you could end up decrementing the counter twice and send it below 0 - which would cause it to wrap around to a huge value since memcache stores unsigned values. I suppose you could plan for this and interpret the 8B value returned by memcache as a signed int instead and you should be ok.
David Underhill
There is no race condition for the size of N, because you only decrement by N when your returned updated value is *evenly divisible* by N. Memcached enforces atomicity. Two concurrent requests will see N, and N+1. You will never go negative.If you get to 2*N, then exactly one request will see that, and decrement by N as well.The main drawback is that if some request updates to get to exactly N, and then crashes before being able to record or decrement the value, then that batch of N will be lost.
Jon Watte