https://bz.apache.org/bugzilla/show_bug.cgi?id=66564

            Bug ID: 66564
           Summary: Improvements to WebResource Cache eviction
           Product: Tomcat 11
           Version: 11.0.0-M3
          Hardware: PC
            Status: NEW
          Severity: enhancement
          Priority: P2
         Component: Catalina
          Assignee: dev@tomcat.apache.org
          Reporter: steven.kea...@servicenow.com
  Target Milestone: -------

Created attachment 38540
  --> https://bz.apache.org/bugzilla/attachment.cgi?id=38540&action=edit
patch for enhancement to WebResource Cache eviction

Summary:  

The current Cache implementation for WebResource objects can be improved.
Every 10s it sorts all of the items in the cache in order to pre-evict
10% of the space in LRU order, to enable application threads to cache objects
without
having to evict any items.  If the cache space grows so that an application
thread DOES have to evict some items, it evicts items in a worse than random
order.  

By persisting the results of sorting the cache items across calls to background
processing, the expensive time spent sorting becomes much reduced.  
Further benefits are that when an application thread DOES have to evict some
items,
it can usually evict items in LRU order rather than the random order.

Details:

Here is a description of the current implementation, which
is needed to understand the issues being addressed.

The Cache object is a cache for WebResource objects.

1) Every 10s a background process runs which evicts enough
entries from the cache to reduce the cache space (amount of memory,
not number of entries) to 90% of the maximum.  It does this
by sorting the cache entries from oldest validation time to
youngest validation time and by evicting  entries in that order
until the goal is met.  However, it will NOT evict an entry which
has been validated within its its ttl, which defaults to 5s.

2) The advantage of "pre-eviction" objects from the Cache is that 
in many cases it enables items to be added to the cache without 
having to evict any items to keep the cache space within limits. 
Since the background process
reduces the cache space to 90% of the maximum every 10s, the Cache would have 
to add new items adding up to more than 10% of the cache space in 10s in order
to have the cache space exceed its limit.  

3) The advantage of "pre-evicting" objects in a background process is that
this (hopefully) moves most of the eviction work to a background thread
which allows the application threads to do Cache inserts without 
doing any evictions.

4) When a new item is added to the cache, Cache checks the cache space
to ensure it is still within the desired limit.  As discussed above,
in many cases the insert will NOT cause the cache space to exceed its limit
because of the pre-eviction of objects by a background process.  Unfortunately,
it is still possible for the cache space to exceed its limit.

5) When the addition of a new item grows the Cache to exceed the cache space
limit, it will
evict a "random" subset of the cache until it reduces the cache space to
95% of its max.  It evicts a "random" subset to avoid the lengthy computation
needed
to find the LRU (least recently used) items.  It finds the "random" subset by
evicting items in the resourceCache in iteration order.  Since the
resourceCache is a map
from WebAppPath to CachedResource, this means it preferentially
evicts items whose WebAppPath hashes to values that map to cache buckets at
small indices in the resourceCache.  
This is random in the sense that hash values are random, but is non random in
the sense that
certain WebAppPaths will always have a much higher probability of being evicted
than others.

6) However, it will NOT evict an entry which has been validated within its its
ttl, which defaults to 5s.
Because of this fact, it is possible that the insert of the new item in the
cache would result in the
cache space limit being exceeded and Cache would NOT be able to evict enough
entries to reduce the
space.  In this case the new item is NOT inserted, or more specifically
it is inserted and then removed. 

Here are the issues with this implementation that we fix:

7) In section (5), why does the Cache reduce the cache space to 95% in of max 
instead of reducing it to just below the max?
Wouldn't it make more sense for each cache insert to evict a small number of
items than for one
cache insert to evict a large number of items and others evict no items?
My guess is that this was done because if each cache insert evicted a small
number of items
then each insert would require the creation of an iterator over resourceCache. 
Perhaps the
overhead of this iterator creation was a concern?  (ConcurrentHashSet iterators
work by iterating over
all of the hash buckets from first to last; since Cache evicts items in this
order then the index 
of the first hash bucket with an item gets progressively larger as more items
are evicted in this way,
which increases the inititalization time of an iterator).    

8) The time spent in (1) is wasteful.  Even if just a few items are evicted the
background
process does alot of work.  Every time the background process runs
it sorts all of the items in resourceCache, by making a TreeMap with an
appropriate
Comparator and inserting all items in resourceCache.  This takes time
O(n*log(n))
where n is the number of items in resourceCache.  As n gets large this time can
become
significant.  It discards this TreeMap at the
conclusion of the background process, and the garbage generation that results
is also unwelcome.  

