maintenance: make VictoriaMetrics flush recovery durable - #4289
Conversation
4c96c2a to
318619d
Compare
|
Author remediation update: Immediate flush completion now clears its guard in finally, rechecks the queue, and schedules the next batch when refill raced with the active flush. Failed HTTP batches remain queued and retry before later data, periodic scheduling is restored from finally while the timer is usable, rejected scheduling cannot leave the pending flag stuck, and shutdown performs a bounded best-effort drain with an observable retained count. Refill-race, 503 retry and requeue, and shutdown-drain contracts passed; the full warehouse reactor also passed (62 tests). License and label checks are green; backend jobs are still running at the time of this update. Maintainer review remains required. |
|
CI follow-up: backend build, Maven E2E, image E2E, license, and label checks have all completed successfully on the current head. |
318619d to
419e0e8
Compare
|
Addressed the long-interval periodic retry gap in the latest head ( |
|
Thanks for this — the problems you're fixing are real, and I confirmed all three against master:
All worth fixing. But I think the new backpressure model is a blocking regression. Unbounded backpressure freezes the persistence pipeline sendVictoriaMetrics() now loops with no exit condition other than success or shutdown: while (!offered) { saveData() runs on the single warehouse-persistent-data-storage thread (DataStorageDispatch.java:74-91), which also drives:
If VictoriaMetrics is rejecting writes and the buffer fills, that thread blocks indefinitely and all four stop. metricsDataToStorageQueue is an unbounded LinkedBlockingQueue (InMemoryCommonDataQueue.java:58), so the backlog grows until the heap is exhausted. To be precise about the blast radius: threshold alerting is not immediately affected — MetricsRealTimeAlertCalculator.java:132 consumes pollMetricsDataToAlerter(), a separate queue and thread. But once the storage queue exhausts the heap, that goes down with everything else. The existing availability check does not save you here. checkVictoriaMetricsDatasourceAvailable() probes vmselect (VictoriaMetricsClusterDataStorage.java:147, vmClusterProps.select().url()), while this PR is about vminsert write failures. With vminsert down and vmselect healthy, serverAvailable stays true and execution falls straight into the loop above. Trading bounded data loss for an unbounded stall is a bad trade for a monitoring system — a history-store outage should degrade history, not freeze status calculation and real-time writes. Suggestion: either keep a bounded retry (N attempts / a total time budget) and then drop with a counter or metric, or move the VM write path onto its own executor so backpressure cannot propagate back into the shared consumer thread. The same defects remain in the single-node storage VictoriaMetricsDataStorage has essentially the same code and is untouched by this PR:
Since the single-node deployment is the more common one, fixing only the cluster variant leaves most users exposed. Worth either covering both here or extracting the shared flush logic. Minor Replacing log.error("... failed. {}", responseEntity.getBody()) with status-only logging removes the main diagnostic for write rejections (bad label, out of disk, retention policy). Keeping the body at DEBUG would preserve troubleshooting without the noise. |
|
Addressed the review findings in commit Changes:
Human validation:
The new-head backend, Maven E2E, license, and label checks are currently queued by GitHub and have not started yet. AI assistance: used for draft implementation and test iteration. @Duansg, please re-review this head when convenient. |
|
Two follow-ups: the flush lock doesn't cover the HTTP write, and dropped metrics have no signal outside the log Both apply to VictoriaMetricsClusterDataStorage and VictoriaMetricsDataStorage — the code is parallel in the two files. Replacing the unbounded while (!offered) retry with a bounded offer is the right call, and pulling the standalone storage in alongside the cluster one closes the gap the earlier revision left. Two things I'd like to see addressed before this lands.
flushBufferedMetrics() releases metricsFlushLock before the POST, and leaves retryBatch pointing at the batch for the whole duration of the write: List<VictoriaMetricsContent> batch;
synchronized (metricsFlushLock) {
if (retryBatch.isEmpty()) {
List<VictoriaMetricsContent> nextBatch = new ArrayList<>(...);
metricsBufferQueue.drainTo(nextBatch, ...);
retryBatch = nextBatch;
}
batch = retryBatch;
} // lock released here
if (batch.isEmpty()) return true;
if (!trySaveData(batch)) { ... return false; } // HTTP POST, unsynchronised
synchronized (metricsFlushLock) {
if (retryBatch == batch) retryBatch = Collections.emptyList();
}There is no in-flight marker. A second caller entering this method during the POST sees retryBatch non-empty, takes the same list reference, and sends it again. The success path clears by reference identity, so the loser of the race simply doesn't clear — the samples have already gone twice. The critical section guards the drain and the clear, but not the state that actually matters. This is latent rather than live today, and I want to be precise about why, because the reason isn't in this file:
So the mutual exclusion protecting retryBatch during the write comes from the timer's single-threaded execution model plus stop()'s join — not from metricsFlushLock. Nothing in flushBufferedMetrics() states or enforces that dependency, and it stops holding as soon as anyone supplies the timer with a task executor, adds a second flush trigger, or calls doSaveData — now public and delegating straight to trySaveData — from another thread. Suggested fix, either is cheap:
Unrelated but worth noting alongside it: the retry path is at-least-once by construction — a POST that times out after VictoriaMetrics accepted the batch will be resent on the next flush. That's inherent to retrying and separate from the concurrency point above; I mention it only so the two aren't conflated when someone looks at duplicate samples.
After MAX_BUFFER_OFFER_ATTEMPTS (3) offers at MAX_WAIT_MS (500ms) each — roughly 1.5s — the producer gives up on the rest of the batch: if (!offered) {
recordDroppedMetrics(contentList.size() - index, "buffer remained full");
return;
}That discards every remaining item from index onward, permanently. Bounded loss instead of blocking the warehouse-persistent-data-storage thread is the right trade — that thread also drives calculateMonitorStatus(), realTimeDataWriter.saveData() and PostCollectPlugin, so blocking it was worse. But the loss needs to be visible, and right now it isn't:
An operator whose VM insert endpoint is degraded will silently lose history with no indication in the product. Exposing the counter — as an actuator/Micrometer gauge, or through the existing storage-status endpoint that the monitor pages already poll — would make the trade-off honest. |
Summary
finally, and clear the pending guard when timer scheduling is rejectedRegression proof
The result-oriented contracts were run against the previous implementation first. They demonstrated that:
The updated implementation proves that all refilled batches reach the HTTP boundary, the failed periodic batch is retried byte-for-byte on the one-second recovery path without creating a second periodic chain, and shutdown drains the queue once before rejecting later writes.
Validation
./mvnw -pl hertzbeat-warehouse -Dtest=VictoriaMetricsClusterDataStorageTest#retriesPeriodicFlushFailuresQuicklyWhenTheConfiguredIntervalIsLong test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false— passed withflushInterval=3600./mvnw -pl hertzbeat-warehouse -Dtest=VictoriaMetricsClusterDataStorageTest test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false— 3 tests passed./mvnw -pl hertzbeat-warehouse -am test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false— reactor passed; common-core 584, common-spring 102, warehouse 62 testsgit diff --check— passedOperational impact
No configuration migration is required. A periodic failure no longer waits for the full configured interval: retained data enters the bounded one-second retry path. During a sustained VictoriaMetrics outage, the queue applies backpressure to writers after one failed batch is retained. Shutdown performs a best-effort final flush and logs the retained item count if the destination still rejects it.
AI assistance: used for draft implementation and test iteration.
Human validation: executed the focused long-interval failure/recovery contract and the full warehouse reactor listed above.
Risk notes: recovery remains in-memory; a process crash cannot preserve buffered metrics, and prolonged destination outages can slow warehouse producers through deliberate backpressure.