Siyao Meng created HDDS-16815:
---------------------------------
Summary: Container data scanner thread ends when a chunk's stored
bytes per checksum is invalid, and a large stored value sizes the scan's read
buffer
Key: HDDS-16815
URL: https://issues.apache.org/jira/browse/HDDS-16815
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
CR-5-report-invalid-stored-bytes-per-checksum-in-container-scanner.patch,
TestBugCR5ScannerStoredBytesPerChecksum.java
h3. Mechanism
The container data scan reads the bytes per checksum stored in a chunk's
checksum metadata and trusts it. {{KeyValueContainerCheck.scanBlock}} passes it
to {{BUFFER_POOL.getBuffer(bytesPerChecksum)}} to size the read buffer, and
{{verifyChecksum}} uses it to walk the chunk and, for the last interval, as the
divisor in {{chunk.getLen() % bytesPerChecksum}}.
For a chunk whose stored checksum type is not NONE and whose stored bytes per
checksum is zero or negative, the scan throws an unchecked exception: zero
gives an {{IndexOutOfBoundsException}} fetching the first computed checksum
from an empty list (or an {{ArithmeticException}} divide by zero, depending on
the chunk), and a negative value gives an {{IllegalArgumentException}}
allocating the buffer. The data scan runs on CLOSED, QUASI_CLOSED and UNHEALTHY
containers, so the exception can be thrown for any such container.
The background data scanner does not survive this.
{{AbstractBackgroundContainerScanner.scanContainers}} catches only
{{InterruptedException}} and {{IOException}} per container, and {{run()}}
catches the escaping exception, logs "exiting because of exception" and
returns. There is one background data scanner thread per volume, and nothing
restarts it, so one such chunk on a volume ends that volume's data scanning
until the datanode restarts, and the scanner's metrics are unregistered. The
on-demand scanner catches the exception in the scan future but leaves the
container in {{containerRescheduleCheckSet}}, so later on-demand scans of that
container are never queued.
A large stored bytes per checksum (up to {{Integer.MAX_VALUE}}) is used to
reserve a direct read buffer of that size, so one such chunk makes each scan of
its container try to reserve up to about 2 GB of direct memory.
h3. Trigger
A closed (or quasi-closed or unhealthy) container holds a chunk whose stored
checksum metadata has a checksum type other than NONE and a bytes per checksum
that is zero, negative, or far larger than the chunk. A data scan of that
container (background or on demand) reads the stored value.
h3. Impact
* A stored bytes per checksum of zero or less: the data scan throws an
unchecked exception. The volume's background data scanner thread ends and does
not restart until the datanode restarts, and the on-demand scanner stops
scanning that container. Other volumes' scanner threads and the metadata
scanner are not affected.
* A very large stored bytes per checksum: each data scan of the container
reserves a direct buffer of that size, which can exhaust the datanode's direct
memory.
h3. Reproduction
PASS on the unmodified pin, deterministic.
[^TestBugCR5ScannerStoredBytesPerChecksum.java]
({{hadoop-hdds/container-service}}) builds real closed {{KeyValueContainer}}s
on disk and drives the real {{KeyValueContainerCheck}},
{{BackgroundContainerDataScanner}} and {{OnDemandContainerScanner}}; only the
{{ContainerController}} is a mock. The stored value is put in place by
rewriting the block metadata in the container DB, the way the existing scanner
tests set up their containers. Five cases: the data scan throws for a stored
value of 0 and of -1; the background scanner thread ends although the volume
has not failed and the scanner was not shut down; the on-demand scanner never
schedules the container again after the scan fails; a stored value of
{{Integer.MAX_VALUE}} makes the scan reserve over 1 GB of direct memory. A
passing run means the defect is present. With the patch all five fail (4
failures, 1 error): the two scan cases and the on-demand case fail on their
assertion because the scan returns a result instead of throwing and the
container is scheduled again, the buffer case fails on its assertion because
the read buffer stays small, and the background scanner case ends with a
timeout waiting for the thread to end, because the thread stays alive.
h3. Patch
[^CR-5-report-invalid-stored-bytes-per-checksum-in-container-scanner.patch],
against 1e528b97aed3eef1be6846f1f0767c1ef98a883a. It also applies to master at
8ca44914771.
{{KeyValueContainerCheck.scanBlock}} now treats a stored bytes per checksum of
zero or less as a chunk it cannot verify: it records a {{CORRUPT_CHUNK}} scan
error and adds the chunk to the merkle tree as not matching, instead of reading
it, and keeps scanning the rest of the container. A new {{readBufferSize}}
helper caps the direct read buffer, so an interval larger than
{{OZONE_SCM_CHUNK_MAX_SIZE}} (the largest chunk a datanode reads at once) does
not allocate a buffer larger than the chunk file, and the last-interval
{{buffer.limit(...)}} is clamped to the buffer capacity.
{{AbstractBackgroundContainerScanner.scanContainers}} and
{{OnDemandContainerScanner.performOnDemandScan}} now also catch
{{RuntimeException}} per container, so an unexpected failure scanning one
container is logged and skipped instead of ending the background scanner thread
or leaving the container scheduled. {{InterruptedException}} handling is
unchanged, so shutdown still works.
A container that already holds such a chunk is, after the patch, scanned to
completion and the chunk is reported as corrupt, so the container is marked
UNHEALTHY. The chunk's data on disk is not touched. Containers whose chunks all
have a valid stored bytes per checksum are unaffected.
The patch covers the container data scan only: other readers of a chunk with
such stored metadata (chunk and block reads, reconciliation, file checksum
computation) still fail for that read or task (from reading the code, not run
with the patch).
Covered by new tests in existing suites:
{{TestKeyValueContainerCheck.testInvalidBytesPerChecksumReportedAsCorruptChunk}}
and {{testBytesPerChecksumLargerThanChunkFile}},
{{TestBackgroundContainerDataScanner.testUnexpectedScanFailureDoesNotStopIteration}},
and
{{TestOnDemandContainerScanner.testContainerScannedAgainAfterUnexpectedScanFailure}}.
Without the patch the first, third and fourth of these fail with the unchecked
exception escaping the scan, and the second does not compile because it also
checks the new {{readBufferSize}} helper; with it the unit suites of the
{{keyvalue}}, {{ozoneimpl}} and {{checksum}} packages in
{{hadoop-hdds/container-service}} pass (931 tests, 10 skipped by the suites),
the scanner integration suites pass (21 tests), checkstyle is clean, and the
reproduction's five cases fail as described above. Not run on a released
version.
Found by code review of the container reconciliation paths in the datanode and
SCM, as part of the TLA+ verification effort under HDDS-15926, on commit
1e528b97aed3eef1be6846f1f0767c1ef98a883a. Checked against HDDS issues and
apache/ozone pull requests for duplicates before filing. The attached patch is
a proposal for review. Generated with Specula (Claude Opus 5.5).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]