Siyao Meng created HDDS-16812:
---------------------------------
Summary: Evicted XceiverClientRatis is closed inline under the
XceiverClientManager cache lock, stalling a writer for 3 seconds
Key: HDDS-16812
URL: https://issues.apache.org/jira/browse/HDDS-16812
Project: Apache Ozone
Issue Type: Bug
Components: Ozone Client
Affects Versions: 2.3.0
Reporter: Siyao Meng
Assignee: Siyao Meng
h3. Problem
A thread calling {{XceiverClientManager.acquireClient}} can be blocked for
about 3 seconds because it ends up closing another, idle {{XceiverClientRatis}}
that was just evicted from the client cache. The close runs on the caller
thread and inside {{synchronized (clientCache)}}, so any other thread calling
{{acquireClient}} or {{releaseClient}} on the same manager has to wait as well.
HDDS-15849 fixed the same pattern for {{XceiverClientGrpc}} by making its close
use {{shutdownNow()}}. The {{XceiverClientRatis}} path is not covered, because
there the wait happens inside {{RaftClient.close()}}.
h3. How it was observed
HBase 2.6.7 region server writing its WAL (filesystem WAL provider, one WAL
file, hflush per sync) to Ozone master (2.3.0-SNAPSHOT at 24cf9aaa28e, Ratis
3.3.1), 3 datanodes, RATIS/THREE. Client settings:
{{ozone.client.incremental.chunk.list=true}},
{{ozone.client.stream.putblock.piggybacking=true}},
{{ozone.client.key.write.concurrency=-1}},
{{ozone.client.hbase.enhancements.allowed=true}}. Workload: YCSB load of 1M
rows with 16 threads.
In 2 of 15 load runs, all in flight WAL syncs stalled once for about 3 seconds.
HBase logged "Slow sync cost" of 3029 to 3043 ms on all five sync runner
threads at the same instant, and "append-pool-0 took 3029 ms appending an edit
to wal". The other run showed 3211 to 3217 ms. Datanode side Ratis timers
showed no stall at that time (leader write max about 100 to 240 ms). 8 ms
before the syncs completed, the region server logged:
{noformat}
WARN [Thread-80] grpc.GrpcUtil: Timed out gracefully shutting down connection:
ManagedChannelOrphanWrapper{delegate=ManagedChannelImpl{logId=21,
target=172.18.0.3:9858}}.
{noformat}
Thread dumps taken every second show the WAL append thread in the following
state for three consecutive dumps, while starting a new block (shaded package
prefixes removed):
{noformat}
"regionserver/datanode:16020.append-pool-0" WAITING (parking)
at org.apache.ratis.util.PeerProxyMap.close(PeerProxyMap.java:185)
at
org.apache.ratis.client.impl.RaftClientRpcWithProxy.close(RaftClientRpcWithProxy.java:53)
at org.apache.ratis.grpc.client.GrpcClientRpc.close(GrpcClientRpc.java:234)
at org.apache.ratis.client.impl.RaftClientImpl.close(RaftClientImpl.java:472)
at
org.apache.hadoop.hdds.scm.XceiverClientRatis.closeRaftClient(XceiverClientRatis.java:233)
at
org.apache.hadoop.hdds.scm.XceiverClientRatis.close(XceiverClientRatis.java:227)
at
org.apache.hadoop.hdds.scm.XceiverClientSpi.cleanup(XceiverClientSpi.java:87)
at
org.apache.hadoop.hdds.scm.XceiverClientSpi.setEvicted(XceiverClientSpi.java:79)
at
org.apache.hadoop.hdds.scm.XceiverClientManager$1.onRemoval(XceiverClientManager.java:106)
- locked (a com.google.common.cache.LocalCache$LocalManualCache)
at
com.google.common.cache.LocalCache.processPendingNotifications(LocalCache.java:1845)
at
com.google.common.cache.LocalCache$Segment.runUnlockedCleanup(LocalCache.java:3493)
at
com.google.common.cache.LocalCache$Segment.postWriteCleanup(LocalCache.java:3469)
at
com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2174)
...
at
org.apache.hadoop.hdds.scm.XceiverClientManager.getClient(XceiverClientManager.java:190)
at
org.apache.hadoop.hdds.scm.XceiverClientManager.acquireClient(XceiverClientManager.java:157)
- locked (a com.google.common.cache.LocalCache$LocalManualCache)
at
org.apache.hadoop.hdds.scm.XceiverClientCreator.acquireClient(XceiverClientCreator.java:111)
at
org.apache.hadoop.hdds.scm.storage.BlockOutputStream.<init>(BlockOutputStream.java:204)
at
org.apache.hadoop.hdds.scm.storage.RatisBlockOutputStream.<init>(RatisBlockOutputStream.java:83)
at
org.apache.hadoop.ozone.client.io.BlockOutputStreamEntry.createOutputStream(BlockOutputStreamEntry.java:156)
at
org.apache.hadoop.ozone.client.io.BlockOutputStreamEntry.checkStream(BlockOutputStreamEntry.java:115)
at
org.apache.hadoop.ozone.client.io.BlockOutputStreamEntry.write(BlockOutputStreamEntry.java:182)
at
org.apache.hadoop.ozone.client.io.KeyOutputStream.writeToOutputStream(KeyOutputStream.java:299)
at
org.apache.hadoop.ozone.client.io.KeyOutputStream.handleWrite(KeyOutputStream.java:275)
"Thread-80" TIMED_WAITING (parking)
at java.util.concurrent.CountDownLatch.await
at
io.grpc.internal.ManagedChannelImpl.awaitTermination(ManagedChannelImpl.java:797)
at org.apache.ratis.grpc.GrpcUtil.shutdownManagedChannel(GrpcUtil.java:245)
at
org.apache.ratis.grpc.client.GrpcClientProtocolClient.close(GrpcClientProtocolClient.java:164)
at org.apache.ratis.util.PeerProxyMap.closeProxy(PeerProxyMap.java:201)
...
{noformat}
HBase has a single WAL append thread, so no second thread was waiting on the
cache lock in these dumps. The effect on other {{acquireClient}} and
{{releaseClient}} callers follows from the code.
h3. Mechanism
Checked on master at 8ca44914771 with Ratis 3.3.1. The code involved is the
same as at 24cf9aaa28e.
# {{XceiverClientManager}} keeps clients in a Guava cache built with
{{expireAfterAccess}} ({{scm.container.client.idle.threshold}}, default 10s),
{{maximumSize}} and a removal listener.
# Guava runs removal listeners on whichever thread next writes to the cache.
Here that is the writer inside {{acquireClient}}, which already holds
{{synchronized (clientCache)}}.
# The listener calls {{XceiverClientSpi.setEvicted()}}, and {{cleanup()}} calls
{{close()}} when the reference count is zero.
# {{XceiverClientRatis.close()}} calls {{RaftClient.close()}}.
{{PeerProxyMap.close()}} joins on closing every peer proxy, and
{{GrpcUtil.shutdownManagedChannel}} waits up to 3 s for graceful channel
termination, then up to 2 s more after {{shutdownNow()}}. The client and admin
channels of a peer are shut down one after the other, so the worst case is
about 10 s.
The same blocking close under the lock is reachable from {{releaseClient}},
through {{decrementReference()}} on an already evicted client and through
{{clientCache.invalidate(key)}}.
Expiry is measured from the last {{acquireClient}} call, not from the last use
or release. With the default 10s threshold, the client of a block that took
longer than 10 s to write is already expired when it is released, so the client
being closed can belong to a pipeline that was written to moments earlier.
The graceful shutdown completes only after the datanode completes the client's
request streams, which the Ratis server does once every outstanding request on
the stream has finished. So the stall needs a request from the evicted client
to be still pending on the datanode. Which request that was in these two cases
was not determined. Most evictions in the same runs did not stall.
h3. Proposed fix
Close evicted clients on a background thread owned by {{XceiverClientManager}},
outside the cache lock.
# {{XceiverClientSpi.setEvicted()}} and {{decrementReference()}} no longer call
{{close()}}. They return whether the client should now be closed, and
{{XceiverClientManager}} hands that client to a single daemon thread. This
covers all three paths above.
# Closes still run one at a time, as they effectively did under the cache lock,
so the number of threads does not grow with the number of evicted clients.
# The number of clients waiting to be closed is bounded by
{{scm.container.client.max.size}}. If the bound is reached, the client is
closed by the caller as it is today, which keeps backpressure in the extreme
case.
# {{XceiverClientManager.close()}} waits for pending closes.
A change in Ratis to let {{RaftClient.close()}} return without waiting for
graceful channel termination would shorten these closes further, but is not
needed for this fix.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]