Skip to content

Commit bb46325

Browse files
kafka_consumer: cap total broker_timestamps history by a per-cluster memory budget (DataDog#24508)
* kafka_consumer: cap broker_timestamps retained history by a per-cluster budget The DSM broker_timestamps cache grows unbounded with partition count (up to timestamp_history_size samples for every partition), reaching multiple GiB on large clusters. Bound it to a fixed entry budget (~0.5 GiB/cluster): the per-partition history depth is scaled down as partition count grows (budget // num_partitions, clamped to [2, timestamp_history_size]). Small clusters keep full history; large clusters shrink to a shallow depth, so total memory stays bounded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add changelog entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1385ff7 commit bb46325

3 files changed

Lines changed: 39 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cap the total DSM broker timestamps history per cluster with a memory budget that scales the per-partition depth by partition count, bounding cluster-check-runner memory on large clusters.

kafka_consumer/datadog_checks/kafka_consumer/kafka_consumer.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
MAX_TIMESTAMPS = 1000
2323

24+
# Total broker-timestamp entries retained per cluster, ~0.5 GiB at ~89 bytes/entry. The
25+
# per-partition history is scaled down from this budget as the partition count grows.
26+
MAX_TIMESTAMP_ENTRIES = 6_000_000
27+
2428
LAG_EXTRAPOLATION_LIMIT_SECONDS = 600
2529

2630

@@ -297,20 +301,27 @@ def _earliest_consumer_offsets(self, consumer_offsets):
297301
earliest[topic_partition] = offset
298302
return earliest
299303

304+
def _max_history(self, num_partitions):
305+
"""Per-partition timestamp cap, scaled so total retained entries stay within the budget."""
306+
if num_partitions <= 0:
307+
return self._max_timestamps
308+
return max(2, min(self._max_timestamps, MAX_TIMESTAMP_ENTRIES // num_partitions))
309+
300310
def _add_broker_timestamps(self, broker_timestamps, highwater_offsets, prune_floors=None):
301311
prune_floors = prune_floors or {}
312+
max_history = self._max_history(len(highwater_offsets))
302313
for (topic, partition), highwater_offset in highwater_offsets.items():
303314
timestamps = broker_timestamps["{}_{}".format(topic, partition)]
304315
# Reset detected: clear the whole cache. Low-offset survivors are from the
305316
# previous generation and VW pins the minimum endpoint, so they'd never age out.
306317
if any(o > highwater_offset for o in timestamps):
307318
timestamps.clear()
308319
timestamps[highwater_offset] = time()
309-
if len(timestamps) >= self._max_timestamps:
320+
if len(timestamps) >= max_history:
310321
prune_floor = prune_floors.get((topic, partition))
311322
if prune_floor is not None:
312323
_prune_below_anchor(timestamps, prune_floor)
313-
_visvalingam_whyatt(timestamps, max(2, self._max_timestamps // 2))
324+
_visvalingam_whyatt(timestamps, max(2, max_history // 2))
314325

315326
def _save_broker_timestamps(self, broker_timestamps, persistent_cache_key):
316327
"""Saves broker timestamps to persistent cache."""

kafka_consumer/tests/test_unit.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import logging
77
import marshal
8+
from collections import defaultdict
89
from contextlib import nullcontext as does_not_raise
910

1011
import mock
@@ -528,6 +529,30 @@ def test_check_clears_cache_on_partial_reset(kafka_instance, check, dd_run_check
528529
assert 100 in timestamps
529530

530531

532+
def test_max_history_scales_with_partition_count(check, kafka_instance):
533+
kafka_consumer_check = check(kafka_instance) # default timestamp_history_size = 1000
534+
assert kafka_consumer_check._max_history(0) == 1000
535+
assert kafka_consumer_check._max_history(1) == 1000
536+
assert kafka_consumer_check._max_history(6000) == 1000 # 6M / 6000 = 1000 (breakeven)
537+
assert kafka_consumer_check._max_history(60000) == 100 # 6M / 60000
538+
assert kafka_consumer_check._max_history(6_000_000) == 2 # floored at 2
539+
540+
541+
def test_add_broker_timestamps_caps_total_by_budget(check, kafka_instance, monkeypatch):
542+
import datadog_checks.kafka_consumer.kafka_consumer as kc
543+
544+
monkeypatch.setattr(kc, 'MAX_TIMESTAMP_ENTRIES', 1000)
545+
kafka_consumer_check = check(kafka_instance)
546+
# 100 partitions against a 1000-entry budget -> per-partition cap of 10.
547+
highwater_offsets = {("topic1", p): 500 for p in range(100)}
548+
broker_timestamps = defaultdict(dict)
549+
broker_timestamps["topic1_0"] = {i: float(i) for i in range(60)}
550+
551+
kafka_consumer_check._add_broker_timestamps(broker_timestamps, highwater_offsets)
552+
553+
assert len(broker_timestamps["topic1_0"]) <= 10
554+
555+
531556
@pytest.mark.parametrize(
532557
'timestamp_history_size, initial_cache, highwater_offset, '
533558
'highwater_time, consumer_offset, expected_cache_size, expected_lag',

0 commit comments

Comments
 (0)