Skip to content

Commit a26a12d

Browse files
committed
Add cluster-wide shard limit
Adds a safety limit on the number of shards in a cluster, based on the number of nodes in the cluster. The limit is checked on operations that add (or activate) shards, such as index creation, snapshot restoration, and opening closed indices, and can be changed via the cluster settings API. Closes elastic#20705
1 parent 0158b59 commit a26a12d

File tree

7 files changed

+351
-3
lines changed

7 files changed

+351
-3
lines changed

server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ public interface Custom extends NamedDiffable<Custom>, ToXContentFragment, Clust
127127

128128
}
129129

130+
public static final Setting<Integer> SETTING_CLUSTER_MAX_SHARDS_PER_NODE =
131+
Setting.intSetting("cluster.shards.max_per_node", 1000, Property.Dynamic, Property.NodeScope);
132+
130133
public static final Setting<Boolean> SETTING_READ_ONLY_SETTING =
131134
Setting.boolSetting("cluster.blocks.read_only", false, Property.Dynamic, Property.NodeScope);
132135

@@ -162,6 +165,7 @@ public interface Custom extends NamedDiffable<Custom>, ToXContentFragment, Clust
162165
private final ImmutableOpenMap<String, Custom> customs;
163166

164167
private final transient int totalNumberOfShards; // Transient ? not serializable anyway?
168+
private final int totalOpenIndexShards;
165169
private final int numberOfShards;
166170

167171
private final String[] allIndices;
@@ -183,12 +187,17 @@ public interface Custom extends NamedDiffable<Custom>, ToXContentFragment, Clust
183187
this.customs = customs;
184188
this.templates = templates;
185189
int totalNumberOfShards = 0;
190+
int totalOpenIndexShards = 0;
186191
int numberOfShards = 0;
187192
for (ObjectCursor<IndexMetaData> cursor : indices.values()) {
188193
totalNumberOfShards += cursor.value.getTotalNumberOfShards();
189194
numberOfShards += cursor.value.getNumberOfShards();
195+
if (IndexMetaData.State.OPEN.equals(cursor.value.getState())) {
196+
totalOpenIndexShards += cursor.value.getTotalNumberOfShards();
197+
}
190198
}
191199
this.totalNumberOfShards = totalNumberOfShards;
200+
this.totalOpenIndexShards = totalOpenIndexShards;
192201
this.numberOfShards = numberOfShards;
193202

194203
this.allIndices = allIndices;
@@ -661,10 +670,29 @@ public <T extends Custom> T custom(String type) {
661670
}
662671

663672

673+
/**
674+
* Gets the total number of shards from all indices, including replicas and
675+
* closed indices.
676+
* @return The total number shards from all indices.
677+
*/
664678
public int getTotalNumberOfShards() {
665679
return this.totalNumberOfShards;
666680
}
667681

682+
/**
683+
* Gets the total number of active shards from all indices. Includes
684+
* replicas, but does not include shards that are part of closed indices.
685+
* @return The total number of active shards from all indices.
686+
*/
687+
public int getTotalOpenIndexShards() {
688+
return this.totalOpenIndexShards;
689+
}
690+
691+
/**
692+
* Gets the number of primary shards from all indices, not including
693+
* replicas.
694+
* @return The number of primary shards from all indices.
695+
*/
668696
public int getNumberOfShards() {
669697
return this.numberOfShards;
670698
}

server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import java.util.List;
8585
import java.util.Locale;
8686
import java.util.Map;
87+
import java.util.Optional;
8788
import java.util.Set;
8889
import java.util.concurrent.atomic.AtomicInteger;
8990
import java.util.function.BiFunction;
@@ -597,18 +598,38 @@ public void onFailure(String source, Exception e) {
597598

598599
private void validate(CreateIndexClusterStateUpdateRequest request, ClusterState state) {
599600
validateIndexName(request.index(), state);
600-
validateIndexSettings(request.index(), request.settings());
601+
validateIndexSettings(request.index(), request.settings(), state);
601602
}
602603

603-
public void validateIndexSettings(String indexName, Settings settings) throws IndexCreationException {
604+
public void validateIndexSettings(String indexName, Settings settings, ClusterState clusterState) throws IndexCreationException {
604605
List<String> validationErrors = getIndexSettingsValidationErrors(settings);
606+
607+
Optional<String> shardAllocation = getShardLimitError(indexName, settings, clusterState);
608+
shardAllocation.ifPresent(validationErrors::add);
609+
605610
if (validationErrors.isEmpty() == false) {
606611
ValidationException validationException = new ValidationException();
607612
validationException.addValidationErrors(validationErrors);
608613
throw new IndexCreationException(indexName, validationException);
609614
}
610615
}
611616

617+
private Optional<String> getShardLimitError(String indexName, Settings settings, ClusterState clusterState) {
618+
int currentOpenShards = clusterState.getMetaData().getTotalOpenIndexShards();
619+
int maxShardsPerNode = clusterService.getClusterSettings().get(MetaData.SETTING_CLUSTER_MAX_SHARDS_PER_NODE);
620+
int nodeCount = clusterState.getNodes().getDataNodes().size();
621+
long maxShardsInCluster = maxShardsPerNode * nodeCount;
622+
int shardsToCreate = IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.get(settings)
623+
* (1 + IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.get(settings));
624+
625+
if ((currentOpenShards + shardsToCreate) > maxShardsInCluster) {
626+
return Optional.of("creating [" + indexName + "] "
627+
+ "would create " + shardsToCreate + " total shards, but this cluster currently has "
628+
+ currentOpenShards + "/" + maxShardsInCluster + " maximum shards open");
629+
}
630+
return Optional.empty();
631+
}
632+
612633
List<String> getIndexSettingsValidationErrors(Settings settings) {
613634
String customPath = IndexMetaData.INDEX_DATA_PATH_SETTING.get(settings);
614635
List<String> validationErrors = new ArrayList<>();

server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexStateService.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.ElasticsearchException;
2323
import org.elasticsearch.Version;
2424
import org.elasticsearch.action.ActionListener;
25+
import org.elasticsearch.action.ActionRequestValidationException;
2526
import org.elasticsearch.action.admin.indices.close.CloseIndexClusterStateUpdateRequest;
2627
import org.elasticsearch.action.admin.indices.open.OpenIndexClusterStateUpdateRequest;
2728
import org.elasticsearch.action.support.ActiveShardsObserver;
@@ -175,6 +176,8 @@ public ClusterState execute(ClusterState currentState) {
175176
}
176177
}
177178

179+
validateShardLimit(currentState, request);
180+
178181
if (indicesToOpen.isEmpty()) {
179182
return currentState;
180183
}
@@ -217,4 +220,32 @@ public ClusterState execute(ClusterState currentState) {
217220
});
218221
}
219222

