Ray Mattingly created HBASE-30341:
-------------------------------------

             Summary: FSHLog WAL lockup from an uncaught exception in 
publishSyncOnRingBuffer
                 Key: HBASE-30341
                 URL: https://issues.apache.org/jira/browse/HBASE-30341
             Project: HBase
          Issue Type: Bug
          Components: wal
    Affects Versions: 2.6.7
            Reporter: Ray Mattingly


h2. Summary

In {{FSHLog.publishSyncOnRingBuffer(long, boolean)}} the Disruptor ring buffer 
sequence is claimed by the caller *before* {{getSyncFuture()}} is invoked, and 
{{getSyncFuture()}} sits outside the {{try}} block:
{code:java}
protected SyncFuture publishSyncOnRingBuffer(long sequence, boolean forceSync) {
  // here we use ring buffer sequence as transaction id
  SyncFuture syncFuture = getSyncFuture(sequence, forceSync);   // <-- can throw
  try {
    RingBufferTruck truck = this.disruptor.getRingBuffer().get(sequence);
    truck.load(syncFuture);
  } finally {
    this.disruptor.getRingBuffer().publish(sequence);            // <-- skipped
  }
  return syncFuture;
}
{code}
The sequence comes from {{getSequenceOnRingBuffer()}} (i.e. 
{{{}RingBuffer.next(){}}}), so if {{getSyncFuture()}} throws, that slot is 
claimed and {*}never published{*}.

Disruptor's consumer waits on a sequence barrier for a contiguous published 
sequence. An unpublished hole means {{RingBufferEventHandler}} can never 
advance past it, so every subsequent append and sync on that WAL blocks 
forever. A single failed request is thereby converted into a permanently wedged 
WAL and, in practice, a dead RegionServer.
h2. Observed in production

Seen once on a 2.6.x cluster running {{{}hbase.wal.provider=filesystem{}}}. The 
trigger was an NPE out of {{SyncFutureCache.getIfPresentOrNew()}} – the Guava 
{{LocalCache}} write-queue was in an inconsistent state, so removing the entry 
double-unlinked it:
{code:java}
ERROR org.apache.hadoop.hbase.ipc.RpcServer: Unexpected throwable object
java.lang.NullPointerException: Cannot invoke
  
"org.apache.hbase.thirdparty.com.google.common.cache.ReferenceEntry.setNextInWriteQueue(...)"
  because "previous" is null
  at ...common.cache.LocalCache.connectWriteOrder(LocalCache.java:1818)
  at ...common.cache.LocalCache$WriteQueue.remove(LocalCache.java:3725)
  at 
...common.cache.LocalCache$Segment.removeValueFromChain(LocalCache.java:3249)
  at ...common.cache.LocalCache$Segment.remove(LocalCache.java:3079)
  at ...common.cache.LocalCache.remove(LocalCache.java:4273)
  at 
org.apache.hadoop.hbase.regionserver.wal.SyncFutureCache.getIfPresentOrNew(SyncFutureCache.java:61)
  at 
org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.getSyncFuture(AbstractFSWAL.java:1093)
  at 
org.apache.hadoop.hbase.regionserver.wal.FSHLog.publishSyncOnRingBuffer(FSHLog.java:789)
  at 
org.apache.hadoop.hbase.regionserver.wal.FSHLog.publishSyncOnRingBuffer(FSHLog.java:784)
  at 
org.apache.hadoop.hbase.regionserver.wal.FSHLog.publishSyncThenBlockOnCompletion(FSHLog.java:801)
  at org.apache.hadoop.hbase.regionserver.wal.FSHLog.doSync(FSHLog.java:836)
  at 
org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.sync(AbstractFSWAL.java:605)
  at org.apache.hadoop.hbase.regionserver.HRegion.sync(HRegion.java:8383)
  at org.apache.hadoop.hbase.regionserver.HRegion.doWALAppend(HRegion.java:7956)
  at 
org.apache.hadoop.hbase.regionserver.HRegion.doMiniBatchMutate(HRegion.java:4672)
  at org.apache.hadoop.hbase.regionserver.HRegion.batchMutate(HRegion.java:4597)
  at 
org.apache.hadoop.hbase.regionserver.RSRpcServices.doBatchOp(RSRpcServices.java:1078)
  at 
org.apache.hadoop.hbase.regionserver.RSRpcServices.multi(RSRpcServices.java:2933)
{code}
Timeline on the affected RegionServer, which was hosting 51 regions:
||Time (UTC)||Event||
|14:44:56.379|NPE above. WAL write path wedges immediately.|
|14:44:56.382|First mutation that will hang for 30s begins.|
|14:45:03 - :09|{{{}LruBlockCache{}}}, {{{}BucketCache{}}}, 
{{LocatedBlocksRefresher}} and replication stats all still logging normally – 
the JVM is healthy.|
|14:45:26|Three {{Multi}} calls surface as {{responseTooSlow}} at 
{{{}processingtimems=30001{}}}, {{{}fsreadtime=0{}}}, {{{}queuetimems=0{}}}. 
Logging then goes quiet.|
|14:45:30 - :45|RPC call-queue backlog grows ~33% in 15s.|
|14:45:48|External supervisor kills the host on an availability threshold.|
|14:45:50|Master sees the server as CRASHED, schedules ServerCrashProcedure.|
|14:45:51|RegionServer's own report gets {{{}YouAreDeadException{}}}, begins 
abort.|
|14:46:06|"Aborting region server timed out, terminating forcibly and does not 
wait for any running shutdown hooks or finalizers."|
|14:46:16|SCP completes; 51 regions reassigned after splitting 65 WAL files.|

Notes on the failure shape:
 * No {{JvmPauseMonitor}} entries and no slow-sync warnings in the window. This 
was not GC, HDFS, or load – the {{responseTooSlow}} rate had been flat for the 
preceding two hours and then stopped entirely.
 * Background threads kept logging on schedule throughout, so only the WAL 
write path was affected.
 * Because reads and writes share the RPC handler pool, handlers accumulated in 
{{blockOnSync}} and were never returned. Read and write downtime for this 
server were measured as effectively identical (149040 ms vs 149034 ms in one 
minute; 45015 ms vs 45016 ms in the next). A write-path-only bug produced full 
read and write unavailability.
 * The abort path itself could not complete – closing regions requires flushing 
memstores, which requires the wedged WAL – so the process needed forcible 
termination and recovery depended entirely on WAL replay.

h2. Proposed fix

Two independent changes; the first is the one that prevents the lockup.
 # Make the publish unconditional. Move {{getSyncFuture()}} inside the {{try}} 
so a throw still publishes the claimed sequence and the consumer can drain. The 
truck needs to carry the failure (or be published empty) so that 
{{RingBufferEventHandler}} can complete the entry rather than trip over an 
unloaded truck. This turns a wedged WAL into one failed request.
 # Make {{SyncFutureCache.getIfPresentOrNew()}} non-throwing – catch 
{{RuntimeException}} from the {{asMap().remove()}} and fall back to {{{}new 
SyncFuture(){}}}. The cache is purely an allocation optimisation and should 
never be able to fail a write.

More generally, any exception thrown between {{RingBuffer.next()}} and 
{{RingBuffer.publish()}} is unrecoverable for the WAL. It may be worth auditing 
the other claim sites for the same pattern.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to