Skip to content

client/resource_group: add client-side demand observability#10604

Open
okJiang wants to merge 6 commits into
tikv:masterfrom
okJiang:codex/rc-client-observability-latest
Open

client/resource_group: add client-side demand observability#10604
okJiang wants to merge 6 commits into
tikv:masterfrom
okJiang:codex/rc-client-observability-latest

Conversation

@okJiang

@okJiang okJiang commented Apr 20, 2026

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: ref #10488, ref #10581

Resource control observability on the client side does not show the difference between real demand and post-throttling consumption. It is also hard to tell whether pressure is local to one TiDB instance or spread across the whole resource group.

What is changed and how does it work?

This PR adds client-side metrics and slow-path logs so we can explain demand, limiter state, and per-instance pressure before requests are throttled.

client/resource_group: add client-side demand observability

It includes:

  • a pre-throttling demand_ru_per_sec metric collected before Reserve()
  • client-side balance, fill rate, burst limit, average RU/s, throttled state, and consumption breakdown metrics
  • slow-path structured logs for degraded mode, slow token requests, long waits, and throttling
  • cleanup coverage so stale resource-group labels are removed correctly

Check List

Tests

  • Unit test
  • Manual test (add detailed scripts or steps below)

Manual test:

  • Verified in a local tiup playground setup with 3 PD, 2 TiDB, and 3 TiKV.
  • Created a low-fill-rate resource group and confirmed Grafana shows demand_ru_per_sec > fill_rate, higher client wait, and throttling on the hot TiDB instance.
  • Confirmed stale metrics are cleaned when the resource group is removed.

Code changes

  • Has the configuration change

Side effects

  • Increased code complexity

Related changes

Release note

Add client-side resource control metrics for pre-throttling demand, limiter state, and per-instance RU consumption breakdowns.

Summary by CodeRabbit

  • New Features

    • Expanded resource-group telemetry with RU consumption by type, token balance/fill rate/burst limits, average and demand RU/s, throttled-state gauge, and read/write byte + KV/SQL CPU cost counters.
  • Improvements

    • Enhanced structured logging for token and throttling events, including client identity and involved resource groups.
    • Improved concurrent safety for limiter read-only inspections.
    • Improved per-group metric cleanup to remove additional stale/inactive metric label values.
  • Tests

    • Added/updated unit tests to validate new metric emissions, throttling/demand behavior, concurrent logging behavior, and metric cleanup.

@ti-chi-bot

ti-chi-bot Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@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/work-in-progress Indicates that a PR should not merge because it is a work in progress. dco-signoff: yes Indicates the PR's author has signed the dco. labels Apr 20, 2026
@ti-chi-bot

ti-chi-bot Bot commented Apr 20, 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 yisaer 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

@coderabbitai

coderabbitai Bot commented Apr 20, 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

Run ID: 370423cc-1e4b-4fa5-8242-a8bd3e8aaf55

📥 Commits

Reviewing files that changed from the base of the PR and between 369e6aa and 916fcba.

📒 Files selected for processing (1)
  • client/resource_group/controller/group_controller.go
💤 Files with no reviewable changes (1)
  • client/resource_group/controller/group_controller.go

📝 Walkthrough

Walkthrough

Adds per-resource-group Prometheus metrics, demand RU/sec estimation, structured logging, client identity propagation, metric cleanup, limiter read concurrency, updated tests, and an indirect Go dependency.

Changes

Resource Group Metrics Observability

