Skip to content

Commit 6925dea

Browse files
authored
ZOOKEEPER-4767: New implementation of prometheus quantile metrics based on DataSketches
Reviewers: tisonkun, anmolnar Author: Shawyeok Closes #2086 from Shawyeok/sketches-summary
1 parent 6d6ae51 commit 6925dea

7 files changed

Lines changed: 616 additions & 57 deletions

File tree

zookeeper-metrics-providers/zookeeper-prometheus-metrics/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<properties>
3535
<prometheus.version>1.3.10</prometheus.version>
3636
<jetty.version>9.4.58.v20250814</jetty.version>
37+
<datasketches.version>7.0.1</datasketches.version>
3738
</properties>
3839
<dependencies>
3940
<dependency>
@@ -75,6 +76,11 @@
7576
<artifactId>prometheus-metrics-config</artifactId>
7677
<version>${prometheus.version}</version>
7778
</dependency>
79+
<dependency>
80+
<groupId>org.apache.datasketches</groupId>
81+
<artifactId>datasketches-java</artifactId>
82+
<version>${datasketches.version}</version>
83+
</dependency>
7884
<dependency>
7985
<groupId>org.eclipse.jetty</groupId>
8086
<artifactId>jetty-server</artifactId>

zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/main/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProvider.java

Lines changed: 117 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
import java.util.Properties;
3232
import java.util.concurrent.ConcurrentHashMap;
3333
import java.util.concurrent.ConcurrentMap;
34+
import java.util.concurrent.ScheduledExecutorService;
35+
import java.util.concurrent.ScheduledThreadPoolExecutor;
36+
import java.util.concurrent.ThreadFactory;
37+
import java.util.concurrent.TimeUnit;
38+
import java.util.concurrent.atomic.AtomicInteger;
3439
import java.util.function.BiConsumer;
3540
import javax.servlet.ServletException;
3641
import javax.servlet.http.HttpServletRequest;
@@ -81,6 +86,9 @@ public class PrometheusMetricsProvider implements MetricsProvider {
8186

8287
private Server server;
8388
private int numWorkerThreads;
89+
private long workerShutdownTimeoutMs = 1000;
90+
private int summaryRotateSeconds = 60;
91+
private ScheduledExecutorService summaryRotateExecutor;
8492
private String host;
8593

8694
// SSL Configuration fields
@@ -101,7 +109,27 @@ public class PrometheusMetricsProvider implements MetricsProvider {
101109
public static final String HTTP_PORT = "httpPort";
102110
public static final String EXPORT_JVM_INFO = "exportJvmInfo";
103111
public static final String HTTPS_PORT = "httpsPort";
112+
/**
113+
* @deprecated DataSketches-based summaries are lock-free per-thread and no longer require
114+
* worker threads. This property is ignored. See ZOOKEEPER-4741.
115+
*/
116+
@Deprecated
104117
public static final String NUM_WORKER_THREADS = "numWorkerThreads";
118+
/**
119+
* @deprecated DataSketches-based summaries no longer use a bounded worker queue. This property
120+
* is ignored. See ZOOKEEPER-4741.
121+
*/
122+
@Deprecated
123+
public static final String MAX_QUEUE_SIZE = "maxQueueSize";
124+
/** Timeout in ms for shutting down the summary rotation executor. */
125+
public static final String WORKER_SHUTDOWN_TIMEOUT_MS = "workerShutdownTimeoutMs";
126+
/**
127+
* Interval in seconds for rotating per-thread DataSketches into the aggregated result that is
128+
* exposed via {@code /metrics}. Quantiles from observations in the current interval become
129+
* visible after the next rotation. Default is 60 seconds.
130+
*/
131+
public static final String PROMETHEUS_SUMMARY_ROTATE_INTERVAL_SECONDS =
132+
"prometheusMetricsSummaryRotateIntervalSeconds";
105133
public static final String SSL_KEYSTORE_LOCATION = "ssl.keyStore.location";
106134
public static final String SSL_KEYSTORE_PASSWORD = "ssl.keyStore.password";
107135
public static final String SSL_KEYSTORE_TYPE = "ssl.keyStore.type";
@@ -144,6 +172,14 @@ public void configure(Properties configuration) throws MetricsProviderLifeCycleE
144172
this.httpsPort = Integer.parseInt(configuration.getProperty(HTTPS_PORT, "-1"));
145173
this.exportJvmInfo = Boolean.parseBoolean(configuration.getProperty(EXPORT_JVM_INFO, "true"));
146174
this.numWorkerThreads = Integer.parseInt(configuration.getProperty(NUM_WORKER_THREADS, "10"));
175+
if (configuration.containsKey(NUM_WORKER_THREADS) || configuration.containsKey(MAX_QUEUE_SIZE)) {
176+
LOG.warn("The configuration {} and {} are deprecated and ignored. See ZOOKEEPER-4741.",
177+
NUM_WORKER_THREADS, MAX_QUEUE_SIZE);
178+
}
179+
this.workerShutdownTimeoutMs =
180+
Long.parseLong(configuration.getProperty(WORKER_SHUTDOWN_TIMEOUT_MS, "1000"));
181+
this.summaryRotateSeconds =
182+
Integer.parseInt(configuration.getProperty(PROMETHEUS_SUMMARY_ROTATE_INTERVAL_SECONDS, "60"));
147183

148184
// If httpsPort is specified, parse all SSL properties
149185
if (this.httpsPort != -1) {
@@ -244,6 +280,7 @@ public void start() throws MetricsProviderLifeCycleException {
244280
LOG.info("Prometheus metrics provider with Jetty started. HTTP port: {}, HTTPS port: {}",
245281
httpPort != -1 ? httpPort : "disabled", httpsPort != -1 ? httpsPort : "disabled");
246282

283+
startSummaryRotateTask();
247284
} catch (Exception e) {
248285
LOG.error("Failed to start Prometheus Jetty server", e);
249286
// Ensure server is stopped on startup failure
@@ -252,6 +289,48 @@ public void start() throws MetricsProviderLifeCycleException {
252289
}
253290
}
254291

292+
private void startSummaryRotateTask() {
293+
summaryRotateExecutor = new ScheduledThreadPoolExecutor(1, new SummaryRotateThreadFactory());
294+
summaryRotateExecutor.scheduleAtFixedRate(() -> {
295+
try {
296+
rootContext.rotateAllSummaries();
297+
} catch (Exception err) {
298+
LOG.error("Cannot rotate Prometheus summaries", err);
299+
}
300+
}, summaryRotateSeconds, summaryRotateSeconds, TimeUnit.SECONDS);
301+
}
302+
303+
private void shutdownSummaryRotateExecutor() {
304+
if (summaryRotateExecutor == null) {
305+
return;
306+
}
307+
LOG.info("Shutting down Prometheus summary rotate executor with timeout {}ms", workerShutdownTimeoutMs);
308+
summaryRotateExecutor.shutdown();
309+
try {
310+
if (!summaryRotateExecutor.awaitTermination(workerShutdownTimeoutMs, TimeUnit.MILLISECONDS)) {
311+
LOG.warn("Summary rotate executor did not terminate in {}ms; forcing shutdown",
312+
workerShutdownTimeoutMs);
313+
summaryRotateExecutor.shutdownNow();
314+
}
315+
} catch (InterruptedException e) {
316+
Thread.currentThread().interrupt();
317+
summaryRotateExecutor.shutdownNow();
318+
} finally {
319+
summaryRotateExecutor = null;
320+
}
321+
}
322+
323+
private static class SummaryRotateThreadFactory implements ThreadFactory {
324+
private static final AtomicInteger counter = new AtomicInteger(1);
325+
326+
@Override
327+
public Thread newThread(Runnable runnable) {
328+
Thread thread = new Thread(runnable, "PrometheusSummaryRotate-" + counter.getAndIncrement());
329+
thread.setDaemon(true);
330+
return thread;
331+
}
332+
}
333+
255334
private void setKeyStoreScanner(SslContextFactory.Server sslContextFactory) {
256335
KeyStoreScanner keystoreScanner = new KeyStoreScanner(sslContextFactory);
257336
keystoreScanner.setScanInterval(SCAN_INTERVAL);
@@ -335,6 +414,7 @@ private ServerConnector createSslConnector(Server server, int acceptors, int sel
335414

336415
@Override
337416
public void stop() {
417+
shutdownSummaryRotateExecutor();
338418
if (server != null) {
339419
try {
340420
LOG.info("Stopping Prometheus Jetty server.");
@@ -482,16 +562,12 @@ public void unregisterGaugeSet(final String name) {
482562
unregisterGauge(name);
483563
}
484564

485-
private io.prometheus.metrics.core.metrics.Summary createPrometheusSummary(String name, DetailLevel detailLevel,
486-
String... labelNames) {
487-
io.prometheus.metrics.core.metrics.Summary.Builder builder = io.prometheus.metrics.core.metrics.Summary
488-
.builder().name(name).help(name + " summary").quantile(0.5, 0.05); // Median
489-
565+
private SketchesSummary createSketchesSummary(String name, DetailLevel detailLevel, String... labelNames) {
566+
SketchesSummary.Builder builder = SketchesSummary.build(name, name + " summary")
567+
.quantile(0.5); // Median
490568
if (detailLevel == DetailLevel.ADVANCED) {
491-
builder.quantile(0.95, 0.05) // 95th percentile
492-
.quantile(0.99, 0.05); // 99th percentile
569+
builder.quantile(0.95).quantile(0.99); // 95th and 99th percentile
493570
}
494-
495571
if (labelNames.length > 0) {
496572
builder.labelNames(labelNames);
497573
}
@@ -508,9 +584,7 @@ public Summary getSummary(String name, DetailLevel detailLevel) {
508584
throw new IllegalArgumentException(
509585
"Already registered a summary as " + key + " with a different detail level");
510586
}
511-
io.prometheus.metrics.core.metrics.Summary prometheusSummary = createPrometheusSummary(key,
512-
detailLevel);
513-
return new PrometheusSummaryWrapper(prometheusSummary);
587+
return new PrometheusSummaryWrapper(createSketchesSummary(key, detailLevel), key);
514588
});
515589
}
516590

@@ -524,11 +598,16 @@ public SummarySet getSummarySet(String name, DetailLevel detailLevel) {
524598
throw new IllegalArgumentException(
525599
"Already registered a summary set as " + key + " with a different detail level");
526600
}
527-
io.prometheus.metrics.core.metrics.Summary prometheusSummary = createPrometheusSummary(key, detailLevel,
528-
LABEL);
529-
return new PrometheusLabelledSummaryWrapper(prometheusSummary);
601+
return new PrometheusLabelledSummaryWrapper(createSketchesSummary(key, detailLevel, LABEL), key);
530602
});
531603
}
604+
605+
void rotateAllSummaries() {
606+
basicSummaries.values().forEach(s -> s.inner.rotate());
607+
advancedSummaries.values().forEach(s -> s.inner.rotate());
608+
basicSummarySets.values().forEach(s -> s.inner.rotate());
609+
advancedSummarySets.values().forEach(s -> s.inner.rotate());
610+
}
532611
}
533612

534613
// --- Wrapper classes to adapt Prometheus metrics to ZooKeeper's metric interfaces ---
@@ -578,29 +657,43 @@ public void inc(String key) {
578657
}
579658
}
580659

581-
private static class PrometheusSummaryWrapper implements Summary {
582-
private final io.prometheus.metrics.core.metrics.Summary prometheusSummary;
660+
static class PrometheusSummaryWrapper implements Summary {
661+
// VisibleForTesting
662+
final SketchesSummary inner;
663+
private final String name;
583664

584-
public PrometheusSummaryWrapper(io.prometheus.metrics.core.metrics.Summary prometheusSummary) {
585-
this.prometheusSummary = prometheusSummary;
665+
PrometheusSummaryWrapper(SketchesSummary inner, String name) {
666+
this.inner = inner;
667+
this.name = name;
586668
}
587669

588670
@Override
589671
public void add(long value) {
590-
this.prometheusSummary.observe(value);
672+
try {
673+
inner.observe(value);
674+
} catch (IllegalArgumentException err) {
675+
LOG.error("invalid delta {} for metric {}", value, name, err);
676+
}
591677
}
592678
}
593679

594-
private static class PrometheusLabelledSummaryWrapper implements SummarySet {
595-
private final io.prometheus.metrics.core.metrics.Summary prometheusSummary;
680+
static class PrometheusLabelledSummaryWrapper implements SummarySet {
681+
// VisibleForTesting
682+
final SketchesSummary inner;
683+
private final String name;
596684

597-
public PrometheusLabelledSummaryWrapper(io.prometheus.metrics.core.metrics.Summary prometheusSummary) {
598-
this.prometheusSummary = prometheusSummary;
685+
PrometheusLabelledSummaryWrapper(SketchesSummary inner, String name) {
686+
this.inner = inner;
687+
this.name = name;
599688
}
600689

601690
@Override
602691
public void add(String key, long value) {
603-
this.prometheusSummary.labelValues(key).observe(value);
692+
try {
693+
inner.labels(key).observe(value);
694+
} catch (IllegalArgumentException err) {
695+
LOG.error("invalid value {} for metric {} with key {}", value, name, key, err);
696+
}
604697
}
605698
}
606699
}

0 commit comments

Comments
 (0)