Skip to content

Commit 4e4d100

Browse files
Keshav Singhdahlerlend
authored andcommitted
WL#15829: Selective offload project changes
This worklog introduces dynamic offload of Queries to RAPID in following ways: When system variable rapid_use_dynamic_offload is 0/false , then we fall back to normal cost threshold classifier, which also implies that when use secondary engine is set to forced, eligible queries will go to secondary engine, regardless of cost threshold or this classifier. When rapid_use_dynamic_offload is 1/true, then we proceed with looking for optimal execution engine for this queries, if secondary engine is found more optimal, then query is offloaded, otherwise it is sent back to mysql. This is handled in following scenarios: 1. Static Scenario: When there's no Change propagation or Queue on RAPID side, this introduces decision tree which has > 85 % precision in training which queries should be faster on mysql or which queries should be faster on mysql, and accepts or rejects queries. the decision tree takes around 20-100 microseconds for fast queries, hence minimal overhead, for bigger queries this introduces overhead of upto maximum observed 700 microseconds, these end up with long execution time, hence not a problem. For very fast queries, defined here by having cost < 10 and of the form point select, dynamic offload is not applied, since 100 % of these queries (out of 16667 samples) are faster on MySQL. Additionally, routing these "very fast queries" through dynamic offload leads to performance regressions due to 3 phase optimisation. 2. Dynamic Scenario: When there's CP or queuing on RAPID, this worklog introduces dynamic feature normalization to factor into account extra catch up time RAPID needs, and factoring in that, attempts to verify if RAPID is still the best engine for execution. If queue is too long or CP is too long, this mechanism wants to progressively start shifting queries to mysql, moving gradually towards the heavier queries The steps in this worklog with respect to query lifecycle in server with secondary_engine = ON, are described below: query | Primary Tentatively optimisation -> mysql optimises for Innodb | secondary_engine_pre_prepare_hook -> following Rapid function called: | RapidCachePrimaryInfoAtPrimaryTentativelyStep | If dynamic offload is enabled and query is not "very fast": | This caches features from mysql plan in rapid_statement_context | to be used for dynamic offload. | If dynamic offload is disabled or the query is "very fast": | This function invokes standary mysql cost threshold classifier, | which decides if query needs further RAPID optimisation. | | |-> if returns False, then query proceeds to Innodb for execution |-> if returns true, step below is called | Secondary optimisation -> mysql optimises for RAPID | prepare_secondary_engine -> following Rapid function is called: | RapidPrepareEstimateQueryCosts | In this function, Dynamic offload combines mysql plan features | retrieved from rapid_statement_context | and RAPID info such as rapid base table cardinality, | dict encoding projection, varlen projection size, rapid queue | size in to decide if query should be offloaded to RAPID. | |->if returns True, then query proceeds to Innodb for execution |->if returns False, step below is called | optimize_secondary_engine -> following Rapid function is called | RapidOptimize | In this function, Dynamic offload retrieves info from | rapid_statement_context and additionally looks at Change | propagation lag to decide if query should be offloaded to rapid | |->if returns True, then query proceeds to Innodb for execution |->if returns False, then query goes to Rapid Execution. Following new MYSQL ERR log messages are printed with this WL, when dynamic offload is enabled, and query is not a "very fast query". 1. SelOffload allow decision 1 : as secondary not forced 1 and enable var value 1 and transactional enabled 1 and( big shape detected 0 or small shape detected 1 ) inno: 10737418240 , rpd: 4294967296 , no lh table: 1 Message such as this shows if dynamic offload is used to classify this query or not. If not, why not, using each of the conditions. 1 = pass, 0 = not pass. 2. myqid=65 Selective offload classifier #1#1#1 f_mysql_total_ts_nrows <= 2105.5 : 0.173916, f_MySQLCost <= 68.3899040222168 : 0.028218, f_count_all_base_tables = 0 , f_count_ref_index_ts = 0 ,f_BaseTableSumNrows <= 278177.5 : 0.173916 are_all_ts_index_ref = true outcome=0 Line such as this serialises what leg of decision tree decided outcome of this query 0 -> back to mysql 1 -> keep on rapid. each leg is uniquely searchable via identifier such as #1#1#1 here. This worklog additionally introduces python scripts to run queries on mysql client with multiple queries and multiple dmls at once, in various modes such as simulator mode and standard benchmark modes. By Default this WL is enabled, but before release it will be disabled. This is tracked via BUG#36343189 #no-close. Perf mode unittests will be enabled on jenkins after this wl. Further cleanup will be done via BUG#36368437 #no-close. Bugs tackled via this WL: BUG#35738194, Enh#34132523, Bug#36343208 Unrelated bugs fixed: BUG#35987975 Old gerrit review : 25567 (abandoned due to 1000 update limit reached) Change-Id: Ie5f9fdcd8b55a669d04b389d3aec5f6b33f0fe2e
1 parent 381f565 commit 4e4d100

