Skip to content

Commit 4e78a45

Browse files
authored
ref(server): Replace bandwidth EWMA with lock-free GCRA buckets (#554)
Bandwidth rate limiting now uses debt-based GCRA (Generic Cell Rate Algorithm) buckets instead of EWMA estimation. Each bucket tracks a single atomic timestamp — the point at which all recorded traffic is "paid off." Admission checks compare this deadline against wall-clock time plus a configurable burst tolerance, with no background estimation task required. This removes the 50ms EWMA tick and its associated state (per-scope/per-usecase shadow EWMA maps, the `EwmaEstimator` struct), replacing them with lock-free `AtomicU64::update` operations that are both simpler and more responsive — debt is visible immediately after bytes are recorded rather than after the next estimation tick. The `burst_ms` config parameter (default: 1000ms) controls how much transient overshoot is tolerated before rejection. EWMA gauges are removed from the KEDA endpoint since the monotonic counters with `irate()` are strictly more responsive. The throughput background task is simplified to emit only the limit and map-size gauges at 1s intervals. The bandwidth limit gauge is now emitted once at construction.
1 parent b74a53a commit 4e78a45

6 files changed

Lines changed: 386 additions & 409 deletions

File tree

objectstore-server/docs/architecture.md

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -182,26 +182,27 @@ Limits can be set at multiple granularities:
182182

183183
### Bandwidth
184184

185-
Bandwidth limiting uses an **exponentially weighted moving average** (EWMA) to
186-
estimate current throughput. Payload streams are wrapped in a
187-
`MeteredPayloadStream` that reports bytes consumed. When the estimated bandwidth
188-
exceeds the configured limit, new requests are rejected.
185+
Bandwidth limiting uses **debt-based GCRA** (Generic Cell Rate Algorithm)
186+
buckets that track a theoretical arrival time. Payload streams are wrapped in a
187+
`MeteredPayloadStream` that track consumed bytes. When accumulated debt exceeds
188+
the configured threshold, new requests are rejected.
189+
190+
The `burst_ms` parameter controls how much transient overshoot is tolerated
191+
before rejection (in milliseconds). Defaults to `1000` (1 second).
189192

190193
Like throughput, bandwidth limits can be set at multiple granularities:
191194

192195
- **Global**: a maximum bytes-per-second across all traffic (`global_bps`)
193196
- **Per-usecase**: a percentage of the global limit for each usecase
194197
- **Per-scope**: a percentage of the global limit for each scope value
195198

196-
Each granularity maintains its own EWMA estimator. The `MeteredPayloadStream`
197-
increments all applicable accumulators (global + per-usecase + per-scope) for
198-
every chunk polled. For non-streamed payloads (e.g., batch INSERT where the
199-
size is known upfront), bytes are recorded directly via
200-
[`record_bandwidth`](state::Services::record_bandwidth).
199+
Each granularity maintains its own bucket. The `MeteredPayloadStream` charges
200+
all applicable buckets (global + per-usecase + per-scope) for every chunk
201+
polled. For non-streamed payloads, bytes are recorded directly.
201202

202203
Rate-limited requests receive HTTP 429. When `report_only` is enabled, all
203-
accounting and metrics (EWMA and limit gauges, KEDA metrics) remain active, but
204-
requests exceeding the limit are admitted instead of rejected.
204+
accounting and metrics remain active, but requests exceeding the limit are
205+
admitted instead of rejected.
205206

206207
### Web Concurrency Limit
207208

@@ -244,20 +245,14 @@ metrics so that it remains available when the server is at capacity.
244245

245246
### Exposed Metrics
246247

247-
#### EWMA Gauges
248-
249-
Pre-smoothed rates, self-contained per scrape (no `irate()` arithmetic needed):
248+
#### Gauges
250249

251250
| Resource | Utilization | Limit |
252251
|---|---|---|
253-
| Bandwidth | `objectstore_bandwidth_ewma` | `objectstore_bandwidth_limit` (only when `global_bps` is set) |
254-
| Throughput | `objectstore_throughput_ewma` | `objectstore_throughput_limit` (only when `global_rps` is set) |
255252
| HTTP concurrency | `objectstore_requests_in_flight` | `objectstore_requests_limit` |
256253
| Task concurrency | `objectstore_tasks_running` | `objectstore_tasks_limit` |
257254

258-
Throughput uses an EWMA with a 50 ms tick and α = 0.2, matching the existing
259-
bandwidth estimator. The accumulator counts fully admitted requests (requests
260-
that pass all throughput checks).
255+
Limit gauges are emitted only when the corresponding limit is configured.
261256

262257
#### Counters
263258

@@ -269,30 +264,10 @@ KEDA queries for an unsmoothed, immediately responsive rate:
269264
| `objectstore_bytes_total` | Total bytes transferred since startup |
270265
| `objectstore_requests_total` | Total admitted requests since startup |
271266

272-
### Example KEDA ScaledObject Triggers
273-
274-
#### Using EWMA gauges (backward-compatible)
275-
276-
Scale on the highest utilization across all four resources:
277-
278-
```yaml
279-
triggers:
280-
- type: prometheus
281-
metadata:
282-
serverAddress: http://prometheus:9090
283-
query: |
284-
max(
285-
objectstore_bandwidth_ewma / objectstore_bandwidth_limit
286-
or objectstore_throughput_ewma / objectstore_throughput_limit
287-
or objectstore_requests_in_flight / objectstore_requests_limit
288-
or objectstore_tasks_running / objectstore_tasks_limit
289-
)
290-
threshold: "0.7"
291-
```
292-
293-
#### Using counters with `irate()` (more responsive)
267+
### Example KEDA ScaledObject Trigger
294268

295-
Uses the last two scraped values for an instantaneous rate with no smoothing lag:
269+
Scale on the highest utilization across all resources using `irate()` for
270+
bandwidth/throughput rates:
296271

297272
```yaml
298273
triggers:

objectstore-server/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,7 @@ mod tests {
10401040
pct: 10
10411041
bandwidth:
10421042
global_bps: 1048576
1043+
burst_ms: 2000
10431044
usecase_pct: 50
10441045
scope_pct: 25
10451046
report_only: true
@@ -1074,6 +1075,7 @@ mod tests {
10741075
},
10751076
bandwidth: BandwidthLimits {
10761077
global_bps: Some(1_048_576),
1078+
burst_ms: 2000,
10771079
usecase_pct: Some(50),
10781080
scope_pct: Some(25),
10791081
report_only: true,

objectstore-server/src/endpoints/keda.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
//!
33
//! Exposes a `/keda` endpoint in Prometheus text format (version 0.0.4) with:
44
//!
5-
//! - **EWMA gauges** (`objectstore_bandwidth_ewma`, `objectstore_throughput_ewma`): pre-computed
6-
//! rates, self-contained per scrape with no `irate()` arithmetic needed (kept for backward compat).
75
//! - **Monotonic counters** (`objectstore_bytes_total`, `objectstore_requests_total`):
86
//! cumulative totals since startup; use `irate(counter[window])` in KEDA queries for an
97
//! unsmoothed, immediately responsive rate.
@@ -21,10 +19,8 @@ pub fn router() -> Router<ServiceState> {
2119
}
2220

2321
async fn keda(State(state): State<ServiceState>) -> impl IntoResponse {
24-
let bw_ewma = state.rate_limiter.bandwidth_ewma();
2522
let bw_limit = state.rate_limiter.bandwidth_limit();
2623
let bw_total = state.rate_limiter.bandwidth_total_bytes();
27-
let tp_rps = state.rate_limiter.throughput_rps();
2824
let tp_limit = state.rate_limiter.throughput_limit();
2925
let tp_total = state.rate_limiter.throughput_total_admitted();
3026
let req_in_flight = state.request_counter.count();
@@ -36,15 +32,9 @@ async fn keda(State(state): State<ServiceState>) -> impl IntoResponse {
3632
"# HELP objectstore_bytes_total Total bytes transferred since startup\n\
3733
# TYPE objectstore_bytes_total counter\n\
3834
objectstore_bytes_total {bw_total}\n\
39-
# HELP objectstore_bandwidth_ewma Current bandwidth in bytes/s (EWMA)\n\
40-
# TYPE objectstore_bandwidth_ewma gauge\n\
41-
objectstore_bandwidth_ewma {bw_ewma}\n\
4235
# HELP objectstore_requests_total Total admitted requests since startup\n\
4336
# TYPE objectstore_requests_total counter\n\
4437
objectstore_requests_total {tp_total}\n\
45-
# HELP objectstore_throughput_ewma Current admitted request rate in requests/s (EWMA)\n\
46-
# TYPE objectstore_throughput_ewma gauge\n\
47-
objectstore_throughput_ewma {tp_rps}\n\
4838
# HELP objectstore_requests_in_flight Current in-flight HTTP requests\n\
4939
# TYPE objectstore_requests_in_flight gauge\n\
5040
objectstore_requests_in_flight {req_in_flight}\n\

0 commit comments

Comments
 (0)