Skip to content

Commit d8ba1d5

Browse files
authored
Merge pull request #9082 from apache/STORM-3871-sweep-orphaned-dependency-blobs
STORM-3871: sweep dependency blobs that outlive their topology's cleanup
2 parents f96065c + 8c1fdc1 commit d8ba1d5

6 files changed

Lines changed: 321 additions & 6 deletions

File tree

storm-server/src/main/java/org/apache/storm/DaemonConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ public class DaemonConfig implements Validated {
270270
* it takes to delete an inbox jar file is going to be somewhat more than NIMBUS_CLEANUP_INBOX_JAR_EXPIRATION_SECS
271271
* (depending on how often NIMBUS_CLEANUP_FREQ_SECS is set to).
272272
*
273+
* <p>This is also how long a dependency blob uploaded by a client may go without any topology referring to it
274+
* before nimbus deletes it from the blob store, which covers the time between uploading the dependencies of a
275+
* topology and submitting it.
276+
*
273277
* @see #NIMBUS_CLEANUP_INBOX_FREQ_SECS
274278
*/
275279
@IsInteger

storm-server/src/main/java/org/apache/storm/blobstore/KeySequenceNumber.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,27 @@ public KeySequenceNumber(String key, NimbusInfo nimbusInfo) {
124124
}
125125

126126
public synchronized int getKeySequenceNumber(CuratorFramework zkClient) throws KeyNotFoundException {
127+
return getKeySequenceNumber(zkClient, true);
128+
}
129+
130+
/**
131+
* Hand over the sequence number for the copy of the key held by this nimbus.
132+
*
133+
* @param zkClient the zookeeper client
134+
* @param mayCreateKey whether the key may be created when zookeeper does not know it, which is only right for the
135+
* leader storing a blob that a client uploads. Any other nimbus mirrors a key the leader created,
136+
* so a key zookeeper does not know was deleted and must not be registered again.
137+
* @return the sequence number
138+
* @throws KeyNotFoundException if the key is not in zookeeper and may not be created, or it is deleted meanwhile
139+
*/
140+
public synchronized int getKeySequenceNumber(CuratorFramework zkClient, boolean mayCreateKey) throws KeyNotFoundException {
127141
TreeSet<Integer> sequenceNumbers = new TreeSet<Integer>();
128142
try {
129143
// Key has not been created yet and it is the first time it is being created
130144
if (zkClient.checkExists().forPath(BlobStoreUtils.getBlobStoreSubtree() + "/" + key) == null) {
145+
if (!mayCreateKey) {
146+
throw new KeeperException.NoNodeException(BlobStoreUtils.getBlobStoreSubtree() + "/" + key);
147+
}
131148
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT)
132149
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE).forPath(BLOBSTORE_MAX_KEY_SEQUENCE_SUBTREE + "/" + key);
133150
zkClient.setData().forPath(BLOBSTORE_MAX_KEY_SEQUENCE_SUBTREE + "/" + key,

storm-server/src/main/java/org/apache/storm/blobstore/LocalFsBlobStore.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,26 @@ private void setupBlobstore() throws AuthorizationException, KeyNotFoundExceptio
139139
LOG.debug("Creating list of key entries for blobstore inside zookeeper {} local {}", activeKeys, activeLocalKeys);
140140
for (String key : activeLocalKeys) {
141141
try {
142-
state.setupBlob(key, nimbusInfo, getVersionForKey(key, nimbusInfo, zkClient));
142+
state.setupBlob(key, nimbusInfo, getVersionForKey(key, nimbusInfo, zkClient, false));
143143
} catch (KeyNotFoundException e) {
144144
// invalid key, remove it from blobstore
145145
store.deleteBlob(key, NIMBUS_SUBJECT);
146146
}
147147
}
148148
}
149149

150+
/**
151+
* Tell whether this nimbus is the leader, which is the only one that may register a key zookeeper does not know.
152+
* Without a leader elector there is a single nimbus, which is then the leader.
153+
*/
154+
private boolean isLeader() {
155+
try {
156+
return leaderElector == null || leaderElector.isLeader();
157+
} catch (Exception e) {
158+
throw new RuntimeException(e);
159+
}
160+
}
161+
150162

151163
private void blobSync() throws Exception {
152164
if ("distributed".equals(conf.get(Config.STORM_CLUSTER_MODE))) {
@@ -216,11 +228,14 @@ public AtomicOutputStream createBlob(String key, SettableBlobMeta meta, Subject
216228
}
217229
BlobStoreFileOutputStream outputStream = null;
218230
try {
231+
//Taken before anything is written, so that a non-leader downloading a key that was deleted meanwhile is
232+
//refused without leaving a copy behind.
233+
int version = getVersionForKey(key, this.nimbusInfo, zkClient, isLeader());
219234
outputStream = new BlobStoreFileOutputStream(fbs.write(META_PREFIX + key, true));
220235
outputStream.write(Utils.thriftSerialize(meta));
221236
outputStream.close();
222237
outputStream = null;
223-
this.stormClusterState.setupBlob(key, this.nimbusInfo, getVersionForKey(key, this.nimbusInfo, zkClient));
238+
this.stormClusterState.setupBlob(key, this.nimbusInfo, version);
224239
return new BlobStoreFileOutputStream(fbs.write(DATA_PREFIX + key, true));
225240
} catch (IOException e) {
226241
throw new RuntimeException(e);

storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ public static List<ACL> getNimbusAcls(Map<String, Object> conf) {
440440
private final TimeCacheMap<String, WritableByteChannel> uploaders;
441441
private final BlobStore blobStore;
442442
private final TopoCache topoCache;
443+
//When a dependency blob was first seen unreferenced by any topology, only used by the cleanup pass.
444+
private final Map<String, Long> orphanedDependencyKeysDetectedMs = new HashMap<>();
443445
@SuppressWarnings("deprecation")
444446
private final TimeCacheMap<String, BufferInputStream> blobDownloaders;
445447
@SuppressWarnings("deprecation")
@@ -775,8 +777,18 @@ static List<String> getKeyListFromId(Map<String, Object> conf, String id) {
775777

776778
public static int getVersionForKey(String key, NimbusInfo nimbusInfo,
777779
CuratorFramework zkClient) throws KeyNotFoundException {
780+
return getVersionForKey(key, nimbusInfo, zkClient, true);
781+
}
782+
783+
/**
784+
* Get the version to register the copy of a blob held by a nimbus under.
785+
*
786+
* @see KeySequenceNumber#getKeySequenceNumber(CuratorFramework, boolean)
787+
*/
788+
public static int getVersionForKey(String key, NimbusInfo nimbusInfo,
789+
CuratorFramework zkClient, boolean mayCreateKey) throws KeyNotFoundException {
778790
KeySequenceNumber kseq = new KeySequenceNumber(key, nimbusInfo);
779-
return kseq.getKeySequenceNumber(zkClient);
791+
return kseq.getKeySequenceNumber(zkClient, mayCreateKey);
780792
}
781793

782794
private static StormTopology readStormTopology(String topoId, TopoCache tc) throws KeyNotFoundException, AuthorizationException,
@@ -3095,6 +3107,45 @@ public void forceDeleteTopoDistDir(String topoId) throws IOException {
30953107
Utils.forceDelete(ServerConfigUtils.masterStormDistRoot(conf, topoId));
30963108
}
30973109

3110+
/**
3111+
* Remove the dependency blobs that no topology has referred to for longer than the inbox jar expiration.
3112+
*
3113+
* <p>{@link #rmDependencyBlobsInTopology} only runs in the pass that cleans up the owning topology, and a dependency
3114+
* blob key carries no topology id, so a blob that outlives that pass can never be traced back to it. That happens
3115+
* when a submission fails after its dependencies were uploaded, or when another nimbus still holds a copy of the blob
3116+
* and it is downloaded back after it was removed here. This sweep reclaims those.
3117+
*
3118+
* <p>Only keys that are provably unique to one topology are considered: an older client that finds a shareable key
3119+
* in the store does not upload it again but refers to it, so such a key may be about to be used. A client uploads the
3120+
* dependencies before it submits the topology, so a key is only removed once it has been seen unreferenced for
3121+
* {@link DaemonConfig#NIMBUS_INBOX_JAR_EXPIRATION_SECS}, the time nimbus gives an uploaded topology jar to be
3122+
* submitted. When a key was first seen unreferenced is only kept in memory, so a new leader starts the wait over.
3123+
*
3124+
* @param referenced the dependency blob keys referenced by topologies that are not being cleaned up
3125+
*/
3126+
@VisibleForTesting
3127+
void sweepOrphanedDependencyBlobs(Set<String> referenced) {
3128+
try {
3129+
long graceMs = TimeUnit.SECONDS.toMillis(
3130+
ObjectReader.getInt(conf.get(DaemonConfig.NIMBUS_INBOX_JAR_EXPIRATION_SECS), 3600));
3131+
long nowMs = Time.currentTimeMillis();
3132+
Set<String> orphaned = blobStore.filterAndListKeys(
3133+
key -> isProvablyUniqueDependencyKey(key) && !referenced.contains(key) ? key : null);
3134+
//Forget the keys that are gone or referenced again, so that being orphaned later waits the full time again.
3135+
orphanedDependencyKeysDetectedMs.keySet().retainAll(orphaned);
3136+
for (String key : orphaned) {
3137+
long unreferencedMs = nowMs - orphanedDependencyKeysDetectedMs.computeIfAbsent(key, k -> nowMs);
3138+
if (unreferencedMs >= graceMs) {
3139+
LOG.info("Removing dependency blob {}, no topology has referred to it for {} ms", key, unreferencedMs);
3140+
rmBlobKey(blobStore, key, stormClusterState);
3141+
orphanedDependencyKeysDetectedMs.remove(key);
3142+
}
3143+
}
3144+
} catch (Exception e) {
3145+
LOG.warn("Could not sweep the dependency blobs that no topology refers to", e);
3146+
}
3147+
}
3148+
30983149
/**
30993150
* Cleanup topologies and Jars.
31003151
*/
@@ -3134,6 +3185,12 @@ public void doCleanup() {
31343185
idToExecutors.getAndUpdate(new Dissoc<>(topoId));
31353186
}
31363187

3188+
//Catches the dependency blobs that outlived the pass that cleaned up their topology. Without knowing the
3189+
//references nothing can be told to be unused, so nothing is swept then.
3190+
if (stillReferenced != null) {
3191+
sweepOrphanedDependencyBlobs(stillReferenced);
3192+
}
3193+
31373194
long cleanupDurationMs = Time.deltaMs(cleanupStartMs);
31383195
if (cleanupDurationMs > 10000) {
31393196
LOG.warn("doCleanup is taking too long, topoIdSelectionDurationMs={}, cleanupDurationMs={}",
@@ -4378,7 +4435,9 @@ public void createStateInZookeeper(String key) throws TException {
43784435
BlobStore store = blobStore;
43794436
NimbusInfo ni = nimbusHostPortInfo;
43804437
if (store instanceof LocalFsBlobStore) {
4381-
state.setupBlob(key, ni, getVersionForKey(key, ni, zkClient));
4438+
//A non-leader only registers its copy of a key the leader created. If zookeeper does not know the key
4439+
//any more it was deleted while the copy was downloaded, and registering it would bring it back.
4440+
state.setupBlob(key, ni, getVersionForKey(key, ni, zkClient, isLeader()));
43824441
}
43834442
LOG.debug("Created state in zookeeper {} {} {}", state, store, ni);
43844443
} catch (KeyNotFoundException e) {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.storm.blobstore;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertNull;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
24+
25+
import org.apache.storm.generated.KeyNotFoundException;
26+
import org.apache.storm.nimbus.NimbusInfo;
27+
import org.apache.storm.shade.org.apache.curator.framework.CuratorFramework;
28+
import org.apache.storm.shade.org.apache.curator.framework.CuratorFrameworkFactory;
29+
import org.apache.storm.shade.org.apache.curator.retry.ExponentialBackoffRetry;
30+
import org.apache.storm.testing.InProcessZookeeper;
31+
import org.junit.jupiter.api.Test;
32+
33+
class KeySequenceNumberTest {
34+
private static final String KEY = "dep-lib-11111111-1111-1111-1111-111111111111.jar";
35+
private static final String KEY_PATH = "/blobstore/" + KEY;
36+
private static final String MAX_SEQUENCE_PATH = "/blobstoremaxkeysequencenumber/" + KEY;
37+
private static final NimbusInfo LEADER = new NimbusInfo("nimbus-1", 6627, false);
38+
private static final NimbusInfo PEER = new NimbusInfo("nimbus-2", 6627, false);
39+
40+
/**
41+
* Replays the blob store state changes behind the nimbus logs reported on STORM-3871. A non-leader that downloaded
42+
* a blob while the leader deleted it registered the key again as new, and every other nimbus, the leader included,
43+
* then downloaded the blob back from it.
44+
*/
45+
@Test
46+
void aNonLeaderCannotRegisterAKeyAgainThatWasDeletedWhileItDownloadedIt() throws Exception {
47+
try (InProcessZookeeper zk = new InProcessZookeeper();
48+
CuratorFramework zkClient = newClient(zk)) {
49+
// the client uploads the dependency: createBlob, then createStateInZookeeper when it closes the stream
50+
assertEquals(1, register(zkClient, LEADER, true));
51+
assertEquals(2, register(zkClient, LEADER, true));
52+
53+
// the topology is cleaned up and the leader deletes the blob, as LocalFsBlobStore#deleteBlob does
54+
zkClient.delete().deletingChildrenIfNeeded().forPath(KEY_PATH);
55+
zkClient.delete().deletingChildrenIfNeeded().forPath(MAX_SEQUENCE_PATH);
56+
57+
// the non-leader finishes its download and would register the key as if it were new
58+
assertThrows(KeyNotFoundException.class, () -> register(zkClient, PEER, false));
59+
assertNull(zkClient.checkExists().forPath(KEY_PATH));
60+
assertNull(zkClient.checkExists().forPath(MAX_SEQUENCE_PATH));
61+
}
62+
}
63+
64+
@Test
65+
void aNonLeaderStillRegistersItsCopyOfAKeyTheLeaderCreated() throws Exception {
66+
try (InProcessZookeeper zk = new InProcessZookeeper();
67+
CuratorFramework zkClient = newClient(zk)) {
68+
assertEquals(1, register(zkClient, LEADER, true));
69+
70+
assertEquals(0, register(zkClient, PEER, false));
71+
}
72+
}
73+
74+
private static CuratorFramework newClient(InProcessZookeeper zk) {
75+
CuratorFramework zkClient = CuratorFrameworkFactory.newClient("localhost:" + zk.getPort(),
76+
new ExponentialBackoffRetry(1000, 3));
77+
zkClient.start();
78+
return zkClient;
79+
}
80+
81+
/**
82+
* Do what IStormClusterState#setupBlob does with the version KeySequenceNumber hands out.
83+
*/
84+
private static int register(CuratorFramework zkClient, NimbusInfo nimbus, boolean mayCreateKey) throws Exception {
85+
int version = new KeySequenceNumber(KEY, nimbus).getKeySequenceNumber(zkClient, mayCreateKey);
86+
if (zkClient.checkExists().forPath(KEY_PATH) != null) {
87+
for (String child : zkClient.getChildren().forPath(KEY_PATH)) {
88+
if (child.startsWith(nimbus.toHostPortString())) {
89+
zkClient.delete().forPath(KEY_PATH + "/" + child);
90+
}
91+
}
92+
}
93+
zkClient.create().creatingParentsIfNeeded().forPath(KEY_PATH + "/" + nimbus.toHostPortString() + "-" + version);
94+
return version;
95+
}
96+
}

0 commit comments

Comments
 (0)