rdhabalia opened a new pull request, #8715:
URL: https://github.com/apache/hadoop/pull/8715

   ### Description of PR
   
   Multi-tenant HDFS clusters often need to pin a subset of data, identified by 
its HDFS path, to a dedicated pool of DataNodes. This prevents noisy or 
isolation-sensitive tenants from sharing read/write capacity with the rest of 
the cluster.
   
   The default `BlockPlacementPolicy` has no notion of path-to-DataNode 
affinity, so operators typically resort to separate clusters or brittle 
rack-based workarounds.
   
   ### Approach
   
   Introduce a pluggable `DatanodeAffinityManager` abstraction, resolved via 
reflection from:
   
   ```properties
   dfs.datanode.affinity.manager.classname
   ```
   
   An affinity group maps:
   
   * **Source path regex** → **DataNode hostname regex**
   
   On refresh, the manager resolves each DataNode hostname regex against the 
live cluster and builds a restricted `NetworkTopology` for each group 
containing only that group's eligible DataNodes.
   
   ### Components
   
   * **`DatanodeManager`**
   
     * Instantiates the configured affinity manager.
     * Removes affinity DataNodes from the default `NetworkTopology` as they 
register, ensuring the default placement policy cannot select them.
     * Prunes DataNodes from affinity structures when they are decommissioned 
or removed.
     * Re-triggers an affinity refresh on `dfsadmin -refreshNodes`.
   
   * **`BlockManager`**
   
     * Maintains one `BlockPlacementPolicy` per affinity group.
     * Each policy is backed by the group's restricted topology.
     * Routes initial block placement (`chooseTarget4NewBlock`) through the 
matching group's policy.
     * Routes mid-write pipeline recovery (`chooseTarget4AdditionalDatanode`) 
through the matching group's policy.
     * Ensures replacement nodes for failed pipeline members remain within the 
isolated pool.
     * Uses the default placement policy when no affinity group matches.
   
   * **`FileDatanodeAffinityManager`**
   
     * Built-in implementation that loads affinity groups from a JSON file.
     * Configuration:
   
       ```properties
       dfs.datanode.affinity.file.path
       ```
     * Reloads the configuration on `dfsadmin -refreshNodes`.
   
   * **`hdfs fsck`**
   
     * Supports:
   
       ```bash
       hdfs fsck <path> -favored-nodes
       ```
     * Prints the affinity-resolved DataNodes for a path as a dry run.
     * Does not require the path to exist.
   
   ## Isolation vs. Availability
   
   If an affinity group cannot place all requested replicas because of:
   
   * an under-provisioned group,
   * all group DataNodes being unavailable, or
   * a `datanodesRegex` matching no nodes,
   
   the default behavior falls back to the shared pool so the write can still 
succeed. Spillover is logged at `WARN`.
   
   Strict isolation can be enabled with:
   
   ```properties
   dfs.namenode.affinity.strict.isolation.enabled=true
   ```
   
   In strict mode, initial placement fails closed unless **all requested 
replicas** can be placed inside the affinity group.
   
   This is important because placing only `minReplication` replicas in-group 
would allow the redundancy monitor to repair the remaining replicas onto 
shared-pool DataNodes, leaking data outside the isolated group.
   
   ## Scope and Limitations
   
   Affinity currently governs:
   
   * Initial block placement.
   * In-pipeline recovery.
   
   Background replication and EC reconstruction still use the default placement 
policy.
   
   Making these paths affinity-aware would require a reverse block-to-path 
lookup inside a lock-sensitive hot loop and is intentionally left as follow-up 
work. This is documented in `BlockManager`.
   
   ### DataNode Regex Matching
   
   DataNode regex patterns are matched using `find()` against each node's:
   
   ```text
   hostname:port
   ```
   
   Therefore, a fully anchored pattern must account for the trailing port.
   
   For example:
   
   ```regex
   ^dn-tenant-a[0-9]+\.example\.com(:\d+)?$
   ```
   
   ## Configuration
   
   | Property                                         | Description             
                                  | Default          |
   | ------------------------------------------------ | 
--------------------------------------------------------- | ---------------- |
   | `dfs.datanode.affinity.manager.classname`        | Configured affinity 
manager implementation                | Empty / disabled |
   | `dfs.datanode.affinity.file.path`                | JSON configuration file 
