Skip to content

Commit 5ac88c7

Browse files
authored
Performance improvements to use RWLock to access LRUQueryCache (#13306)
Elasticsearch (which based on lucene) can automatically infer types for users with its dynamic mapping feature. When users index some low cardinality fields, such as gender / age / status... they often use some numbers to represent the values, while ES will infer these fields as long, and ES uses BKD as the index of long fields. Just as #541 said, when the data volume grows, building the result set of low-cardinality fields will make the CPU usage and load very high even if we use a boolean query with filter clauses for low-cardinality fields. One reason is that it uses a ReentrantLock to limit accessing LRUQueryCache. QPS and costs of their queries are often high, which often causes trying locking failures when obtaining the cache, resulting in low concurrency in accessing the cache. So I replace the ReentrantLock with a ReentrantReadWriteLock. I only use the read lock when I need to get the cache for a query,
1 parent f3a5211 commit 5ac88c7

2 files changed

Lines changed: 50 additions & 41 deletions

File tree

lucene/CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ Improvements
292292
Optimizations
293293
---------------------
294294

295+
* GITHUB#13306: Use RWLock to access LRUQueryCache to reduce contention. (Boice Huang)
296+
295297
* GITHUB#13252: Replace handwritten loops compare with Arrays.compareUnsigned in SegmentTermsEnum. (zhouhui)
296298

297299
* GITHUB#12996: Reduce ArrayUtil#grow in decompress. (Zhang Chao)

lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
import java.util.Map;
3333
import java.util.Set;
3434
import java.util.concurrent.atomic.AtomicBoolean;
35-
import java.util.concurrent.locks.ReentrantLock;
35+
import java.util.concurrent.atomic.LongAdder;
36+
import java.util.concurrent.locks.ReentrantReadWriteLock;
3637
import java.util.function.Predicate;
3738
import org.apache.lucene.index.IndexReader;
3839
import org.apache.lucene.index.IndexReaderContext;
@@ -96,14 +97,15 @@ public class LRUQueryCache implements QueryCache, Accountable {
9697
// mostRecentlyUsedQueries. This is why write operations are performed under a lock
9798
private final Set<Query> mostRecentlyUsedQueries;
9899
private final Map<IndexReader.CacheKey, LeafCache> cache;
99-
private final ReentrantLock lock;
100+
private final ReentrantReadWriteLock.ReadLock readLock;
101+
private final ReentrantReadWriteLock.WriteLock writeLock;
100102
private final float skipCacheFactor;
103+
private final LongAdder hitCount;
104+
private final LongAdder missCount;
101105

102106
// these variables are volatile so that we do not need to sync reads
103107
// but increments need to be performed under the lock
104108
private volatile long ramBytesUsed;
105-
private volatile long hitCount;
106-
private volatile long missCount;
107109
private volatile long cacheCount;
108110
private volatile long cacheSize;
109111

@@ -129,11 +131,15 @@ public LRUQueryCache(
129131
}
130132
this.skipCacheFactor = skipCacheFactor;
131133

132-
uniqueQueries = new LinkedHashMap<>(16, 0.75f, true);
134+
uniqueQueries = Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true));
133135
mostRecentlyUsedQueries = uniqueQueries.keySet();
134136
cache = new IdentityHashMap<>();
135-
lock = new ReentrantLock();
137+
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
138+
writeLock = lock.writeLock();
139+
readLock = lock.readLock();
136140
ramBytesUsed = 0;
141+
hitCount = new LongAdder();
142+
missCount = new LongAdder();
137143
}
138144

139145
/**
@@ -177,8 +183,7 @@ public boolean test(LeafReaderContext context) {
177183
* @lucene.experimental
178184
*/
179185
protected void onHit(Object readerCoreKey, Query query) {
180-
assert lock.isHeldByCurrentThread();
181-
hitCount += 1;
186+
hitCount.add(1);
182187
}
183188

184189
/**
@@ -188,9 +193,8 @@ protected void onHit(Object readerCoreKey, Query query) {
188193
* @lucene.experimental
189194
*/
190195
protected void onMiss(Object readerCoreKey, Query query) {
191-
assert lock.isHeldByCurrentThread();
192196
assert query != null;
193-
missCount += 1;
197+
missCount.add(1);
194198
}
195199

196200
/**
@@ -201,7 +205,7 @@ protected void onMiss(Object readerCoreKey, Query query) {
201205
* @lucene.experimental
202206
*/
203207
protected void onQueryCache(Query query, long ramBytesUsed) {
204-
assert lock.isHeldByCurrentThread();
208+
assert writeLock.isHeldByCurrentThread();
205209
this.ramBytesUsed += ramBytesUsed;
206210
}
207211

