Skip to content

Commit 2d1f43e

Browse files
committed
Final draft of case study for landing page caching
1 parent 4b721b3 commit 2d1f43e

2 files changed

Lines changed: 76 additions & 13 deletions

File tree

diskcache/recipes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,10 @@ def wrapper(*args, **kwargs):
321321
def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1):
322322
"""Memoizing cache decorator with cache stampede protection.
323323
324-
Cache stampedes are a type of cascading failure that can occur when
325-
parallel computing systems using memoization come under heavy load. This
326-
behaviour is sometimes also called dog-piling, cache miss storm, cache
327-
choking, or the thundering herd problem.
324+
Cache stampedes are a type of system overload that can occur when parallel
325+
computing systems using memoization come under heavy load. This behaviour
326+
is sometimes also called dog-piling, cache miss storm, cache choking, or
327+
the thundering herd problem.
328328
329329
The memoization decorator implements cache stampede protection through
330330
early recomputation. Early recomputation of function results will occur

docs/case-study-landing-page-caching.rst

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1216
Imagine 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

3539
There is a huge improvement in average latency now but some requests experience
3640
worse 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
3842
the landing page requires significant resources then the spikes may be
3943
prohibitive.
4044

@@ -48,12 +52,71 @@ synchronize generating the landing page::
4852
... time.sleep(0.2)
4953

5054
The 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

Comments
 (0)