for `FileDatanodeAffinityManager` | -                |
   | `dfs.namenode.affinity.strict.isolation.enabled` | Enable strict affinity 
isolation                          | `false`          |
   
   ## Concurrency and Correctness Hardening
   
   ### 1. Full-Replica Placement
   
   Both strict and non-strict modes require the affinity group to place **all 
requested replicas** before returning in-group targets.
   
   Accepting a partial placement, even when it satisfies `minReplication`, 
creates an under-replicated block. The redundancy monitor could then repair the 
remaining replicas onto shared-pool DataNodes.
   
   Behavior on shortfall:
   
   * **Strict mode:** Fail closed.
   * **Non-strict mode:** Spill the **entire block** to the shared pool using 
the default placement policy.
   
   This guarantees that the block is fully replicated at write time and avoids 
both isolation leaks and silent under-replication windows.
   
   ### 2. Registration/Removal Races During Refresh
   
   Registration and removal races during lock-free configuration refresh are 
reconciled.
   
   `internalRefresh()` first takes a live snapshot before publishing the 
refreshed state. After publishing:
   
   1. `postAffinityRefresh()` re-evaluates every live DataNode against the 
current affinity patterns.
   
      * A DataNode that registered during the refresh is still moved into its 
appropriate affinity group.
      * The node cannot leak back into the default topology.
   
   2. A post-publish purge re-runs `onDatanodeRemoved()` for any snapshot node 
that is no longer live.
   
      * A DataNode removed during refresh cannot remain as a stale entry in the 
newly published structures.
   
   The isolated-address set is a stable set that is mutated in place rather 
than replaced. This ensures concurrent registration updates are not lost.
   
   ### 3. DataNode Replacement
   
   A DataNode can re-register with the same `host:port` but a new descriptor or 
storage UUID.
   
   Affinity group topologies store descriptor objects while membership is 
address-keyed. Without reconciliation, a replacement could leave a stale 
descriptor selectable for placement.
   
   `onDatanodeRegistered()` now:
   
   * Detects replacement DataNodes.
   * Swaps stale descriptors for the live descriptor.
   * Uses object identity to guard the replacement.
   * Removes stale descriptors for the same transfer endpoint regardless of 
which topology path they reside under.
   * Handles replacements that also move to a different rack or IP.
   
   `postAffinityRefresh()` routes every live DataNode through this 
reconciliation logic rather than using an address-based short circuit.
   
   ### 4. Per-Group Topology and Storage Types
   
   The per-group restricted topology is intentionally **not** a 
storage-type-aware `DFSNetworkTopology`.
   
   `DFSNetworkTopology` captures each node's storage-type counts when the node 
is added and updates them through the descriptor's parent back-pointer.
   
   Affinity DataNodes are removed from the default topology, which nulls their 
parent back-pointer. Consequently, storage-type counts in a 
`DFSNetworkTopology` affinity topology could become stale.
   
   This is particularly problematic because:
   
   1. A DataNode registers with an empty storage map.
   2. The DataNode is added to its affinity group.
   3. Storage reports arrive later.
   4. A `DFSNetworkTopology` group would retain a storage count of zero.
   5. Storage-type-gated selection could therefore return no target.
   
   After a NameNode restart, this could cause affinity placement to silently 
fail until a manual:
   
   ```bash
   dfsadmin -refreshNodes
   ```
   
   To avoid this:
   
   * When the cluster uses the default `DFSNetworkTopology`, each affinity 
group uses a plain `NetworkTopology`.
   * The topology does not maintain storage-type counts.
   * The placement policy selects an eligible leaf and validates the storage 
type against the **live DataNode descriptor**.
   
   This ensures storage information remains current.
   
   When:
   
   ```properties
   dfs.use.dfs.network.topology=false
   ```
   
   the configured `net.topology.impl` is honored instead.
   
   For example:
   
   ```text
   NetworkTopologyWithNodeGroup
   ```
   
   can be used for a node-group-based placement policy. Such implementations do 
not maintain storage counts and therefore do not have the same 
stale-storage-accounting issue.
   
   The placement policy's `initialize()` still validates that the supplied 
cluster map is compatible with the configured topology implementation.
   
   ### 5. Pipeline Recovery
   
   `chooseTarget4AdditionalDatanode()` routes through the affinity group's 