9) The practice (see sections (1) and (6)) of avoiding the eviction of cache
entries which have been validated in 
the last ttl seconds makes no sense.  The ttl is the MAXIMUM time that a cache
entry can be assumed valid
and is not the MINIMUM time that it should live in the cache.  The harm of this
practice is the
needless complexity of having to handle the possibility that Cache may not be
able to admit a new entry because
it is unable to evict enough older entries.  Also and more importantly, this
practice favors the presence in the cache
of older validated items in the cache over newer validated items in the cache,
which is opposite the 
practice of keeping the most recently used items in the cache.

10) The current implementation does something dangerous:  it creates a TreeMap
of the items in resourceCache,
sorting on the CachedResource.nextCheck field.  This is dangerous because the
nextCheck field might be
updated  concurrently during the TreeMap initialization.  This violates the
precondition of the TreeMap that
the key field should not be altered while an item is in the map.

Here is how we propose to fix these issues:

11) we add a lastUsed field to the CachedResource object for tracking the last
time a resource was accessed,
and a lastUsedForSorting field which is a snapshot of the lastUsed field at a
time T.

12) the background process will create an array resourceCacheLru[] containing
the items in resourceCache at time T
ordered by ascending lastUsedForSorting time. And it will maintain a variable
resourceCacheLruEvictNext which is
the index of the next item in resourceCacheLru[] to check for eviction.  
This array will persist even after the background process
returns.  Then each time the background process runs it can evict items in LRU
order by using these persisted
variables, instead of recreating this sorted list every time.  Specifically, it
evicts the next item by
code like this:

    int indexToEvict = resourceCacheLruEvictNext++;
        if (indexToEvict < resourceCacheLru.size()) {
        CachedResource cr = resourceCacheLru[indexToEvict];
            if (evictable(cr)) {
            reourceCache.remove(cr.getWebAppPath());
            resourceCacheLru[indexToEvict] = null;
            }
    }

13) Of course as the useful items in resourceCacheLru[] are reduced, the
background process must eventually
rebuild resoureCacheLru[].  When the space used by remaining evictable items in
resourceCacheLru[] is less than 10% of the max,
and the current space usage of resourceCache is >= 50% of the max,
then the background process will rebuild resourceCacheLru[] and reset
resourceCacheLruEvictNext.  This ensures
that upon background process completing, new items whose cumulative space is
atleast 20% of max can 
be inserted into the cache before resourceCacheLru[]
becomes useless for finding the next item to evict. The 20% figure results from
the fact that background process
pre-evicts 10% of max space, and ensures that the remaining evictable items are
>= 10% of max.   

14) One might point out that upon return to the background process 
the items in resourceCacheLru[] do not accurately order the items in
resourceCache, since there will probably
have been inserts, evictions, and retrievals of resourceCache items in the
interim.  This is true, however
resourceCacheLru[] does have useful information.  First note that newly
inserted items, and items accessed
since resourceCacheLru[] was created, will have a lastUsed value > any
resourceCacheLru[i].lastUsedForSorting
value. If we ignore these items then the remaining items in resourceCacheLru[]
are indeed the least recently
used items in resourceCache, ordered by ascending lastUsed.  It is just that
resourceCacheLru[] does not 
order ALL of the items in resourceCache.  This leads to this definition for
evictable() as used in (12):

    boolean evictable(CachedResource cr) {
       // cr.lastUsed is set to -1 when cr is deleted from the resourceCache
           // cr.lastUsed is given a larger value when cr is retrieved from the
cache.
       return cr.lastUsed != -1 && cr.lastUsed == cr.lastUsedForSorting;
    }

15) we no longer avoid evicting CachedResource which have been validated in the
last ttl.  
As discussed in (9) this makes little sense, and in return we can always
guarantee that we
can reduce cache space below the limit.

16) To calculate "space used by remaining evictable items in
resourceCacheLru[]" efficiently, 
which is required in (12), we maintain a variable resourceCacheLruEvictableSize
which is equal
to the cumulative size of the items in resourceCacheLru[] at creation time, and
is reduced when
an item is accessed the first time since creation, or deleted from the cache
since creation.

17) We modify (5), which describes what happens when we insert an item into the
cache and it expands the
cache space above the maximum, as follows.  
Instead of evicting a "random" element of resourceCache, we first try evicting
using resourceCacheLru[] as described in (12). This is a big improvement
because 
it means we are evicting based on the LRU policy
rather than the random policy.  Only if the useful info in resourceCacheLru[]
is exhausted do
we then evict "random" elements of resourceCache by using a persisted iterator
over resourceCache.
Also, in this case we evict just enough items to reduce the cache space below
the max which means that each
application thread pays a relatively equal cost.  

If you like the enhancement then I would develop comprehensive test cases for
the new behavior.

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to