File tree

10 files changed

+166
-37
lines changed

10 files changed

+166
-37
lines changed

sql/handler.h

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2580,9 +2580,13 @@ using se_after_commit_t = void (*)(void *arg);
25802580
using se_before_rollback_t = void (*)(void *arg);
25812581

25822582
/**
2583-
* Notify plugins when a SELECT query was executed. The plugins will be notified
2584-
* only if the query is not considered "fast-running", i.e., its estimated cost
2585-
* is less than the currently configured 'secondary_engine_cost_threshold'.
2583+
Notify plugins when a SELECT query was executed. The plugins will be notified
2584+
only if the query is not considered secondary engine relevant, i.e.:
2585+
1. for a query with missing secondary_engine_statement_ctx, its estimated cost
2586+
is greater than the currently configured 'secondary_engine_cost_threshold'
2587+
2. for queries with secondary_engine_statement_ctx, wherever
2588+
secondary_engine_statement_ctx::is_primary_engine_optimal() returns False
2589+
indicating secondary engine relevance.
25862590
*/
25872591
using notify_after_select_t = void (*)(THD *thd, SelectExecutedIn executed_in);
25882592

@@ -2592,6 +2596,19 @@ using notify_after_select_t = void (*)(THD *thd, SelectExecutedIn executed_in);
25922596
using notify_create_table_t = void (*)(struct HA_CREATE_INFO *create_info,
25932597
const char *db, const char *table_name);
25942598