Layer / File(s) Summary
Metrics schema and contracts
client/resource_group/controller/metrics/metrics.go
Adds RU, token-bucket, rate, throttling, and byte/CPU cost metrics; updates the failed-request label.
Controller state and demand accounting
client/resource_group/controller/group_controller.go
Adds client identity, metric handles, demand moving averages, trickle accounting, structured fields, and RU/component observation across request and response paths.
Global wiring and cleanup
client/resource_group/controller/global_controller.go, client/resource_group/controller/global_controller_test.go
Passes client identity through controller creation, enriches slow-request logs, removes expanded metric labels during cleanup, and tests cleanup behavior.
Limiter concurrency and supporting updates
client/resource_group/controller/limiter.go, client/resource_group/controller/group_controller_test.go, client/go.mod
Uses read locks for limiter inspection, updates test construction and isolation, adds metric and concurrency coverage, and adds the indirect dependency.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RequestHandler
  participant groupCostController
  participant tokenCounter
  participant Prometheus
  RequestHandler->>groupCostController: acquire tokens and process request
  groupCostController->>tokenCounter: calculate demand and consumption deltas
  groupCostController->>Prometheus: observe RU, token, byte, and CPU metrics
  groupCostController->>RequestHandler: return wait or response accounting
Loading

Suggested labels: type/development, lgtm, approved, ok-to-test

Suggested reviewers: jmpotato, rleungx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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 is concise and accurately highlights the main change: client-side demand observability for resource control.
Description check ✅ Passed The description follows the template well, with issue references, change summary, tests, checklist items, and a release note.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 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.

@ti-chi-bot ti-chi-bot Bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Apr 20, 2026
@okJiang okJiang marked this pull request as ready for review May 9, 2026 06:15
@ti-chi-bot ti-chi-bot Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 9, 2026

@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: 2

🧹 Nitpick comments (3)
client/resource_group/controller/group_controller.go (2)

307-311: 💤 Low value

Minor: collapse the throttled gauge if/else.

Trivial readability improvement; pre-existing boolToFloat (or inline conversion) is enough.

♻️ Suggested change
-	if gc.isThrottled.Load() {
-		gc.metrics.throttledGauge.Set(1)
-	} else {
-		gc.metrics.throttledGauge.Set(0)
-	}
+	var throttled float64
+	if gc.isThrottled.Load() {
+		throttled = 1
+	}
+	gc.metrics.throttledGauge.Set(throttled)
🤖 Prompt for 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.

In `@client/resource_group/controller/group_controller.go` around lines 307 - 311,
The if/else that sets the throttled gauge can be collapsed into a single call:
replace the conditional block that checks gc.isThrottled.Load() and calls
gc.metrics.throttledGauge.Set(1/0) with one call to
gc.metrics.throttledGauge.Set(boolToFloat(gc.isThrottled.Load())) or an inline
conversion (e.g., use a conditional expression to produce 1.0/0.0) so the value
is computed from gc.isThrottled.Load() and passed directly to
throttledGauge.Set.

480-487: 💤 Low value

Throttled-state-change log message is ambiguous on the "exit" transition.

When wasThrottled is true and isThrottled flips to false, the log emits the generic "throttled state changed", while the entry transition gets the descriptive "resource group entered throttled mode". Mirroring with an explicit "exited" message makes log-based alerting and debugging easier.

♻️ Suggested change
-		message := "[resource group controller] throttled state changed"
-		if isThrottled {
-			message = "[resource group controller] resource group entered throttled mode"
-		}
+		message := "[resource group controller] resource group exited throttled mode"
+		if isThrottled {
+			message = "[resource group controller] resource group entered throttled mode"
+		}
🤖 Prompt for 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.