223+
private void validateShardLimit(ClusterState currentState, OpenIndexClusterStateUpdateRequest request) {
224+
int currentOpenShards = currentState.getMetaData().getTotalOpenIndexShards();
225+
int maxShardsInCluster = getMaxAllowedShardCount(currentState);
226+
int shardsToOpen = Arrays.stream(request.indices())
227+
.mapToInt(index -> getTotalShardCount(currentState, index))
228+
.sum();
229+
if ((currentOpenShards + shardsToOpen) > maxShardsInCluster) {
230+
ActionRequestValidationException exception = new ActionRequestValidationException();
231+
String[] indexNames = Arrays.stream(request.indices()).map(Index::getName).toArray(String[]::new);
232+
exception.addValidationError("opening " + Arrays.toString(indexNames)
233+
+ " would open " + shardsToOpen + " total shards, but this cluster currently has "
234+
+ currentOpenShards + "/" + maxShardsInCluster + " maximum shards open");
235+
throw exception;
236+
}
237+
}
238+
239+
240+
private int getMaxAllowedShardCount(ClusterState state) {
241+
int maxShardsPerNode = clusterService.getClusterSettings().get(MetaData.SETTING_CLUSTER_MAX_SHARDS_PER_NODE);
242+
int nodeCount = state.getNodes().getDataNodes().size();
243+
return maxShardsPerNode * nodeCount;
244+
}
245+
246+
private int getTotalShardCount(ClusterState state, Index index) {
247+
IndexMetaData indexMetaData = state.metaData().index(index);
248+
return indexMetaData.getNumberOfShards() * (1 + indexMetaData.getNumberOfReplicas());
249+
}
250+
220251
}

server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ public void apply(Settings value, Settings current, Settings previous) {
189189
MappingUpdatedAction.INDICES_MAPPING_DYNAMIC_TIMEOUT_SETTING,
190190
MetaData.SETTING_READ_ONLY_SETTING,
191191
MetaData.SETTING_READ_ONLY_ALLOW_DELETE_SETTING,
192+
MetaData.SETTING_CLUSTER_MAX_SHARDS_PER_NODE,
192193
RecoverySettings.INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING,
193194
RecoverySettings.INDICES_RECOVERY_RETRY_DELAY_STATE_SYNC_SETTING,
194195
RecoverySettings.INDICES_RECOVERY_RETRY_DELAY_NETWORK_SETTING,

server/src/main/java/org/elasticsearch/snapshots/RestoreService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ public ClusterState execute(ClusterState currentState) {
268268
// Index doesn't exist - create it and start recovery
269269
// Make sure that the index we are about to create has a validate name
270270
MetaDataCreateIndexService.validateIndexName(renamedIndexName, currentState);
271-
createIndexService.validateIndexSettings(renamedIndexName, snapshotIndexMetaData.getSettings());
271+
createIndexService.validateIndexSettings(renamedIndexName, snapshotIndexMetaData.getSettings(), currentState);
272272
IndexMetaData.Builder indexMdBuilder = IndexMetaData.builder(snapshotIndexMetaData).state(IndexMetaData.State.OPEN).index(renamedIndexName);
273273
indexMdBuilder.settings(Settings.builder().put(snapshotIndexMetaData.getSettings()).put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()));
274274
if (!request.includeAliases() && !snapshotIndexMetaData.getAliases().isEmpty()) {

0 commit comments

Comments
 (0)