-
Notifications
You must be signed in to change notification settings - Fork 626
HDDS-5916. Datanodes stuck in leader election in Kubernetes #3186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
84b56f8
cb4adb9
e51aa4b
50ded1c
5c9addf
a7e47b7
71f1b28
d7dbf36
9452d81
b89eed1
e346e8b
878f989
b807db6
424c788
e6156e6
f94bb3d
c7993ae
52d20d2
aa2fb97
627a568
4404106
8bbca39
aeeee27
db20d84
62c46eb
1a81703
2c5f812
27f2d76
7941838
7ce0077
e742158
d8f4822
801e856
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,7 +125,7 @@ private void persistContainerDatanodeDetails() { | |
| File idPath = new File(dataNodeIDPath); | ||
| DatanodeDetails datanodeDetails = this.context.getParent() | ||
| .getDatanodeDetails(); | ||
| if (datanodeDetails != null && !idPath.exists()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the motivation for dropping this check?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is because when the datanode got restarted in k8s, the IP will be changed. So the original info in this file is not accurate any more. This will make sure we update with the latest info. And when we are not using k8s, I think it is not harmful to always update this file whenever the node restarts. |
||
| if (datanodeDetails != null) { | ||
| try { | ||
| ContainerUtils.writeDatanodeDetailsTo(datanodeDetails, idPath); | ||
| } catch (IOException ex) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,9 @@ | |
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| import com.google.gson.ExclusionStrategy; | ||
| import com.google.gson.FieldAttributes; | ||
| import org.apache.hadoop.hdds.scm.net.NodeImpl; | ||
| import org.apache.hadoop.util.StringUtils; | ||
| import org.apache.hadoop.util.Time; | ||
|
|
||
|
|
@@ -58,10 +61,29 @@ public class EventQueue implements EventPublisher, AutoCloseable { | |
|
|
||
| private boolean isRunning = true; | ||
|
|
||
| private static final Gson TRACING_SERIALIZER = new GsonBuilder().create(); | ||
| private static final Gson TRACING_SERIALIZER = new GsonBuilder() | ||
| .setExclusionStrategies(new DatanodeDetailsGsonExclusionStrategy()) | ||
| .create(); | ||
|
|
||
| private boolean isSilent = false; | ||
|
|
||
| // The field parent in DatanodeDetails class has the circular reference | ||
| // which will result in Gson infinite recursive parsing. We need to exclude | ||
| // this field when generating json string for DatanodeDetails object | ||
| static class DatanodeDetailsGsonExclusionStrategy | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change can be merged as a quick PR and not wait on this PR. |
||
| implements ExclusionStrategy { | ||
| @Override | ||
| public boolean shouldSkipField(FieldAttributes f) { | ||
| return f.getDeclaringClass() == NodeImpl.class | ||
| && f.getName().equals("parent"); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean shouldSkipClass(Class<?> aClass) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Add new handler to the event queue. | ||
| * <p> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,6 +147,13 @@ public final class SCMEvents { | |
| public static final TypedEvent<DatanodeDetails> NEW_NODE = | ||
| new TypedEvent<>(DatanodeDetails.class, "New_Node"); | ||
|
|
||
| /** | ||
| * This event will be triggered whenever a datanode is registered with | ||
| * SCM with a different Ip or host name. | ||
| */ | ||
| public static final TypedEvent<DatanodeDetails> NODE_IP_OR_HOSTNAME_UPDATE = | ||
| new TypedEvent<>(DatanodeDetails.class, "Node_Ip_Or_Hostname_Update"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of "IP or hostname" I think we can simply say "address", here and elsewhere, too (e.g. in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure. |
||
|
|
||
| /** | ||
| * This event will be triggered whenever a datanode is moved from healthy to | ||
| * stale state. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,7 @@ public NewNodeHandler(PipelineManager pipelineManager, | |
| public void onMessage(DatanodeDetails datanodeDetails, | ||
| EventPublisher publisher) { | ||
| try { | ||
| pipelineManager.closeStalePipelines(datanodeDetails); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is closeStalePipelines necessary here? Since when SCM processes the register command, it should be able to distinguish the new node / updated node, and here should be only responsible for the new node case
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. This is necessary. I believe in my testing, if a datanode is dead for a long time, SCM will remove it from the registration list. When the node comes up with a different IP, it first registers with SCM, and SCM treat it as a new node. But the old pipeline with the old IPs may still be there. Another way to achieve this is to delete the pipelines if SCM is going to remove the dead nodes. But I am not that familiar with this part of the code. I may need to have a further look.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to your implementation, when the node comes up with a different IP, it will register first with SCM, SCM node manager will get it as long as its UUID is not changed through
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry. I think I misstated my case. You are right. When a datanode is dead for a long time, SCM actually won't remove it from its registration list. So when this node with the same uuid comes up again with different IP, it will fall to update address condition, instead of registering new node. However, there is another case. If the SCM also restarts, then it will lose all its in memory node registration map, but it still have all the old pipelines since pipelines are read from persistent. So in this case, if the datanode changes its IP, and come to register with SCM, SCM will treat it as a new node instead of a known node with different IP. So in this case, we still need to close all the stale pipelines which has the old IPs for this datanode. Please let me know if my above statement makes sense to you. Thanks!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sense, thx for the explanation. |
||
| serviceManager.notifyEventTriggered(Event.NEW_NODE_HANDLER_TRIGGERED); | ||
|
|
||
| if (datanodeDetails.getPersistedOpState() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /** | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with this | ||
| * work for additional information regarding copyright ownership. The ASF | ||
| * licenses this file to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * <p> | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * <p> | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations under | ||
| * the License. | ||
| */ | ||
| package org.apache.hadoop.hdds.scm.node; | ||
|
|
||
| import org.apache.hadoop.hdds.protocol.DatanodeDetails; | ||
| import org.apache.hadoop.hdds.protocol.proto.HddsProtos; | ||
| import org.apache.hadoop.hdds.scm.ha.SCMService; | ||
| import org.apache.hadoop.hdds.scm.ha.SCMServiceManager; | ||
| import org.apache.hadoop.hdds.scm.node.states.NodeNotFoundException; | ||
| import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; | ||
| import org.apache.hadoop.hdds.server.events.EventHandler; | ||
| import org.apache.hadoop.hdds.server.events.EventPublisher; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Handles datanode ip or hostname change event. | ||
| */ | ||
| public class NodeIpOrHostnameUpdateHandler | ||
| implements EventHandler<DatanodeDetails> { | ||
| private static final Logger LOG = | ||
| LoggerFactory.getLogger(NodeIpOrHostnameUpdateHandler.class); | ||
|
|
||
| private final PipelineManager pipelineManager; | ||
| private final NodeDecommissionManager decommissionManager; | ||
| private final SCMServiceManager serviceManager; | ||
|
|
||
| public NodeIpOrHostnameUpdateHandler(PipelineManager pipelineManager, | ||
| NodeDecommissionManager decommissionManager, | ||
| SCMServiceManager serviceManager) { | ||
| this.pipelineManager = pipelineManager; | ||
| this.decommissionManager = decommissionManager; | ||
| this.serviceManager = serviceManager; | ||
| } | ||
|
|
||
| @Override | ||
| public void onMessage(DatanodeDetails datanodeDetails, | ||
| EventPublisher publisher) { | ||
| try { | ||
| pipelineManager.closeStalePipelines(datanodeDetails); | ||
| serviceManager.notifyEventTriggered(SCMService.Event | ||
| .NODE_IP_OR_HOSTNAME_UPDATE_HANDLER_TRIGGERED); | ||
|
|
||
| if (datanodeDetails.getPersistedOpState() | ||
| != HddsProtos.NodeOperationalState.IN_SERVICE) { | ||
| decommissionManager.continueAdminForNode(datanodeDetails); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be better to depend on the guarantees of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. continueAdminForNode implements the logic for when the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure |
||
| } | ||
| } catch (NodeNotFoundException e) { | ||
| // Should not happen, as the node has just registered to call this event | ||
| // handler. | ||
| LOG.warn( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log as an error.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||
| "NodeNotFound when updating the node Ip or host name to the " + | ||
| "decommissionManager", | ||
| e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -359,33 +359,34 @@ public RegisteredCommand register( | |||||||
| .build(); | ||||||||
| } | ||||||||
|
|
||||||||
| InetAddress dnAddress = Server.getRemoteIp(); | ||||||||
| if (dnAddress != null) { | ||||||||
| // Mostly called inside an RPC, update ip | ||||||||
| datanodeDetails.setHostName(dnAddress.getHostName()); | ||||||||
| datanodeDetails.setIpAddress(dnAddress.getHostAddress()); | ||||||||
| } | ||||||||
|
|
||||||||
| String dnsName; | ||||||||
| String networkLocation; | ||||||||
| datanodeDetails.setNetworkName(datanodeDetails.getUuidString()); | ||||||||
| if (useHostname) { | ||||||||
| dnsName = datanodeDetails.getHostName(); | ||||||||
| } else { | ||||||||
| dnsName = datanodeDetails.getIpAddress(); | ||||||||
| } | ||||||||
| networkLocation = nodeResolve(dnsName); | ||||||||
| if (networkLocation != null) { | ||||||||
| datanodeDetails.setNetworkLocation(networkLocation); | ||||||||
| } | ||||||||
|
|
||||||||
| if (!isNodeRegistered(datanodeDetails)) { | ||||||||
| InetAddress dnAddress = Server.getRemoteIp(); | ||||||||
| if (dnAddress != null) { | ||||||||
| // Mostly called inside an RPC, update ip and peer hostname | ||||||||
| datanodeDetails.setHostName(dnAddress.getHostName()); | ||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I delete this line, because these days, when I tested it, I found sometimes dnAddress.getHostName() returns IP instead of hostName, which makes the datanode restarting not work. Please let me know if it is OK to delete this line. @GeorgeJahad @adoroszlai
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sokui @adoroszlai I'm nervous about removing the call to setHostName(). I just took around and it seems to get used in many places. I've included some below: ozone/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/XceiverClientManager.java Line 259 in f57a019
ozone/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/freon/DatanodeChunkGenerator.java Line 171 in f57a019
Line 576 in f57a019
Why does restart not work when it returns the IP string instead of the host string?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @GeorgeJahad , To not change the old code path, I added the if condition: when
This is the whole story. That's why now I keep the old code path same, but if
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If you are sure this is true, then I'm fine with the change.
I'm confused about this statement. The old code path is when "(!isNodeRegistered(datanodeDetails))" is true, isn't it? not when "(!useHostname)" is true? what am I missing?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Old codepath means current
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the confusion. The old path is the current master. So I made the following change: from To
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I was saying is that even we delete this line |
||||||||
| datanodeDetails.setIpAddress(dnAddress.getHostAddress()); | ||||||||
| } | ||||||||
| try { | ||||||||
| String dnsName; | ||||||||
| String networkLocation; | ||||||||
| datanodeDetails.setNetworkName(datanodeDetails.getUuidString()); | ||||||||
| if (useHostname) { | ||||||||
| dnsName = datanodeDetails.getHostName(); | ||||||||
| } else { | ||||||||
| dnsName = datanodeDetails.getIpAddress(); | ||||||||
| } | ||||||||
| networkLocation = nodeResolve(dnsName); | ||||||||
| if (networkLocation != null) { | ||||||||
| datanodeDetails.setNetworkLocation(networkLocation); | ||||||||
| } | ||||||||
|
|
||||||||
| clusterMap.add(datanodeDetails); | ||||||||
| nodeStateManager.addNode(datanodeDetails, layoutInfo); | ||||||||
| // Check that datanode in nodeStateManager has topology parent set | ||||||||
| DatanodeDetails dn = nodeStateManager.getNode(datanodeDetails); | ||||||||
| Preconditions.checkState(dn.getParent() != null); | ||||||||
| addEntryTodnsToUuidMap(dnsName, datanodeDetails.getUuidString()); | ||||||||
| addEntryToDnsToUuidMap(dnsName, datanodeDetails.getUuidString()); | ||||||||
| // Updating Node Report, as registration is successful | ||||||||
| processNodeReport(datanodeDetails, nodeReport); | ||||||||
| LOG.info("Registered Data node : {}", datanodeDetails); | ||||||||
|
|
@@ -399,6 +400,44 @@ public RegisteredCommand register( | |||||||
| LOG.error("Cannot find datanode {} from nodeStateManager", | ||||||||
| datanodeDetails.toString()); | ||||||||
| } | ||||||||
| } else { | ||||||||
| // Update datanode if it is registered but the ip or hostname changes | ||||||||
| try { | ||||||||
| final DatanodeInfo datanodeInfo = | ||||||||
| nodeStateManager.getNode(datanodeDetails); | ||||||||
| if (!datanodeInfo.getIpAddress().equals(datanodeDetails.getIpAddress()) | ||||||||
| || !datanodeInfo.getHostName() | ||||||||
| .equals(datanodeDetails.getHostName())) { | ||||||||
| LOG.info("Updating data node {} from {} to {}", | ||||||||
| datanodeDetails.getUuidString(), | ||||||||
| datanodeInfo, | ||||||||
| datanodeDetails); | ||||||||
| if (clusterMap.contains(datanodeInfo)) { | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be better to implement
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||||||||
| clusterMap.remove(datanodeInfo); | ||||||||
| } | ||||||||
| clusterMap.add(datanodeDetails); | ||||||||
|
|
||||||||
| String oldDnsName; | ||||||||
| if (useHostname) { | ||||||||
| oldDnsName = datanodeInfo.getHostName(); | ||||||||
| } else { | ||||||||
| oldDnsName = datanodeInfo.getIpAddress(); | ||||||||
| } | ||||||||
| removeEntryFromDnsToUuidMap(oldDnsName); | ||||||||
| addEntryToDnsToUuidMap(dnsName, datanodeDetails.getUuidString()); | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here better to implement a new method
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||||||||
|
|
||||||||
| nodeStateManager.updateNode(datanodeDetails, layoutInfo); | ||||||||
| DatanodeDetails dn = nodeStateManager.getNode(datanodeDetails); | ||||||||
| Preconditions.checkState(dn.getParent() != null); | ||||||||
| processNodeReport(datanodeDetails, nodeReport); | ||||||||
| LOG.info("Updated Datanode to: {}", dn); | ||||||||
| scmNodeEventPublisher | ||||||||
| .fireEvent(SCMEvents.NODE_IP_OR_HOSTNAME_UPDATE, dn); | ||||||||
| } | ||||||||
| } catch (NodeNotFoundException e) { | ||||||||
| LOG.error("Cannot find datanode {} from nodeStateManager", | ||||||||
| datanodeDetails); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| return RegisteredCommand.newBuilder().setErrorCode(ErrorCode.success) | ||||||||
|
|
@@ -415,11 +454,9 @@ public RegisteredCommand register( | |||||||
| * @param dnsName String representing the hostname or IP of the node | ||||||||
| * @param uuid String representing the UUID of the registered node. | ||||||||
| */ | ||||||||
| @SuppressFBWarnings(value = "AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION", | ||||||||
| justification = "The method is synchronized and this is the only place " + | ||||||||
| "dnsToUuidMap is modified") | ||||||||
| private synchronized void addEntryTodnsToUuidMap( | ||||||||
| String dnsName, String uuid) { | ||||||||
| @SuppressFBWarnings(value = "AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION") | ||||||||
| private synchronized void addEntryToDnsToUuidMap( | ||||||||
| String dnsName, String uuid) { | ||||||||
| Set<String> dnList = dnsToUuidMap.get(dnsName); | ||||||||
| if (dnList == null) { | ||||||||
| dnList = ConcurrentHashMap.newKeySet(); | ||||||||
|
|
@@ -428,6 +465,19 @@ private synchronized void addEntryTodnsToUuidMap( | |||||||
| dnList.add(uuid); | ||||||||
| } | ||||||||
|
|
||||||||
| private synchronized void removeEntryFromDnsToUuidMap(String dnsName) { | ||||||||
| if (!dnsToUuidMap.containsKey(dnsName)) { | ||||||||
| return; | ||||||||
| } | ||||||||
| Set<String> dnSet = dnsToUuidMap.get(dnsName); | ||||||||
| if (dnSet.contains(dnsName)) { | ||||||||
| dnSet.remove(dnsName); | ||||||||
| } | ||||||||
| if (dnSet.isEmpty()) { | ||||||||
| dnsToUuidMap.remove(dnsName); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Send heartbeat to indicate the datanode is alive and doing well. | ||||||||
| * | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might as well print the actual value calculated in the debug log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure