All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.cluster.routing.allocation.DataTier Maven / Gradle / Ivy

There is a newer version: 8.13.4
Show newest version
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

package org.elasticsearch.cluster.routing.allocation;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexModule;
import org.elasticsearch.index.shard.IndexSettingProvider;
import org.elasticsearch.snapshots.SearchableSnapshotsSettings;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * The {@code DataTier} class encapsulates the formalization of the "content",
 * "hot", "warm", and "cold" tiers as node roles. In contains the
 * roles themselves as well as helpers for validation and determining if a node
 * has a tier configured.
 */
public class DataTier {

    public static final String DATA_CONTENT = "data_content";
    public static final String DATA_HOT = "data_hot";
    public static final String DATA_WARM = "data_warm";
    public static final String DATA_COLD = "data_cold";
    public static final String DATA_FROZEN = "data_frozen";

    public static final Set ALL_DATA_TIERS = org.elasticsearch.core.Set.of(
        DATA_CONTENT,
        DATA_HOT,
        DATA_WARM,
        DATA_COLD,
        DATA_FROZEN
    );

    // this setting is for migrating from 7.x (where a tier preference was not required, and did not necessarily
    // have a default value), to 8.x (where a tier preference will be required, and a default value will be injected).
    // it will be removed as a breaking change in some future version, likely 9.0.
    public static final String ENFORCE_DEFAULT_TIER_PREFERENCE = "cluster.routing.allocation.enforce_default_tier_preference";
    public static final Setting ENFORCE_DEFAULT_TIER_PREFERENCE_SETTING = Setting.boolSetting(
        ENFORCE_DEFAULT_TIER_PREFERENCE,
        false,
        Property.Dynamic,
        Property.NodeScope
    );

    public static final String TIER_PREFERENCE = "index.routing.allocation.include._tier_preference";

    public static final Setting.Validator DATA_TIER_SETTING_VALIDATOR = new DataTierSettingValidator();

    private static final Settings DATA_CONTENT_TIER_PREFERENCE_SETTINGS = Settings.builder().put(TIER_PREFERENCE, DATA_CONTENT).build();

    private static final Settings DATA_HOT_TIER_PREFERENCE_SETTINGS = Settings.builder().put(TIER_PREFERENCE, DATA_HOT).build();

    private static final Settings NULL_TIER_PREFERENCE_SETTINGS = Settings.builder().putNull(TIER_PREFERENCE).build();

    public static final Setting TIER_PREFERENCE_SETTING = new Setting<>(
        new Setting.SimpleKey(TIER_PREFERENCE),
        DataTierSettingValidator::getDefaultTierPreference,
        Function.identity(),
        DATA_TIER_SETTING_VALIDATOR,
        Property.Dynamic,
        Property.IndexScope
    );

    static {
        for (String tier : ALL_DATA_TIERS) {
            assert tier.equals(DATA_FROZEN) || tier.contains(DATA_FROZEN) == false
                : "can't have two tier names containing ["
                    + DATA_FROZEN
                    + "] because it would break setting validation optimizations"
                    + " in the data tier allocation decider";
        }
    }

    // Represents an ordered list of data tiers from frozen to hot (or slow to fast)
    private static final List ORDERED_FROZEN_TO_HOT_TIERS = org.elasticsearch.core.List.of(
        DATA_FROZEN,
        DATA_COLD,
        DATA_WARM,
        DATA_HOT
    );

    private static final Map PREFERENCE_TIER_CONFIGURATIONS;

    private static final Map PREFERENCE_TIER_CONFIGURATION_SETTINGS;

    static {
        final Map tmp = new HashMap<>();
        final Map tmpSettings = new HashMap<>();
        for (int i = 0, ordered_frozen_to_hot_tiersSize = ORDERED_FROZEN_TO_HOT_TIERS.size(); i < ordered_frozen_to_hot_tiersSize; i++) {
            String tier = ORDERED_FROZEN_TO_HOT_TIERS.get(i);
            final String prefTierString = String.join(",", ORDERED_FROZEN_TO_HOT_TIERS.subList(i, ORDERED_FROZEN_TO_HOT_TIERS.size()))
                .intern();
            tmp.put(tier, prefTierString);
            tmpSettings.put(tier, Settings.builder().put(DataTier.TIER_PREFERENCE, prefTierString).build());
        }
        PREFERENCE_TIER_CONFIGURATIONS = org.elasticsearch.core.Map.copyOf(tmp);
        PREFERENCE_TIER_CONFIGURATION_SETTINGS = org.elasticsearch.core.Map.copyOf(tmpSettings);
    }

