Skip to content

feat(inkless:consume): add byte-rate limit for lagging/consolidation cold-path fetches#719

Draft
jeqo wants to merge 1 commit into
mainfrom
jeqo/lagging-byte-rate-limit
Draft

feat(inkless:consume): add byte-rate limit for lagging/consolidation cold-path fetches#719
jeqo wants to merge 1 commit into
mainfrom
jeqo/lagging-byte-rate-limit

Conversation

@jeqo

@jeqo jeqo commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The lagging cold path only had a request-rate limit (storage GET cost/QPS), leaving nothing to bound object-storage read bandwidth per node. Add an independent byte-rate limiter that meters the physical S3 GET range size (byteRange().size(), including bounding-range read amplification).

  • Consumer: new fetch.lagging.consumer.byte.rate.limit (default 0 = disabled).
  • Consolidation: new diskless.consolidation.fetch.lagging.byte.rate.limit, distinct from the existing rate.limit.bytes.per.second quota which meters processed record bytes, not physical reads.
  • Second Bucket4j bucket (capacity = byteRate); acquire is tryConsume-first so each limiter records a per-limiter throttle hit while sharing one wait-time histogram. Oversized fetches (byteRange > capacity) are charged a full bucket and let through instead of deadlocking, tracked via an oversized meter.
  • Startup WARN when the consumer byte-rate limit is below produce.buffer.max.bytes.
  • Hedges remain exempt from both limiters.

…cold-path fetches

The lagging cold path only had a request-rate limit (storage GET cost/QPS),
leaving nothing to bound object-storage read bandwidth per node. Add an
independent byte-rate limiter that meters the physical S3 GET range size
(byteRange().size(), including bounding-range read amplification).

- Consumer: new fetch.lagging.consumer.byte.rate.limit (default 0 = disabled).
- Consolidation: new diskless.consolidation.fetch.lagging.byte.rate.limit,
  distinct from the existing rate.limit.bytes.per.second quota which meters
  processed record bytes, not physical reads.
- Second Bucket4j bucket (capacity = byteRate); acquire is tryConsume-first so
  each limiter records a per-limiter throttle hit while sharing one wait-time
  histogram. Oversized fetches (byteRange > capacity) are charged a full bucket
  and let through instead of deadlocking, tracked via an oversized meter.
- Startup WARN when the consumer byte-rate limit is below produce.buffer.max.bytes.
- Hedges remain exempt from both limiters.

Copilot AI left a comment

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.

Pull request overview

This PR adds an additional cold-path limiter for Inkless fetches: a byte-rate (bandwidth) limiter that meters the physical object-storage read size, complementing the existing request-rate limiter (QPS/cost). It also wires this new limiter into both consumer lagging fetches and diskless consolidation cold-path fetches, with supporting metrics, documentation, and tests.

Changes:

  • Add a configurable byte-rate limiter for lagging/cold-path fetches and plumb it through Reader → FetchPlanner (including “oversized fetch” handling).
  • Introduce new metrics to attribute throttling to request-rate vs byte-rate limiters while keeping a combined wait-time histogram.
  • Add new configuration keys (Inkless + server-side consolidation) and update docs/tests accordingly.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
storage/inkless/src/test/java/io/aiven/inkless/consume/ReaderTest.java Updates Reader constructor call sites for the new byte-rate limiter parameter.
storage/inkless/src/test/java/io/aiven/inkless/consume/FetchPlannerTest.java Adds byte-rate limiter test coverage and updates FetchPlanner construction call sites.
storage/inkless/src/test/java/io/aiven/inkless/config/InklessConfigTest.java Validates default/explicit values for the new lagging consumer byte-rate config.
storage/inkless/src/main/java/io/aiven/inkless/consume/Reader.java Builds and passes the new byte-rate bucket (and capacity) into FetchPlanner.
storage/inkless/src/main/java/io/aiven/inkless/consume/InklessFetchMetrics.java Adds throttle/oversized meters and updates wait-time metric description.
storage/inkless/src/main/java/io/aiven/inkless/consume/FetchPlanner.java Applies request-rate and byte-rate limiting (tryConsume-first, then blocking consume) and records per-limiter throttle/oversized metrics.
storage/inkless/src/main/java/io/aiven/inkless/consume/FetchHandler.java Wires the new InklessConfig byte-rate limit into Reader construction.
storage/inkless/src/main/java/io/aiven/inkless/config/InklessConfig.java Adds config definition + startup WARN heuristic if byte-rate limit is below produced object size.
server-common/src/main/java/org/apache/kafka/server/config/ServerConfigs.java Adds the consolidation cold-path byte-rate config key and definition.
docs/inkless/metrics.rst Documents the new InklessFetch byte-rate/request-rate throttling metrics.
docs/inkless/configs.rst Documents fetch.lagging.consumer.byte.rate.limit.
core/src/main/scala/kafka/server/ReplicaManager.scala Passes consolidation byte-rate limit through to Reader for consolidation fetches.
core/src/main/scala/kafka/server/KafkaConfig.scala Exposes the new consolidation byte-rate limit config via KafkaConfig.

Comment on lines +1806 to +1815
// Large capacity with a fetch cost near it: after draining, the fetch's tryConsume
// fails (only a tiny fraction refills during scheduling), so it must block on refill.
final long capacity = 1_000_000L;
final long fetchByteSize = 500_000L;
final Bucket byteRateLimiter = Bucket.builder()
.addLimit(limit -> limit.capacity(capacity).refillGreedy(capacity, Duration.ofSeconds(1)))
.build();
// Pre-drain: consume the full bucket so the next consume must wait for a refill.
assertThat(byteRateLimiter.tryConsume(capacity)).isTrue();

Comment on lines +1838 to +1847
// Low rate so a single token takes ~500ms to refill: after draining, the fetch's
// tryConsume(1) fails during scheduling and must block on refill. The 500ms margin
// keeps the test robust even if the lagging executor is slow to pick up the task.
final int capacity = 2;
final Bucket rateLimiter = Bucket.builder()
.addLimit(limit -> limit.capacity(capacity).refillGreedy(capacity, Duration.ofSeconds(1)))
.build();
// Pre-drain the request bucket.
assertThat(rateLimiter.tryConsume(capacity)).isTrue();

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants