[ 
https://issues.apache.org/jira/browse/HDDS-16812?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-16812:
------------------------------
    Status: Patch Available  (was: Open)

> 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
>            Priority: Major
>              Labels: pull-request-available
>
> 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]

Reply via email to