Skip to content

*: delete per-store metrics when a store is tombstoned - #11127

Open
bufferflies wants to merge 6 commits into
tikv:masterfrom
bufferflies:hotcache-metrics-cleanup
Open

*: delete per-store metrics when a store is tombstoned#11127
bufferflies wants to merge 6 commits into
tikv:masterfrom
bufferflies:hotcache-metrics-cleanup

Conversation

@bufferflies

@bufferflies bufferflies commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Close #11126

What is changed and how does it work?

Several per-store Prometheus metrics were never cleaned up on store
removal, leaking stale time series until the next PD leader election
forced a full Reset(). Most visibly, pd_hotcache_status entries in
HotPeerCache.gc() were only removed from the in-memory map, never
from the GaugeVec itself.

Add DeleteStoreMetrics helpers to pkg/schedule/filter,
pkg/schedule/hbstream, and pkg/schedule/schedulers, and wire them
into both the classic bury path (server/cluster.BuryStoreLocked /
removeStoreStatistics) and the standalone scheduling microservice's
store watcher (pkg/mcs/scheduling/server/meta), matching the
existing pattern used by statistics.ResetStoreStatistics.

Also switch storeStatusGauge cleanup in ResetStoreStatistics from a
hand-maintained type-string list (which had already drifted out of
sync with what observe()/ObserveHotStat() actually set) to
DeletePartialMatch, so it cannot silently drift again.

The per-store heartbeat/bucket-report histograms and counters
defined directly in package server and pkg/mcs/scheduling/server
cannot be reached from the bury path without a cross-package cycle,
so they are swept periodically instead (serverMetricsLoop and
Cluster.collectMetrics respectively).

Check List

Tests

  • Unit test

Release note

Fix per-store Prometheus metrics (hotcache, scheduler, filter, heartbeat-stream, and heartbeat/bucket-report metrics) not being removed after a store is tombstoned, which previously left stale time series in `/metrics` until the next PD leader election.

Summary by CodeRabbit

  • Bug Fixes

    • Removed stores are now fully cleared from monitoring metrics, including heartbeat, scheduler, filtering, bucket-report, and hot-peer statistics.
    • Prevents stale time series from appearing after stores are removed or buried.
    • Periodic cleanup removes metrics recreated by late heartbeats.
    • Cleanup continues across collection cycles to ensure removed-store data is cleared.
  • Tests

    • Expanded coverage to verify store-related metrics are deleted during statistics cleanup.

Several per-store Prometheus metrics were never cleaned up on store
removal, leaking stale time series until the next PD leader election
forced a full Reset(). Most visibly, pd_hotcache_status entries in
HotPeerCache.gc() were only removed from the in-memory map, never
from the GaugeVec itself.

Add DeleteStoreMetrics helpers to pkg/schedule/filter,
pkg/schedule/hbstream, and pkg/schedule/schedulers, and wire them
into both the classic bury path (server/cluster.BuryStoreLocked /
removeStoreStatistics) and the standalone scheduling microservice's
store watcher (pkg/mcs/scheduling/server/meta), matching the
existing pattern used by statistics.ResetStoreStatistics.

Also switch storeStatusGauge cleanup in ResetStoreStatistics from a
hand-maintained type-string list (which had already drifted out of
sync with what observe()/ObserveHotStat() actually set) to
DeletePartialMatch, so it cannot silently drift again.

The per-store heartbeat/bucket-report histograms and counters
defined directly in package server and pkg/mcs/scheduling/server
cannot be reached from the bury path without a cross-package cycle,
so they are swept periodically instead (serverMetricsLoop and
Cluster.collectMetrics respectively).

Close tikv#11126.

Signed-off-by: bufferflies <tongjian3@foxmail.com>
Signed-off-by: bufferflies <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/needs-triage-completed dco-signoff: yes Indicates the PR's author has signed the dco. labels Aug 10, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign rleungx for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a377786b-f368-4624-be11-e6a5f88d88f4

📥 Commits

Reviewing files that changed from the base of the PR and between c7b7180 and 9cef474.

