You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by ji...@apache.org on 2020/10/08 18:29:35 UTC
[helix] 01/01: ZkClient should not keep retrying getChildren() due
to large number of children (#1109)
This is an automated email from the ASF dual-hosted git repository.
jiajunwang pushed a commit to branch helix-0.9.x
in repository https://gitbox.apache.org/repos/asf/helix.git
commit cc0fdaae965d77e6b9347ea9ddecf597d00d8117
Author: Huizhi Lu <ih...@gmail.com>
AuthorDate: Thu Jul 23 11:03:54 2020 -0700
ZkClient should not keep retrying getChildren() due to large number of children (#1109)
For ZkClient's getChildren() operation, if there are a large number of children and the response packet size exceeds jute.maxbuffer default value 4MB on zk client side, ZkClient will get a ConnectionLossException and keep retrying connecting to ZK. The consequence is, the infinite retry may cause heavy GC on ZK server and kill ZK server.
This commit implements a workaround to exit retry loop for getChildren() if a large number of children cause connection loss.
---
.../helix/manager/zk/zookeeper/ZkClient.java | 62 +++++++++++++-
.../apache/helix/manager/zk/TestRawZkClient.java | 95 ++++++++++++++++++----
2 files changed, 138 insertions(+), 19 deletions(-)
diff --git a/helix-core/src/main/java/org/apache/helix/manager/zk/zookeeper/ZkClient.java b/helix-core/src/main/java/org/apache/helix/manager/zk/zookeeper/ZkClient.java
index 1b4d1ca..89b3c7a 100644
--- a/helix-core/src/main/java/org/apache/helix/manager/zk/zookeeper/ZkClient.java
+++ b/helix-core/src/main/java/org/apache/helix/manager/zk/zookeeper/ZkClient.java
@@ -71,8 +71,14 @@ import org.slf4j.LoggerFactory;
* WARN: Do not use this class directly, use {@link org.apache.helix.manager.zk.ZkClient} instead.
*/
public class ZkClient implements Watcher {
- private static Logger LOG = LoggerFactory.getLogger(ZkClient.class);
- private static long MAX_RECONNECT_INTERVAL_MS = 30000; // 30 seconds
+ private static final Logger LOG = LoggerFactory.getLogger(ZkClient.class);
+
+ private static final long MAX_RECONNECT_INTERVAL_MS = 30000; // 30 seconds
+
+ // If number of children exceeds this limit, getChildren() should not retry on connection loss.
+ // This is a workaround for exiting retry on connection loss because of large number of children.
+ // TODO: remove it once we have a better way to exit retry for this case
+ private static final int NUM_CHILDREN_LIMIT;
private final IZkConnection _connection;
private final long _operationRetryTimeoutInMillis;
@@ -90,6 +96,13 @@ public class ZkClient implements Watcher {
private PathBasedZkSerializer _pathBasedZkSerializer;
private ZkClientMonitor _monitor;
+ static {
+ // 100K is specific for helix messages which use UUID, making packet length just below 4 MB.
+ // Set it here for unit test to use reflection to change value
+ // because compilers optimize constants by replacing them inline.
+ NUM_CHILDREN_LIMIT = 100 * 1000;
+ }
+
private class IZkDataListenerEntry {
final IZkDataListener _dataListener;
final boolean _prefetchData;
@@ -713,11 +726,33 @@ public class ZkClient implements Watcher {
protected List<String> getChildren(final String path, final boolean watch) {
long startT = System.currentTimeMillis();
+
try {
List<String> children = retryUntilConnected(new Callable<List<String>>() {
+ private int connectionLossRetryCount = 0;
+
@Override
public List<String> call() throws Exception {
- return getConnection().getChildren(path, watch);
+ try {
+ return getConnection().getChildren(path, watch);
+ } catch (ConnectionLossException e) {
+ // Issue: https://github.com/apache/helix/issues/962
+ // Connection loss might be caused by an excessive number of children.
+ // Infinitely retrying connecting may cause high GC in ZK server and kill ZK server.
+ // This is a workaround to check numChildren to have a chance to exit retry loop.
+ // Check numChildren stat every other 3 connection loss, because there is a higher
+ // possibility that connection loss is caused by other factors such as network
+ // connectivity, session expired, etc.
+ // TODO: remove this check once we have a better way to exit infinite retry
+ ++connectionLossRetryCount;
+ if (connectionLossRetryCount >= 3) {
+ checkNumChildrenLimit(path);
+ connectionLossRetryCount = 0;
+ }
+
+ // Re-throw the ConnectionLossException for retryUntilConnected() to catch and retry.
+ throw e;
+ }
}
});
record(path, null, startT, ZkClientMonitor.AccessType.READ);
@@ -1765,4 +1800,25 @@ public class ZkClient implements Watcher {
}
}
}
+
+ private void checkNumChildrenLimit(String path) throws KeeperException {
+ Stat stat = getStat(path);
+ if (stat == null) {
+ return;
+ }
+
+ if (stat.getNumChildren() > NUM_CHILDREN_LIMIT) {
+ LOG.error("Failed to get children for path {} because of connection loss. "
+ + "Number of children {} exceeds limit {}, aborting retry.", path,
+ stat.getNumChildren(),
+ NUM_CHILDREN_LIMIT);
+ // MarshallingErrorException could represent transport error: exceeding the
+ // Jute buffer size. So use it to exit retry loop and tell that zk is not able to
+ // transport the data because packet length is too large.
+ throw new KeeperException.MarshallingErrorException();
+ } else {
+ LOG.debug("Number of children {} is less than limit {}, not exiting retry.",
+ stat.getNumChildren(), NUM_CHILDREN_LIMIT);
+ }
+ }
}
diff --git a/helix-core/src/test/java/org/apache/helix/manager/zk/TestRawZkClient.java b/helix-core/src/test/java/org/apache/helix/manager/zk/TestRawZkClient.java
index ce109b7..9cea4c2 100644
--- a/helix-core/src/test/java/org/apache/helix/manager/zk/TestRawZkClient.java
+++ b/helix-core/src/test/java/org/apache/helix/manager/zk/TestRawZkClient.java
@@ -19,20 +19,10 @@ package org.apache.helix.manager.zk;
* under the License.
*/
-import javax.management.MBeanServer;
-import javax.management.ObjectName;
-import java.lang.management.ManagementFactory;
-import java.util.Random;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.IZkStateListener;
import org.I0Itec.zkclient.ZkServer;
+import org.I0Itec.zkclient.exception.ZkException;
import org.apache.helix.SystemPropertyKeys;
import org.apache.helix.TestHelper;
import org.apache.helix.ZNRecord;
@@ -42,12 +32,8 @@ import org.apache.helix.monitoring.mbeans.MBeanRegistrar;
import org.apache.helix.monitoring.mbeans.MonitorDomainNames;
import org.apache.helix.monitoring.mbeans.ZkClientMonitor;
import org.apache.helix.monitoring.mbeans.ZkClientPathMonitor;
-import org.apache.zookeeper.CreateMode;
-import org.apache.zookeeper.KeeperException;
-import org.apache.zookeeper.WatchedEvent;
-import org.apache.zookeeper.Watcher;
+import org.apache.zookeeper.*;
import org.apache.zookeeper.Watcher.Event.KeeperState;
-import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.testng.Assert;
import org.testng.AssertJUnit;
@@ -55,6 +41,20 @@ import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
public class TestRawZkClient extends ZkUnitTestBase {
private final String TEST_TAG = "test_monitor";
private final String TEST_ROOT = "/my_cluster/IDEALSTATES";
@@ -412,4 +412,67 @@ public class TestRawZkClient extends ZkUnitTestBase {
zkClient.delete("/tmp/asyncOversize");
}
}
+
+
+ /*
+ * Tests getChildren() when there are an excessive number of children and connection loss happens,
+ * the operation should terminate and exit retry loop.
+ */
+ @Test
+ public void testGetChildrenOnLargeNumChildren() throws Exception {
+ // Default packetLen is 4M. It is static final and initialized
+ // when first zkClient is created.
+ // So we could not just set "jute.maxbuffer" to change the value.
+ // Reflection is needed to change the value.
+ // Remove "final" modifier
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ boolean isModifierAccessible = modifiersField.isAccessible();
+ modifiersField.setAccessible(true);
+
+ Field packetLenField = ClientCnxn.class.getDeclaredField("packetLen");
+ Field childrenLimitField =
+ org.apache.helix.manager.zk.zookeeper.ZkClient.class.getDeclaredField("NUM_CHILDREN_LIMIT");
+ modifiersField.setInt(packetLenField, packetLenField.getModifiers() & ~Modifier.FINAL);
+ modifiersField.setInt(childrenLimitField, childrenLimitField.getModifiers() & ~Modifier.FINAL);
+
+ boolean isPacketLenAccessible = packetLenField.isAccessible();
+ packetLenField.setAccessible(true);
+ int originPacketLen = packetLenField.getInt(null);
+ // Keep 150 bytes for successfully creating each child node.
+ packetLenField.set(null, 150);
+
+ boolean isChildrenLimitAccessible = childrenLimitField.isAccessible();
+ childrenLimitField.setAccessible(true);
+ int originChildrenLimit = childrenLimitField.getInt(null);
+ childrenLimitField.set(null, 2);
+
+ String path = "/" + TestHelper.getTestMethodName();
+ // Create 5 children to make packet length of children exceed 150 bytes
+ // and cause connection loss for getChildren() operation
+ for (int i = 0; i < 5; i++) {
+ _zkClient.createPersistent(path + "/" + UUID.randomUUID().toString(), true);
+ }
+
+ try {
+ _zkClient.getChildren(path);
+ Assert.fail("Should not successfully get children.");
+ } catch (ZkException expected) {
+ Assert.assertEquals(expected.getMessage(),
+ "org.apache.zookeeper.KeeperException$MarshallingErrorException: "
+ + "KeeperErrorCode = MarshallingError");
+ } finally {
+ packetLenField.set(null, originPacketLen);
+ packetLenField.setAccessible(isPacketLenAccessible);
+
+ childrenLimitField.set(null, originChildrenLimit);
+ childrenLimitField.setAccessible(isChildrenLimitAccessible);
+
+ modifiersField.setAccessible(isModifierAccessible);
+
+ Assert.assertTrue(TestHelper.verify(() -> {
+ _zkClient.deleteRecursively(path);
+ return !_zkClient.exists(path);
+ }, TestHelper.WAIT_DURATION));
+ }
+ }
}