2599+
/**
2600+
Secondary engine hook called after PRIMARY_TENTATIVELY optimization is
2601+
complete, and decides if secondary engine optimization will be performed, and
2602+
comparison of primary engine cost and secondary engine cost will determine
2603+
which engine to use for execution.
2604+
@param[in] thd current thd.
2605+
@return :
2606+
@retval true When secondary_engine's prepare hook is to be further called
2607+
@retval false When secondary_engine's prepare hook is NOT to be further called
2608+
2609+
*/
2610+
using secondary_engine_pre_prepare_hook_t = bool (*)(THD *thd);
2611+
25952612
/**
25962613
* Notify plugins when a table is dropped.
25972614
*/
@@ -2968,6 +2985,11 @@ struct handlerton {
29682985
secondary_engine_check_optimizer_request_t
29692986
secondary_engine_check_optimizer_request;
29702987

2988+
/* Pointer to a function that is called at the end of the PRIMARY_TENTATIVELY
2989+
* optimization stage, which also decides that the statement should be
2990+
* attempted offloaded to a secondary storage engine. */
2991+
secondary_engine_pre_prepare_hook_t secondary_engine_pre_prepare_hook;
2992+
29712993
se_before_commit_t se_before_commit;
29722994
se_after_commit_t se_after_commit;
29732995
se_before_rollback_t se_before_rollback;

sql/sp_instr.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,9 @@ bool sp_lex_instr::validate_lex_and_execute_core(THD *thd, uint *nextp,
717717
thd->set_secondary_engine_optimization(
718718
Secondary_engine_optimization::PRIMARY_TENTATIVELY);
719719

720+
auto scope_guard = create_scope_guard(
721+
[thd] { thd->set_secondary_engine_statement_context(nullptr); });
722+
720723
while (true) {
721724
DBUG_EXECUTE_IF("simulate_bug18831513", { invalidate(); });
722725
if (is_invalid() || (m_lex->has_udf() && !m_first_execution)) {

sql/sql_class.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,11 @@ void THD::set_transaction(Transaction_ctx *transaction_ctx) {
874874
m_transaction.reset(transaction_ctx);
875875
}
876876

877+
void THD::set_secondary_engine_statement_context(
878+
std::unique_ptr<Secondary_engine_statement_context> context) {
879+
m_secondary_engine_statement_context = std::move(context);
880+
}
881+
877882
bool THD::set_db(const LEX_CSTRING &new_db) {
878883
bool result;
879884
/*
@@ -1434,6 +1439,8 @@ THD::~THD() {
14341439
DBUG_TRACE;
14351440
DBUG_PRINT("info", ("THD dtor, this %p", this));
14361441

1442+
assert(m_secondary_engine_statement_context == nullptr);
1443+
14371444
if (has_incremented_gtid_automatic_count) {
14381445
gtid_state->decrease_gtid_automatic_tagged_count();
14391446
}

sql/sql_class.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,24 @@ using Event_tracking_data =
921921
std::pair<Event_tracking_class, Event_tracking_information *>;
922922
using Event_tracking_data_stack = std::stack<Event_tracking_data>;
923923

924+
/**
925+
Base class for secondary engine statement context objects. Secondary
926+
storage engines may create classes derived from this one which
927+
contain state they need to preserve in lifecycle of this query.
928+
*/
929+
class Secondary_engine_statement_context {
930+
public:
931+
/**
932+
Destructs the secondary engine statement context object. It is
933+
called after the query execution has completed. Secondary engines
934+
may override the destructor in subclasses and add code that
935+
performs cleanup tasks that are needed after query execution.
936+
*/
937+
virtual ~Secondary_engine_statement_context() = default;
938+
939+
virtual bool is_primary_engine_optimal() const { return true; }
940+
};
941+
924942
/**
925943
@class THD
926944
For each client connection we create a separate thread with THD serving as
@@ -1046,6 +1064,12 @@ class THD : public MDL_context_owner,
10461064
*/
10471065
String m_rewritten_query;
10481066

1067+
/**
1068+
Current query's secondary engine statement context.
1069+
*/
1070+
std::unique_ptr<Secondary_engine_statement_context>
1071+
m_secondary_engine_statement_context;
1072+
10491073
public:
10501074
/* Used to execute base64 coded binlog events in MySQL server */
10511075
Relay_log_info *rli_fake;
@@ -1071,6 +1095,18 @@ class THD : public MDL_context_owner,
10711095
*/
10721096
void rpl_detach_engine_ha_data();
10731097

1098+
/*
1099+
Set secondary_engine_statement_context to new context.
1100+
This function assumes existing m_secondary_engine_statement_context is empty,
1101+
such that there's only context throughout the query's lifecycle.
1102+
*/
1103+
void set_secondary_engine_statement_context(
1104+
std::unique_ptr<Secondary_engine_statement_context> context);
1105+
1106+
Secondary_engine_statement_context *secondary_engine_statement_context() {
1107+
return m_secondary_engine_statement_context.get();
1108+
}
1109+
10741110
/**
10751111
When the thread is a binlog or slave applier it reattaches the engine
10761112
ha_data associated with it and memorizes the fact of that.
@@ -1126,6 +1162,7 @@ class THD : public MDL_context_owner,
11261162
@sa system_status_var::last_query_cost
11271163
*/
11281164
double m_current_query_cost;
1165+
11291166
/**
11301167
Current query partial plans.
11311168
@sa system_status_var::last_query_partial_plans

sql/sql_cmd_ddl_table.cc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,7 @@ static bool populate_table(THD *thd, LEX *lex) {
123123

124124
if (unit->execute(thd)) return true;
125125

126-
if (thd->m_current_query_cost >
127-
thd->variables.secondary_engine_cost_threshold) {
128-
notify_plugins_after_select(thd, lex->m_sql_cmd);
129-
}
126+
notify_plugins_after_select(thd, lex->m_sql_cmd);
130127

131128
return false;
132129
}

sql/sql_parse.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2227,6 +2227,7 @@ bool dispatch_command(THD *thd, const COM_DATA *com_data,
22272227

22282228
thd->bind_parameter_values = nullptr;
22292229
thd->bind_parameter_values_count = 0;
2230+
thd->set_secondary_engine_statement_context(nullptr);
22302231

22312232
/* Need to set error to true for graceful shutdown */
22322233
if ((thd->lex->sql_command == SQLCOM_SHUTDOWN) &&

sql/sql_prepare.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,6 +2860,9 @@ bool Prepared_statement::execute_loop(THD *thd, String *expanded_query,
28602860
bool error;
28612861
bool reprepared_for_types [[maybe_unused]] = false;
28622862

2863+
auto scope_guard = create_scope_guard(
2864+
[thd] { thd->set_secondary_engine_statement_context(nullptr); });
2865+
28632866
/* Check if we got an error when sending long data */
28642867
if (m_arena.get_state() == Query_arena::STMT_ERROR) {
28652868
my_message(m_last_errno, m_last_error, MYF(0));

sql/sql_select.cc

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,33 @@ void accumulate_statement_cost(const LEX *lex) {
874874
lex->thd->m_current_query_cost = total_cost;
875875
}
876876

877+
namespace {
878+
/**
879+
Gets the secondary storage engine pre prepare hook function, if any. If no
880+
hook is found, this function returns false. If hook function is found, it
881+
returns the return value of the hook. Please refer to
882+
secondary_engine_pre_prepare_hook_t definition for description of its return
883+
value.
884+
*/
885+
bool SecondaryEngineCallPrePrepareHook(THD *thd,
886+
const LEX_CSTRING &secondary_engine) {
887+
handlerton *hton = nullptr;
888+
plugin_ref ref = ha_resolve_by_name(thd, &secondary_engine, false);
889+
if (ref != nullptr) {
890+
hton = plugin_data<handlerton *>(ref);
891+
}
892+
893+
if (hton != nullptr) {
894+
secondary_engine_pre_prepare_hook_t secondary_engine_pre_prepare_hook =
895+
hton->secondary_engine_pre_prepare_hook;
896+
if (secondary_engine_pre_prepare_hook != nullptr) {
897+
return secondary_engine_pre_prepare_hook(thd);
898+
}
899+
}
900+
return false;
901+
}
902+
} // namespace
903+
877904
/**
878905
Checks if a query should be retried using a secondary storage engine.
879906
@@ -894,7 +921,10 @@ static bool retry_with_secondary_engine(THD *thd) {
894921

895922
// Don't retry if there is a property of the statement that prevents use of
896923
// secondary engines.
897-
if (sql_cmd->eligible_secondary_storage_engine(thd) == nullptr) {
924+
const LEX_CSTRING *secondary_engine =
925+
thd->lex->m_sql_cmd->eligible_secondary_storage_engine(thd);
926+
927+
if (secondary_engine == nullptr) {
898928
sql_cmd->disable_secondary_storage_engine();
899929
return false;
900930
}
@@ -917,33 +947,19 @@ static bool retry_with_secondary_engine(THD *thd) {
917947
return true;
918948
}
919949

920-
// Only attempt to use the secondary engine if the estimated cost of the query
921-
// is higher than the specified cost threshold.
922-
// We allow any query to be executed in the secondary_engine when it involves
923-
// external tables.
924-
if (!has_external_table(thd->lex) &&
925-
(thd->m_current_query_cost <=
926-
thd->variables.secondary_engine_cost_threshold)) {
927-
Opt_trace_context *const trace = &thd->opt_trace;
928-
if (trace->is_started()) {
929-
const Opt_trace_object wrapper(trace);
930-
Opt_trace_object oto(trace, "secondary_engine_not_used");
931-
oto.add_alnum("reason",
932-
"The estimated query cost does not exceed "
933-
"secondary_engine_cost_threshold.");
934-
oto.add("cost", thd->m_current_query_cost);
935-
oto.add("threshold", thd->variables.secondary_engine_cost_threshold);
936-
}
937-
return false;
938-
}
939-
940-
return true;
950+
return SecondaryEngineCallPrePrepareHook(thd, *secondary_engine);
941951
}
942952

943953
bool optimize_secondary_engine(THD *thd) {
944954
if (retry_with_secondary_engine(thd)) {
955+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while)
956+
DBUG_EXECUTE_IF("emulate_user_query_kill", {
957+
thd->get_stmt_da()->set_error_status(thd, ER_QUERY_INTERRUPTED);
958+
return true;
959+
});
945960
thd->get_stmt_da()->reset_diagnostics_area();
946961
thd->get_stmt_da()->set_error_status(thd, ER_PREPARE_FOR_SECONDARY_ENGINE);
962+
947963
return true;
948964
}
949965

@@ -972,6 +988,21 @@ bool optimize_secondary_engine(THD *thd) {
972988
}
973989

974990
void notify_plugins_after_select(THD *thd, const Sql_cmd *cmd) {
991+
/* Return if one of the 2 conditions is true:
992+
* 1. when secondary engine statement context is not present, query cost is
993+
* lower than the secondary than the engine threshold.
994+
* 2. When secondary engine statement context is present, primary engine
995+
* is the better execution engine for this query.
996+
* This prevents calling plugin_foreach for short queries, reducing the
997+
* overhead. */
998+
if (((thd->secondary_engine_statement_context() == nullptr) &&
999+
thd->m_current_query_cost <=
1000+
thd->variables.secondary_engine_cost_threshold) ||
1001+
((thd->secondary_engine_statement_context() != nullptr) &&
1002+
thd->secondary_engine_statement_context()
1003+
->is_primary_engine_optimal())) {
1004+
return;
1005+
}
9751006
auto executed_in = (cmd != nullptr && cmd->using_secondary_storage_engine())
9761007
? SelectExecutedIn::kSecondaryEngine
9771008
: SelectExecutedIn::kPrimaryEngine;
@@ -1027,13 +1058,7 @@ bool Sql_cmd_dml::execute_inner(THD *thd) {
10271058
} else {
10281059
if (unit->execute(thd)) return true;
10291060

1030-
/* Only call the plugin hook if the query cost is higher than the secondary
1031-
* engine threshold. This prevents calling plugin_foreach for short queries,
1032-
* reducing the overhead. */
1033-
if (thd->m_current_query_cost >
1034-
thd->variables.secondary_engine_cost_threshold) {
1035-
notify_plugins_after_select(thd, lex->m_sql_cmd);
1036-
}
1061+
notify_plugins_after_select(thd, lex->m_sql_cmd);
10371062
}
10381063

10391064
return false;

storage/innobase/include/ha_prototypes.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ ulong thd_parallel_read_threads(THD *thd);
472472

473473
/** Return the maximum buffer size to use for DDL.
474474
@param[in] thd Session instance, or nullptr to query the global
475-
innodb_parallel_read_threads value.
475+
innodb_ddl_buffer_size value.
476476
@return memory upper limit in bytes. */
477477
[[nodiscard]] ulong thd_ddl_buffer_size(THD *thd);
478478

storage/secondary_engine_mock/ha_mock.cc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "sql/join_optimizer/access_path.h"
4747
#include "sql/join_optimizer/make_join_hypergraph.h"
4848
#include "sql/join_optimizer/walk_access_paths.h"
49+
#include "sql/opt_trace.h"
4950
#include "sql/sql_class.h"
5051
#include "sql/sql_const.h"
5152
#include "sql/sql_lex.h"
@@ -97,6 +98,11 @@ class LoadedTables {
9798

9899
LoadedTables *loaded_tables{nullptr};
99100

101+
/**
102+
Statement context class for the MOCK engine.
103+
*/
104+
class Mock_statement_context : public Secondary_engine_statement_context {};
105+
100106
/**
101107
Execution context class for the MOCK engine. It allocates some data
102108
on the heap when it is constructed, and frees it when it is
@@ -228,6 +234,33 @@ int ha_mock::unload_table(const char *db_name, const char *table_name,
228234

229235
} // namespace mock
230236

237+
namespace {
238+
bool SecondaryEnginePrePrepareHook(THD *thd) {
239+
if (thd->m_current_query_cost <=
240+
static_cast<double>(thd->variables.secondary_engine_cost_threshold)) {
241+
Opt_trace_context *const trace = &thd->opt_trace;
242+
if (trace->is_started()) {
243+
const Opt_trace_object wrapper(trace);
244+
Opt_trace_object oto(trace, "secondary_engine_not_used");
245+
oto.add_alnum("reason",
246+
"The estimated query cost does not exceed "
247+
"secondary_engine_cost_threshold.");
248+
oto.add("cost", thd->m_current_query_cost);
249+
oto.add("threshold", thd->variables.secondary_engine_cost_threshold);
250+
}
251+
return false;
252+
}
253+
254+
if (thd->secondary_engine_statement_context() == nullptr) {
255+
/* Prepare this query's specific statment context */
256+
std::unique_ptr<Secondary_engine_statement_context> ctx =
257+
std::make_unique<Mock_statement_context>();
258+
thd->set_secondary_engine_statement_context(std::move(ctx));
259+
}
260+
return true;
261+
}
262+
} // namespace
263+
231264
static bool PrepareSecondaryEngine(THD *thd, LEX *lex) {
232265
DBUG_EXECUTE_IF("secondary_engine_mock_prepare_error", {
233266
my_error(ER_SECONDARY_ENGINE_PLUGIN, MYF(0), "");
@@ -365,6 +398,7 @@ static int Init(MYSQL_PLUGIN p) {
365398
hton->flags = HTON_IS_SECONDARY_ENGINE;
366399
hton->db_type = DB_TYPE_UNKNOWN;
367400
hton->prepare_secondary_engine = PrepareSecondaryEngine;
401+
hton->secondary_engine_pre_prepare_hook = SecondaryEnginePrePrepareHook;
368402
hton->optimize_secondary_engine = OptimizeSecondaryEngine;
369403
hton->compare_secondary_engine_cost = CompareJoinCost;
370404
hton->secondary_engine_flags =

0 commit comments

Comments
 (0)