📒 Files selected for processing (4)
  • pkg/mcs/scheduling/server/cluster.go
  • pkg/schedule/filter/metrics.go
  • pkg/schedule/hbstream/metric.go
  • server/cluster/scheduling_controller.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/mcs/scheduling/server/cluster.go
  • server/cluster/scheduling_controller.go

📝 Walkthrough

Walkthrough

The change adds per-store Prometheus metric deletion helpers. Store removal, burial, periodic cleanup, and hot-cache garbage collection now delete metrics for removed stores. Tests verify that hot-cache metrics are removed after garbage collection.

Changes

Removed-store metric cleanup

Layer / File(s) Summary
Metric deletion helpers and status reset
pkg/mcs/scheduling/server/metrics.go, server/metrics.go, pkg/schedule/filter/metrics.go, pkg/schedule/hbstream/metric.go, pkg/schedule/schedulers/metrics.go, pkg/statistics/store_collection.go
Added label-based deletion and reset helpers for per-store metrics across scheduling, heartbeat, filter, bucket, scheduler, and store-status metrics.
Store removal lifecycle wiring
pkg/mcs/scheduling/server/meta/watcher.go, pkg/mcs/scheduling/server/cluster.go, server/cluster/cluster.go, server/cluster/scheduling_controller.go, server/server.go
Store removal, burial, scheduling collection, and periodic metrics collection now delete metrics for removed stores.
Hot-cache garbage collection validation
pkg/statistics/hot_peer_cache.go, pkg/statistics/hot_peer_cache_test.go
Hot-cache garbage collection deletes removed-store status series. Tests verify cache and metric removal.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • tikv/pd#11033: Both changes modify pkg/mcs/scheduling/server/meta/watcher.go, but this change adds metric cleanup for removed stores.

Suggested labels: ok-to-test

Suggested reviewers: rleungx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: deleting per-store metrics when a store is tombstoned.
Description check ✅ Passed The description includes the issue, implementation details, unit-test coverage, and release note; omitted optional checklist sections do not prevent review.
Linked Issues check ✅ Passed The changes satisfy issue #11126 by deleting hot-cache metrics for removed stores and add consistent cleanup for related per-store metrics.
Out of Scope Changes check ✅ Passed The changes remain within the metric-cleanup objective and support consistent cleanup across classic and standalone scheduling paths.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/statistics/hot_peer_cache_test.go`:
- Around line 841-843: The garbage-collection assertions currently recreate
removed gauge series through WithLabelValues, so they do not verify deletion. In
the assertions near the hot-cache GC test, replace those calls with
hotCacheStatusGauge.DeletePartialMatch using the partial labels for each store
and cache type, and assert that each deletion removes the expected series.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d5d34920-8b53-4652-97e2-635ab9ae28ac

📥 Commits

Reviewing files that changed from the base of the PR and between 3211aa5 and 5052fa3.

📒 Files selected for processing (13)
  • pkg/mcs/scheduling/server/cluster.go
  • pkg/mcs/scheduling/server/meta/watcher.go
  • pkg/mcs/scheduling/server/metrics.go
  • pkg/schedule/filter/metrics.go
  • pkg/schedule/hbstream/metric.go
  • pkg/schedule/schedulers/metrics.go
  • pkg/statistics/hot_peer_cache.go
  • pkg/statistics/hot_peer_cache_test.go
  • pkg/statistics/store_collection.go
  • server/cluster/cluster.go
  • server/cluster/scheduling_controller.go
  • server/metrics.go
  • server/server.go

Comment thread pkg/statistics/hot_peer_cache_test.go Outdated
…tombstoned stores

collectMetrics/cleanupRemovedStoreMetrics run on a fixed ticker and,
for every store still IsRemoved() this tick, called DeleteStoreMetrics
unconditionally. DeletePartialMatch takes an exclusive lock and does a
linear scan of the whole metric vector, so a store lingering in the
tombstoned-but-not-yet-fully-removed state (up to the tombstone GC
interval) triggered that scan on every tick for no reason after the
first pass.

Track which store IDs have already been cleaned in a per-ticker-owned
map (no lock needed, since only that ticker's single goroutine touches
it), skip stores already in it, and drop entries once a store is no
longer tombstoned (fully removed), so the tracking map itself stays
bounded.

Signed-off-by: bufferflies <tongjian3@foxmail.com>
Signed-off-by: bufferflies <1045931706@qq.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/server.go`:
- Around line 786-794: Update cleanupRemovedStoreMetrics and the
metric-reporting paths so late heartbeat or bucket-report writes for tombstoned
stores cannot recreate deleted series. Either reject those writes while the
store remains in removed, or make cleanup repeatedly delete metrics after each
write; preserve cleanedRemovedStoreMetrics bookkeeping consistently with the
chosen approach.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 68b250e4-34a7-4997-b6d0-a662465090cf

