[
https://issues.apache.org/jira/browse/HDDS-17278?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Andrey Yarovoy reassigned HDDS-17278:
-------------------------------------
Assignee: Andrey Yarovoy
> Processes that write with Ratis data streaming don't exit until they are
> killed
> -------------------------------------------------------------------------------
>
> Key: HDDS-17278
> URL: https://issues.apache.org/jira/browse/HDDS-17278
> Project: Apache Ozone
> Issue Type: Bug
> Components: Ozone Client
> Reporter: Andrey Yarovoy
> Assignee: Andrey Yarovoy
> Priority: Major
>
> #### Problem
> With `ozone.fs.datastream.enabled=true`, a JVM that has written a file
> through Ratis data streaming doesn't exit
> when its main thread returns, unless it explicitly closes its
> `FileSystem`/`OzoneClient` first. The Ratis streaming
> client's shared Netty worker group runs a **non-daemon** thread, and that
> thread keeps the JVM alive.
> Most Hadoop code doesn't close cached `FileSystem` instances. It relies on
> Hadoop's `ClientFinalizer` shutdown hook
> to close them at exit. That hook runs only once the JVM starts shutting down,
> which the non-daemon thread prevents.
> So the client is never closed, and its reference to the shared worker group
> is never released. Nothing breaks the
> cycle except an external kill.
> The normal (non-streaming) write path is not affected: its gRPC Netty threads
> (`grpc-default-worker-ELG-*`) are
> daemon threads.
> #### Impact
> Every MapReduce map task that writes with data streaming hangs after it
> completes. The AppMaster waits
> `mapreduce.task.exit.timeout` (default 60 s), takes a thread dump and kills
> the container. The container's slot
> stays occupied for that whole time, so **every extra wave of map tasks starts
> 60 s late**.
> Measured with TestDFSIO on a 12-datanode cluster (Ozone 2.2.0, Kerberos, gRPC
> TLS, RATIS/THREE, FSO bucket over
> `ofs://`), streaming against the normal write path, same day and same cluster:
> | TestDFSIO -write, job time | normal | data streaming |
> |---|---|---|
> | 250 files (1 wave) | 31 s | 31 s |
> | 500 files (2 waves) | 39–70 s | 103–133 s (+63 to +72 s) |
> | 1000 files (3 waves) | 51–131 s | 186–240 s (+109 to +134 s) |
> The penalty is about 60 s per extra wave whatever the file size (128 MB to 1
> GB). Across 20 write jobs, total job
> time went from 15.2 to 27.8 min, although each task wrote 27% faster with
> streaming. Other short-lived clients that
> stream and don't close their client hang the same way: CLI tools, Spark or
> Hive executors, custom applications.
> #### Evidence
> YARN logs of a 500-file write (`application_1792563382394_0007`): all 359
> first-wave map attempts, one per map
> slot, follow the same sequence:
> ```
> 23:30:54,052 TaskAttemptImpl: attempt_..._m_000250_0 TaskAttempt Transitioned
> from RUNNING to SUCCESS_FINISHING_CONTAINER
> 23:31:58,011 AbstractLivelinessMonitor: Expired:attempt_..._m_000250_0 Timed
> out after 60 secs [TaskAttemptFinishingMonitor]
> ```
> All 359 thread dumps, written to the containers' stdout before the kill, show
> the main thread already returned
> (`DestroyJavaVM` waiting) and exactly one non-daemon application thread:
> ```
> "NettyClientStreamRpc-workerGroup--thread1" #69 prio=5 os_prio=0 ... runnable
> java.lang.Thread.State: RUNNABLE
> at org.apache.ratis.thirdparty.io.netty.channel.epoll.Native.epollWait(Native
> Method)
> at
> org.apache.ratis.thirdparty.io.netty.channel.epoll.EpollEventLoop.epollWaitNoTimerChange(EpollEventLoop.java:308)
> at
> org.apache.ratis.thirdparty.io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:365)
> at
> org.apache.ratis.thirdparty.io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:1007)
> at java.lang.Thread.run(Thread.java:840)
> ```
> Client log of the same task: `raft.netty.dataStream.client.worker-group.share
> = true (default)`,
> `worker-group.size = 160 (default)`, `Create EpollEventLoopGroup for
> NettyClientStreamRpc-workerGroup`.
> #### Code path (Ratis 3.3.1)
> - `NettyClientStreamRpc$WorkerGroupGetter.SHARED`: with
> `raft.netty.dataStream.client.worker-group.share=true` (the
> default), one `EventLoopGroup` per JVM, held by a `ReferenceCountedObject`
> and shut down (`shutdownGracefully`)
> only when the last `NettyClientStreamRpc` is closed.
> - `NettyUtils.newEventLoopGroup(name, size, useEpoll)` →
> `ConcurrentUtils.newThreadFactory(name)`: creates plain
> `Thread`s without `setDaemon(true)`, so each event-loop thread inherits the
> daemon flag of the thread that starts
> it. Netty starts it lazily from the writer thread (here `main`), so it is
> non-daemon.
> - Ozone: `XceiverClientRatis` creates the `RaftClient` with its
> `DataStreamApi`. The `NettyClientStreamRpc` is
> closed only by `XceiverClientManager.close()`, reached from
> `FileSystem.close()` / `OzoneClient.close()`.
> #### How to reproduce
> ```java
> public static void main(String[] args) throws Exception {
> Configurationconf= newOzoneConfiguration();
> conf.setBoolean("ozone.fs.datastream.enabled", true);
> FileSystemfs= FileSystem.get(URI.create("ofs://<om>/"), conf);
> try (FSDataOutputStreamout= fs.create(newPath("/vol/ratis3bucket/f"))) {
> out.write(newbyte[8 << 20]); // > ozone.fs.datastream.auto.threshold (4MB),
> so it streams
> }
> // no fs.close(): the JVM never exits
> }
> ```
> Datanodes need `hdds.container.ratis.datastream.enabled=true`. Adding
> `fs.close()` before returning makes it exit,
> and so does setting `ozone.fs.datastream.enabled=false`.
> #### Proposed fix
> 1. **Ratis (root cause, companion issue):** create the DataStream client's
> worker-group threads as daemon threads,
> as gRPC does for its client event loops. A client-side I/O pool shouldn't
> keep a JVM alive.
> 2. **Ozone, until a fixed Ratis is available:** if Ozone can supply the
> thread factory or event loop group used by the
> streaming client, use daemon threads. If it can't, document the issue for
> `ozone.fs.datastream.enabled`.
> 3. **Test:** a test that runs the reproducer above in a child JVM and asserts
> that it exits within a few seconds.
> Workaround: lower `mapreduce.task.exit.timeout` (eg. 5000) and
> `mapreduce.task.exit.timeout.check-interval-ms` (eg.
> 1000) for MapReduce jobs, or close the `FileSystem` explicitly in
> applications.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]