diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/DataTree.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/DataTree.java index fa0c53a4257..a35332881f2 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/DataTree.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/DataTree.java @@ -96,6 +96,9 @@ public class DataTree { private static final Logger LOG = LoggerFactory.getLogger(DataTree.class); + private static final int UTF8_SINGLE_BYTE_LIMIT = 0x80; + private static final int UTF8_TWO_BYTE_LIMIT = 0x800; + private final RateLogger RATE_LOGGER = new RateLogger(LOG, 15 * 60 * 1000); /** @@ -151,7 +154,30 @@ public class DataTree { /** * This hashtable lists the paths of the ephemeral nodes of a session. */ - private final Map> ephemerals = new ConcurrentHashMap<>(); + private final Map ephemerals = new ConcurrentHashMap<>(); + + private static final class EphemeralNodes { + + private final HashSet paths = new HashSet<>(); + private volatile long pathBytes; + + private void add(String path) { + synchronized (paths) { + if (paths.add(path)) { + pathBytes += getPathByteSize(path); + } + } + } + + private void remove(String path) { + synchronized (paths) { + if (paths.remove(path)) { + pathBytes -= getPathByteSize(path); + } + } + } + + } /** * This set contains the paths of all container nodes @@ -189,14 +215,13 @@ public class DataTree { private final DigestCalculator digestCalculator; - @SuppressWarnings("unchecked") public Set getEphemerals(long sessionId) { - HashSet ret = ephemerals.get(sessionId); + EphemeralNodes ret = ephemerals.get(sessionId); if (ret == null) { return new HashSet<>(); } - synchronized (ret) { - return (HashSet) ret.clone(); + synchronized (ret.paths) { + return new HashSet<>(ret.paths); } } @@ -225,11 +250,26 @@ public int getWatchCount() { } public int getEphemeralsCount() { - int result = 0; - for (HashSet set : ephemerals.values()) { - result += set.size(); + return ephemerals.values().stream().mapToInt(session -> session.paths.size()).sum(); + } + + /** + * Returns the maximum total size of a session's ephemeral paths in UTF-8 bytes, + * or zero if there are no ephemeral nodes. The total excludes + * {@code closeSession} serialization overhead. + */ + public long getMaxSessionEphemeralPathBytes() { + return ephemerals.values().stream().mapToLong(session -> session.pathBytes).max().orElse(0L); + } + + private static long getPathByteSize(String path) { + long size = 0; + // Valid znode paths exclude surrogate characters, so each character uses at most three UTF-8 bytes. + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + size += c < UTF8_SINGLE_BYTE_LIMIT ? 1 : c < UTF8_TWO_BYTE_LIMIT ? 2 : 3; } - return result; + return size; } /** @@ -488,10 +528,7 @@ public void createNode(final String path, byte[] data, List acl, long ephem ttls.add(path); ServerMetrics.getMetrics().TTL_NODE_CREATED_COUNT.add(1); } else if (ephemeralOwner != 0) { - HashSet list = ephemerals.computeIfAbsent(ephemeralOwner, k -> new HashSet<>()); - synchronized (list) { - list.add(path); - } + ephemerals.computeIfAbsent(ephemeralOwner, k -> new EphemeralNodes()).add(path); } if (outputStat != null) { child.copyStat(outputStat); @@ -592,11 +629,9 @@ public void deleteNode(String path, long zxid) throws NoNodeException { ttls.remove(path); ServerMetrics.getMetrics().TTL_NODE_DELETED_COUNT.add(1); } else if (owner != 0) { - Set nodes = ephemerals.get(owner); - if (nodes != null) { - synchronized (nodes) { - nodes.remove(path); - } + EphemeralNodes session = ephemerals.get(owner); + if (session != null) { + session.remove(path); } } } @@ -969,7 +1004,7 @@ public ProcessTxnResult processTxn(TxnHeader header, Record txn, boolean isSubTx long sessionId = header.getClientId(); if (txn != null) { killSession(sessionId, header.getZxid(), - ephemerals.remove(sessionId), + removeEphemerals(sessionId), ((CloseSessionTxn) txn).getPaths2Delete()); } else { killSession(sessionId, header.getZxid()); @@ -1144,7 +1179,12 @@ void killSession(long session, long zxid) { // so there is no need for synchronization. The list is not // changed here. Only create and delete change the list which // are again called from FinalRequestProcessor in sequence. - killSession(session, zxid, ephemerals.remove(session), null); + killSession(session, zxid, removeEphemerals(session), null); + } + + private Set removeEphemerals(long sessionId) { + EphemeralNodes session = ephemerals.remove(sessionId); + return session == null ? null : session.paths; } void killSession(long session, long zxid, Set paths2DeleteLocal, @@ -1372,6 +1412,7 @@ public void deserialize(InputArchive ia, String tag) throws IOException { aclCache.deserialize(ia); nodes.clear(); pTrie.clear(); + ephemerals.clear(); nodeDataSize.set(0); String path = ia.readString("path"); while (!"/".equals(path)) { @@ -1399,8 +1440,7 @@ public void deserialize(InputArchive ia, String tag) throws IOException { } else if (ephemeralType == EphemeralType.TTL) { ttls.add(path); } else if (owner != 0) { - HashSet list = ephemerals.computeIfAbsent(owner, k -> new HashSet<>()); - list.add(path); + ephemerals.computeIfAbsent(owner, k -> new EphemeralNodes()).add(path); } } path = ia.readString("path"); @@ -1472,10 +1512,10 @@ public synchronized WatchesSummary getWatchesSummary() { */ public void dumpEphemerals(PrintWriter writer) { writer.println("Sessions with Ephemerals (" + ephemerals.keySet().size() + "):"); - for (Entry> entry : ephemerals.entrySet()) { + for (Entry entry : ephemerals.entrySet()) { writer.print("0x" + Long.toHexString(entry.getKey())); writer.println(":"); - Set tmp = entry.getValue(); + Set tmp = entry.getValue().paths; if (tmp != null) { synchronized (tmp) { for (String path : tmp) { @@ -1498,9 +1538,9 @@ public void shutdownWatcher() { */ public Map> getEphemerals() { Map> ephemeralsCopy = new HashMap<>(); - for (Entry> e : ephemerals.entrySet()) { - synchronized (e.getValue()) { - ephemeralsCopy.put(e.getKey(), new HashSet<>(e.getValue())); + for (Entry e : ephemerals.entrySet()) { + synchronized (e.getValue().paths) { + ephemeralsCopy.put(e.getKey(), new HashSet<>(e.getValue().paths)); } } return ephemeralsCopy; diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java index 46deb4f0b14..171113bd9d8 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java @@ -133,6 +133,7 @@ public class ZooKeeperServer implements SessionExpirer, ServerStats.Provider { // Add a enable/disable option for now, we should remove this one when // this feature is confirmed to be stable public static final String CLOSE_SESSION_TXN_ENABLED = "zookeeper.closeSessionTxn.enabled"; + private static final String MAX_SESSION_EPHEMERAL_PATH_BYTES = "max_session_ephemeral_path_bytes"; private static boolean closeSessionTxnEnabled = true; private volatile CountDownLatch restoreLatch; // exclusive lock for taking snapshot and restore @@ -2023,6 +2024,8 @@ protected void registerMetrics() { rootContext.registerGauge("watch_count", zkdb.getDataTree()::getWatchCount); rootContext.registerGauge("ephemerals_count", zkdb.getDataTree()::getEphemeralsCount); + rootContext.registerGauge(MAX_SESSION_EPHEMERAL_PATH_BYTES, + () -> getZKDatabase().getDataTree().getMaxSessionEphemeralPathBytes()); rootContext.registerGauge("approximate_data_size", zkdb.getDataTree()::cachedApproximateDataSize); @@ -2073,6 +2076,7 @@ protected void unregisterMetrics() { rootContext.unregisterGauge("watch_count"); rootContext.unregisterGauge("ephemerals_count"); + rootContext.unregisterGauge(MAX_SESSION_EPHEMERAL_PATH_BYTES); rootContext.unregisterGauge("approximate_data_size"); rootContext.unregisterGauge("global_sessions"); diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/Commands.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/Commands.java index ca28aa56f0b..bfde15b9848 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/Commands.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/Commands.java @@ -581,6 +581,7 @@ public CommandResponse runGet(ZooKeeperServer zkServer, Map kwar * - "znode_count": Integer * - "watch_count": Integer * - "ephemerals_count": Integer + * - "max_session_ephemeral_path_bytes": Number (UTF-8 bytes) * - "approximate_data_size": Long * - "open_file_descriptor_count": Long (unix only) * - "max_file_descriptor_count": Long (unix only) diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/DataTreeTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/DataTreeTest.java index 57f8de41716..7167e475704 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/DataTreeTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/DataTreeTest.java @@ -33,11 +33,15 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; import org.apache.jute.BinaryInputArchive; import org.apache.jute.BinaryOutputArchive; import org.apache.jute.InputArchive; @@ -51,10 +55,15 @@ import org.apache.zookeeper.common.PathTrie; import org.apache.zookeeper.data.Stat; import org.apache.zookeeper.metrics.MetricsUtils; +import org.apache.zookeeper.txn.CloseSessionTxn; import org.apache.zookeeper.txn.CreateTxn; import org.apache.zookeeper.txn.TxnHeader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +71,184 @@ public class DataTreeTest extends ZKTestCase { protected static final Logger LOG = LoggerFactory.getLogger(DataTreeTest.class); + private static final int TEST_NODE_DATA_SIZE = 1024; + private static final int CONCURRENT_UPDATE_COUNT = 1000; + private static final int READER_TIMEOUT_SECONDS = 10; + private static final int CONCURRENT_TEST_TIMEOUT_SECONDS = 30; + + @Test + public void testMaxSessionEphemeralPathBytes() throws Exception { + DataTree tree = new DataTree(); + assertEquals(0L, tree.getMaxSessionEphemeralPathBytes()); + + tree.createNode("/a", new byte[TEST_NODE_DATA_SIZE], null, 1, -1, 1, 1); + tree.createNode("/bb", new byte[0], null, 1, -1, 2, 1); + tree.createNode("/cccc", new byte[0], null, 2, -1, 3, 1); + assertEquals(5L, tree.getMaxSessionEphemeralPathBytes()); + + assertThrows(NodeExistsException.class, + () -> tree.createNode("/a", new byte[0], null, 1, -1, 4, 1)); + assertEquals(5L, tree.getMaxSessionEphemeralPathBytes()); + + tree.createNode("/ddd", new byte[0], null, 1, -1, 5, 1); + assertEquals(9L, tree.getMaxSessionEphemeralPathBytes()); + tree.deleteNode("/ddd", 6); + assertEquals(5L, tree.getMaxSessionEphemeralPathBytes()); + tree.deleteNode("/a", 7); + assertEquals(5L, tree.getMaxSessionEphemeralPathBytes()); + tree.deleteNode("/cccc", 8); + assertEquals(3L, tree.getMaxSessionEphemeralPathBytes()); + tree.deleteNode("/bb", 9); + assertEquals(0L, tree.getMaxSessionEphemeralPathBytes()); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("ephemeralUtf8Paths") + public void testMaxSessionEphemeralPathBytesUsesUtf8(String description, String path) throws Exception { + DataTree tree = new DataTree(); + tree.createNode(path, new byte[0], null, 1, -1, 1, 1); + assertEquals(path.getBytes(StandardCharsets.UTF_8).length, tree.getMaxSessionEphemeralPathBytes()); + tree.deleteNode(path, 2); + assertEquals(0L, tree.getMaxSessionEphemeralPathBytes()); + } + + private static Stream ephemeralUtf8Paths() { + return Stream.of( + Arguments.of("ASCII", "/plain"), + Arguments.of("last printable ASCII character", "/~"), + Arguments.of("two-byte Latin character", "/é"), + Arguments.of("last two-byte character", "/߿"), + Arguments.of("first three-byte character", "/ࠀ"), + Arguments.of("three-byte CJK character", "/中"), + Arguments.of("mixed character widths", "/~é中")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("nonSessionEphemeralOwners") + public void testMaxSessionEphemeralPathBytesIgnoresOtherNodeTypes(String description, long owner) throws Exception { + String previous = System.getProperty(EphemeralType.EXTENDED_TYPES_ENABLED_PROPERTY); + System.setProperty(EphemeralType.EXTENDED_TYPES_ENABLED_PROPERTY, Boolean.TRUE.toString()); + try { + DataTree tree = new DataTree(); + String ephemeralPath = "/é中"; + tree.createNode(ephemeralPath, new byte[0], null, 1, -1, 1, 1); + long expected = ephemeralPath.getBytes(StandardCharsets.UTF_8).length; + assertEquals(expected, tree.getMaxSessionEphemeralPathBytes()); + + String ignoredPath = "/node-with-a-long-path"; + tree.createNode(ignoredPath, new byte[TEST_NODE_DATA_SIZE], null, owner, -1, 2, 1); + assertEquals(expected, tree.getMaxSessionEphemeralPathBytes()); + tree.deleteNode(ignoredPath, 3); + assertEquals(expected, tree.getMaxSessionEphemeralPathBytes()); + tree.deleteNode(ephemeralPath, 4); + assertEquals(0L, tree.getMaxSessionEphemeralPathBytes()); + } finally { + if (previous == null) { + System.clearProperty(EphemeralType.EXTENDED_TYPES_ENABLED_PROPERTY); + } else { + System.setProperty(EphemeralType.EXTENDED_TYPES_ENABLED_PROPERTY, previous); + } + } + } + + private static Stream nonSessionEphemeralOwners() { + return Stream.of( + Arguments.of("persistent", 0L), + Arguments.of("container", EphemeralType.CONTAINER_EPHEMERAL_OWNER), + Arguments.of("TTL", EphemeralType.TTL.toEphemeralOwner(TimeUnit.SECONDS.toMillis(1)))); + } + + @ParameterizedTest(name = "includePaths={0}") + @ValueSource(booleans = {true, false}) + public void testMaxSessionEphemeralPathBytesAfterClosingSessions(boolean includePaths) throws Exception { + DataTree tree = new DataTree(); + tree.createNode("/first", new byte[0], null, 1, -1, 1, 1); + tree.createNode("/extra", new byte[0], null, 1, -1, 2, 1); + tree.createNode("/second", new byte[0], null, 2, -1, 3, 1); + tree.createNode("/another", new byte[0], null, 2, -1, 4, 1); + assertEquals(15L, tree.getMaxSessionEphemeralPathBytes()); + + tree.killSession(2, 5); + assertEquals(12L, tree.getMaxSessionEphemeralPathBytes()); + assertFalse(tree.getEphemerals().containsKey(2L)); + + TxnHeader header = new TxnHeader(1, 0, 6, 1, ZooDefs.OpCode.closeSession); + CloseSessionTxn txn = includePaths ? new CloseSessionTxn(Arrays.asList("/first", "/extra")) : null; + assertEquals(0, tree.processTxn(header, txn).err); + assertEquals(0L, tree.getMaxSessionEphemeralPathBytes()); + assertTrue(tree.getEphemerals().isEmpty()); + } + + @Test + public void testMaxSessionEphemeralPathBytesAfterDeserialize() throws Exception { + DataTree source = new DataTree(); + source.createNode("/a", new byte[0], null, 1, -1, 1, 1); + source.createNode("/bb", new byte[0], null, 1, -1, 2, 1); + source.createNode("/中é", new byte[0], null, 2, -1, 3, 1); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + source.serialize(BinaryOutputArchive.getArchive(output), "test"); + byte[] snapshot = output.toByteArray(); + + DataTree restored = new DataTree(); + restored.createNode("/stale-node-with-a-long-path", new byte[0], null, 3, -1, 1, 1); + for (int i = 0; i < 2; i++) { + restored.deserialize(BinaryInputArchive.getArchive(new ByteArrayInputStream(snapshot)), "test"); + assertEquals(6L, restored.getMaxSessionEphemeralPathBytes()); + assertEquals(3, restored.getEphemeralsCount()); + assertTrue(restored.getEphemerals(3).isEmpty()); + } + + restored.deleteNode("/中é", 4); + assertEquals(5L, restored.getMaxSessionEphemeralPathBytes()); + restored.killSession(1, 5); + assertEquals(0L, restored.getMaxSessionEphemeralPathBytes()); + assertEquals(6L, source.getMaxSessionEphemeralPathBytes()); + + restored.deserialize(BinaryInputArchive.getArchive(new ByteArrayInputStream(snapshot)), "test"); + assertEquals(6L, restored.getMaxSessionEphemeralPathBytes()); + output.reset(); + new DataTree().serialize(BinaryOutputArchive.getArchive(output), "test"); + restored.deserialize(BinaryInputArchive.getArchive(new ByteArrayInputStream(output.toByteArray())), "test"); + assertEquals(0L, restored.getMaxSessionEphemeralPathBytes()); + assertEquals(0, restored.getEphemeralsCount()); + } + + @Test + @Timeout(value = CONCURRENT_TEST_TIMEOUT_SECONDS) + public void testMaxSessionEphemeralPathBytesDuringConcurrentUpdates() throws Exception { + DataTree tree = new DataTree(); + String basePath = "/base"; + String temporaryPath = "/中"; + long basePathBytes = basePath.getBytes(StandardCharsets.UTF_8).length; + long combinedPathBytes = basePathBytes + temporaryPath.getBytes(StandardCharsets.UTF_8).length; + tree.createNode(basePath, new byte[0], null, 1, -1, 1, 1); + AtomicBoolean running = new AtomicBoolean(true); + CountDownLatch nodeObserved = new CountDownLatch(1); + CompletableFuture reader = CompletableFuture.runAsync(() -> { + while (running.get()) { + long max = tree.getMaxSessionEphemeralPathBytes(); + assertTrue(max == basePathBytes || max == combinedPathBytes, "Unexpected total path size: " + max); + if (max == combinedPathBytes) { + nodeObserved.countDown(); + } + } + }); + try { + for (int i = 0; i < CONCURRENT_UPDATE_COUNT; i++) { + tree.createNode(temporaryPath, new byte[0], null, 1, -1, 2 + 2 * i, 1); + if (i == 0) { + assertTrue(nodeObserved.await(READER_TIMEOUT_SECONDS, TimeUnit.SECONDS), + "Reader did not observe the added node"); + } + tree.deleteNode(temporaryPath, 3 + 2 * i); + } + } finally { + running.set(false); + reader.get(READER_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + assertEquals(basePathBytes, tree.getMaxSessionEphemeralPathBytes()); + } + /** * For ZOOKEEPER-1755 - Test race condition when taking dumpEphemerals and * removing the session related ephemerals from DataTree structure diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/ZooKeeperServerTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/ZooKeeperServerTest.java index 6a6cfd7ab59..d18fde8f22a 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/ZooKeeperServerTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/ZooKeeperServerTest.java @@ -40,9 +40,58 @@ import org.apache.zookeeper.server.util.QuotaMetricsUtils; import org.apache.zookeeper.test.ClientBase; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class ZooKeeperServerTest extends ZKTestCase { + private static final String MAX_SESSION_EPHEMERAL_PATH_BYTES = "max_session_ephemeral_path_bytes"; + + @Test + public void testMaxSessionEphemeralPathBytesGaugeLifecycle() throws Exception { + ZooKeeperServer server = new ZooKeeperServer(); + ZKDatabase database = new ZKDatabase(mock(FileTxnSnapLog.class)); + server.setZKDatabase(database); + server.sessionTracker = mock(SessionTracker.class); + server.registerMetrics(); + try { + assertEquals(0L, MetricsUtils.currentServerMetrics().get(MAX_SESSION_EPHEMERAL_PATH_BYTES)); + database.getDataTree().createNode("/中", new byte[0], null, 1, -1, 1, 1); + assertEquals(4L, MetricsUtils.currentServerMetrics().get(MAX_SESSION_EPHEMERAL_PATH_BYTES)); + database.getDataTree().killSession(1, 2); + assertEquals(0L, MetricsUtils.currentServerMetrics().get(MAX_SESSION_EPHEMERAL_PATH_BYTES)); + } finally { + server.unregisterMetrics(); + } + assertFalse(MetricsUtils.currentServerMetrics().containsKey(MAX_SESSION_EPHEMERAL_PATH_BYTES)); + } + + @ParameterizedTest(name = "replaceDatabase={0}") + @ValueSource(booleans = {true, false}) + public void testMaxSessionEphemeralPathBytesGaugeUsesCurrentDatabase(boolean replaceDatabase) throws Exception { + ZooKeeperServer server = new ZooKeeperServer(); + ZKDatabase database = new ZKDatabase(mock(FileTxnSnapLog.class)); + database.getDataTree().createNode("/old", new byte[0], null, 1, -1, 1, 1); + server.setZKDatabase(database); + server.sessionTracker = mock(SessionTracker.class); + server.registerMetrics(); + try { + assertEquals(4L, MetricsUtils.currentServerMetrics().get(MAX_SESSION_EPHEMERAL_PATH_BYTES)); + + if (replaceDatabase) { + database = new ZKDatabase(mock(FileTxnSnapLog.class)); + server.setZKDatabase(database); + } else { + database.clear(); + } + assertEquals(0L, MetricsUtils.currentServerMetrics().get(MAX_SESSION_EPHEMERAL_PATH_BYTES)); + database.getDataTree().createNode("/longer", new byte[0], null, 2, -1, 2, 1); + assertEquals(7L, MetricsUtils.currentServerMetrics().get(MAX_SESSION_EPHEMERAL_PATH_BYTES)); + } finally { + server.unregisterMetrics(); + } + } + @Test public void testDirSize() throws Exception { ZooKeeperServer zks = null; diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/CommandsTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/CommandsTest.java index 4b693ef9eb1..6ce158a692f 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/CommandsTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/CommandsTest.java @@ -39,6 +39,9 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.ZooKeeper; import org.apache.zookeeper.metrics.MetricsUtils; import org.apache.zookeeper.server.ServerCnxnFactory; import org.apache.zookeeper.server.ServerStats; @@ -188,6 +191,7 @@ public void testMonitor() throws IOException, InterruptedException { new Field("znode_count", Integer.class), new Field("watch_count", Integer.class), new Field("ephemerals_count", Integer.class), + new Field("max_session_ephemeral_path_bytes", Long.class), new Field("approximate_data_size", Long.class), new Field("open_file_descriptor_count", Long.class), new Field("max_file_descriptor_count", Long.class), @@ -223,6 +227,21 @@ public void testMonitor() throws IOException, InterruptedException { testCommand("monitor", fieldsArray); } + @Test + public void testMonitorMaxSessionEphemeralPathBytes() throws Exception { + ZooKeeperServer server = serverFactory.getZooKeeperServer(); + try (ZooKeeper zk = createClient()) { + zk.create("/a", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); + zk.create("/中", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); + CommandResponse response = Commands.runGetCommand("monitor", server, new HashMap<>(), null, null); + assertEquals(6L, response.toMap().get("max_session_ephemeral_path_bytes")); + + zk.delete("/中", -1); + response = Commands.runGetCommand("monitor", server, new HashMap<>(), null, null); + assertEquals(2L, response.toMap().get("max_session_ephemeral_path_bytes")); + } + } + @Test public void testRuok() throws IOException, InterruptedException { testCommand("ruok"); diff --git a/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/commands.mdx b/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/commands.mdx index d566ec76f21..c16f085d593 100644 --- a/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/commands.mdx +++ b/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/commands.mdx @@ -94,7 +94,7 @@ Moving forward, Four Letter Words will be deprecated, please use ```bash $ echo mntr | nc localhost 2185 - zk_version 3.4.0 + zk_version 3.10.0 zk_avg_latency 0.7561 - be account to four decimal places zk_max_latency 0 zk_min_latency 0 @@ -105,6 +105,7 @@ Moving forward, Four Letter Words will be deprecated, please use zk_znode_count 4 zk_watch_count 0 zk_ephemerals_count 0 + zk_max_session_ephemeral_path_bytes 0 zk_approximate_data_size 27 zk_learners 4 - only exposed by the Leader zk_synced_followers 4 - only exposed by the Leader @@ -116,6 +117,11 @@ Moving forward, Four Letter Words will be deprecated, please use The output is compatible with java properties format and the content may change over time (new keys added). Your scripts should expect changes. ATTENTION: Some of the keys are platform specific and some of the keys are only exported by the Leader. + + **New in 3.10.0:** `zk_max_session_ephemeral_path_bytes` reports the maximum + total size of a session's ephemeral paths, in UTF-8 bytes. See + [maximum ephemeral path size per session](/admin-ops/monitor-and-audit-logs#maximum-ephemeral-path-size-per-session). + The output contains multiple lines with the following format: ``` @@ -420,6 +426,10 @@ Available commands include: summaries of the data tree (among other things). Returns each as its own field. + **New in 3.10.0:** Returns `max_session_ephemeral_path_bytes`, the maximum + total size of a session's ephemeral paths, in UTF-8 bytes. See + [maximum ephemeral path size per session](/admin-ops/monitor-and-audit-logs#maximum-ephemeral-path-size-per-session). + - _observer_connection_stat_reset/orst_ : Reset all observer connection statistics. Companion command to _observers_. No new fields returned. diff --git a/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/monitor-and-audit-logs.mdx b/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/monitor-and-audit-logs.mdx index c5481ecd986..d0db1756820 100644 --- a/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/monitor-and-audit-logs.mdx +++ b/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/monitor-and-audit-logs.mdx @@ -11,7 +11,33 @@ request processors, and more. ### Metrics -All available metrics are defined in `ServerMetrics.java`. +`ServerMetrics.java` defines counters and summaries. `ZooKeeperServer.java` +registers server gauges. + +#### Maximum Ephemeral Path Size per Session + +**New in 3.10.0:** `max_session_ephemeral_path_bytes` is the maximum total size of +full ephemeral znode paths owned by a single session on this server, in UTF-8 +bytes. It includes path separators and excludes znode data and persistent, +container, and TTL nodes. + +For example, `/a` and `/中` occupy two and four bytes. A session that owns both +has a total of six bytes. If another session owns only `/bbb` (four bytes), the +metric is six. + +The value is zero when there are no ephemeral znodes. Deleting nodes or closing +sessions can reduce it. After loading or restoring a snapshot, the metric reports +the value for the restored tree. Concurrent updates can change session totals +during collection, so the metric is not an atomic snapshot. + +Prometheus and the AdminServer `monitor` command expose +`max_session_ephemeral_path_bytes`. The four-letter `mntr` command exposes +`zk_max_session_ephemeral_path_bytes`. The metric uses the existing export +configuration. + +The metric helps estimate `closeSession` transaction sizes relative to +`jute.maxbuffer`. It excludes serialization overhead: a four-byte length prefix +per path, the list length, and transaction headers. ### Configuring the Metrics Provider