policy **only when the surviving pipeline replicas actually belong to that 
group's restricted topology**.
   
   Otherwise, it uses the default placement policy.
   
   This distinction is required for non-strict availability mode:
   
   1. An under-provisioned affinity group spills the **whole block** to the 
shared pool.
   2. The surviving pipeline replicas are therefore shared-pool DataNodes.
   3. Those DataNodes are not present in the affinity group's restricted 
topology.
   4. Forcing the affinity policy would count out-of-topology survivors against 
the group's `getMaxNodesPerRack()` calculation.
   5. `numChosen + numAdditional` could exceed the group's leaf count.
   6. The additional target count could become zero.
   7. No replacement DataNode would be returned.
   8. With the client default `best-effort=false`, the write could fail.
   
   Using the default policy for a spilled block avoids this failure.
   
   It also does not introduce an isolation leak because:
   
   * The block is already entirely in the shared pool.
   * Affinity DataNodes are excluded from the default topology.
   
   ## Testing
   
   `TestDatanodeAffinityBlockPlacement` contains **14 tests** covering:
   
   * All replicas for an affinity path landing only on the group's DataNodes.
   * Non-affinity paths using the full cluster.
   * Isolation surviving DataNode re-registration.
   * Stale-node pruning on DataNode removal.
   * Strict isolation failing for:
   
     * Unsatisfiable placement.
     * Partially provisioned groups.
   * Non-strict fallback succeeding through the shared pool for:
   
     * Empty groups.
     * Partially provisioned groups.
   * Whole-block spillover with no replica remaining on the under-provisioned 
affinity group.
   * Pipeline recovery of a spilled shared-pool block succeeding through the 
default policy.
   * In-group pipeline recovery remaining within the affinity group.
   * Malformed records with null regex values being skipped without aborting 
refresh.
   * DataNodes registering during configuration refresh being reconciled into 
their affinity group instead of leaking into the default topology.
   * Replaced DataNodes with the same address but a new UUID being reconciled 
to the live descriptor.
   * DataNode replacement working both:
   
     * In place.
     * When the replacement moves to a different rack/path.
   * Per-group topology being a plain `NetworkTopology` rather than a 
storage-type-aware `DFSNetworkTopology`.
   * Storage accounting remaining current for affinity DataNodes.
   
   The removal-race purge reuses `onDatanodeRemoved()`, whose pruning behavior 
is covered by the removal test.
   
   The exact mid-refresh interleaving is not unit-tested because reproducing it 
deterministically would require intrusive production test hooks.
   
   ## Results
   
   The implementation enables **strict tenant isolation of write and read 
traffic** onto a dedicated DataNode pool while keeping the rest of the cluster 
on the default placement policy.
   
   Affinity configuration can be refreshed at runtime without requiring a 
NameNode restart.
   
   
   <!--
     Thanks for sending a pull request!
       1. If this is your first time, please read our contributor guidelines: 
https://cwiki.apache.org/confluence/display/HADOOP/How+To+Contribute
       2. Make sure your PR title starts with JIRA issue id, e.g., 
'HADOOP-17799. Your PR title ...'.
   -->
   
   
   ### How was this patch tested?
   Tested by newly added unit test
   
   ### For code changes:
   
   - [ ] Does the title of this PR start with the corresponding JIRA issue id 
(e.g. 'HADOOP-17799. Your PR title ...')?
   - [ ] Object storage: Have the integration tests been executed and the 
endpoint
         declared according to the connector-specific documentation? *Note: 
Automated CI
         testing doesn't cover all cases so manual testing with cloud storage 
is still
         required.*
   - [ ] If adding new dependencies to the code, are these dependencies 
licensed in a way that is compatible for inclusion under [ASF 
2.0](http://www.apache.org/legal/resolved.html#category-a)?
   - [ ] If applicable, have you updated the `LICENSE`, `LICENSE-binary`, 
`NOTICE-binary` files?
   
   ### AI Tooling
   
   If an AI tool was used:
   
   - [ ] The PR includes the phrase "Contains content generated by <tool>"
         where <tool> is the name of the AI tool used.
   - [ ] My use of AI contributions follows the ASF legal policy
         https://www.apache.org/legal/generative-tooling.html
   
   


-- 
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]


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

Reply via email to