📥 Commits

Reviewing files that changed from the base of the PR and between 5052fa3 and 2ff912f.

📒 Files selected for processing (2)
  • pkg/mcs/scheduling/server/cluster.go
  • server/server.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/mcs/scheduling/server/cluster.go

Comment thread server/server.go Outdated
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.07767% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.36%. Comparing base (e000290) to head (2f87de5).
⚠️ Report is 5 commits behind head on master.

❌ Your patch check has failed because the patch coverage (64.07%) is below the target coverage (74.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@           Coverage Diff            @@
##           master   #11127    +/-   ##
========================================
  Coverage   79.35%   79.36%            
========================================
  Files         542      542            
  Lines       76993    77187   +194     
========================================
+ Hits        61097    61256   +159     
- Misses      11594    11630    +36     
+ Partials     4302     4301     -1     
Flag Coverage Δ
unittests 79.36% <64.07%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…den test

- pkg/statistics/store_collection.go: fix import group ordering flagged
  by the statics CI check (gci) -- prometheus/client_golang belongs in
  the default group before the pingcap group, not merged into it.

- server/server.go, pkg/mcs/scheduling/server/cluster.go: drop the
  cleanedRemovedStoreMetrics dedup tracking added earlier. It was
  unsafe: HandleRegionHeartbeat (both server/grpc_service.go and
  pkg/mcs/scheduling/server/grpc_service.go) only checks store == nil,
  not store.IsRemoved(), before recording regionHeartbeat*/
  bucketReport* metrics, unlike HandleStoreHeartbeat which rejects
  tombstoned stores up front via checkStore(). A late region heartbeat
  for an already-cleaned, still-tombstoned store could therefore
  recreate a metric series that the dedup tracking would then never
  sweep again. Go back to deleting unconditionally every tick; a
  DeletePartialMatch on labels that no longer exist is a cheap no-op.

- pkg/statistics/hot_peer_cache_test.go: assert on
  hotCacheStatusGauge.DeletePartialMatch's return count instead of
  WithLabelValues+ToFloat64. The latter recreates a fresh, zero-valued
  series on every call regardless of whether gc() actually deleted the
  old one, so it verified less than it looked like; the former proves
  no series remain.

Signed-off-by: bufferflies <tongjian3@foxmail.com>
Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies

Copy link
Copy Markdown
Contributor Author

/retest-required

…ery tick

filter.DeleteStoreMetrics and hbstream.DeleteStoreMetrics were only
called once, from BuryStoreLocked at tombstone time. That one-shot
delete gets undone almost immediately for both packages:

- Every scheduler's Schedule() still runs cluster.GetStores() (which
  includes tombstoned-but-not-fully-removed stores) through
  StoreStateFilter each cycle. The rejection itself is what increments
  filterSourceCounter/filterTargetCounter, so as long as the store
  stays known, the counters get recreated on the very next scheduling
  cycle.
- HeartbeatStreams' keepalive ticker sends to every entry in s.streams
  as long as storeInformer.GetStore(storeID) != nil, without checking
  IsRemoved(). Nothing unbinds a store's stream on bury, only on a
  failed Send, so heartbeatStreamCounter keeps getting rewritten for
  as long as the tombstoned store's stream stays connected.

