@@ -2,17 +2,21 @@ Case Study: Landing Page Caching
22================================
33
44:doc: `DiskCache <index >` version 4 added recipes for cache stampede mitigation.
5- Let's look at how that applies to landing page caching.
5+ Cache stampedes are a type of system overload that can occur when parallel
6+ computing systems using memoization come under heavy load. This behaviour is
7+ sometimes also called dog-piling, cache miss storm, cache choking, or the
8+ thundering herd problem. Let's look at how that applies to landing page
9+ caching.
610
711 >>> import time
812 >>> def generate_landing_page ():
913 ... time.sleep(0.2 ) # Work really hard.
1014 ... # Return HTML response.
1115
1216Imagine a website under heavy load with a function used to generate the landing
13- page. There are two processes each with five threads for a total of ten
14- concurrent workers. Also assume that generating the landing page takes about
15- two hundred milliseconds.
17+ page. There are five processes each with two threads for a total of ten
18+ concurrent workers. The landing page is loaded constantly and takes about two
19+ hundred milliseconds to generate .
1620
1721.. image :: _static/no-caching.png
1822
@@ -26,15 +30,15 @@ regenerates the page with a consistently slow latency.
2630 ... def generate_landing_page ():
2731 ... time.sleep(0.2 )
2832
29- With traditional caching, the result of generating the landing page can be
30- memoized for one second. After each second, the cached HTML expires and all ten
31- workers rush to regenerate the result.
33+ Assume the result of generating the landing page can be memoized for one
34+ second. Memoization supports a traditional caching strategy. After each second,
35+ the cached HTML expires and all ten workers rush to regenerate the result.
3236
3337.. image :: _static/traditional-caching.png
3438
3539There is a huge improvement in average latency now but some requests experience
3640worse latency than before due to the added overhead of caching. The cache
37- stampede is visible now as the spikes in the concurrency graph. If generating
41+ stampede is visible too as the spikes in the concurrency graph. If generating
3842the landing page requires significant resources then the spikes may be
3943prohibitive.
4044
@@ -48,12 +52,71 @@ synchronize generating the landing page::
4852 ... time.sleep(0.2)
4953
5054The double-checked locking uses two memoization decorators to optimistically
51- look up the cache result before locking.
55+ look up the cached result before locking. With `expire ` set to zero, the
56+ cache's get-operation is performed but the set-operation is skipped. Only the
57+ inner-nested memoize decorator will update the cache.
5258
5359.. image :: _static/synchronized-locking.png
5460
61+ The number of concurrent workers is now greatly improved. Rather than having
62+ ten workers all attempt to generate the same result, a single worker generates
63+ the result and the other ten benefit. The maximum latency has increased however
64+ as three layers of caching and locking wrap the function.
65+
66+ Ideally, the system would anticipate the pending expiration of the cached item
67+ and would recompute the result in a separate thread of execution. Coordinating
68+ recomputation would be a function of the number of workers, the expiration
69+ time, and the duration of computation. Fortunately, Vattani, et al. published
70+ the solution in "Optimal Probabilistic Cache Stampede Prevention" in 2015.
71+
72+ >>> @ dc.memoize_stampede(cache, expire = 1 )
73+ ... def generate_landing_page ():
74+ ... time.sleep(0.2 )
75+
76+ Early probabilistic recomputation uses a random number generator to simulate a
77+ cache miss prior to expiration. The new result is then computed in a separate
78+ thread while the cached result is returned to the caller. When the cache item
79+ is missing, the result is computed and cached synchronously.
80+
5581.. image :: _static/early-recomputation.png
5682
83+ The latency is now its theoretical best. An initial warmup execution takes two
84+ hundred milliseconds and the remaining calls all return immediately from the
85+ cache. Behind the scenes, separate threads of execution are recomputing the
86+ result of workers and updating the cache. The concurrency graph shows a nearly
87+ constant stream of workers recomputing the function's result.
88+
89+ >>> @ dc.memoize_stampede(cache, expire = 1 , beta = 0.5 )
90+ ... def generate_landing_page ():
91+ ... time.sleep(0.2 )
92+
93+ Vattani described an additional parameter, :math: `\beta `, which could be used
94+ to tune the eagerness of recomputation. As the number and frequency of
95+ concurrent worker calls increases, eagerness can be lessened by decreasing the
96+ :math: `\beta ` parameter. The default value of :math: `\beta ` is one, and above
97+ it is set to half.
98+
5799.. image :: _static/early-recomputation-05.png
58100
101+ Latency is now still its theoretical best while the worker load has decreased
102+ significantly. The likelihood of simulated cache misses is now half what it was
103+ before. The value was determined through experimentation.
104+
105+ >>> @ dc.memoize_stampede(cache, expire = 1 , beta = 0.3 )
106+ ... def generate_landing_page ():
107+ ... time.sleep(0.2 )
108+
109+ Lets see what happens when :math: `\beta ` is set too low.
110+
59111.. image :: _static/early-recomputation-03.png
112+
113+ When set too low, the cache item expires before a new value is recomputed. The
114+ real cache miss then causes the workers to synchronously recompute the landing
115+ page and cache the result. With no barrier in place, eleven workers cause a
116+ cache stampede. The eleven workers are composed of ten synchronous workers and
117+ one in a background thread. The best way to customize :math: `\beta ` is through
118+ experimentation, otherwise the default is very reasonable.
119+
120+ :doc: `DiskCache <index >` provides data types and recipes for memoization and
121+ mitigation of cache stampedes. The decorators provided are composable for a
122+ variety of scenarios. The best way to get started is with the :doc: `tutorial `.
0 commit comments