Skip to content

Commit 6da8218

Browse files
author
Andrzej Religa
committed
Bug#33989165 MySQL Router should not disconnect client connections upon adding a member
When the user adds a node to the cluster (cluster.addInstance()) there is a short period of time, when this node is present in the GR metadata but still not present in the cluster metadata. This breaks Router assumptions, as the function that calculates quorum assumes this will never happen. This assumption is wrong and it is not satisfied in this case. The Router treat this scenario as no quorum (even thought the new node is ONLINE) and drops all the existing connections. It brings the service back once the new node appears in the cluster metadata. This patch fixes this behavior. The GR quorum no longer depends on cluster metadata, only on what GR is reporting. Once the quorum decision is made, and there is a quorum, the Router uses the nodes that are present in both GR and cluster metadata as a valid destinations. Change-Id: I1831acc553678d453bca63b6c0a7dd8f705c9bf0
1 parent 2a5180a commit 6da8218

19 files changed

+365
-157
lines changed

router/src/metadata_cache/src/cluster_metadata_gr.cc

Lines changed: 34 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,8 @@ void GRClusterMetadata::update_cluster_status(
418418
// with recovering nodes
419419
// (cornercase)
420420
log_warning(
421-
"quorum for cluster '%s' consists only of recovering nodes!",
421+
"quorum for cluster '%s' consists only of recovering nodes or "
422+
"nodes that are not defined in the metadata!",
422423
cluster.name.c_str());
423424
found_quorum = true; // no point in futher search
424425
break;
@@ -469,31 +470,29 @@ GRClusterStatus GRClusterMetadata::check_cluster_status(
469470
std::vector<metadata_cache::ManagedInstance> &instances,
470471
const std::map<std::string, GroupReplicationMember> &member_status,
471472
bool &metadata_gr_discrepancy) const noexcept {
472-
// In ideal world, the best way to write this function would be to completely
473-
// ignore nodes in `instances` and operate on information from `member_status`
474-
// only. However, there is one problem: the host:port information contained
475-
// there may not be accurate (localhost vs external addressing issues), and we
476-
// are forced to use the host:port from `instances` instead. This leads to
477-
// nasty corner-cases if inconsistencies exist between the two sets, however.
478-
479-
// Therefore, this code will work well only under one assumption:
480-
// All nodes in `member_status` are present in `instances`. This assumption
481-
// should hold unless a user "manually" adds new nodes to the cluster
482-
// without adding them to metadata (and the user is not allowed to do that).
483-
484-
// Detect violation of above assumption (alarm if there's a node in
485-
// `member_status` not present in `instances`). It's O(n*m), but the CPU time
486-
// is negligible while keeping code simple.
487-
488473
using metadata_cache::ServerMode;
489474
using metadata_cache::ServerRole;
490475
using GR_State = GroupReplicationMember::State;
491476
using GR_Role = GroupReplicationMember::Role;
492477

493478
metadata_gr_discrepancy = false;
494-
auto number_of_all_members = member_status.size();
479+
const auto number_of_all_members = member_status.size();
480+
size_t number_of_online_members = 0, number_of_recovering_members = 0;
481+
// Check if the GR has a quorum. Warn about nodes that are present in the GR
482+
// and missing in the cluster metadata.
495483
for (const auto &status_node : member_status) {
496484
using MI = metadata_cache::ManagedInstance;
485+
486+
switch (status_node.second.state) {
487+
case GR_State::Online:
488+
number_of_online_members++;
489+
break;
490+
case GR_State::Recovering:
491+
number_of_recovering_members++;
492+
break;
493+
default:;
494+
}
495+
497496
auto found = std::find_if(instances.begin(), instances.end(),
498497
[&status_node](const MI &metadata_node) {
499498
return status_node.first ==
@@ -505,40 +504,22 @@ GRClusterStatus GRClusterMetadata::check_cluster_status(
505504
node_in_metadata, EventStateTracker::EventId::GRNodeInMetadata,
506505
status_node.first);
507506
if (!node_in_metadata) {
508-
if (status_node.second.state == GR_State::Recovering) {
509-
const auto log_level =
510-
node_in_metadata_changed ? LogLevel::kInfo : LogLevel::kDebug;
511-
log_custom(log_level,
512-
"GR member %s:%d (%s) Recovering, missing in the metadata, "
513-
"ignoring",
514-
status_node.second.host.c_str(), status_node.second.port,
515-
status_node.first.c_str());
516-
// if the node is Recovering and it is missing in the metadata it can't
517-
// increase the pool used for quorum calculations. This is for example
518-
// important when we have single node cluster and we add another node
519-
// with cloning. While cloning the new node will be present in the GR
520-
// tables but missing in the metadata.
521-
--number_of_all_members;
522-
} else {
523-
const auto log_level =
524-
node_in_metadata_changed ? LogLevel::kWarning : LogLevel::kDebug;
525-
log_custom(
526-
log_level, "GR member %s:%d (%s) %s, missing in the metadata",
527-
status_node.second.host.c_str(), status_node.second.port,
528-
status_node.first.c_str(), to_string(status_node.second.state));
529-
}
507+
const auto log_level =
508+
node_in_metadata_changed ? LogLevel::kWarning : LogLevel::kDebug;
509+
log_custom(log_level,
510+
"GR member %s:%d (%s), state: '%s' missing in the metadata, "
511+
"ignoring",
512+
status_node.second.host.c_str(), status_node.second.port,
513+
status_node.first.c_str(),
514+
to_string(status_node.second.state));
530515

531-
// we want to set this in both cases as it increases the metadata refresh
532-
// rate
516+
// increases the metadata refresh rate
533517
metadata_gr_discrepancy = true;
534518
}
535519
}
536520

537-
// we do two things here:
538-
// 1. for all `instances`, set .mode according to corresponding .status found
521+
// for all `instances`, set .mode according to corresponding .status found
539522
// in `member_status`
540-
// 2. count nodes which are part of quorum (online/recovering nodes)
541-
unsigned int quorum_count = 0;
542523
bool have_primary_instance = false;
543524
bool have_secondary_instance = false;
544525
for (auto &member : instances) {
@@ -556,13 +537,11 @@ GRClusterStatus GRClusterMetadata::check_cluster_status(
556537
have_primary_instance = true;
557538
member.role = ServerRole::Primary;
558539
member.mode = ServerMode::ReadWrite;
559-
quorum_count++;
560540
break;
561541
case GR_Role::Secondary:
562542
have_secondary_instance = true;
563543
member.role = ServerRole::Secondary;
564544
member.mode = ServerMode::ReadOnly;
565-
quorum_count++;
566545
break;
567546
}
568547
break;
@@ -571,10 +550,6 @@ GRClusterStatus GRClusterMetadata::check_cluster_status(
571550
case GR_State::Offline: // online node with disabled GR maps to this
572551
case GR_State::Error:
573552
case GR_State::Other:
574-
// This could be done with a fallthrough but latest gcc (7.1)
575-
// generates a warning for that and there is no sane and portable way
576-
// to suppress it.
577-
if (GR_State::Recovering == status->second.state) quorum_count++;
578553
member.role = ServerRole::Unavailable;
579554
member.mode = ServerMode::Unavailable;
580555
break;
@@ -593,10 +568,13 @@ GRClusterStatus GRClusterMetadata::check_cluster_status(
593568
}
594569
}
595570

596-
// quorum_count is based on nodes from `instances` instead of `member_status`.
597-
// This is okay, because all nodes in `member_status` are present in
598-
// `instances` (our assumption described at the top)
599-
bool have_quorum = (quorum_count > number_of_all_members / 2);
571+
// 1 node: [ 1 ONL ] -> OK
572+
// 1 node: [ 1 REC ] -> OK (not possible, checked in a later step )
573+
// 2 nodes: [ 1 ONL, 1 REC ] -> OK
574+
// 2 nodes: [ 1 ONL, 1 OFF ] -> NOT OK
575+
// 3 nodes: [ 1 ONL, 1 REC, 1 OFF ] -> OK
576+
bool have_quorum = (number_of_online_members + number_of_recovering_members) >
577+
number_of_all_members / 2;
600578

601579
// if we don't have quorum, we don't allow any access. Some configurations
602580
// might allow RO access in this case, but we don't support it at the momemnt

router/src/router/src/cluster_metadata.cc

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -531,24 +531,26 @@ bool check_group_replication_online(MySQLSession *mysql) {
531531

532532
bool check_group_has_quorum(MySQLSession *mysql) {
533533
std::string q =
534-
"SELECT SUM(IF(member_state = 'ONLINE', 1, 0)) as num_onlines, COUNT(*) "
535-
"as num_total"
536-
" FROM performance_schema.replication_group_members";
534+
"SELECT SUM(IF(member_state = 'ONLINE', 1, 0)) as num_onlines, "
535+
"SUM(IF(member_state = 'RECOVERING', 1, 0)) as num_recovering, "
536+
"COUNT(*) as num_total "
537+
"FROM performance_schema.replication_group_members";
537538

538539
std::unique_ptr<MySQLSession::ResultRow> result(
539540
mysql->query_one(q)); // throws MySQLSession::Error
540541
if (result) {
541-
if (result->size() != 2) {
542+
if (result->size() != 3) {
542543
throw std::out_of_range(
543544
"Invalid number of values returned from "
544545
"performance_schema.replication_group_members: "
545-
"expected 2 got " +
546+
"expected 3 got " +
546547
std::to_string(result->size()));
547548
}
548549
int online = strtoi_checked((*result)[0]);
549-
int all = strtoi_checked((*result)[1]);
550-
// log_info("%d members online out of %d", online, all);
551-
if (online >= all / 2 + 1) return true;
550+
int recovering = strtoi_checked((*result)[1]);
551+
int all = strtoi_checked((*result)[2]);
552+
553+
if ((online + recovering) > all / 2) return true;
552554
return false;
553555
}
554556

router/tests/component/data/local_modules/common_statements.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ var defaults = {
9393

9494
gr_member_state: "ONLINE",
9595
gr_members_all: 3,
96+
gr_members_recovering: 0,
9697
gr_members_online: 3,
9798
};
9899

@@ -400,13 +401,17 @@ function get_response(stmt_key, options) {
400401
case "router_select_members_count":
401402
return {
402403
"stmt":
403-
"SELECT SUM(IF(member_state = 'ONLINE', 1, 0)) as num_onlines, COUNT(*) as num_total FROM performance_schema.replication_group_members",
404+
"SELECT SUM(IF(member_state = 'ONLINE', 1, 0)) as num_onlines, SUM(IF(member_state = 'RECOVERING', 1, 0)) as num_recovering, COUNT(*) as num_total FROM performance_schema.replication_group_members",
404405
"result": {
405406
"columns": [
406407
{"type": "LONGLONG", "name": "num_onlines"},
408+
{"type": "LONGLONG", "name": "num_recovering"},
407409
{"type": "LONGLONG", "name": "num_total"}
408410
],
409-
"rows": [[options.gr_members_online, options.gr_members_all]]
411+
"rows": [[
412+
options.gr_members_online, , options.gr_members_recovering,
413+
options.gr_members_all
414+
]]
410415
}
411416
};
412417
case "router_select_replication_group_name":

router/tests/component/data/local_modules/gr_memberships.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ exports.single_host_cluster_nodes = function(host, classic_port, uuid) {
3939
exports.gr_members = function(host, port_and_state) {
4040
return port_and_state.map(function(current_value) {
4141
return [
42-
current_value[0], // use port as uuid
42+
current_value[0], // uuid
4343
host,
44-
current_value[0], // port
45-
current_value[1] // member_state
44+
current_value[1], // port
45+
current_value[2] // member_state
4646
];
4747
});
4848
};
@@ -56,11 +56,11 @@ exports.members = function(id_host_and_port) {
5656
exports.cluster_nodes = function(host, cluster_instances) {
5757
return cluster_instances.map(function(current_value) {
5858
return [
59-
current_value[0], // classic port as uuid
59+
current_value[0], // uuid
6060
host,
61-
current_value[0], // classic port
62-
current_value[1], // x port
63-
current_value[2], // attributes
61+
current_value[1], // classic port
62+
current_value[2], // x port
63+
current_value[3], // attributes
6464
];
6565
});
6666
};

router/tests/component/data/metadata_3_secondaries_primary_failover.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ var gr_node_host = "127.0.0.1";
1717
var nodes = function(host, port_and_state) {
1818
return port_and_state.map(function(current_value) {
1919
return [
20-
current_value[0], host, current_value[0], current_value[1],
21-
current_value[2]
20+
current_value[0], host, current_value[1], current_value[2],
21+
current_value[3]
2222
];
2323
});
2424
};

router/tests/component/data/metadata_3_secondaries_primary_failover_v2_gr.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ var gr_node_host = "127.0.0.1";
1717
var nodes = function(host, port_and_state) {
1818
return port_and_state.map(function(current_value) {
1919
return [
20-
current_value[0], host, current_value[0], current_value[1],
21-
current_value[2]
20+
current_value[0], host, current_value[1], current_value[2],
21+
current_value[3]
2222
];
2323
});
2424
};

router/tests/component/data/metadata_dynamic_nodes_v2_ar.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ if (mysqld.global.cluster_type === undefined) {
5353
var nodes = function(host, port_and_state) {
5454
return port_and_state.map(function(current_value) {
5555
return [
56-
current_value[0], host, current_value[0], current_value[1],
57-
current_value[2]
56+
current_value[0], host, current_value[1], current_value[2],
57+
current_value[3]
5858
];
5959
});
6060
};

router/tests/component/data/metadata_dynamic_nodes_v2_gr.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ const online_gr_nodes = members
7373
})
7474
.length;
7575

76+
const recovering_gr_nodes = members
77+
.filter(function(memb, indx) {
78+
return (memb[3] === "RECOVERING");
79+
})
80+
.length;
81+
7682
const member_state = members[mysqld.global.gr_pos] ?
7783
members[mysqld.global.gr_pos][3] :
7884
undefined;
@@ -82,6 +88,7 @@ var options = {
8288
gr_member_state: member_state,
8389
gr_members_all: members.length,
8490
gr_members_online: online_gr_nodes,
91+
gr_members_recovering: recovering_gr_nodes,
8592
innodb_cluster_instances: gr_memberships.cluster_nodes(
8693
mysqld.global.gr_node_host, mysqld.global.cluster_nodes),
8794
gr_id: mysqld.global.gr_id,

router/tests/component/data/metadata_dynamic_nodes_version_update.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ if (mysqld.global.transaction_count === undefined) {
7070
var nodes = function(host, port_and_state) {
7171
return port_and_state.map(function(current_value) {
7272
return [
73-
current_value[0], host, current_value[0], current_value[1],
74-
current_value[2]
73+
current_value[0], host, current_value[1], current_value[2],
74+
current_value[3]
7575
];
7676
});
7777
};

router/tests/component/data/metadata_dynamic_nodes_version_update_v2_ar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ var nodes = function(host, port_and_state) {
8989
return [
9090
current_value[0],
9191
host,
92-
current_value[0],
9392
current_value[1],
93+
current_value[2],
9494
];
9595
});
9696
};

router/tests/component/data/metadata_dynamic_nodes_version_update_v2_gr.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ if (mysqld.global.bootstrap_target_type === undefined) {
9292
var nodes = function(host, port_and_state) {
9393
return port_and_state.map(function(current_value) {
9494
return [
95-
current_value[0], host, current_value[0], current_value[1],
96-
current_value[2]
95+
current_value[0], host, current_value[1], current_value[2],
96+
current_value[3]
9797
];
9898
});
9999
};

router/tests/component/data/metadata_http_auth_backend.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ if (mysqld.global.error_on_md_query === undefined) {
3030
var nodes = function(host, port_and_state) {
3131
return port_and_state.map(function(current_value) {
3232
return [
33-
current_value[0], host, current_value[0], current_value[1],
34-
current_value[2]
33+
current_value[0], host, current_value[1], current_value[2],
34+
current_value[3]
3535
];
3636
});
3737
};

router/tests/component/data/rest_api_enable.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ if (mysqld.global.gr_nodes === undefined &&
4040
var nodes = function(host, port_and_state) {
4141
return port_and_state.map(function(current_value) {
4242
return [
43-
current_value[0], host, current_value[0], current_value[1],
44-
current_value[2]
43+
current_value[0], host, current_value[1], current_value[2],
44+
current_value[3]
4545
];
4646
});
4747
};

router/tests/component/test_bootstrap.cc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ TEST_F(RouterBootstrapTest, bootstrap_and_run_from_symlinked_dir) {
121121
launch_mysql_server_mock(runtime_json_stmts, server_port, EXIT_SUCCESS, false,
122122
http_port);
123123
set_mock_metadata(http_port, "cluster-specific-id", {GRNode{server_port}}, 0,
124-
{ClusterNode{server_port, server_x_port}});
124+
{ClusterNode{server_port, "uuid-1", server_x_port}});
125125

126126
SCOPED_TRACE("// launch router with bootstrapped config");
127127
launch_router({"-c", bootstrap_dir.name() + "/mysqlrouter.conf"});
@@ -1714,8 +1714,9 @@ TEST_P(ConfUseGrNotificationParamTest, ConfUseGrNotificationParam) {
17141714
// launch mock server that is our metadata server
17151715
launch_mysql_server_mock(runtime_json_stmts, server_port, EXIT_SUCCESS, false,
17161716
http_port);
1717-
set_mock_metadata(http_port, "cluster-specific-id", {GRNode{server_port}}, 0,
1718-
{ClusterNode{server_port, server_x_port}});
1717+
set_mock_metadata(http_port, "cluster-specific-id",
1718+
{GRNode{server_port, "uuid-1"}}, 0,
1719+
{ClusterNode{server_port, "uuid-1", server_x_port}});
17191720

17201721
// check that the Router accepts the config file
17211722
auto &router2 = launch_router({"-c", conf_file});

router/tests/component/test_gr_notifications.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,11 @@ class GrNotificationsTest : public RouterComponentTest {
151151
gr_id_.reset(new JsonValue(gr_id.c_str(), gr_id.length(), allocator));
152152

153153
gr_nodes_.reset(new JsonValue(rapidjson::kArrayType));
154-
for (auto &gr_node : gr_node_ports) {
154+
for (auto [i, gr_node] : stdx::views::enumerate(gr_node_ports)) {
155155
JsonValue node(rapidjson::kArrayType);
156+
const std::string uuid = "uuid-" + std::to_string(i + 1);
157+
node.PushBack(JsonValue(uuid.c_str(), uuid.length(), allocator),
158+
allocator);
156159
node.PushBack(JsonValue(static_cast<int>(gr_node)), allocator);
157160
node.PushBack(JsonValue("ONLINE", strlen("ONLINE"), allocator),
158161
allocator);
@@ -164,6 +167,9 @@ class GrNotificationsTest : public RouterComponentTest {
164167
cluster_node_ports.empty() ? gr_node_ports : cluster_node_ports;
165168
for (auto [i, cluster_node] : stdx::views::enumerate(cluster_nodes)) {
166169
JsonValue node(rapidjson::kArrayType);
170+
const std::string uuid = "uuid-" + std::to_string(i + 1);
171+
node.PushBack(JsonValue(uuid.c_str(), uuid.length(), allocator),
172+
allocator);
167173
node.PushBack(JsonValue(static_cast<int>(cluster_node)), allocator);
168174
node.PushBack(JsonValue(static_cast<int>(gr_node_xports[i])), allocator);
169175
node.PushBack(JsonValue("{}", strlen("{}"), allocator), allocator);

0 commit comments

Comments
 (0)