Neither has any cleanup tied to full removal either (unlike
clusterStatusGauge's DeleteClusterStatusMetrics), so once recreated
they would never be swept again.

Move both into the existing per-tick collectSchedulingMetrics
(server/cluster) and collectMetrics (pkg/mcs/scheduling/server) loops,
which already iterate every store every cycle for other metrics,
mirroring the same unconditional-delete pattern already used there for
regionHeartbeat*/bucketReport* metrics. Keep the one-shot calls in
BuryStoreLocked for immediacy; the periodic sweep makes them safe
against the ongoing rewrites.

Found via a systematic post-merge review of PR tikv#11127 after CI went
green, using the github-pr-review skill.

Signed-off-by: bufferflies <tongjian3@foxmail.com>
Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies

Copy link
Copy Markdown
Contributor Author

/retest-required

1 similar comment
@bufferflies

Copy link
Copy Markdown
Contributor Author

/retest-required

@bufferflies

Copy link
Copy Markdown
Contributor Author

/test pull-unit-test-next-gen-2

1 similar comment
@bufferflies

Copy link
Copy Markdown
Contributor Author

/test pull-unit-test-next-gen-2

resetSchedulingMetrics (classic) and resetMetrics (mcs scheduling
service) already reset schedulerStatusGauge, hotSpotStatusGauge, and
other scheduling-related metrics when the scheduling service/job
stops (context cancellation or primary handoff), but filter and
heartbeat-stream counters were left out, so a standby replica or a
freshly-stopped scheduling job could keep showing stale values from
when it was last active.

Add ResetFilterMetrics and ResetHeartbeatStreamMetrics, following the
existing Reset*Metrics naming convention, and call them from both
reset functions for parity with the other metrics already reset
there.

Signed-off-by: bufferflies <tongjian3@foxmail.com>
Signed-off-by: bufferflies <1045931706@qq.com>
delete(f.regionsOfStore, storeID)
delete(f.thresholdsOfStore, storeID)
delete(f.metrics, storeID)
hotCacheStatusGauge.DeletePartialMatch(prometheus.Labels{"store": storeTag(storeID), "type": f.kind.String()})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cleanup only runs from HotPeerCache.UpdateStat() via gc(). If an idle store is removed and no later hot-peer update arrives, this branch never executes, so the pd_hotcache_status series for that store remains indefinitely—the main remove-tombstone scenario in #11126 is therefore still uncovered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f87de5: gc() is now also called from HotCache.CollectMetrics()'s existing periodic tick, so cleanup no longer depends on UpdateStat being triggered by unrelated activity.

Comment thread server/server.go
// late write would never get swept again for as long as the store stays
// tombstoned-but-not-yet-removed. DeletePartialMatch on labels that no longer
// exist is a cheap no-op, so repeating it every tick is safe.
for _, store := range rc.GetStores() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanupRemovedStoreMetrics can only discover tombstoned stores while they remain in RaftCluster.GetStores(). A late heartbeat or bucket write after the last sweep followed by final store-metadata deletion recreates a series that no subsequent sweep can identify, leaving it permanently stale.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f87de5: cleanupRemovedStoreMetrics now tracks the set of store IDs seen tombstoned on the previous tick and, when one drops out of GetStores() entirely between ticks, performs one more delete for it.

evictedSlowStoreStatusGauge.DeletePartialMatch(prometheus.Labels{"store": storeID})
evictedStoppingStoreStatusGauge.DeleteLabelValues(storeID)
slowStoreTriggerLimitGauge.DeletePartialMatch(prometheus.Labels{"store": storeID})
storeSlowTrendEvictedStatusGauge.DeletePartialMatch(prometheus.Labels{"store": storeID})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HotPendingSum is also keyed by the store label and is written by the hot-region scheduler, but this helper never deletes it. Consequently, pd_scheduler_hot_pending_sum series survive store tombstoning even when every caller invokes this cleanup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f87de5: DeleteStoreMetrics now also deletes HotPendingSum.

Comment thread server/metrics.go Outdated

// DeleteStoreMetrics deletes the per-store heartbeat/bucket-report metrics of a store.
func DeleteStoreMetrics(storeAddress, id string) {
labels := prometheus.Labels{"address": storeAddress, "store": id}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deletion matches both the current address and store ID, but PD allows an existing store ID to change address. If the store emitted metrics at address A and later moves to B, tombstone cleanup matches only B and permanently leaves the A-labeled heartbeat and bucket series.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f87de5: DeleteStoreMetrics, ResetStoreStatistics, and the mcs-scheduling equivalent now match on the store label alone, dropping the address requirement (and the pkg/mcs/router watcher, which had the same pattern).

for _, s := range stores {
statsMap.Observe(s)
statistics.ObserveHotStat(s, c.hotStat.StoresStats)
if s.IsRemoved() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObserveHotStat runs immediately before this branch and can recreate storeStatusGauge from retained rolling stats for a tombstoned store, while this block never resets that vector. The one-shot reset in the meta watcher is therefore undone on the next collection tick, and the series becomes unreachable after final metadata deletion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f87de5: the per-store cleanup block now also calls statistics.ResetStoreStatistics, via a small deleteTombstonedStoreMetrics helper shared with the final-removal catch-up path, so it's reapplied every tick alongside the other metrics.

…moval race

Five issues raised in review, all fixed:

- pkg/statistics/hot_cache.go: HotPeerCache.gc() was only ever triggered
  from UpdateStat, so a store removed while the cluster is idle (or
  while it was the only store still receiving hot-peer updates) would
  never have its hotCacheStatusGauge series cleaned up -- the main
  remove-tombstone scenario in tikv#11126. Call gc() from CollectMetrics's
  existing periodic, activity-independent tick instead; it already
  self-throttles via topNTTL, so calling it every tick is cheap.

- pkg/statistics/store_collection.go, server/metrics.go,
  pkg/mcs/scheduling/server/metrics.go: ResetStoreStatistics and
  DeleteStoreMetrics matched on address as well as store ID, but PD
  allows an existing store ID to change address (e.g. after a TiKV
  restart with a new IP). Matching both permanently leaked any series
  recorded under a previous address. Match on the store label alone;
  it's the stable identifier. Updated all call sites accordingly,
  including pkg/mcs/router's watcher, which had the same pattern.

- pkg/schedule/schedulers/metrics.go: HotPendingSum is keyed by store
  but wasn't deleted by DeleteStoreMetrics.

- server/server.go, pkg/mcs/scheduling/server/cluster.go,
  server/cluster/scheduling_controller.go: the periodic sweeps can
  only discover tombstoned stores while they remain in GetStores(). A
  write landing after the last sweep but before the store's final
  metadata deletion recreates a series that no later sweep can reach,
  since the per-tick loop only considers stores GetStores() currently
  returns. Track the set of store IDs seen tombstoned on the previous
  tick; when one drops out of GetStores() entirely between ticks
  (i.e. it was fully removed), do one more delete for it. This now
  needs only the store ID, not a cached address, since the address
  match was dropped above.

- pkg/mcs/scheduling/server/cluster.go: ObserveHotStat runs just
  before the per-store cleanup block and can recreate storeStatusGauge
  from rolling stats that were never retired for a tombstoned store
  (unlike classic mode, nothing here calls RemoveRollingStoreStats).
  The block never reset that vector, so the meta watcher's one-shot
  reset was undone on the very next collection tick. Call
  ResetStoreStatistics from the same per-tick sweep as the other
  per-store metrics, via a small deleteTombstonedStoreMetrics helper
  shared with the final-removal catch-up path above.

Signed-off-by: bufferflies <tongjian3@foxmail.com>
Signed-off-by: bufferflies <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 12, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@bufferflies: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-unit-test-next-gen-3 2f87de5 link true /test pull-unit-test-next-gen-3
pull-unit-test-next-gen-2 2f87de5 link true /test pull-unit-test-next-gen-2

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@bufferflies
bufferflies requested a review from rleungx August 13, 2026 07:24
Comment thread server/server.go
// it tombstoned but before removal completed -- would otherwise be unreachable
// by any future sweep. One more delete call closes that window; it's a no-op if
// nothing was actually rewritten.
for storeID := range s.recentlyTombstonedStores {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recentlyTombstonedStores only contains stores observed by an earlier one-minute tick. If a store is buried and remove-tombstone deletes it before that tick, this loop never learns its ID; the existing heartbeat/bucket series (and any late writes) therefore remain in this process indefinitely, so the cleanup still misses a supported manual-removal path.

if s.IsRemoved() {
storeID := s.GetID()
current[storeID] = struct{}{}
deleteTombstonedStoreMetrics(strconv.FormatUint(storeID, 10))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeletePartialMatch takes an exclusive lock and scans the entire vector even when nothing matches, so running 12 such calls for every tombstoned store every 10 seconds makes this path O(tombstones × series), and quadratic for recreated per-store filter series. With the reported 1k-store case, the metrics loop repeatedly performs thousands of full scans and contends with heartbeat and scheduler writes.

// about (heartbeat/bucket metrics, storeStatusGauge/clusterStatusGauge, filter
// counters, and heartbeat-stream counters) for a tombstoned store.
func deleteTombstonedStoreMetrics(storeID string) {
statistics.ResetStoreStatistics(storeID)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResetStoreStatistics also deletes pd_cluster_status, including the store_tombstone_count series that statsMap.Observe intentionally emits just above and the dashboard sums. Because this helper runs after every observation, standalone scheduling permanently hides tombstoned stores from that metric, unlike classic mode where the next collection restores it.

Comment thread server/cluster/cluster.go
addr := store.GetAddress()
storeIDStr := strconv.FormatUint(storeID, 10)
statistics.ResetStoreStatistics(addr, storeIDStr)
statistics.ResetStoreStatistics(storeIDStr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pd_hotspot_status is also keyed by address and store, but none of these tombstone helpers delete it. CollectHotSpotMetrics can keep it alive while cached hot peers remain and stops iterating the store after final metadata deletion; an immediate remove-tombstone or an address change therefore leaves this series indefinitely despite the per-store scheduler cleanup.

@bufferflies
bufferflies requested a review from rleungx August 14, 2026 03:18
delete(f.regionsOfStore, storeID)
delete(f.thresholdsOfStore, storeID)
delete(f.metrics, storeID)
hotCacheStatusGauge.DeletePartialMatch(prometheus.Labels{"store": storeTag(storeID), "type": f.kind.String()})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gc only iterates peersOfStore, but calcHotThresholds creates five pd_hotcache_status children and a thresholdsOfStore entry before any peer is hot. A store that never enters peersOfStore is therefore never visited here, so its threshold series survive final store removal and #11126 remains reproducible for idle or non-hot stores.

deleteTombstonedStoreMetrics(strconv.FormatUint(storeID, 10))
}
}
c.recentlyTombstonedStores = current

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recentlyTombstonedStores drops the ID immediately after the first absent-store sweep, but StoreHeartbeat records an error counter and duration for an unknown store after final metadata deletion. Any later heartbeat recreates these series after the last possible delete, and subsequent ticks can no longer identify the store, so the standalone scheduling service still leaks per-store heartbeat metrics.

storeID := s.GetID()
current[storeID] = struct{}{}
storeIDStr := strconv.FormatUint(storeID, 10)
filter.DeleteStoreMetrics(storeIDStr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The periodic cleanup here only covers filter and heartbeat-stream metrics. A pre-bury store snapshot can republish storeStatusGauge after ResetStoreStatistics, while evict-stopping and slow-trend cleanup recreate scheduler gauges with Set(0) after schedulers.DeleteStoreMetrics; neither family is swept again, so those per-store series can still persist after final removal.

statistics.ResetLabelStatsMetrics()
// reset hot cache metrics
statistics.ResetHotCacheStatusMetrics()
filter.ResetFilterMetrics()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetSchedulingMetrics still leaves most per-store scheduler vectors untouched: ResetSchedulerMetrics only resets schedulerStatusGauge, ruleStatusGauge, and regionLabelStatusGauge, while opInfluenceStatus, balanceDirectionCounter, the evicted-store gauges, balanceRangeGauge, and similar vectors remain. After a primary handoff or scheduling-job stop, the inactive replica therefore continues exposing stale store-labeled series even though the newly added filter and heartbeat-stream metrics are reset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the dco. do-not-merge/needs-triage-completed release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hotcache: pd_hotcache_status metrics not cleaned up for removed stores

2 participants