Optimize health DB lookups (#22246)
* optimize health DB lookups by reusing onewayalloc arena and adding reset functionality * Enhance onewayalloc reset functionality to improve memory management and clarify usage in comments
Stelios Fragkakis committed
Apr 22, 2026 at 18:42 UTC
d46928ad5ecbde931bda0ef7066fd889fd19548e
5 files changed
+162
-6
src/health/health_event_loop.c
+29
-3
@@ -317,7 +317,13 @@ static void do_eval_expression(
317
}
318
319
// returns the number of runnable alerts
320
-static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_delay, time_t now, time_t *next_run) {
320
+//
321
+// The caller owns `owa` and provides it so a single arena can be reused
322
+// across all hosts of one iteration (and, in the multi-threaded variant,
323
+// across all hosts a single worker processes). The arena is reset between
324
+// alerts inside this function, so peak memory stays bounded by one alert's
325
+// scratch no matter how many hosts flow through it.
326
+static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_delay, time_t now, time_t *next_run, ONEWAYALLOC *owa) {
327
size_t runnable = 0;
328
struct health_alert_status_counts status_counts = { 0 };
329
bool snapshot_complete = true;
@@ -369,9 +375,22 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
375
return;
376
}
377
378
+ // Reuse the caller-provided arena across every alert's DB lookup. Each
379
+ // alert's scratch (RRDR + query state) is released via onewayalloc_reset
380
+ // at the top of the next iteration — trims the page list back to a
381
+ // single head page. Net effect: one mmap/munmap for the whole health
382
+ // iteration, regardless of host count or alert count.
383
+
384
// the first loop is to lookup values from the db
385
RRDCALC *rc;
386
foreach_rrdcalc_in_rrdhost_read(host, rc) {
387
+ // Reclaim the previous alert's query scratch before starting the
388
+ // next one. The arena is reused across every alert of every host in
389
+ // this iteration, so this reset trims trailing pages left by the
390
+ // previous alert (same host or prior host). No-op only on the very
391
+ // first alert after the arena was created.
392
+ onewayalloc_reset(owa);
393
+
394
if(unlikely(!service_running(SERVICE_HEALTH) || !rrdhost_should_run_health(host))) {
395
snapshot_complete = false;
396
break;
@@ -464,7 +483,8 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
483
break;
484
}
485
467
- int ret = rrdset2value_api_v1(rc->rrdset, NULL, &rc->value, rrdcalc_dimensions(rc), 1,
486
+ int ret = rrdset2value_api_v1_with_owa(owa,
487
+ rc->rrdset, NULL, &rc->value, rrdcalc_dimensions(rc), 1,
488
rc->config.after, rc->config.before, rc->config.time_group, group_options,
489
0, rc->config.options | RRDR_OPTION_SELECTED_TIER,
490
&rc->db_after,&rc->db_before,
@@ -781,15 +801,21 @@ static void health_event_loop(void) {
801
worker_is_busy(WORKER_HEALTH_JOB_RRD_LOCK);
802
uint64_t loop = __atomic_add_fetch(&health_evloop_iteration, 1, __ATOMIC_RELAXED);
803
804
+ // Single onewayalloc arena reused across every host in this iteration —
805
+ // one mmap/munmap pair for the whole cycle instead of per host.
806
+ ONEWAYALLOC *iter_owa = onewayalloc_create(0);
807
+
808
RRDHOST *host;
809
dfe_start_reentrant(rrdhost_root_index, host) {
810
if(unlikely(!service_running(SERVICE_HEALTH)))
811
break;
812
789
- health_event_loop_for_host(host, apply_hibernation_delay, now, &next_run);
813
+ health_event_loop_for_host(host, apply_hibernation_delay, now, &next_run, iter_owa);
814
}
815
dfe_done(host);
816
817
+ onewayalloc_destroy(iter_owa);
818
+
819
if(unlikely(!service_running(SERVICE_HEALTH)))
820
break;
821
src/libnetdata/onewayalloc/onewayalloc.c
+45
@@ -184,6 +184,51 @@ void *onewayalloc_doublesize(ONEWAYALLOC *owa, const void *src, size_t oldsize)
184
return dst;
185
}
186
187
+void onewayalloc_reset(ONEWAYALLOC *owa) {
188
+ if (!owa) return;
189
+
190
+#ifdef FSANITIZE_ADDRESS
191
+ // Under the sanitizer path, onewayalloc_mallocz goes straight to the
192
+ // system allocator and nothing is tracked in the owa page list — there
193
+ // is nothing to reset. Individual allocations are released by callers
194
+ // via onewayalloc_freez() (which calls freez() under the sanitizer).
195
+ return;
196
+#endif
197
+
198
+ OWA_PAGE *head = (OWA_PAGE *)owa;
199
+
200
+ // Free every page except the head; we keep the head so the caller can
201
+ // reuse the arena without another mmap.
202
+ size_t freed_size = 0;
203
+ OWA_PAGE *page = head->next;
204
+ while (page) {
205
+ OWA_PAGE *p = page;
206
+ page = page->next;
207
+ freed_size += p->size;
208
+ if (p->mmap)
209
+ nd_munmap(p, p->size);
210
+ else
211
+ freez(p);
212
+ }
213
+
214
+ if (freed_size)
215
+ __atomic_sub_fetch(&onewayalloc_total_memory, freed_size, __ATOMIC_RELAXED);
216
+
217
+ // Roll the head page's bump cursor back to the position right after the
218
+ // OWA_PAGE header, and rewire the single-page list so head == last.
219
+ head->next = NULL;
220
+ head->last = head;
221
+ head->offset = natural_alignment(sizeof(OWA_PAGE));
222
+
223
+ // stats_pages / stats_pages_size describe the arena's *current* footprint
224
+ // (what is mapped right now), so they reflect the single-page post-reset
225
+ // state. stats_mallocs_made / stats_mallocs_size are lifetime counters
226
+ // (total allocations ever served by this arena) and are intentionally
227
+ // preserved across resets, to stay useful for diagnostics.
228
+ head->stats_pages = 1;
229
+ head->stats_pages_size = head->size;
230
+}
231
+
232
void onewayalloc_destroy(ONEWAYALLOC *owa) {
233
if(!owa) return;
234
src/libnetdata/onewayalloc/onewayalloc.h
+15
@@ -8,6 +8,21 @@ typedef void ONEWAYALLOC;
8
ONEWAYALLOC *onewayalloc_create(size_t size_hint);
9
void onewayalloc_destroy(ONEWAYALLOC *owa);
10
11
+// Reset the arena to an empty state without destroying it. Frees every
12
+// page except the head, and rolls the head page's bump pointer back to
13
+// its initial offset so the arena is reusable. Intended for callers that
14
+// do many short bursts of allocations back-to-back (e.g. evaluating all
15
+// the alerts of one host) and want to amortise the mmap/munmap cost of
16
+// create/destroy across the whole burst.
17
+//
18
+// Important: reset does NOT zero the retained head page. Subsequent
19
+// onewayalloc_mallocz() calls may return memory that still holds bytes
20
+// from the previous burst — this matches the mallocz() contract (the "z"
21
+// means "fatal on failure", not "zeroed"). Callers that need zeroed
22
+// memory must use onewayalloc_callocz() just as they would against a
23
+// freshly-created arena; the reset path does not relax that contract.
24
+void onewayalloc_reset(ONEWAYALLOC *owa);
25
+
26
void *onewayalloc_mallocz(ONEWAYALLOC *owa, size_t size);
27
void *onewayalloc_callocz(ONEWAYALLOC *owa, size_t nmemb, size_t size);
28
char *onewayalloc_strdupz(ONEWAYALLOC *owa, const char *s);
src/web/api/formatters/rrd2json.c
+41
-3
@@ -10,8 +10,9 @@ void rrd_stats_api_v1_chart(RRDSET *st, BUFFER *wb)
10
buffer_json_finalize(wb);
11
}
12
13
-int rrdset2value_api_v1(
14
- RRDSET *st
13
+int rrdset2value_api_v1_with_owa(
14
+ ONEWAYALLOC *owa
15
+ , RRDSET *st
16
, BUFFER *wb
17
, NETDATA_DOUBLE *n
18
, const char *dimensions
@@ -34,9 +35,10 @@ int rrdset2value_api_v1(
35
, QUERY_SOURCE query_source
36
, STORAGE_PRIORITY priority
37
) {
38
+ internal_fatal(!owa, "rrdset2value_api_v1_with_owa(): owa must be non-NULL");
39
+
40
int ret = HTTP_RESP_INTERNAL_SERVER_ERROR;
41
39
- ONEWAYALLOC *owa = onewayalloc_create(0);
42
RRDR *r = rrd2rrdr_legacy(
43
owa,
44
st,
@@ -95,6 +97,42 @@ int rrdset2value_api_v1(
97
98
cleanup:
99
rrdr_free(owa, r);
100
+ return ret;
101
+}
102
+
103
+int rrdset2value_api_v1(
104
+ RRDSET *st
105
+ , BUFFER *wb
106
+ , NETDATA_DOUBLE *n
107
+ , const char *dimensions
108
+ , size_t points
109
+ , time_t after
110
+ , time_t before
111
+ , RRDR_TIME_GROUPING group_method
112
+ , const char *group_options
113
+ , time_t resampling_time
114
+ , uint32_t options
115
+ , time_t *db_after
116
+ , time_t *db_before
117
+ , size_t *db_points_read
118
+ , size_t *db_points_per_tier
119
+ , size_t *result_points_generated
120
+ , int *value_is_null
121
+ , NETDATA_DOUBLE *anomaly_rate
122
+ , time_t timeout
123
+ , size_t tier
124
+ , QUERY_SOURCE query_source
125
+ , STORAGE_PRIORITY priority
126
+) {
127
+ // Back-compat wrapper for callers that don't need to reuse the arena
128
+ // across calls (e.g. the badge code path). Creates a throwaway owa.
129
+ ONEWAYALLOC *owa = onewayalloc_create(0);
130
+ int ret = rrdset2value_api_v1_with_owa(
131
+ owa, st, wb, n, dimensions, points, after, before,
132
+ group_method, group_options, resampling_time, options,
133
+ db_after, db_before, db_points_read, db_points_per_tier,
134
+ result_points_generated, value_is_null, anomaly_rate,
135
+ timeout, tier, query_source, priority);
136
onewayalloc_destroy(owa);
137
return ret;
138
}
src/web/api/formatters/rrd2json.h
+32
@@ -49,6 +49,38 @@ int rrdset2value_api_v1(
49
, STORAGE_PRIORITY priority
50
);
51
52
+// Same contract as rrdset2value_api_v1 but uses a caller-provided
53
+// onewayalloc arena. Lets a caller that issues many back-to-back queries
54
+// (e.g. evaluating all alerts on one host) amortise the mmap/munmap cost
55
+// of owa create/destroy across the whole burst. The caller owns the owa's
56
+// lifetime; between calls they should invoke onewayalloc_reset() to reclaim
57
+// trailing pages and keep peak memory bounded. `owa` must be non-NULL.
58
+int rrdset2value_api_v1_with_owa(
59
+ ONEWAYALLOC *owa
60
+ , RRDSET *st
61
+ , BUFFER *wb
62
+ , NETDATA_DOUBLE *n
63
+ , const char *dimensions
64
+ , size_t points
65
+ , time_t after
66
+ , time_t before
67
+ , RRDR_TIME_GROUPING group_method
68
+ , const char *group_options
69
+ , time_t resampling_time
70
+ , uint32_t options
71
+ , time_t *db_after
72
+ , time_t *db_before
73
+ , size_t *db_points_read
74
+ , size_t *db_points_per_tier
75
+ , size_t *result_points_generated
76
+ , int *value_is_null
77
+ , NETDATA_DOUBLE *anomaly_rate
78
+ , time_t timeout
79
+ , size_t tier
80
+ , QUERY_SOURCE query_source
81
+ , STORAGE_PRIORITY priority
82
+);
83
+
84
static inline bool rrdr_dimension_should_be_exposed(RRDR_DIMENSION_FLAGS rrdr_dim_flags, RRDR_OPTIONS options) {
85
if(unlikely((options & RRDR_OPTION_RETURN_RAW) && (rrdr_dim_flags & RRDR_DIMENSION_QUERIED)))
86
return true;