    /**
     * Returns true if the given tier name is a valid tier
     */
    public static boolean validTierName(String tierName) {
        return ALL_DATA_TIERS.contains(tierName);
    }

    /**
     * Based on the provided target tier it will return a comma separated list of preferred tiers.
     * ie. if `data_cold` is the target tier, it will return `data_cold,data_warm,data_hot`.
     * This is usually used in conjunction with {@link #TIER_PREFERENCE_SETTING}.
     */
    public static String getPreferredTiersConfiguration(String targetTier) {
        final String res = PREFERENCE_TIER_CONFIGURATIONS.get(targetTier);
        if (res == null) {
            throw new IllegalArgumentException("invalid data tier [" + targetTier + "]");
        }
        return res;
    }

    public static Settings getPreferredTiersConfigurationSettings(String targetTier) {
        final Settings res = PREFERENCE_TIER_CONFIGURATION_SETTINGS.get(targetTier);
        if (res == null) {
            throw new IllegalArgumentException("invalid data tier [" + targetTier + "]");
        }
        return res;
    }

    /**
     * Returns true iff the given settings have a data tier setting configured
     */
    public static boolean isExplicitDataTier(Settings settings) {
        /*
         * This method can be called before the o.e.n.NodeRoleSettings.NODE_ROLES_SETTING is
         * initialized. We do not want to trigger initialization prematurely because that will bake
         *  the default roles before plugins have had a chance to register them. Therefore,
         * to avoid initializing this setting prematurely, we avoid using the actual node roles
         * setting instance here in favor of the string.
         */
        if (settings.hasValue("node.roles")) {
            return settings.getAsList("node.roles").stream().anyMatch(DataTier::validTierName);
        }
        return false;
    }

