cheeeee opened a new pull request, #23445:
URL: https://github.com/apache/kafka/pull/23445

   The Fetch response is sent zero copy, so the read from the log happens on the
   network thread while the response is written to the socket:
   
   ```
   Selector.pollSelectionKeys -> KafkaChannel.write -> NetworkSend.writeTo
     -> MultiRecordsSend -> DefaultRecordsSend -> FileRecords.writeTo
     -> {Plaintext,Ssl}TransportLayer.transferFrom -> FileChannel.transferTo
   ```
   
   A failure of the medium and a client that went away both arrive at that call 
as
   a bare `java.io.IOException`, and `Selector` classifies every `IOException`
   there as a disconnect, at DEBUG. A partially unreadable log file therefore 
makes
   the partition unreadable while the cluster believes the broker is healthy.
   
   This change does the disambiguation the issue asks for. It does **not** 
offline
   the log directory yet; see "Scope" below.
   
   ### Reproduction
   
   Released 4.3.1 distribution, single-node KRaft broker, two log directories, 
one
   of them on a `dm-dust` device. The tail of one segment is marked unreadable
   while its head and the indexes stay in the page cache, so offset translation
   succeeds and the failure can only land in the transfer. Then one large fetch
   spans into the marked tail.
   
   From the guest's tracefs:
   
   ```
   data-plane-kafk-1296  sys_sendfile64(out_fd: 0xb3, in_fd: 0xaf, offset: 
0xc8ffdea8, count: 0xd737a)
   data-plane-kafk-1296  sys_sendfile64 -> 0xfffffffffffffffb        <- -EIO
   ```
   
   This needs `CONFIG_DM_DUST`, which stock distribution kernels do not enable, 
so
   the broker runs inside a User-Mode Linux guest built with the device-mapper
   fault-injection targets.
   
   Worth stating because it explains why the application is the only place this 
can
   be handled: a failed read of *file data* produces no ext4 error and no 
kernel log
   line, and leaves the filesystem writable — measured: `EXT4-fs error` count 0,
   empty `dmesg`, and a write to the same filesystem immediately afterwards
   succeeded. The errno handed to Kafka is the only signal in the stack.
   
   ### Change
   
   On the error path of `FileRecords.writeTo`, re-read the region that failed to
   transfer:
   
   * the file still reads -> the socket was at fault; the original exception is
     rethrown untouched;
   * the file does not read -> `KafkaStorageException` naming the file and the
     position, with the original exception as its cause.
   
   The message text of the exception is deliberately not inspected: it is the
   platform's `strerror` output, and the error sets of the underlying transfer
   differ between platforms. Where the re-read cannot conclude anything — a
   position at or past the end of the file — the original exception is 
preserved,
   which is today's behaviour. The check sits in `FileRecords`, above both
   transport layers, so it covers the plaintext and the SSL paths named in the
   issue; the file name and position needed for the re-read are only known 
there.
   
   Cost: one extra read, on the error path only.
   
   ### Testing
   
   `FileRecordsTest`, three cases, driven through the test-visible constructor 
with
   a mocked `FileChannel` so they are deterministic on any platform:
   
   * `testWriteToPreservesExceptionWhenFileIsStillReadable`
   * `testWriteToReportsStorageFailureWhenRegionIsUnreadable`
   * `testWriteToPreservesExceptionAtEndOfFile`
   
   `FileRecordsTest` 56 tests, 0 failures; `:clients:checkstyleMain` and
   `:clients:checkstyleTest` pass.
   
   End to end, the same released 4.3.1 distribution was run twice in the guest 
with
   the identical scenario, differing only in this one class:
   
   | | before | after |
   |---|---|---|
   | `sendfile64` returning `-EIO` | 162 | 168 |
   | `KafkaStorageException` in the broker log | 0 | 168 |
   | `DEBUG ... disconnected` | 170 | 8 |
   | `WARN ... Unexpected error` | 0 | 168 |
   | log lines naming the unreadable file | 3 | 171 |
   | log directory offlined | 0 | 0 |
   | broker alive at the end | yes | yes |
   
   162 of the 170 disconnects were disk failures wearing a network label; the 8
   that remain are real client disconnects. The `-EIO` counts differ slightly
   between runs because which fetch lands on a marked block depends on 
scheduling.
   
   What the operator gets now:
   
   ```
   WARN [SocketServer listenerType=BROKER, nodeId=1] Unexpected error from 
/127.0.0.1 ...
   org.apache.kafka.common.errors.KafkaStorageException: Failed to read 922390 
bytes
       at position 638976 of /tmp/bad/logs/t1-0/00000000000000000000.log while 
writing a response
   Caused by: java.io.IOException: Input/output error
   ```
   
   ### Scope, and what is deliberately left out
   
   The issue asks for two things: disambiguate, and offline the log directory. 
This
   PR does the first. The second needs a decision I would rather take with a
   committer than guess at:
   
   1. **Where to cross the module boundary.** `clients` cannot depend on 
`storage`,
      so `Selector` cannot call `LogDirFailureChannel`. The cheapest route is 
to let
      `ChannelState` carry the cause — it already carries 
`AuthenticationException`
      for exactly this reason — and have 
`SocketServer.Processor.processDisconnected`
      route it. A listener injected into `Selector` from the server side is 
cleaner
      layering and more code. Which do you prefer?
   
   2. **Granularity.** Kafka's failure unit is the log directory, so one 
unreadable
      4 KiB block would offline a directory of otherwise healthy replicas, to be
      re-fetched over the network. Defensible, but it is a policy choice worth
      making explicitly.
   
   3. **Whether either of the above needs a KIP.** `FileRecords` is in an
      `internal` package, but `ChannelState` is not.
   
   Other limitations of the evidence above, stated so they are not discovered
   later: only the plaintext path was exercised end to end; only a persistent
   failure was injected, so the transient case — where the re-read succeeds and 
the
   failure is attributed to the socket by design — has no measurement behind it;
   and the discrimination relies on `read(2)` failing for an unreadable region,
   which holds on Linux.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to