In `@client/resource_group/controller/group_controller.go` around lines 480 - 487,
The current log in the throttled-state transition only emits a specific message
when entering throttled mode but uses a generic "throttled state changed" on
exit; update the conditional in the block that compares wasThrottled and
gc.isThrottled.Load() so that when isThrottled is false it logs a clear exit
message (e.g., "[resource group controller] resource group exited throttled
mode") instead of the generic one, using the same log.Info call and
gc.logFields(0, nil)... to keep context.
client/resource_group/controller/metrics/metrics.go (1)

248-282: ⚡ Quick win

Counter metrics should use _total suffix instead of _sum.

Plain counters must follow Prometheus naming convention with the _total suffix. The metrics consume_by_type, read_byte_sum, write_byte_sum, kv_cpu_time_ms_sum, and sql_cpu_time_ms_sum are all CounterVec instances and should be renamed:

  • consume_by_typeconsume_by_type_total
  • read_byte_sumread_bytes_total
  • write_byte_sumwrite_bytes_total
  • kv_cpu_time_ms_sumkv_cpu_time_ms_total
  • sql_cpu_time_ms_sumsql_cpu_time_ms_total

The _sum suffix is reserved for auto-generated suffixes on histograms and summaries. Fixing this now prevents confusion with Prometheus tooling and avoids breaking changes once these metrics are consumed by dashboards or alerting rules.

🤖 Prompt for 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.

In `@client/resource_group/controller/metrics/metrics.go` around lines 248 - 282,
The Prometheus counter metric names use `_sum` instead of the required `_total`;
update the CounterOpts.Name fields and any code that references them: change
`consume_by_type` → `consume_by_type_total`, `read_byte_sum` →
`read_bytes_total`, `write_byte_sum` → `write_bytes_total`, `kv_cpu_time_ms_sum`
→ `kv_cpu_time_ms_total`, and `sql_cpu_time_ms_sum` → `sql_cpu_time_ms_total`
(these correspond to the CounterVecs declared as ReadByteCost, WriteByteCost,
KVCPUCost, SQLCPUCost and the consume-by-type CounterVec), ensure you update all
usages/registration and any dashboards/labels referencing those metric names to
the new `_total` names to keep semantics and help text consistent.
🤖 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 `@client/go.mod`:
- Line 29: The go.mod entry for "github.com/kylelemons/godebug v1.1.0" appears
unused; run "go mod why github.com/kylelemons/godebug" to confirm whether any
direct dependency requires it, inspect codebase for any imports of
"github.com/kylelemons/godebug" or its consumers, and if it's not needed remove
the entry by running "go mod tidy"; if it is required transitively by an
observability/library dependency, document that relationship or add the explicit
import in the dependent package so the dependency is intentional.

In `@client/resource_group/controller/group_controller.go`:
- Around line 164-170: There's a data race on tokenCounter's demand/avg fields;
protect these by adding a dedicated mutex to tokenCounter and using it wherever
those fields are read or written: acquire the mutex in calcDemandAvg (and the
called calcMovingAvgAt) before mutating demandTotalRU, demandRUPerSec,
demandRUPerSecLastRU, demandAvgLastTime; also acquire the same mutex when
reading demandRUPerSec and avgRUPerSec in logFields and when
updateAvgRequestResourcePerSec writes avgRUPerSec so all concurrent access to
these symbols (tokenCounter.demandTotalRU, demandRUPerSec, demandRUPerSecLastRU,
demandAvgLastTime, avgRUPerSec) is synchronized.

---

Nitpick comments:
In `@client/resource_group/controller/group_controller.go`:
- Around line 307-311: The if/else that sets the throttled gauge can be
collapsed into a single call: replace the conditional block that checks
gc.isThrottled.Load() and calls gc.metrics.throttledGauge.Set(1/0) with one call
to gc.metrics.throttledGauge.Set(boolToFloat(gc.isThrottled.Load())) or an
inline conversion (e.g., use a conditional expression to produce 1.0/0.0) so the
value is computed from gc.isThrottled.Load() and passed directly to
throttledGauge.Set.
- Around line 480-487: The current log in the throttled-state transition only
emits a specific message when entering throttled mode but uses a generic
"throttled state changed" on exit; update the conditional in the block that
compares wasThrottled and gc.isThrottled.Load() so that when isThrottled is
false it logs a clear exit message (e.g., "[resource group controller] resource
group exited throttled mode") instead of the generic one, using the same
log.Info call and gc.logFields(0, nil)... to keep context.

In `@client/resource_group/controller/metrics/metrics.go`:
- Around line 248-282: The Prometheus counter metric names use `_sum` instead of
the required `_total`; update the CounterOpts.Name fields and any code that
references them: change `consume_by_type` → `consume_by_type_total`,
`read_byte_sum` → `read_bytes_total`, `write_byte_sum` → `write_bytes_total`,
`kv_cpu_time_ms_sum` → `kv_cpu_time_ms_total`, and `sql_cpu_time_ms_sum` →
`sql_cpu_time_ms_total` (these correspond to the CounterVecs declared as
ReadByteCost, WriteByteCost, KVCPUCost, SQLCPUCost and the consume-by-type
CounterVec), ensure you update all usages/registration and any dashboards/labels
referencing those metric names to the new `_total` names to keep semantics and
help text consistent.
🪄 Autofix (Beta)

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

Run ID: 9825b70a-d0c9-40d8-a4fb-978dd35ad1ef

📥 Commits

Reviewing files that changed from the base of the PR and between e430bd0 and 877ff3c.

⛔ Files ignored due to path filters (1)
  • client/go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • client/go.mod
  • client/resource_group/controller/global_controller.go
  • client/resource_group/controller/global_controller_test.go
  • client/resource_group/controller/group_controller.go
  • client/resource_group/controller/group_controller_test.go
  • client/resource_group/controller/limiter.go
  • client/resource_group/controller/metrics/metrics.go

Comment thread client/go.mod
Comment thread client/resource_group/controller/group_controller.go
return calcMovingAvgAt(gc.run.now, &counter.avgRUPerSec, &counter.avgRUPerSecLastRU, &counter.avgLastTime, new)
}

func (gc *groupCostController) calcDemandAvg(counter *tokenCounter, delta float64) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

calcDemandAvg is called from acquireTokens request goroutine so data race might happen.

Suggestion: accumulate demandTotalRU under gc.mu in the request path, compute the EMA in the main loop — same pattern as avgRUPerSec.

failpoint.Inject("triggerUpdate", func() {
gc.lowRUNotifyChan <- notifyMsg{}
})
if waitDuration > slowNotifyFilterDuration {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

slowNotifyFilterDuration == 10 ms might be too short for token wait scenario? (this also applies to other log.Warn added in this PR)

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.

Agreed. A successful token wait is expected throttling behavior, so logging every successful wait over 10ms at WARN is too noisy under high QPS. #10997 removed these successful-wait WARNs on release-nextgen-202603 and has merged. I applied the equivalent change here in b8a835c and 916fcba: successful request-side and response-side wait WARNs are removed, failed-wait WARNs remain, and SuccessfulRequestDuration preserves its existing semantics.

okJiang added 4 commits July 10, 2026 10:51
Signed-off-by: okjiang <819421878@qq.com>
Signed-off-by: okjiang <819421878@qq.com>
Signed-off-by: okjiang <819421878@qq.com>
Signed-off-by: okjiang <819421878@qq.com>
@okJiang okJiang force-pushed the codex/rc-client-observability-latest branch from 16c761a to 369e6aa Compare July 10, 2026 02:55
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.27941% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.27%. Comparing base (c2a47d8) to head (369e6aa).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10604      +/-   ##
==========================================
+ Coverage   79.22%   79.27%   +0.04%     
==========================================
  Files         541      541              
  Lines       75965    76212     +247     
==========================================
+ Hits        60187    60414     +227     
- Misses      11531    11543      +12     
- Partials     4247     4255       +8     
Flag Coverage Δ
unittests 79.27% <92.27%> (+0.04%) ⬆️

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.

Successful token waits are expected while resource control throttles requests.
Logging every wait over 10ms at WARN can flood TiDB logs under high QPS.

Keep the existing histogram for observability and record the accumulated retry
and reservation wait duration.

Signed-off-by: lhy1024 <19542290+lhy1024@users.noreply.github.com>
@lhy1024

lhy1024 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Dependency update: #10997 fixed this logging issue on release-nextgen-202603 and merged in 19ee324. The equivalent change has now been applied to this PR in b8a835c and 916fcba. Successful-wait WARNs are removed while failed-wait WARNs and the existing SuccessfulRequestDuration semantics are preserved.

Signed-off-by: lhy1024 <19542290+lhy1024@users.noreply.github.com>
@ti-chi-bot

ti-chi-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@okJiang: The following test 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-2 916fcba 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.

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. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants