[
https://issues.apache.org/jira/browse/HDDS-16893?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-16893:
------------------------------
Attachment: TestBugMC1IcrBeforeNodeReport.java
> SCM can grant a datanode's container slot again after the ICR of a new
> container, until the datanode's next node report
> -----------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-16893
> URL: https://issues.apache.org/jira/browse/HDDS-16893
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Priority: Major
> Attachments: MC-1-send-node-report-with-new-container-icr.patch,
> TestBugMC1IcrBeforeNodeReport.java
>
>
> h3. Mechanism
> SCM counts free container slots on a datanode from two inputs
> ({{PendingContainerTracker}}, {{TwoWindowBucket.checkSpaceAndAdd}}): the
> usable space in the last storage report it processed, and the containers it
> has allocated on that datanode but not yet seen reported. It removes the
> pending entry of a new container when it processes the container's ICR
> ({{IncrementalContainerReportHandler.processICR}}) or first sees it in a full
> container report ({{ContainerReportHandler}}).
> The datanode commits the container's space on its volume when it creates the
> container and sends the ICR at once, in an immediately triggered heartbeat.
> That heartbeat carries no fresh node report. The node report is rebuilt only
> by {{NodeReportPublisher}} every {{hdds.node.report.interval}} (60s by
> default) and stored with {{StateContext.refreshFullReport}}. Until the next
> one, SCM counts the new container neither as pending nor in the reported
> committed space, so it can grant the same slot again.
> The question was raised in review of HDDS-14921 (apache/ozone#10000: "Please
> check if node report is also send in ICR") and not followed up.
> h3. Trigger
> Default configuration, no faults. A datanode creates a container on the first
> write after SCM allocated it, so the pending entry normally lasts only
> milliseconds to seconds. For up to one node report interval afterwards, every
> allocation on that datanode is checked against space that does not include
> the new container. This happens for every new container. It becomes visible
> when the datanode is close enough to full that the extra grants exceed its
> remaining slots.
> h3. Impact
> * SCM allocates containers on datanodes that have no room for them, the over
> allocation the tracker was added to prevent (HDDS-14921, HDDS-15024).
> * The datanode then refuses to create such a container ({{DISK_OUT_OF_SPACE}}
> from the volume choice in {{KeyValueContainer.create}}).
> * On a Ratis pipeline the create runs while the first chunk is written
> ({{ContainerStateMachine.writeStateMachineData}}, then the implicit create in
> {{HddsDispatcher.dispatchRequest}}). {{DISK_OUT_OF_SPACE}} is not one of the
> results {{ContainerStateMachine.handleCommandResult}} tolerates, so the state
> machine is marked unhealthy, the Ratis log failure reaches
> {{XceiverServerRatis.handleNodeLogFailure}} and the pipeline is closed. All
> its open containers close, and in flight writes of other clients on that
> pipeline fail and are retried. For EC the write that triggers the create
> fails and the client retries elsewhere. Read from code, not run end to end.
> * Since HDDS-14925, the datanode accepts creates down to a lower hard limit
> than the spare it reports to SCM. With default settings the gap is 0 on
> volumes up to 1TB, 5GB (one default container) at 1.25TB and 80GB on 16TB, so
> on large volumes a few extra grants still succeed inside that band.
> h3. Reproduction
> [^TestBugMC1IcrBeforeNodeReport.java] ({{hadoop-hdds/server-scm}}) uses real
> SCM components ({{SCMNodeManager}}, {{PipelineManagerImpl}},
> {{ContainerManagerImpl.allocateContainer}},
> {{SCMDatanodeHeartbeatDispatcher}}, {{NodeReportHandler}},
> {{IncrementalContainerReportHandler}}). Three datanodes each have one real
> {{HddsVolume}} sized for one container slot, with their storage reports built
> by {{StorageLocationReport}}. It runs deterministically on unmodified source,
> with the default node report interval and a 1GB container size to keep the
> volumes small.
> * {{icrWithoutNodeReportGrantsSlotTwice}} plays the current datanode: create,
> then a heartbeat with only the ICR. A second allocation while the first is
> pending is refused. After the ICR it succeeds on the same full pipeline, and
> the datanode's volume choice for that container throws
> {{DiskOutOfSpaceException}}. The test asserts the correct behaviour and
> fails: {{[second container granted on datanodes with room for one] expected:
> null but was: ContainerInfo\{id=#3, state=OPEN, ...\}}}.
> * {{icrWithNodeReportRefusesSecondSlot}} sends the node report in the same
> heartbeat as the ICR and passes: SCM refuses the second allocation.
> h3. Patch
> [^MC-1-send-node-report-with-new-container-icr.patch], against
> b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at
> 6818707698c.
> When the datanode's ICR sender ({{OzoneContainer}}) sends the ICR of an OPEN
> replica, which only happens after a container create, it first refreshes the
> node report with {{getNodeReport()}}. The heartbeat that carries the ICR then
> normally also carries a node report that includes the committed space, so SCM
> sees that space no later than, or shortly after, it releases the pending
> entry on the ICR. SCM handles {{NODE_REPORT}} and ICRs on different
> executors, so when the node report is processed first the container is
> briefly counted twice, which only delays allocations. SCM code is unchanged.
> Cost: on the first write of each new container, every replica builds a node
> report, which walks the datanode's whole container set
> ({{ContainerSet.getOpenContainerCountsByVolume}}) and takes the volume set
> read lock ({{MutableVolumeSet.getStorageReport}}), inside the ICR sender's
> {{synchronized (containerSet)}}. That is linear in the number of containers
> on the datanode, once per create. The extra report goes in the heartbeat the
> ICR already triggers, so there is no extra RPC; SCM and Recon process one
> more node report per create.
> Remaining windows: the heartbeat thread reads the full reports and the
> incremental reports separately
> ({{StateContext.getAllAvailableReportsUpToLimit}}), so the ICR can
> occasionally go out one heartbeat before the node report; a periodic
> {{NodeReportPublisher}} run that built its report before the create can
> overwrite the fresh one; SCM may process the ICR before the node report of
> the same heartbeat; and a heartbeat that fails before SCM processes it is
> resent with the ICR but without the node report. The same gap for replication
> imports ({{SCMNodeManager.opCompleted}} on the import ICR) is not addressed
> here.
> An SCM side alternative would keep the pending entry after the ICR until SCM
> has processed a node report from a later heartbeat than the one that carried
> the ICR. That is nearly sound and would also cover older datanodes,
> replication imports and the resend after a failed heartbeat. It needs more
> per datanode state, the heartbeat order carried to the separate report
> executors, and it counts each new container twice for up to one node report
> interval. The datanode change is the smaller first step; the two do not
> exclude each other.
> Covered by {{testNewContainerReportSentWithNodeReport}} in the existing
> {{TestOzoneContainer}}. It checks that the node report reaches
> {{StateContext}} before the ICR and contains the container's committed bytes,
> and that later ICRs of the container do not refresh it. Without the change it
> fails for all 8 schema and layout cases. With it, the test and the
> container-service suites around the report path ({{TestHddsDispatcher}},
> {{TestStateContext}}, {{TestReportPublisher}}, {{TestHeartbeatEndpointTask}},
> {{TestKeyValueHandler}}, {{TestKeyValueHandlerWithUnhealthyContainer}},
> {{TestContainerPersistence}}, {{TestCloseContainerCommandHandler}},
> {{TestDeleteContainerCommandHandler}}) pass. Checkstyle is clean. Two tests
> that bind the default port 9886 could not run on the test machine, with or
> without the change.
> Found by TLA+ model checking and code review of the SCM pending container
> allocation tracking under HDDS-15926, on commit
> b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. 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]