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

Siyao Meng updated HDDS-17371:
------------------------------
    Attachment: CR-2-key-mapped-buffer-cache-by-file-identity.patch

> Datanode mmap read cache is keyed by path only, so a container replica 
> recreated at the same path can be served the deleted replica's bytes
> -------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17371
>                 URL: https://issues.apache.org/jira/browse/HDDS-17371
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 2.0.0, 2.1.0, 2.2.0, 2.1.1, 2.1.2, 2.2.1
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: CR-2-key-mapped-buffer-cache-by-file-identity.patch, 
> TestBugCR2StaleMappingAfterContainerRecreate.java
>
>
> h3. Mechanism
> With memory mapped chunk reads enabled, {{ChunkUtils.readData}} gets each 
> mapping from {{MappedBufferManager.computeIfAbsent}}, which caches it in one 
> datanode wide map under the key {{path-offset-size}}. Nothing on delete, 
> import or overwrite removes an entry; an entry is only skipped once GC has 
> cleared its weak reference. Until then a read of the same path, offset and 
> length returns the cached mapping even if the file at that path has been 
> replaced by a new file (new inode). The cached mapping still shows the old 
> file's bytes, because a mapped file that has been unlinked keeps its 
> contents. Overwriting the same file in place is not affected, since the 
> mapping is shared with the file and sees the new bytes.
> h3. Trigger
> * {{ozone.chunk.read.mapped.buffer.max.count}} > 0 (default 0, so off by 
> default) and a read larger than {{ozone.chunk.read.mapped.buffer.threshold}} 
> (32KB).
> * The read buffer capacity must also be above that threshold. 
> {{ChunkManager.getBufferCapacityForChunkRead}} sets it to the chunk's bytes 
> per checksum, so with the default {{ozone.client.bytes.per.checksum}} of 16KB 
> the mapped path is never used. It is used for chunks written with checksum 
> type NONE (capacity {{ozone.chunk.read.buffer.default.size}}, 1MB), chunks 
> written with more than 32KB per checksum, and reads from old clients that ask 
> for a single buffer.
> * A replica of container C is read through the mapped path, then deleted 
> ({{KeyValueHandler.deleteInternal}}), and a new replica of C is created on 
> the same volume, which puts its block files at the same paths. This happens 
> when the datanode becomes the target of EC reconstruction or replication for 
> a container it held before. Its block holds different bytes at the same chunk 
> offset and length, for example another EC replica index, or a healthy copy 
> replacing a corrupt one. Other routes that put a new file at the same path (a 
> disk balancer move back to the volume, a FILE_PER_CHUNK chunk rewrite) carry 
> the same bytes, so they are harmless, and the patch covers them too.
> * A ReadChunk of that block on the new replica before GC clears the old 
> mapping's weak reference. For a buffer that was promoted to the old 
> generation this can take longer than a young collection interval (argued, not 
> measured).
> h3. Impact
> ReadChunk returns the deleted replica's bytes with no error on the datanode. 
> What happens next depends on the checksums:
> * Data written with checksum type NONE: nothing downstream can detect the 
> stale bytes, so they are returned to the application as the block's content, 
> and an EC reconstruction reading them would encode them into the rebuilt 
> replica. Argued from the code, not run.
> * Data written with more than 32KB per checksum: the client's checksum check 
> ({{ozone.client.verify.checksum}}, on by default) rejects the bytes and the 
> client fails over to another replica. EC reconstruction forces checksum 
> verification, and container reconciliation reads with verification on by 
> default. With verification turned off the stale bytes reach the application.
> All of these conditions are needed together: mmap reads enabled, data on the 
> mapped path (checksum type NONE or more than 32KB per checksum), and an SCM 
> driven delete and recreate of the same container on the same volume while the 
> old mapping is still reachable. The stale bytes are always an older copy of 
> the same block of the same container. Container export and the container 
> scanner read files directly and are not affected.
> h3. Reproduction
> [^TestBugCR2StaleMappingAfterContainerRecreate.java] 
> ({{hadoop-hdds/container-service}}) uses one volume and 
> {{FilePerBlockStrategy}} with mmap reads enabled, and only the calls the 
> datanode makes: create replica 1 of container 1, WriteChunk 64KB of 'A' with 
> end of block, ReadChunk, delete it with 
> {{KeyValueContainerUtil.removeContainer}} and {{KeyValueContainer.delete}}, 
> create replica 2 of the same container at the same path, WriteChunk 'B', 
> ReadChunk. Its {{ChunkInfo}} carries no checksum data, so the buffer capacity 
> is the 64KB chunk length and the read takes the mapped path, as a chunk 
> written with checksum type NONE does; with 16KB CRC32 checksum data the same 
> test reads 'B' on unmodified source, because the mapped path is not taken. 
> The first read's buffer is kept reachable, which stands for "no GC yet" and 
> makes the result deterministic. On unmodified source it fails with:
> {noformat}
> [ReadChunk on the recreated replica must return its own bytes, not the 
> deleted replica's]
> expected: 'B'
>  but was: 'A'
> {noformat}
> What was run is this datanode level stale read, on macOS only. Not exercised: 
> Linux, a mini cluster with real EC reconstruction or replication, and an 
> application or an EC reconstruction consuming the stale bytes.
> h3. Suggested fix
> Include the file's identity in the cache key. Invalidating entries on delete, 
> import and overwrite was considered and not chosen: it needs hooks in several 
> places, races with a reader that adds the entry back, and misses any other 
> path that replaces a file.
> h3. Patch
> [^CR-2-key-mapped-buffer-cache-by-file-identity.patch], against 
> 5fed99dab3c3204cd7bf1516074969a38d5eb00d. It also applies to master at 
> 76b5c6947a2.
> The mapped branch of {{ChunkUtils.readData}} now uses the path plus 
> {{BasicFileAttributes.fileKey()}} (device and inode) as the cache's file key, 
> so a replaced file misses the cache and an unchanged file still hits. The 
> identity is read before the file is opened, so a replacement in between 
> caches the newer file's mapping under the older identity, which a later read 
> can only hit if that inode number is reused at the same path while the 
> mapping is live. A failed attribute read is handled like a failed open. The 
> cost is one {{stat}} per mapped read; that path already opens and closes the 
> file on every read, and {{FileChannel}} offers no cheaper way to get the 
> identity. If {{fileKey()}} is null on some file system, the key falls back to 
> today's behaviour.
> Covered by {{TestChunkUtils#mappedReadAfterFileReplacedAtSamePath}}: read a 
> file through the mapped path, replace it at the same path, and expect the new 
> bytes; then overwrite it in place and expect those bytes too. Without the 
> change it fails with "read returned bytes of the replaced file". With the 
> change {{TestChunkUtils}}, {{TestMappedBufferManager}}, 
> {{TestFilePerBlockStrategy}}, {{TestFilePerChunkStrategy}}, 
> {{TestChunkManagerDummyImpl}} and {{TestKeyValueHandler}} pass (91 tests) and 
> checkstyle is clean. The attached reproduction passes with the change.
> Found by code review of the datanode memory mapped chunk read path, as part 
> of the TLA+ verification effort under HDDS-15926, on commit 
> 5fed99dab3c3204cd7bf1516074969a38d5eb00d. 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
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to