@@ -212,7 +216,7 @@ protected void onQueryCache(Query query, long ramBytesUsed) {
212216
* @lucene.experimental
213217
*/
214218
protected void onQueryEviction(Query query, long ramBytesUsed) {
215-
assert lock.isHeldByCurrentThread();
219+
assert writeLock.isHeldByCurrentThread();
216220
this.ramBytesUsed -= ramBytesUsed;
217221
}
218222

@@ -224,7 +228,7 @@ protected void onQueryEviction(Query query, long ramBytesUsed) {
224228
* @lucene.experimental
225229
*/
226230
protected void onDocIdSetCache(Object readerCoreKey, long ramBytesUsed) {
227-
assert lock.isHeldByCurrentThread();
231+
assert writeLock.isHeldByCurrentThread();
228232
cacheSize += 1;
229233
cacheCount += 1;
230234
this.ramBytesUsed += ramBytesUsed;
@@ -237,7 +241,7 @@ protected void onDocIdSetCache(Object readerCoreKey, long ramBytesUsed) {
237241
* @lucene.experimental
238242
*/
239243
protected void onDocIdSetEviction(Object readerCoreKey, int numEntries, long sumRamBytesUsed) {
240-
assert lock.isHeldByCurrentThread();
244+
assert writeLock.isHeldByCurrentThread();
241245
this.ramBytesUsed -= sumRamBytesUsed;
242246
cacheSize -= numEntries;
243247
}
@@ -248,14 +252,14 @@ protected void onDocIdSetEviction(Object readerCoreKey, int numEntries, long sum
248252
* @lucene.experimental
249253
*/
250254
protected void onClear() {
251-
assert lock.isHeldByCurrentThread();
255+
assert writeLock.isHeldByCurrentThread();
252256
ramBytesUsed = 0;
253257
cacheSize = 0;
254258
}
255259

256260
/** Whether evictions are required. */
257261
boolean requiresEviction() {
258-
assert lock.isHeldByCurrentThread();
262+
assert writeLock.isHeldByCurrentThread();
259263
final int size = mostRecentlyUsedQueries.size();
260264
if (size == 0) {
261265
return false;
@@ -265,7 +269,6 @@ boolean requiresEviction() {
265269
}
266270

267271
CacheAndCount get(Query key, IndexReader.CacheHelper cacheHelper) {
268-
assert lock.isHeldByCurrentThread();
269272
assert key instanceof BoostQuery == false;
270273
assert key instanceof ConstantScoreQuery == false;
271274
final IndexReader.CacheKey readerKey = cacheHelper.getKey();
@@ -293,7 +296,7 @@ private void putIfAbsent(Query query, CacheAndCount cached, IndexReader.CacheHel
293296
assert query instanceof BoostQuery == false;
294297
assert query instanceof ConstantScoreQuery == false;
295298
// under a lock to make sure that mostRecentlyUsedQueries and cache remain sync'ed
296-
lock.lock();
299+
writeLock.lock();
297300
try {
298301
Query singleton = uniqueQueries.putIfAbsent(query, query);
299302
if (singleton == null) {
@@ -314,12 +317,12 @@ private void putIfAbsent(Query query, CacheAndCount cached, IndexReader.CacheHel
314317
leafCache.putIfAbsent(query, cached);
315318
evictIfNecessary();
316319
} finally {
317-
lock.unlock();
320+
writeLock.unlock();
318321
}
319322
}
320323

321324
private void evictIfNecessary() {
322-
assert lock.isHeldByCurrentThread();
325+
assert writeLock.isHeldByCurrentThread();
323326
// under a lock to make sure that mostRecentlyUsedQueries and cache keep sync'ed
324327
if (requiresEviction()) {
325328

@@ -347,7 +350,7 @@ private void evictIfNecessary() {
347350

348351
/** Remove all cache entries for the given core cache key. */
349352
public void clearCoreCacheKey(Object coreKey) {
350-
lock.lock();
353+
writeLock.lock();
351354
try {
352355
final LeafCache leafCache = cache.remove(coreKey);
353356
if (leafCache != null) {
@@ -361,25 +364,25 @@ public void clearCoreCacheKey(Object coreKey) {
361364
}
362365
}
363366
} finally {
364-
lock.unlock();
367+
writeLock.unlock();
365368
}
366369
}
367370

368371
/** Remove all cache entries for the given query. */
369372
public void clearQuery(Query query) {
370-
lock.lock();
373+
writeLock.lock();
371374
try {
372375
final Query singleton = uniqueQueries.remove(query);
373376
if (singleton != null) {
374377
onEviction(singleton);
375378
}
376379
} finally {
377-
lock.unlock();
380+
writeLock.unlock();
378381
}
379382
}
380383

381384
private void onEviction(Query singleton) {
382-
assert lock.isHeldByCurrentThread();
385+
assert writeLock.isHeldByCurrentThread();
383386
onQueryEviction(singleton, getRamBytesUsed(singleton));
384387
for (LeafCache leafCache : cache.values()) {
385388
leafCache.remove(singleton);
@@ -388,15 +391,15 @@ private void onEviction(Query singleton) {
388391

389392
/** Clear the content of this cache. */
390393
public void clear() {
391-
lock.lock();
394+
writeLock.lock();
392395
try {
393396
cache.clear();
394397
// Note that this also clears the uniqueQueries map since mostRecentlyUsedQueries is the
395398
// uniqueQueries.keySet view:
396399
mostRecentlyUsedQueries.clear();
397400
onClear();
398401
} finally {
399-
lock.unlock();
402+
writeLock.unlock();
400403
}
401404
}
402405

@@ -409,7 +412,7 @@ private static long getRamBytesUsed(Query query) {
409412

410413
// pkg-private for testing
411414
void assertConsistent() {
412-
lock.lock();
415+
writeLock.lock();
413416
try {
414417
if (requiresEviction()) {
415418
throw new AssertionError(
@@ -455,18 +458,18 @@ void assertConsistent() {
455458
"cacheSize mismatch : " + getCacheSize() + " != " + recomputedCacheSize);
456459
}
457460
} finally {
458-
lock.unlock();
461+
writeLock.unlock();
459462
}
460463
}
461464

462465
// pkg-private for testing
463466
// return the list of cached queries in LRU order
464467
List<Query> cachedQueries() {
465-
lock.lock();
468+
readLock.lock();
466469
try {
467470
return new ArrayList<>(mostRecentlyUsedQueries);
468471
} finally {
469-
lock.unlock();
472+
readLock.unlock();
470473
}
471474
}
472475

@@ -486,11 +489,11 @@ public long ramBytesUsed() {
486489

487490
@Override
488491
public Collection<Accountable> getChildResources() {
489-
lock.lock();
492+
writeLock.lock();
490493
try {
491494
return Accountables.namedAccountables("segment", cache);
492495
} finally {
493-
lock.unlock();
496+
writeLock.unlock();
494497
}
495498
}
496499

@@ -568,7 +571,7 @@ public final long getTotalCount() {
568571
* @see #getMissCount()
569572
*/
570573
public final long getHitCount() {
571-
return hitCount;
574+
return hitCount.sum();
572575
}
573576

574577
/**
@@ -579,7 +582,7 @@ public final long getHitCount() {
579582
* @see #getHitCount()
580583
*/
581584
public final long getMissCount() {
582-
return missCount;
585+
return missCount.sum();
583586
}
584587

585588
/**
@@ -633,11 +636,13 @@ private class LeafCache implements Accountable {
633636
}
634637

635638
private void onDocIdSetCache(long ramBytesUsed) {
639+
assert writeLock.isHeldByCurrentThread();
636640
this.ramBytesUsed += ramBytesUsed;
637641
LRUQueryCache.this.onDocIdSetCache(key, ramBytesUsed);
638642
}
639643

640644
private void onDocIdSetEviction(long ramBytesUsed) {
645+
assert writeLock.isHeldByCurrentThread();
641646
this.ramBytesUsed -= ramBytesUsed;
642647
LRUQueryCache.this.onDocIdSetEviction(key, 1, ramBytesUsed);
643648
}
@@ -649,6 +654,7 @@ CacheAndCount get(Query query) {
649654
}
650655

651656
void putIfAbsent(Query query, CacheAndCount cached) {
657+
assert writeLock.isHeldByCurrentThread();
652658
assert query instanceof BoostQuery == false;
653659
assert query instanceof ConstantScoreQuery == false;
654660
if (cache.putIfAbsent(query, cached) == null) {
@@ -658,6 +664,7 @@ void putIfAbsent(Query query, CacheAndCount cached) {
658664
}
659665

660666
void remove(Query query) {
667+
assert writeLock.isHeldByCurrentThread();
661668
assert query instanceof BoostQuery == false;
662669
assert query instanceof ConstantScoreQuery == false;
663670
CacheAndCount removed = cache.remove(query);
@@ -744,15 +751,15 @@ public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOExcepti
744751
}
745752

746753
// If the lock is already busy, prefer using the uncached version than waiting
747-
if (lock.tryLock() == false) {
754+
if (readLock.tryLock() == false) {
748755
return in.scorerSupplier(context);
749756
}
750757

751758
CacheAndCount cached;
752759
try {
753760
cached = get(in.getQuery(), cacheHelper);
754761
} finally {
755-
lock.unlock();
762+
readLock.unlock();
756763
}
757764

758765
if (cached == null) {
@@ -865,15 +872,15 @@ public int count(LeafReaderContext context) throws IOException {
865872
}
866873

867874
// If the lock is already busy, prefer using the uncached version than waiting
868-
if (lock.tryLock() == false) {
875+
if (readLock.tryLock() == false) {
869876
return -1;
870877
}
871878

872879
CacheAndCount cached;
873880
try {
874881
cached = get(in.getQuery(), cacheHelper);
875882
} finally {
876-
lock.unlock();
883+
readLock.unlock();
877884
}
878885
if (cached == null) {
879886
// Not cached
@@ -911,15 +918,15 @@ public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {
911918
}
912919

913920
// If the lock is already busy, prefer using the uncached version than waiting
914-
if (lock.tryLock() == false) {
921+
if (readLock.tryLock() == false) {
915922
return in.bulkScorer(context);
916923
}
917924

918925
CacheAndCount cached;
919926
try {
920927
cached = get(in.getQuery(), cacheHelper);
921928
} finally {
922-
lock.unlock();
929+
readLock.unlock();
923930
}
924931

925932
if (cached == null) {

0 commit comments

Comments
 (0)