    public static boolean isContentNode(DiscoveryNode discoveryNode) {
        return discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_CONTENT_NODE_ROLE)
            || discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_ROLE);
    }

    public static boolean isHotNode(DiscoveryNode discoveryNode) {
        return discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_HOT_NODE_ROLE)
            || discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_ROLE);
    }

    public static boolean isWarmNode(DiscoveryNode discoveryNode) {
        return discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_WARM_NODE_ROLE)
            || discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_ROLE);
    }

    public static boolean isColdNode(DiscoveryNode discoveryNode) {
        return discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_COLD_NODE_ROLE)
            || discoveryNode.getRoles().contains(DiscoveryNodeRole.DATA_ROLE);
    }

    public static boolean isFrozenNode(DiscoveryNode discoveryNode) {
        return isFrozenNode(discoveryNode.getRoles());
    }

    public static boolean isFrozenNode(final Set roles) {
        return roles.contains(DiscoveryNodeRole.DATA_FROZEN_NODE_ROLE) || roles.contains(DiscoveryNodeRole.DATA_ROLE);
    }

    public static List parseTierList(String tiers) {
        if (Strings.hasText(tiers) == false) {
            // avoid parsing overhead in the null/empty string case
            return org.elasticsearch.core.List.of();
        } else {
            return org.elasticsearch.core.List.of(tiers.split(","));
        }
    }

    /**
     * Compares the provided tiers for coldness order (eg. warm is colder than hot).
     *
     * Similar to {@link java.util.Comparator#compare(Object, Object)} returns
     *   -1 if tier1 is colder than tier2 (ie. compare("data_cold", "data_hot"))
     *   0 if tier1 is as cold as tier2 (ie. tier1.equals(tier2) )
     *   1 if tier1 is warmer than tier2 (ie. compare("data_hot", "data_cold"))
     *
     * The provided tiers parameters must be valid data tiers values (ie. {@link #ALL_DATA_TIERS}.
     * NOTE: `data_content` is treated as "equal to data_hot" in the tiers hierarchy.
     * If invalid tier names are passed the result is non-deterministic.
     */
    public static int compare(String tier1, String tier2) {
        if (tier1.equals(DATA_CONTENT)) {
            tier1 = DATA_HOT;
        }
        if (tier2.equals(DATA_CONTENT)) {
            tier2 = DATA_HOT;
        }
        int indexOfTier1 = ORDERED_FROZEN_TO_HOT_TIERS.indexOf(tier1);
        assert indexOfTier1 >= 0 : "expecting a valid tier to compare but got:" + tier1;
        int indexOfTier2 = ORDERED_FROZEN_TO_HOT_TIERS.indexOf(tier2);
        assert indexOfTier2 >= 0 : "expecting a valid tier to compare but got:" + tier2;

        if (indexOfTier1 == indexOfTier2) {
            return 0;
        } else {
            return indexOfTier1 < indexOfTier2 ? -1 : 1;
        }
    }

    /**
     * This setting provider injects the setting allocating all newly created indices with
     * {@code index.routing.allocation.include._tier: "data_hot"} unless the user overrides the
     * setting while the index is being created (in a create index request for instance)
     */
    public static class DefaultHotAllocationSettingProvider implements IndexSettingProvider {
        private static final Logger logger = LogManager.getLogger(DefaultHotAllocationSettingProvider.class);

        @Override
        public Settings getAdditionalIndexSettings(String indexName, boolean isDataStreamIndex, Settings indexSettings) {
            Set settings = indexSettings.keySet();
            if (settings.contains(TIER_PREFERENCE)) {
                // just a marker -- this null value will be removed or overridden by the template/request settings
                return NULL_TIER_PREFERENCE_SETTINGS;
            } else if (settings.stream().anyMatch(s -> s.startsWith(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_PREFIX + "."))
                || settings.stream().anyMatch(s -> s.startsWith(IndexMetadata.INDEX_ROUTING_EXCLUDE_GROUP_PREFIX + "."))
                || settings.stream().anyMatch(s -> s.startsWith(IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_PREFIX + "."))) {
                    // A different index level require, include, or exclude has been specified, so don't put the setting
                    logger.debug("index [{}] specifies custom index level routing filtering, skipping tier allocation", indexName);
                    return Settings.EMPTY;
                } else {
                    // Otherwise, put the setting in place by default, the "hot"
                    // tier if the index is part of a data stream, the "content"
                    // tier if it is not.
                    if (isDataStreamIndex) {
                        return DATA_HOT_TIER_PREFERENCE_SETTINGS;
                    } else {
                        return DATA_CONTENT_TIER_PREFERENCE_SETTINGS;
                    }
                }
        }
    }

    // visible for testing
    static final class DataTierSettingValidator implements Setting.Validator {

        private static final Collection> dependencies = org.elasticsearch.core.List.of(
            IndexModule.INDEX_STORE_TYPE_SETTING,
            SearchableSnapshotsSettings.SNAPSHOT_PARTIAL_SETTING
        );

        public static String getDefaultTierPreference(Settings settings) {
            if (SearchableSnapshotsSettings.isPartialSearchableSnapshotIndex(settings)) {
                return DATA_FROZEN;
            } else {
                return "";
            }
        }

        @Override
        public void validate(String value) {
            if (Strings.hasText(value)) {
                for (String s : parseTierList(value)) {
                    if (validTierName(s) == false) {
                        throw new IllegalArgumentException(
                            "invalid tier names found in [" + value + "] allowed values are " + ALL_DATA_TIERS
                        );
                    }
                }
            }
        }

        @Override
        public void validate(String value, Map, Object> settings, boolean exists) {
            if (exists && value != null) {
                if (SearchableSnapshotsSettings.isPartialSearchableSnapshotIndex(settings)) {
                    if (value.equals(DATA_FROZEN) == false) {
                        throw new IllegalArgumentException(
                            "only the ["
                                + DATA_FROZEN
                                + "] tier preference may be used for partial searchable snapshots (got: ["
                                + value
                                + "])"
                        );
                    }
                } else {
                    if (value.contains(DATA_FROZEN)) {
                        throw new IllegalArgumentException("[" + DATA_FROZEN + "] tier can only be used for partial searchable snapshots");
                    }
                }
            }
        }

        @Override
        public Iterator> settings() {
            return dependencies.iterator();
        }
    }

    /**
     * Checks each data node in the cluster state to see whether it has the explicit data role or if it has
     * all data tiers (e.g. 'data_hot', 'data_warm', etc). The former condition being treated as a shortcut
     * for the latter condition (see DataTierAllocationDecider#allocationAllowed(String, Set)) for details,
     * as well as the various DataTier#isFooNode(DiscoveryNode) methods.
     *
     * @param clusterState the cluster state
     * @return a set of data nodes that do not have all data roles
     */
    public static Set dataNodesWithoutAllDataRoles(ClusterState clusterState) {
        return clusterState.getNodes().getDataNodes().values().stream().filter(node -> {
            Set roles = node.getRoles().stream().map(DiscoveryNodeRole::roleName).collect(Collectors.toSet());

            return roles.contains(DiscoveryNodeRole.DATA_ROLE.roleName()) == false && roles.containsAll(ALL_DATA_TIERS) == false;
        }).collect(Collectors.toSet());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy