blob: a6ab93e3b03ce0cff015ff30a475434fd5350755 [file] [log] [blame]
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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.
#ifndef SCHEDULING_SIMPLE_SCHEDULER_H
#define SCHEDULING_SIMPLE_SCHEDULER_H
#include <gtest/gtest_prod.h> // for FRIEND_TEST
#include <vector>
#include <string>
#include <list>
#include <boost/heap/binomial_heap.hpp>
#include <boost/unordered_map.hpp>
#include <boost/thread/mutex.hpp>
#include "common/status.h"
#include "scheduling/scheduler.h"
#include "statestore/statestore-subscriber.h"
#include "statestore/statestore.h"
#include "util/metrics.h"
#include "util/runtime-profile.h"
#include "scheduling/admission-controller.h"
#include "gen-cpp/Types_types.h" // for TNetworkAddress
#include "gen-cpp/ResourceBrokerService_types.h"
#include "rapidjson/rapidjson.h"
namespace impala {
class ResourceBroker;
class Coordinator;
class SchedulerWrapper;
/// Performs simple scheduling by matching between a list of backends configured
/// either from the statestore, or from a static list of addresses, and a list
/// of target data locations. The current set of backends is stored in backend_config_.
/// When receiving changes to the backend configuration from the statestore we will make a
/// copy of this configuration, apply the updates to the copy and atomically swap the
/// contents of the backend_config_ pointer.
///
/// TODO: Notice when there are duplicate statestore registrations (IMPALA-23)
/// TODO: Track assignments (assignment_ctx in ComputeScanRangeAssignment) per query
/// instead of per plan node?
/// TODO: Remove disable_cached_reads query option in CDH6
/// TODO: Replace the usage of shared_ptr with atomic_shared_ptr once compilers support
/// it. Alternatively consider using Kudu's rw locks.
/// TODO: Inject global dependencies into the class (for example ExecEnv::GetInstance(),
/// RNG used during scheduling, FLAGS_*)
/// to make it testable.
/// TODO: Benchmark the performance of the scheduler. The tests need to include setups
/// with:
/// - Small and large number of backends.
/// - Small and large query plans.
/// - Scheduling query plans with concurrent updates to the internal backend
/// configuration.
class SimpleScheduler : public Scheduler {
public:
static const std::string IMPALA_MEMBERSHIP_TOPIC;
/// Initialize with a subscription manager that we can register with for updates to the
/// set of available backends.
/// - backend_id - unique identifier for this Impala backend (usually a host:port)
/// - backend_address - the address that this backend listens on
SimpleScheduler(StatestoreSubscriber* subscriber, const std::string& backend_id,
const TNetworkAddress& backend_address, MetricGroup* metrics, Webserver* webserver,
ResourceBroker* resource_broker, RequestPoolService* request_pool_service);
/// Initialize with a list of <host:port> pairs in 'static' mode - i.e. the set of
/// backends is fixed and will not be updated.
SimpleScheduler(const std::vector<TNetworkAddress>& backends, MetricGroup* metrics,
Webserver* webserver, ResourceBroker* resource_broker,
RequestPoolService* request_pool_service);
/// Register with the subscription manager if required
virtual impala::Status Init();
virtual Status Schedule(Coordinator* coord, QuerySchedule* schedule);
virtual Status Release(QuerySchedule* schedule);
virtual void HandlePreemptedReservation(const TUniqueId& reservation_id);
virtual void HandlePreemptedResource(const TUniqueId& client_resource_id);
virtual void HandleLostResource(const TUniqueId& client_resource_id);
private:
/// Type to store hostnames, which can be rfc1123 hostnames or IPv4 addresses.
typedef std::string Hostname;
/// Type to store IPv4 addresses.
typedef std::string IpAddr;
typedef std::list<TBackendDescriptor> BackendList;
/// Map from a host's IP address to a list of backends running on that node.
typedef boost::unordered_map<IpAddr, BackendList> BackendMap;
/// Map from a host's IP address to the next backend to be round-robin scheduled for
/// that host (needed for setups with multiple backends on a single host)
typedef boost::unordered_map<IpAddr, BackendList::const_iterator> NextBackendPerHost;
/// Map from a hostname to its IP address to support hostname based backend lookup.
typedef boost::unordered_map<Hostname, IpAddr> BackendIpAddressMap;
/// Configuration class to store a list of backends per IP address and a mapping from
/// hostnames to IP addresses. backend_ip_map contains entries for all backends in
/// backend_map and needs to be updated whenever backend_map changes. Each plan node
/// creates a read-only copy of the scheduler's current backend_config_ to use during
/// scheduling.
class BackendConfig {
public:
BackendConfig() {}
/// Construct config from list of backends.
BackendConfig(const std::vector<TNetworkAddress>& backends);
void AddBackend(const TBackendDescriptor& be_desc);
void RemoveBackend(const TBackendDescriptor& be_desc);
/// Look up the IP address of 'hostname' in the internal backend maps and return
/// whether the lookup was successful. If 'hostname' itself is a valid IP address then
/// it is copied to 'ip' and true is returned. 'ip' can be NULL if the caller only
/// wants to check whether the lookup succeeds. Use this method to resolve datanode
/// hostnames to IP addresses during scheduling, to prevent blocking on the OS.
bool LookUpBackendIp(const Hostname& hostname, IpAddr* ip) const;
int NumBackends() const { return backend_map().size(); }
const BackendMap& backend_map() const { return backend_map_; }
const BackendIpAddressMap& backend_ip_map() const { return backend_ip_map_; }
private:
BackendMap backend_map_;
BackendIpAddressMap backend_ip_map_;
};
typedef std::shared_ptr<const BackendConfig> BackendConfigPtr;
/// Internal structure to track scan range assignments for a backend host. This struct
/// is used as the heap element in and maintained by AddressableAssignmentHeap.
struct BackendAssignmentInfo {
/// The number of bytes assigned to a backend host.
int64_t assigned_bytes;
/// Each host gets assigned a random rank to break ties in a random but deterministic
/// order per plan node.
const int random_rank;
/// IP address of the backend.
IpAddr ip;
/// Compare two elements of this struct. The key is (assigned_bytes, random_rank).
bool operator>(const BackendAssignmentInfo& rhs) const {
if (assigned_bytes != rhs.assigned_bytes) {
return assigned_bytes > rhs.assigned_bytes;
}
return random_rank > rhs.random_rank;
}
};
/// Heap to compute candidates for scan range assignments. Elements are of type
/// BackendAssignmentInfo and track assignment information for each backend. By default
/// boost implements a max-heap so we use std::greater<T> to obtain a min-heap. This
/// will make the top() element of the heap be the backend with the lowest number of
/// assigned bytes and the lowest random rank.
typedef boost::heap::binomial_heap<BackendAssignmentInfo,
boost::heap::compare<std::greater<BackendAssignmentInfo>>> AssignmentHeap;
/// Map to look up handles to heap elements to modify heap element keys.
typedef boost::unordered_map<IpAddr, AssignmentHeap::handle_type> BackendHandleMap;
/// Class to store backend information in an addressable heap. In addition to
/// AssignmentHeap it can be used to look up heap elements by their IP address and
/// update their key. For each plan node we create a new heap, so they are not shared
/// between concurrent invocations of the scheduler.
class AddressableAssignmentHeap {
public:
const AssignmentHeap& backend_heap() const { return backend_heap_; }
const BackendHandleMap& backend_handles() const { return backend_handles_; }
void InsertOrUpdate(const IpAddr& ip, int64_t assigned_bytes, int rank);
// Forward interface for boost::heap
decltype(auto) size() const { return backend_heap_.size(); }
decltype(auto) top() const { return backend_heap_.top(); }
// Forward interface for boost::unordered_map
decltype(auto) find(const IpAddr& ip) const { return backend_handles_.find(ip); }
decltype(auto) end() const { return backend_handles_.end(); }
private:
// Heap to determine next backend.
AssignmentHeap backend_heap_;
// Maps backend IPs to handles in the heap.
BackendHandleMap backend_handles_;
};
/// Class to store context information on assignments during scheduling. It is
/// initialized with a copy of the global backend information and assigns a random rank
/// to each backend to break ties in cases where multiple backends have been assigned
/// the same number or bytes. It tracks the number of assigned bytes, which backends
/// have already been used, etc. Objects of this class are created in
/// ComputeScanRangeAssignment() and thus don't need to be thread safe.
class AssignmentCtx {
public:
AssignmentCtx(const BackendConfig& backend_config, IntCounter* total_assignments,
IntCounter* total_local_assignments);
/// Among hosts in 'data_locations', select the one with the minimum number of
/// assigned bytes. If backends have been assigned equal amounts of work and
/// 'break_ties_by_rank' is true, then the backend rank is used to break ties.
/// Otherwise the first backend according to their order in 'data_locations' is
/// selected.
const IpAddr* SelectLocalBackendHost(const std::vector<IpAddr>& data_locations,
bool break_ties_by_rank);
/// Select a backend host for a remote read. If there are unused backend hosts, then
/// those will be preferred. Otherwise the one with the lowest number of assigned
/// bytes is picked. If backends have been assigned equal amounts of work, then the
/// backend rank is used to break ties.
const IpAddr* SelectRemoteBackendHost();
/// Return the next backend that has not been assigned to. This assumes that a
/// returned backend will also be assigned to. The caller must make sure that
/// HasUnusedBackends() is true.
const IpAddr* GetNextUnusedBackendAndIncrement();
/// Pick a backend in round-robin fashion from multiple backends on a single host.
void SelectBackendOnHost(const IpAddr& backend_ip, TBackendDescriptor* backend);
/// Build a new TScanRangeParams object and append it to the assignment list for the
/// tuple (backend, node_id) in 'assignment'. Also, update assignment_heap_ and
/// assignment_byte_counters_, increase the counters 'total_assignments_' and
/// 'total_local_assignments_'. 'scan_range_locations' contains information about the
/// scan range and its replica locations.
void RecordScanRangeAssignment(const TBackendDescriptor& backend, PlanNodeId node_id,
const vector<TNetworkAddress>& host_list,
const TScanRangeLocations& scan_range_locations,
FragmentScanRangeAssignment* assignment);
const BackendConfig& backend_config() const { return backend_config_; }
const BackendMap& backend_map() const { return backend_config_.backend_map(); }
/// Print the assignment and statistics to VLOG_FILE.
void PrintAssignment(const FragmentScanRangeAssignment& assignment);
private:
/// A struct to track various counts of assigned bytes during scheduling.
struct AssignmentByteCounters {
int64_t remote_bytes = 0;
int64_t local_bytes = 0;
int64_t cached_bytes = 0;
};
/// Used to look up hostnames to IP addresses and IP addresses to backend.
const BackendConfig& backend_config_;
// Addressable heap to select remote backends from. Elements are ordered by the number
// of already assigned bytes (and a random rank to break ties).
AddressableAssignmentHeap assignment_heap_;
/// Store a random rank per backend host to break ties between otherwise equivalent
/// replicas (e.g., those having the same number of assigned bytes).
boost::unordered_map<IpAddr, int> random_backend_rank_;
// Index into random_backend_order. It points to the first unused backend and is used
// to select unused backends and inserting them into the assignment_heap_.
int first_unused_backend_idx_;
/// Store a random permutation of backend hosts to select backends from.
std::vector<const BackendMap::value_type*> random_backend_order_;
/// Track round robin information per backend host.
NextBackendPerHost next_backend_per_host_;
/// Track number of assigned bytes that have been read from cache, locally, or
/// remotely.
AssignmentByteCounters assignment_byte_counters_;
/// Pointers to the scheduler's counters.
IntCounter* total_assignments_;
IntCounter* total_local_assignments_;
/// Return whether there are backends that have not been assigned a scan range.
bool HasUnusedBackends() const;
/// Return the rank of a backend.
int GetBackendRank(const IpAddr& ip) const;
};
/// The scheduler's backend configuration. When receiving changes to the backend
/// configuration from the statestore we will make a copy of the stored object, apply
/// the updates to the copy and atomically swap the contents of this pointer.
BackendConfigPtr backend_config_;
/// Protect access to backend_config_ which might otherwise be updated asynchronously
/// with respect to reads.
mutable boost::mutex backend_config_lock_;
/// Total number of scan ranges assigned to backends during the lifetime of the
/// scheduler.
int64_t num_assignments_;
/// Map from unique backend id to TBackendDescriptor. Used to track the known backends
/// from the statestore. It's important to track both the backend ID as well as the
/// TBackendDescriptor so we know what is being removed in a given update.
/// Locking of this map is not needed since it should only be read/modified from
/// within the UpdateMembership() function.
typedef boost::unordered_map<std::string, TBackendDescriptor> BackendIdMap;
BackendIdMap current_membership_;
/// MetricGroup subsystem access
MetricGroup* metrics_;
/// Webserver for /backends. Not owned by us.
Webserver* webserver_;
/// Pointer to a subscription manager (which we do not own) which is used to register
/// for dynamic updates to the set of available backends. May be NULL if the set of
/// backends is fixed.
StatestoreSubscriber* statestore_subscriber_;
/// Unique - across the cluster - identifier for this impala backend.
const std::string local_backend_id_;
/// Describe this backend, including the Impalad service address.
TBackendDescriptor local_backend_descriptor_;
ThriftSerializer thrift_serializer_;
/// Locality metrics
IntCounter* total_assignments_;
IntCounter* total_local_assignments_;
/// Initialization metric
BooleanProperty* initialized_;
/// Current number of backends
IntGauge* num_fragment_instances_metric_;
/// Protect active_reservations_ and active_client_resources_.
boost::mutex active_resources_lock_;
/// Map from a Llama reservation id to the coordinator of the query using that
/// reservation. The map is used to cancel queries whose reservation has been preempted.
/// Entries are added in Schedule() calls that result in granted resource allocations.
/// Entries are removed in Release().
typedef boost::unordered_map<TUniqueId, Coordinator*> ActiveReservationsMap;
ActiveReservationsMap active_reservations_;
/// Map from client resource id to the coordinator of the query using that resource.
/// The map is used to cancel queries whose resource(s) have been preempted.
/// Entries are added in Schedule() calls that result in granted resource allocations.
/// Entries are removed in Release().
typedef boost::unordered_map<TUniqueId, Coordinator*> ActiveClientResourcesMap;
ActiveClientResourcesMap active_client_resources_;
/// Resource broker that mediates resource requests between Impala and the Llama.
/// Set to NULL if resource management is disabled.
ResourceBroker* resource_broker_;
/// Used for user-to-pool resolution and looking up pool configurations. Not owned by
/// us.
RequestPoolService* request_pool_service_;
/// Used to make admission decisions in 'Schedule()'
boost::scoped_ptr<AdmissionController> admission_controller_;
/// Helper methods to access backend_config_ (the shared_ptr, not its contents),
/// protecting the access with backend_config_lock_.
BackendConfigPtr GetBackendConfig() const;
void SetBackendConfig(const BackendConfigPtr& backend_config);
/// Return a list of all backends registered with the scheduler.
void GetAllKnownBackends(BackendList* backends);
/// Add the granted reservation and resources to the active_reservations_ and
/// active_client_resources_ maps, respectively.
void AddToActiveResourceMaps(
const TResourceBrokerReservationResponse& reservation, Coordinator* coord);
/// Remove the given reservation and resources from the active_reservations_ and
/// active_client_resources_ maps, respectively.
void RemoveFromActiveResourceMaps(
const TResourceBrokerReservationResponse& reservation);
/// Called asynchronously when an update is received from the subscription manager
void UpdateMembership(const StatestoreSubscriber::TopicDeltaMap& incoming_topic_deltas,
std::vector<TTopicDelta>* subscriber_topic_updates);
/// Webserver callback that produces a list of known backends.
/// Example output:
/// "backends": [
/// "henry-metrics-pkg-cdh5.ent.cloudera.com:22000"
/// ],
void BackendsUrlCallback(const Webserver::ArgumentMap& args,
rapidjson::Document* document);
/// Determine the pool for a user and query options via request_pool_service_.
Status GetRequestPool(const std::string& user, const TQueryOptions& query_options,
std::string* pool) const;
/// Compute the assignment of scan ranges to hosts for each scan node in 'schedule'.
/// Unpartitioned fragments are assigned to the coordinator. Populate the schedule's
/// fragment_exec_params_ with the resulting scan range assignment.
Status ComputeScanRangeAssignment(const TQueryExecRequest& exec_request,
QuerySchedule* schedule);
/// Process the list of scan ranges of a single plan node and compute scan range
/// assignments (returned in 'assignment'). The result is a mapping from hosts to their
/// assigned scan ranges per plan node.
///
/// If exec_at_coord is true, all scan ranges will be assigned to the coordinator host.
/// Otherwise the assignment is computed for each scan range as follows:
///
/// Scan ranges refer to data, which is usually replicated on multiple hosts. All scan
/// ranges where one of the replica hosts also runs an impala backend are processed
/// first. If more than one of the replicas run an impala backend, then the 'memory
/// distance' of each backend is considered. The concept of memory distance reflects the
/// cost of moving data into the processing backend's main memory. Reading from cached
/// replicas is generally considered less costly than reading from a local disk, which
/// in turn is cheaper than reading data from a remote node. If multiple backends of the
/// same memory distance are found, then the one with the least amount of previously
/// assigned work is picked, thus aiming to distribute the work as evenly as possible.
///
/// Finally, scan ranges are considered which do not have an impalad backend running on
/// any of their data nodes. They will be load-balanced by assigned bytes across all
/// backends
///
/// The resulting assignment is influenced by the following query options:
///
/// replica_preference:
/// This value is used as a minimum memory distance for all replicas. For example, by
/// setting this to DISK_LOCAL, all cached replicas will be treated as if they were
/// not cached, but local disk replicas. This can help prevent hot-spots by spreading
/// the assignments over more replicas. Allowed values are CACHE_LOCAL (default),
/// DISK_LOCAL and REMOTE.
///
/// disable_cached_reads:
/// Setting this value to true is equivalent to setting replica_preference to
/// DISK_LOCAL and takes precedence over replica_preference. The default setting is
/// false.
///
/// schedule_random_replica:
/// When equivalent backends with a memory distance of DISK_LOCAL are found for a scan
/// range (same memory distance, same amount of assigned work), then the first one
/// will be picked deterministically. This aims to make better use of OS buffer
/// caches, but can lead to performance bottlenecks on individual hosts. Setting this
/// option to true will randomly change the order in which equivalent replicas are
/// picked for different plan nodes. This helps to compute a more even assignment,
/// with the downside being an increased memory usage for OS buffer caches. The
/// default setting is false. Selection between equivalent replicas with memory
/// distance of CACHE_LOCAL or REMOTE happens based on a random order.
///
/// The method takes the following parameters:
///
/// backend_config: Backend configuration to use for scheduling.
/// node_id: ID of the plan node.
/// node_replica_preference: Query hint equivalent to replica_preference.
/// node_random_replica: Query hint equivalent to schedule_random_replica.
/// locations: List of scan ranges to be assigned to backends.
/// host_list: List of hosts, into which 'locations' will index.
/// exec_at_coord: Whether to schedule all scan ranges on the coordinator.
/// query_options: Query options for the current query.
/// timer: Tracks execution time of ComputeScanRangeAssignment.
/// assignment: Output parameter, to which new assignments will be added.
Status ComputeScanRangeAssignment(const BackendConfig& backend_config,
PlanNodeId node_id, const TReplicaPreference::type* node_replica_preference,
bool node_random_replica, const std::vector<TScanRangeLocations>& locations,
const std::vector<TNetworkAddress>& host_list, bool exec_at_coord,
const TQueryOptions& query_options, RuntimeProfile::Counter* timer,
FragmentScanRangeAssignment* assignment);
/// Populate fragment_exec_params_ in schedule.
void ComputeFragmentExecParams(const TQueryExecRequest& exec_request,
QuerySchedule* schedule);
/// For each fragment in exec_request, compute the hosts on which to run the instances
/// and stores result in fragment_exec_params_.hosts.
void ComputeFragmentHosts(const TQueryExecRequest& exec_request,
QuerySchedule* schedule);
/// Return the id of the leftmost node of any of the given types in 'plan', or
/// INVALID_PLAN_NODE_ID if no such node present.
PlanNodeId FindLeftmostNode(
const TPlan& plan, const std::vector<TPlanNodeType::type>& types);
/// Return the index (w/in exec_request.fragments) of fragment that sends its output to
/// exec_request.fragment[fragment_idx]'s leftmost ExchangeNode.
/// Return INVALID_PLAN_NODE_ID if the leftmost node is not an exchange node.
int FindLeftmostInputFragment(int fragment_idx, const TQueryExecRequest& exec_request);
/// Add all hosts the given scan is executed on to scan_hosts.
void GetScanHosts(TPlanNodeId scan_id, const TQueryExecRequest& exec_request,
const FragmentExecParams& params, std::vector<TNetworkAddress>* scan_hosts);
/// Return true if 'plan' contains a node of the given type.
bool ContainsNode(const TPlan& plan, TPlanNodeType::type type);
/// Return all ids of nodes in 'plan' of any of the given types.
void FindNodes(const TPlan& plan, const std::vector<TPlanNodeType::type>& types,
std::vector<TPlanNodeId>* results);
/// Returns the index (w/in exec_request.fragments) of fragment that sends its output
/// to the given exchange in the given fragment index.
int FindSenderFragment(TPlanNodeId exch_id, int fragment_idx,
const TQueryExecRequest& exec_request);
/// Deterministically resolve a host to one of its IP addresses. This method will call
/// into the OS, so it can take a long time to return. Use this method to resolve
/// hostnames during initialization and while processing statestore updates.
static Status HostnameToIpAddr(const Hostname& hostname, IpAddr* ip);
friend class impala::SchedulerWrapper;
FRIEND_TEST(SimpleAssignmentTest, ComputeAssignmentDeterministicNonCached);
FRIEND_TEST(SimpleAssignmentTest, ComputeAssignmentRandomNonCached);
FRIEND_TEST(SimpleAssignmentTest, ComputeAssignmentRandomDiskLocal);
FRIEND_TEST(SimpleAssignmentTest, ComputeAssignmentRandomRemote);
};
}
#endif