Skip to content

Commit 1dfbd6c

Browse files
committed
HBASE-27225 Add BucketAllocator bucket size statistic logging (apache#4637)
Signed-off-by: Wellington Chevreuil <[email protected]>
1 parent 33b3bbe commit 1dfbd6c

File tree

3 files changed

+171
-35
lines changed

3 files changed

+171
-35
lines changed

hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketAllocator.java

Lines changed: 148 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,15 @@ final class BucketSizeInfo {
168168
// Free bucket means it has space to allocate a block;
169169
// Completely free bucket means it has no block.
170170
private LinkedMap bucketList, freeBuckets, completelyFreeBuckets;
171+
// only modified under synchronization, but also read outside it.
172+
private volatile long fragmentationBytes;
171173
private int sizeIndex;
172174

173175
BucketSizeInfo(int sizeIndex) {
174176
bucketList = new LinkedMap();
175177
freeBuckets = new LinkedMap();
176178
completelyFreeBuckets = new LinkedMap();
179+
fragmentationBytes = 0;
177180
this.sizeIndex = sizeIndex;
178181
}
179182

@@ -193,7 +196,7 @@ public int sizeIndex() {
193196
* Find a bucket to allocate a block
194197
* @return the offset in the IOEngine
195198
*/
196-
public long allocateBlock() {
199+
public long allocateBlock(int blockSize) {
197200
Bucket b = null;
198201
if (freeBuckets.size() > 0) {
199202
// Use up an existing one first...
@@ -206,6 +209,9 @@ public long allocateBlock() {
206209
if (b == null) return -1;
207210
long result = b.allocate();
208211
blockAllocated(b);
212+
if (blockSize < b.getItemAllocationSize()) {
213+
fragmentationBytes += b.getItemAllocationSize() - blockSize;
214+
}
209215
return result;
210216
}
211217

@@ -236,23 +242,38 @@ private synchronized void removeBucket(Bucket b) {
236242
completelyFreeBuckets.remove(b);
237243
}
238244

239-
public void freeBlock(Bucket b, long offset) {
245+
public void freeBlock(Bucket b, long offset, int length) {
240246
assert bucketList.containsKey(b);
241247
// else we shouldn't have anything to free...
242248
assert (!completelyFreeBuckets.containsKey(b));
243249
b.free(offset);
250+
if (length < b.getItemAllocationSize()) {
251+
fragmentationBytes -= b.getItemAllocationSize() - length;
252+
}
244253
if (!freeBuckets.containsKey(b)) freeBuckets.put(b, b);
245254
if (b.isCompletelyFree()) completelyFreeBuckets.put(b, b);
246255
}
247256

248257
public synchronized IndexStatistics statistics() {
249258
long free = 0, used = 0;
259+
int full = 0;
250260
for (Object obj : bucketList.keySet()) {
251261
Bucket b = (Bucket) obj;
252262
free += b.freeCount();
253263
used += b.usedCount();
264+
if (!b.hasFreeSpace()) {
265+
full++;
266+
}
254267
}
255-
return new IndexStatistics(free, used, bucketSizes[sizeIndex]);
268+
int bucketObjectSize = bucketSizes[sizeIndex];
269+
// this is most likely to always be 1 or 0
270+
int fillingBuckets = Math.max(0, freeBuckets.size() - completelyFreeBuckets.size());
271+
// if bucket capacity is not perfectly divisible by a bucket's object size, there will
272+
// be some left over per bucket. for some object sizes this may be large enough to be
273+
// non-trivial and worth tuning by choosing a more divisible object size.
274+
long wastedBytes = (bucketCapacity % bucketObjectSize) * (full + fillingBuckets);
275+
return new IndexStatistics(free, used, bucketObjectSize, full, completelyFreeBuckets.size(),
276+
wastedBytes, fragmentationBytes);
256277
}
257278

258279
@Override
@@ -434,7 +455,7 @@ public synchronized long allocateBlock(int blockSize)
434455
+ "; adjust BucketCache sizes " + BlockCacheFactory.BUCKET_CACHE_BUCKETS_KEY
435456
+ " to accomodate if size seems reasonable and you want it cached.");
436457
}
437-
long offset = bsi.allocateBlock();
458+
long offset = bsi.allocateBlock(blockSize);
438459

439460
// Ask caller to free up space and try again!
440461
if (offset < 0) throw new CacheFullException(blockSize, bsi.sizeIndex());
@@ -455,11 +476,11 @@ private Bucket grabGlobalCompletelyFreeBucket() {
455476
* @param offset block's offset
456477
* @return size freed
457478
*/
458-
public synchronized int freeBlock(long offset) {
479+
public synchronized int freeBlock(long offset, int length) {
459480
int bucketNo = (int) (offset / bucketCapacity);
460481
assert bucketNo >= 0 && bucketNo < buckets.length;
461482
Bucket targetBucket = buckets[bucketNo];
462-
bucketSizeInfos[targetBucket.sizeIndex()].freeBlock(targetBucket, offset);
483+
bucketSizeInfos[targetBucket.sizeIndex()].freeBlock(targetBucket, offset, length);
463484
usedSize -= targetBucket.getItemAllocationSize();
464485
return targetBucket.getItemAllocationSize();
465486
}
@@ -478,77 +499,185 @@ public int sizeOfAllocation(long offset) {
478499
return targetBucket.getItemAllocationSize();
479500
}
480501

502+
/**
503+
* Statistics to give a glimpse into the distribution of BucketCache objects. Each configured
504+
* bucket size, denoted by {@link BucketSizeInfo}, gets an IndexStatistic. A BucketSizeInfo
505+
* allocates blocks of a configured size from claimed buckets. If you have a bucket size of 512k,
506+
* the corresponding BucketSizeInfo will always allocate chunks of 512k at a time regardless of
507+
* actual request.
508+
* <p>
509+
* Over time, as a BucketSizeInfo gets more allocations, it will claim more buckets from the total
510+
* pool of completelyFreeBuckets. As blocks are freed from a BucketSizeInfo, those buckets may be
511+
* returned to the completelyFreeBuckets pool.
512+
* <p>
513+
* The IndexStatistics help visualize how these buckets are currently distributed, through counts
514+
* of items, bytes, and fullBuckets. Additionally, mismatches between block sizes and bucket sizes
515+
* can manifest in inefficient cache usage. These typically manifest in three ways:
516+
* <p>
517+
* 1. Allocation failures, because block size is larger than max bucket size. These show up in
518+
* logs and can be alleviated by adding larger bucket sizes if appropriate.<br>
519+
* 2. Memory fragmentation, because blocks are typically smaller than the bucket size. See
520+
* {@link #fragmentationBytes()} for details.<br>
521+
* 3. Memory waste, because a bucket's itemSize is not a perfect divisor of bucketCapacity. see
522+
* {@link #wastedBytes()} for details.<br>
523+
*/
481524
static class IndexStatistics {
482-
private long freeCount, usedCount, itemSize, totalCount;
525+
private long freeCount, usedCount, itemSize, totalCount, wastedBytes, fragmentationBytes;
526+
private int fullBuckets, completelyFreeBuckets;
483527

528+
/**
529+
* How many more items can be allocated from the currently claimed blocks of this bucket size
530+
*/
484531
public long freeCount() {
485532
return freeCount;
486533
}
487534

535+
/**
536+
* How many items are currently taking up space in this bucket size's buckets
537+
*/
488538
public long usedCount() {
489539
return usedCount;
490540
}
491541

542+
/**
543+
* Combined {@link #freeCount()} + {@link #usedCount()}
544+
*/
492545
public long totalCount() {
493546
return totalCount;
494547
}
495548

549+
/**
550+
* How many more bytes can be allocated from the currently claimed blocks of this bucket size
551+
*/
496552
public long freeBytes() {
497553
return freeCount * itemSize;
498554
}
499555

556+
/**
557+
* How many bytes are currently taking up space in this bucket size's buckets Note: If your
558+
* items are less than the bucket size of this bucket, the actual used bytes by items will be
559+
* lower than this value. But since a bucket size can only allocate items of a single size, this
560+
* value is the true number of used bytes. The difference will be counted in
561+
* {@link #fragmentationBytes()}.
562+
*/
500563
public long usedBytes() {
501564
return usedCount * itemSize;
502565
}
503566

567+
/**
568+
* Combined {@link #totalCount()} * {@link #itemSize()}
569+
*/
504570
public long totalBytes() {
505571
return totalCount * itemSize;
506572
}
507573

574+
/**
575+
* This bucket size can only allocate items of this size, even if the requested allocation size
576+
* is smaller. The rest goes towards {@link #fragmentationBytes()}.
577+
*/
508578
public long itemSize() {
509579
return itemSize;
510580
}
511581

512-
public IndexStatistics(long free, long used, long itemSize) {
513-
setTo(free, used, itemSize);
582+
/**
583+
* How many buckets have been completely filled by blocks for this bucket size. These buckets
584+
* can't accept any more blocks unless some existing are freed.
585+
*/
586+
public int fullBuckets() {
587+
return fullBuckets;
588+
}
589+
590+
/**
591+
* How many buckets are currently claimed by this bucket size but as yet totally unused. These
592+
* buckets are available for reallocation to other bucket sizes if those fill up.
593+
*/
594+
public int completelyFreeBuckets() {
595+
return completelyFreeBuckets;
596+
}
597+
598+
/**
599+
* If {@link #bucketCapacity} is not perfectly divisible by this {@link #itemSize()}, the
600+
* remainder will be unusable by in buckets of this size. A high value here may be optimized by
601+
* trying to choose bucket sizes which can better divide {@link #bucketCapacity}.
602+
*/
603+
public long wastedBytes() {
604+
return wastedBytes;
605+
}
606+
607+
/**
608+
* Every time you allocate blocks in these buckets where the block size is less than the bucket
609+
* size, fragmentation increases by that difference. You can reduce fragmentation by lowering
610+
* the bucket size so that it is closer to the typical block size. This may have the consequence
611+
* of bumping some blocks to the next larger bucket size, so experimentation may be needed.
612+
*/
613+
public long fragmentationBytes() {
614+
return fragmentationBytes;
615+
}
616+
617+
public IndexStatistics(long free, long used, long itemSize, int fullBuckets,
618+
int completelyFreeBuckets, long wastedBytes, long fragmentationBytes) {
619+
setTo(free, used, itemSize, fullBuckets, completelyFreeBuckets, wastedBytes,
620+
fragmentationBytes);
514621
}
515622

516623
public IndexStatistics() {
517-
setTo(-1, -1, 0);
624+
setTo(-1, -1, 0, 0, 0, 0, 0);
518625
}
519626

520-
public void setTo(long free, long used, long itemSize) {
627+
public void setTo(long free, long used, long itemSize, int fullBuckets,
628+
int completelyFreeBuckets, long wastedBytes, long fragmentationBytes) {
521629
this.itemSize = itemSize;
522630
this.freeCount = free;
523631
this.usedCount = used;
524632
this.totalCount = free + used;
633+
this.fullBuckets = fullBuckets;
634+
this.completelyFreeBuckets = completelyFreeBuckets;
635+
this.wastedBytes = wastedBytes;
636+
this.fragmentationBytes = fragmentationBytes;
525637
}
526638
}
527639

528640
public Bucket[] getBuckets() {
529641
return this.buckets;
530642
}
531643

532-
void logStatistics() {
644+
void logDebugStatistics() {
645+
if (!LOG.isDebugEnabled()) {
646+
return;
647+
}
648+
533649
IndexStatistics total = new IndexStatistics();
534650
IndexStatistics[] stats = getIndexStatistics(total);
535-
LOG.info("Bucket allocator statistics follow:\n");
536-
LOG.info(" Free bytes=" + total.freeBytes() + "+; used bytes=" + total.usedBytes()
537-
+ "; total bytes=" + total.totalBytes());
651+
LOG.debug("Bucket allocator statistics follow:");
652+
LOG.debug(
653+
" Free bytes={}; used bytes={}; total bytes={}; wasted bytes={}; fragmentation bytes={}; "
654+
+ "completelyFreeBuckets={}",
655+
total.freeBytes(), total.usedBytes(), total.totalBytes(), total.wastedBytes(),
656+
total.fragmentationBytes(), total.completelyFreeBuckets());
538657
for (IndexStatistics s : stats) {
539-
LOG.info(" Object size " + s.itemSize() + " used=" + s.usedCount() + "; free="
540-
+ s.freeCount() + "; total=" + s.totalCount());
658+
LOG.debug(
659+
" Object size {}; used={}; free={}; total={}; wasted bytes={}; fragmentation bytes={}, "
660+
+ "full buckets={}",
661+
s.itemSize(), s.usedCount(), s.freeCount(), s.totalCount(), s.wastedBytes(),
662+
s.fragmentationBytes(), s.fullBuckets());
541663
}
542664
}
543665

544666
IndexStatistics[] getIndexStatistics(IndexStatistics grandTotal) {
545667
IndexStatistics[] stats = getIndexStatistics();
546-
long totalfree = 0, totalused = 0;
668+
long totalfree = 0, totalused = 0, totalWasted = 0, totalFragmented = 0;
669+
int fullBuckets = 0, completelyFreeBuckets = 0;
670+
547671
for (IndexStatistics stat : stats) {
548672
totalfree += stat.freeBytes();
549673
totalused += stat.usedBytes();
674+
totalWasted += stat.wastedBytes();
675+
totalFragmented += stat.fragmentationBytes();
676+
fullBuckets += stat.fullBuckets();
677+
completelyFreeBuckets += stat.completelyFreeBuckets();
550678
}
551-
grandTotal.setTo(totalfree, totalused, 1);
679+
grandTotal.setTo(totalfree, totalused, 1, fullBuckets, completelyFreeBuckets, totalWasted,
680+
totalFragmented);
552681
return stats;
553682
}
554683

@@ -559,13 +688,6 @@ IndexStatistics[] getIndexStatistics() {
559688
return stats;
560689
}
561690

562-
public long freeBlock(long freeList[]) {
563-
long sz = 0;
564-
for (int i = 0; i < freeList.length; ++i)
565-
sz += freeBlock(freeList[i]);
566-
return sz;
567-
}
568-
569691
public int getBucketIndex(long offset) {
570692
return (int) (offset / bucketCapacity);
571693
}

hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketCache.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ void blockEvicted(BlockCacheKey cacheKey, BucketEntry bucketEntry, boolean decre
570570
* {@link BucketEntry#refCnt} becoming 0.
571571
*/
572572
void freeBucketEntry(BucketEntry bucketEntry) {
573-
bucketAllocator.freeBlock(bucketEntry.offset());
573+
bucketAllocator.freeBlock(bucketEntry.offset(), bucketEntry.getLength());
574574
realCacheSize.add(-1 * bucketEntry.getLength());
575575
}
576576

@@ -728,6 +728,8 @@ public void logStats() {
728728
+ cacheStats.getEvictedCount() + ", " + "evictedPerRun=" + cacheStats.evictedPerEviction()
729729
+ ", " + "allocationFailCount=" + cacheStats.getAllocationFailCount());
730730
cacheStats.reset();
731+
732+
bucketAllocator.logDebugStatistics();
731733
}
732734

733735
public long getRealCacheSize() {
@@ -1083,8 +1085,9 @@ void doDrain(final List<RAMQueueEntry> entries) throws InterruptedException {
10831085
checkIOErrorIsTolerated();
10841086
// Since we failed sync, free the blocks in bucket allocator
10851087
for (int i = 0; i < entries.size(); ++i) {
1086-
if (bucketEntries[i] != null) {
1087-
bucketAllocator.freeBlock(bucketEntries[i].offset());
1088+
BucketEntry bucketEntry = bucketEntries[i];
1089+
if (bucketEntry != null) {
1090+
bucketAllocator.freeBlock(bucketEntry.offset(), bucketEntry.getLength());
10881091
bucketEntries[i] = null;
10891092
}
10901093
}
@@ -1498,7 +1501,7 @@ public BucketEntry writeToCache(final IOEngine ioEngine, final BucketAllocator a
14981501
succ = true;
14991502
} finally {
15001503
if (!succ) {
1501-
alloc.freeBlock(offset);
1504+
alloc.freeBlock(offset, len);
15021505
}
15031506
}
15041507
realCacheSize.add(len);

0 commit comments

Comments
 (0)