Jul
14
2026
--

Inside MySQL 9.7 LTS Features

MySQL 9.7, a Long-Term Support (LTS) release, incorporates a variety of potential features spanning across multiple technical domains. This article covers some of the primary features introduced and evaluates their practical utility within the MySQL database environment.

Following the End-of-Life (EOL) status of MySQL 8.0, this subsequent LTS release is designed to provide enhanced stability alongside significant architectural innovations.

Let’s discuss each of these features below with some examples and usage.

Flow-control monitoring in Group Replication

Flow control monitoring has been improved and provides more granularity by introducing the additional status variables listed below.

  • Gr_flow_control_throttle_count : It denotes the number of transactions that have been throttled.
  • Gr_flow_control_throttle_time_sum :It denotes the time in microseconds that transactions have been throttled.
  • Gr_flow_control_throttle_active_count :It denotes the number of transactions currently being throttled.
  • Gr_flow_control_throttle_last_throttle_timestamp : It denotes the most recent date and time that a transaction was throttled.

To use these status variables, we must install the “Group Replication Flow Control Statistics”  component.

mysql> Install component 'file://component_group_replication_flow_control_stats';

After the component is installed, the statistics will be visible.

mysql> SELECT * FROM performance_schema.global_status WHERE VARIABLE_NAME LIKE 'Gr_flow_control%';
+--------------------------------------------------+----------------+
| VARIABLE_NAME                                    | VARIABLE_VALUE |
+--------------------------------------------------+----------------+
| Gr_flow_control_throttle_active_count            | 0              |
| Gr_flow_control_throttle_count                   | 0              |
| Gr_flow_control_throttle_last_throttle_timestamp |                |
| Gr_flow_control_throttle_time_sum                | 0              |
+--------------------------------------------------+----------------+

Multi-threaded applier extended statistics

We now have additional verbosity for the Applier threads for both Asynchronous and Group Replication topologies. This means we can get more details of the transactions or potential misbehaviours during the transactions applier stage. This feature is particularly useful for troubleshooting performance bottlenecks in multi-threaded replication environments, where understanding the specific cause of lag can be challenging.

This requires installing the “Replication Applier Metrics” component.

mysql> Install component 'file://component_replication_applier_metrics';

Upon successful installation of the requisite component, the performance schema tables facilitate tracking of transaction details and various performance metrics during the replication applier phase. For instance, monitoring the table “replication_applier_metrics” enables observing channel-specific operations.

mysql> SELECT * FROM performance_schema.replication_applier_metrics where CHANNEL_NAME='group_replication_applier'\G;
*************************** 1. row ***************************
                                CHANNEL_NAME: group_replication_applier
                  TOTAL_ACTIVE_TIME_DURATION: 0
                          LAST_APPLIER_START: 0000-00-00 00:00:00
                TRANSACTIONS_COMMITTED_COUNT: 0
                  TRANSACTIONS_ONGOING_COUNT: 0
                  TRANSACTIONS_PENDING_COUNT: 0
       TRANSACTIONS_COMMITTED_SIZE_BYTES_SUM: 0
    TRANSACTIONS_ONGOING_FULL_SIZE_BYTES_SUM: 0
TRANSACTIONS_ONGOING_PROGRESS_SIZE_BYTES_SUM: 0
         TRANSACTIONS_PENDING_SIZE_BYTES_SUM: NULL
                      EVENTS_COMMITTED_COUNT: 0
            WAITS_FOR_WORK_FROM_SOURCE_COUNT: 0
         WAITS_FOR_WORK_FROM_SOURCE_SUM_TIME: 0
            WAITS_FOR_AVAILABLE_WORKER_COUNT: 0
         WAITS_FOR_AVAILABLE_WORKER_SUM_TIME: 0
      WAITS_COMMIT_SCHEDULE_DEPENDENCY_COUNT: 0
   WAITS_COMMIT_SCHEDULE_DEPENDENCY_SUM_TIME: 0
         WAITS_FOR_WORKER_QUEUE_MEMORY_COUNT: 0
      WAITS_FOR_WORKER_QUEUE_MEMORY_SUM_TIME: 0
              WAITS_WORKER_QUEUES_FULL_COUNT: 0
           WAITS_WORKER_QUEUES_FULL_SUM_TIME: 0
             WAITS_DUE_TO_COMMIT_ORDER_COUNT: 0
          WAITS_DUE_TO_COMMIT_ORDER_SUM_TIME: 0
        TIME_TO_READ_FROM_RELAY_LOG_SUM_TIME: 0

In addition to aggregate metrics, MySQL 9.7 provides a way to inspect the progress of individual worker threads via monitoring stats in the “replication_applier_progress_by_worker” table. This level of detail helps administrators identify if a single transaction is monopolising a specific worker, causing overall replication delay.

mysql> SELECT * FROM performance_schema.replication_applier_progress_by_worker\G;
*************************** 1. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 0
                             THREAD_ID: 62
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 2. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 1
                             THREAD_ID: 63
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 3. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 2
                             THREAD_ID: 64
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 4. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 3
                             THREAD_ID: 65
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0

Automatic eviction & rejoin

The Group Replication resource manager now provides auto-eviction functionality, which we can configure using the available options. This basically ensures that the unhealthy node is removed from the Group to maintain the cluster’s high availability and overall performance.

This requires installing the “group replication resource manager” component.

mysql> INSTALL COMPONENT 'file://component_group_replication_resource_manager';

Once the component is available,  we can use various options to decide the node expulsion policy.

1) Applier channel

We can set the applier channel replication lag threshold values using the configuration parameter below.

mysql> set global group_replication_resource_manager.applier_channel_lag = <value>;

If lag exceeds  “applier_channel_lag” threshold 10 times or more in a row, this server is expelled from the group. The status variable below is used for tracking the lag exceed rate.

mysql> show global status like 'Gr_resource_manager_applier_channel_lag';
+-----------------------------------------+-------+
| Variable_name                           | Value |
+-----------------------------------------+-------+
| Gr_resource_manager_applier_channel_lag | 0     |
+-----------------------------------------+-------+


2) Recovery Channel

Similarly, we can define a threshold for the group member recovery process to attempt to rejoin the cluster. 

mysql> set global group_replication_resource_manager.recovery_channel_lag = <value>;

If the secondary’s recovery lag exceeds “recovery_channel_lag”, 10 times or more in succession, the server is expelled from the group. 

mysql show global status like 'Gr_resource_manager_recovery_channel_lag';
+------------------------------------------+-------+
| Variable_name                            | Value |
+------------------------------------------+-------+
| Gr_resource_manager_recovery_channel_lag | 0     |
+------------------------------------------+-------+

3) Memory/Resource Usage

We can also define an expelled condition based on the group member’s memory or resource usage %.

mysql> set global group_replication_resource_manager.memory_used_limit = 10;

If the memory usage exceeds memory_used_limit % by 10 or more consecutive times, the node will be expelled from the group.

mysql> show global status like 'Gr_resource_manager_memory_used%';
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| Gr_resource_manager_memory_used | 78    |
+---------------------------------+-------+
1 row in set (0.002 sec)

In addition to the discussed options above, we can also track various server status variables to monitor group replication and the resource manager component.

mysql> select * from performance_schema.global_status where variable_name in ('Gr_resource_manager_applier_channel_threshold_hits','Gr_resource_manager_applier_channel_eviction_timestamp','Gr_resource_manager_recovery_channel_threshold_hits','Gr_resource_manager_recovery_channel_eviction_timestamp','Gr_resource_manager_memory_threshold_hits','Gr_resource_manager_memory_eviction_timestamp');
+---------------------------------------------------------+----------------+
| VARIABLE_NAME                                           | VARIABLE_VALUE |
+---------------------------------------------------------+----------------+
| Gr_resource_manager_applier_channel_eviction_timestamp  |                |
| Gr_resource_manager_applier_channel_threshold_hits      | 0              |
| Gr_resource_manager_memory_eviction_timestamp           |                |
| Gr_resource_manager_memory_threshold_hits               | 6703           |
| Gr_resource_manager_recovery_channel_eviction_timestamp |                |
| Gr_resource_manager_recovery_channel_threshold_hits     | 0              |
+---------------------------------------------------------+----------------+
6 rows in set (0.003 sec)

The expelled node can attempt to automatically rejoin based on the value of the group_replication_autorejoin_tries variable.

mysql> show variables like '%group_replication_autorejoin_tries%';
+------------------------------------+-------+
| Variable_name                      | Value |
+------------------------------------+-------+
| group_replication_autorejoin_tries | 3     |
+------------------------------------+-------+
1 row in set (0.006 sec)

If the node cannot join, it will perform the behaviour specified in the group_replication_exit_state_action variable.

mysql> show variables like '%group_replication_exit_state_action%';
+-------------------------------------+--------------+
| Variable_name                       | Value        |
+-------------------------------------+--------------+
| group_replication_exit_state_action | OFFLINE_MODE |
+-------------------------------------+--------------+
1 row in set (0.005 sec)

After a server is evicted from the group (for whatever reason), it gets a grace period (group_replication_resource_manager) when it rejoins. During this period, the Resource Manager won’t immediately kick it out again, even if it’s still lagging or breaching the defined threshold as discussed above.

mysql> show variables like '%group_replication_resource_manager.quarantine_time%';
+----------------------------------------------------+-------+
| Variable_name                                      | Value |
+----------------------------------------------------+-------+
| group_replication_resource_manager.quarantine_time | 3600  |
+----------------------------------------------------+-------+

Up-to-date aware Primary election

The Primary election process is more mature and cohesive. The Group Replication Manager now uses the most up-to-date status as a criterion for selecting the new primary.

Here is how the Group Replication Manager performs the most up-to-date primary selection prior to MySQL v9.7.

  1. The lowest MySQL version is checked for each member.
  2. If more than one member is running the lowest MySQL Server version, each member’s weight is determined by the “group_replication_member_weight” system variable.
  3. If there is more than one member running the lowest MySQL Server version, and also more than one of those members has the highest member weight, the third factor considered is the lexicographical order of the generated server UUIDs “server_uuid” of each group member. The member with the lowest server UUID is chosen as the new primary.

In MySQL version 9.7, “group_replication_elect_prefers_most_updated” was introduced, so the failover will be determined by how many transactions are in the secondary backlog. Basically the secondary with the least backlog will be selected as Primary.

Now, it will consider the “most up-to-date” node first, then “weight” and then “UUID”

To use “group_replication_elect_prefers_most_updated”, we need to install the “Group Replication Primary Election” component listed below on each Group Member.

mysql> Install component 'file://component_group_replication_elect_prefers_most_updated';

By default, the most up-to-date group member selection is enabled. We need to make sure it’s enabled on all Group Members. 

mysql> select @@group_replication_elect_prefers_most_updated.enabled;
+--------------------------------------------------------+
| @@group_replication_elect_prefers_most_updated.enabled |
+--------------------------------------------------------+
|                                                      1 |
+--------------------------------------------------------+
1 row in set (0.007 sec)

In the event that a new primary is elected via the most up-to-date selection mechanism, this metric represents the transaction processing differential between the newly designated primary and the secondary node with the highest level of synchronisation.

mysql> show status like 'Gr_latest_primary_election_by_most_uptodate_members_trx_delta';
+---------------------------------------------------------------+-------+
| Variable_name                                                 | Value |
+---------------------------------------------------------------+-------+
| Gr_latest_primary_election_by_most_uptodate_members_trx_delta | 0     |
+---------------------------------------------------------------+-------+

Also, we can track the timestamp of the most recent primary election on the most up-to-date node.

mysql> show status like 'Gr_latest_primary_election_by_most_uptodate_member_timestamp';
+--------------------------------------------------------------+-------+
| Variable_name                                                | Value |
+--------------------------------------------------------------+-------+
| Gr_latest_primary_election_by_most_uptodate_member_timestamp |       |
+--------------------------------------------------------------+-------+
1 row in set (0.005 sec)

The database logs also tell exactly what criteria the primary member selected during failover.

2026-06-14T10:04:02.243809Z 0 [System] [MY-015575] [Repl] Plugin group_replication reported: 'Member with uuid 00021702-2222-2222-2222-222222222222 was elected primary since it was the most up-to-date member with 2755 transactions more than second most up-to-date member 00021703-3333-3333-3333-333333333333. In case of a tie member weight and then uuid lexical order was used over the most updated members.'

MySQL JSON duality views

With the introduction of JSON duality views, we can leverage a single unified JSON document for both relational and hierarchical JSON data. This provides a common, structured JSON format for the application, allowing it to perform both read and write operations.

Let’s see a quick scenario below on how it works.

Below are two relational tables from which we obtain aggregated information in JSON format. 

mysql> CREATE TABLE products (
  product_id INT PRIMARY KEY,
  product_type VARCHAR(100)
);

mysql> CREATE TABLE products_details (
  product_detail_id INT PRIMARY KEY,
  product_id INT,
  name VARCHAR(100),
  active varchar(10)
);

mysql> INSERT INTO products (product_id,product_type) VALUES (1,'IT'), (2,'TEL');
mysql> INSERT INTO products_details (product_detail_id,product_id,name,active) VALUES (1,1,'Laptop','Yes'), (2,2,'Mobile','Yes');

Here is the exact Json View which fetch the columns from the relation table based on the join condition. Each of those relational table columns is mapped with a JSON data structure (_id,v_product_type,v_product_type ), and the complete details of the product details table are fetched into the (product) array.

mysql> CREATE JSON RELATIONAL DUALITY VIEW view_product AS
SELECT JSON_DUALITY_OBJECT( WITH(INSERT,UPDATE,DELETE)
    '_id': product_id,
    'v_product_type': product_type,
    'product': (
        SELECT JSON_ARRAYAGG(
            JSON_DUALITY_OBJECT(WITH(INSERT,UPDATE,DELETE)
                'v_product_detail_id': product_detail_id,
                'v_name': name,
                'v_active': active
                
            )
        )
        FROM products_details
        WHERE products_details.product_id = products.product_id
    )
)
FROM products;

mysql> select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {"_id": 1, "product": [{"v_name": "Laptop", "v_active": "Yes", "v_product_detail_id": 1}], "_metadata": {"etag": "313642c2aa24f0571264332afa140715"}, "v_product_type": "IT"}  |
| {"_id": 2, "product": [{"v_name": "Mobile", "v_active": "Yes", "v_product_detail_id": 2}], "_metadata": {"etag": "3d229ada02ac660f9f6cac994b44831a"}, "v_product_type": "TEL"} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.002 sec)

Once the duality view is created, we can perform both read/write operations.

Reading the duality view

mysql> select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {"_id": 1, "product": [{"v_name": "Laptop", "v_active": "Yes", "v_product_detail_id": 1}], "_metadata": {"etag": "313642c2aa24f0571264332afa140715"}, "v_product_type": "IT"}  |
| {"_id": 2, "product": [{"v_name": "Mobile", "v_active": "Yes", "v_product_detail_id": 2}], "_metadata": {"etag": "3d229ada02ac660f9f6cac994b44831a"}, "v_product_type": "TEL"} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

Writing the underlying table in the duality view

mysql> UPDATE view_product
SET data = JSON_SET(
    data,
    '$.product[0].v_name',
    'Notepad'
)
WHERE JSON_EXTRACT(data, '$._id') = 1;

mysql> select * from products_details;
+-------------------+------------+---------+--------+
| product_detail_id | product_id | name    | active |
+-------------------+------------+---------+--------+
|                 1 |          1 | Notepad | Yes    |
|                 2 |          2 | Mobile  | Yes    |
+-------------------+------------+---------+--------+

After performing the above write operations, we can see that the view now shows the updated data.

mysql > select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {"_id": 1, "product": [{"v_name": "Notepad", "v_active": "Yes", "v_product_detail_id": 1}], "_metadata": {"etag": "72c4368420cdc698842d0ab4bd9315ab"}, "v_product_type": "IT"} |
| {"_id": 2, "product": [{"v_name": "Mobile", "v_active": "Yes", "v_product_detail_id": 2}], "_metadata": {"etag": "3d229ada02ac660f9f6cac994b44831a"}, "v_product_type": "TEL"} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

Hypergraph Optimizer

With the Hypergraph Optimiser, we now have more advanced optimisation for complex queries and a broader set of Join plans than the older traditional method, missing earlier. By using “Join hypergraph”, the optimiser now has better reach to all tables in the join condition.

Hypergraph Optimiser is OFF

mysql> SET optimizer_switch='hypergraph_optimizer=off';

mysql> SELECT t1.k, COUNT(*) AS cnt
FROM sbtest1 t1
JOIN sbtest2 t2 ON t1.id = t2.id
JOIN sbtest3 t3 ON t1.id = t3.id
WHERE t1.k BETWEEN 200000 AND 500000
GROUP BY t1.k
ORDER BY cnt DESC
LIMIT 100;

Output:

| 498870 | 119 |
| 498729 | 119 |
| 497668 | 119 |
| 498076 | 119 |
+--------+-----+
100 rows in set (4.000 sec)

Explain output:

-> Limit: 100 row(s)
    -> Sort: cnt DESC, limit input to 100 row(s) per chunk
        -> Stream results  (cost=1.22e+6 rows=175136)
            -> Group aggregate: count(0)  (cost=1.22e+6 rows=175136)
                -> Nested loop inner join  (cost=1.1e+6 rows=493200)
                    -> Nested loop inner join  (cost=601547 rows=493200)
                        -> Filter: (t1.k between 200000 and 500000)  (cost=99122 rows=493200)
                            -> Covering index range scan on t1 using k_1 over (200000 <= k <= 500000)  (cost=99122 rows=493200)
                        -> Single-row covering index lookup on t2 using PRIMARY (id = t1.id)  (cost=0.919 rows=1)
                    -> Single-row covering index lookup on t3 using PRIMARY (id = t1.id)  (cost=0.919 rows=1)

Hypergraph Optimiser is ON

mysql> SET optimizer_switch='hypergraph_optimizer=on';

mysql> SELECT t1.k, COUNT(*) AS cnt
FROM sbtest1 t1
JOIN sbtest2 t2 ON t1.id = t2.id
JOIN sbtest3 t3 ON t1.id = t3.id
WHERE t1.k BETWEEN 200000 AND 500000
GROUP BY t1.k
ORDER BY cnt DESC
LIMIT 100;

Output:

| 499721 | 119 |
| 499052 | 119 |
| 498870 | 119 |
| 498384 | 119 |
+--------+-----+
100 rows in set (0.498 sec)

Explain output:

-> Sort: cnt DESC, limit input to 100 row(s) per chunk  (cost=1.96e+6..1.96e+6 rows=100)
    -> Table scan on <temporary>  (cost=1.87e+6..1.9e+6 rows=175136)
        -> Aggregate using temporary table  (cost=1.87e+6..1.87e+6 rows=175136)
            -> Inner hash join (t2.id = t3.id)  (cost=990754..1.44e+6 rows=493200)
                -> Covering index scan on t3 using k_1  (cost=0.312..308240 rows=986400)
                -> Hash
                    -> Inner hash join (t1.id = t2.id)  (cost=370988..824021 rows=493200)
                        -> Covering index scan on t2 using k_1  (cost=0.312..308240 rows=986400)
                        -> Hash
                            -> Filter: (t1.k between 200000 and 500000)  (cost=0.416..205287 rows=493200)
                                -> Covering index range scan on t1 using k_1 over (200000 <= k <= 500000)  (cost=0.359..176877 rows=493200)

We can see that with “hypergraph_optimizer=enabled”, the query execution time is almost 8x faster.

The performance difference might not be noticeable with a few joins or a smaller table’s data set, but with more complex joins, it can yield better performance. In the above example, we can see that when “hypergraph_optimizer=enabled”, the optimiser replaces “Nested loop inner join” with “Inner hash join”, which is generally better for large datasets. 

Higher version source allowed

Now, it’s possible that a lower version replica can connect to a higher version source when the major versions differ. That means we don’t have to rely on all replicas being upgraded in one go; we can just upgrade the source, verify it, and later perform rolling upgrades on lower-version replicas as per our own timelines and convenience.

Of course, we have to be cautious not to run any such feature or change on the source that doesn’t support lower-version replicas.

Please note – This won’t be applicable to previous releases, say (8.4, 8.0), as they didn’t restrict such replication connectivity. It would be useful for 9.7 or the next major release.

To enable this functionality, we need to ensure the following variable is enabled on the Replica. By default its enabled on 9.7

mysql> show variables like 'replica_allow_higher_version_source';
+-------------------------------------+-------+
| Variable_name                       | Value |
+-------------------------------------+-------+
| replica_allow_higher_version_source | ON    |
+-------------------------------------+-------+
1 row in set (0.008 sec)

Summary

The above discussion highlights key advancements in MySQL 9.7 LTS, ranging from some innovative or operational improvements to developer-centric features such as “JSON Duality” Views. Also, the “Hypergraph Optimiser” is now available for community release, which was previously exclusive to MySQL Heatwave/Enterprise.  As a Long-Term Support (LTS) release, MySQL 9.7 is structured to provide a stable and consistent environment, prioritising architectural reliability over frequent experimental changes.

One more important mention here: It’s suggested to use MySQL 9.7.1, or the next sub-releases, as 9.7.0 has some higer severity CVE’s. If you are using Percona Server for MySQL (PS), we skipped 9.7.0 and are shipping the fixed 9.7.1 version directly.

Still, it’s highly recommended to test any new component or changes in your lower/staging environment before deploying in production to better assess the overall impact on existing workload, queries, and database behaviour.

 

The post Inside MySQL 9.7 LTS Features appeared first on Percona.

Sep
25
2024
--

How Network Splits/Partitions Impact Group Replication in MySQL

Network Splits/Partition on Group ReplicationIn this blog post, we will explore how network partitions impact group replication and the way it detects and responds to failures. In case you haven’t checked out my previous blog post about group replication recovery strategies, please have a look at them for some insight. Topology: [crayon-66f40f3f3cad1905113104/] Scenario 1: One of the GR nodes […]

Aug
14
2024
--

Effective Strategies for Recovering MySQL Group Replication From Failures

Group replication is a fault-tolerant/highly available replication topology that ensures if the primary node goes down, one of the other candidates or secondary members takes over so write and read operations can continue without any interruptions. However, there are some scenarios where, due to outages, network partitions, or database crashes, the group membership could be broken, or we end […]

Jun
24
2024
--

Understanding Basic Flow Control Activity in MySQL Group Replication: Part One

Understanding Basic Flow Control Activity in MySQL Group ReplicationFlow control is not a new term, and we have already heard it a lot of times in Percona XtraDB Cluster/Galera-based environments.  In very simple terms, it means the cluster node can’t keep up with the cluster write pace. The write rate is too high, or the nodes are oversaturated. Flow control helps avoid excessive […]

Aug
17
2023
--

InnoDB ClusterSet Deployment With MySQLRouter

InnoDB ClusterSet Deployment With MySQLRouter

This blog post will cover the basic setup of the InnoDB ClusterSet environment, which provides disaster tolerance for InnoDB Cluster deployments by associating a primary InnoDB Cluster with one or more replicas in alternate locations/different data centers. InnoDB ClusterSet automatically manages replication from the primary cluster to the replica clusters via a specific ClusterSet Async replication channel. If the primary cluster becomes inaccessible due to a loss of network connectivity or a data center issue, you can make a replica cluster active in its place.

Now, let’s see in detail how exactly we can configure the topology.

InnoDB ClusterSet Deployment

We have used the sandbox environment available via MySQLShell utility for this setup.

Environment

Cluster1:
         127.0.0.1:3308
         127.0.0.1:3309
         127.0.0.1:3310

Cluster2:
         127.0.0.1:3311
         127.0.0.1:3312
         127.0.0.1:3313

Router:
         127.0.0.1:6446/6447

Let’s set up the first cluster (“cluster1”)

  •  Deploying the sandboxes.
MySQL JS > dba.deploySandboxInstance(3308) 
MySQL JS > dba.deploySandboxInstance(3309) 
MySQL JS > dba.deploySandboxInstance(33010)

  •  Then, we need to perform some pre-checks before initiating the cluster.
###connecting to the concerned nodes one by one. 

MySQL JS > shell.connect('root@localhost:3308') 
MySQL localhost:3308 ssl JS > shell.connect('root@localhost:3309') 
MySQL localhost:3309 ssl JS > shell.connect('root@localhost:3310') 

###The below commands will check if satisfying the Innodb cluster requirements (group replication settings) and fix the missing requirements automatically. Here, we have configured a new user "iroot" for cluster deployment. 

MySQL localhost:3308 ssl JS > dba.checkInstanceConfiguration('root@localhost:3308') 
MySQL localhost:3308 ssl JS > dba.configureInstance('root@127.0.0.1:3308',{clusterAdmin: 'iroot', clusterAdminPassword: 'Iroot@1234'}) 

MySQL localhost:3309 ssl JS > dba.checkInstanceConfiguration('root@localhost:3309') 
MySQL localhost:3309 ssl JS > dba.configureInstance('root@127.0.0.1:3309',{clusterAdmin: 'iroot', clusterAdminPassword: 'Iroot@1234'}) 

MySQL localhost:3310 ssl JS > dba.checkInstanceConfiguration('root@localhost:3310') 
MySQL localhost:3310 ssl JS > dba.configureInstance('root@127.0.0.1:3310',{clusterAdmin: 'iroot', clusterAdminPassword: 'Iroot@1234'})

Once all the instances are prepared, we can plan to create the cluster with the seed node. The “createcluster” command will perform all the hidden steps of initializing group replication, and later on, the other nodes join the group with distributed recovery/clone plugin.

InnoDB cluster is built on top of group replication which provides (automatic membership management, fault tolerance, and automatic failover). It provides us with an easy interface to deploy/manage the complex topologies with DR support.

  • We will bootstrap the cluster with an initial node(“localhost:3308″).
MySQL localhost:3310 ssl JS > shell.connect('iroot@localhost:3308') 
MySQL localhost:3308 ssl JS > cluster1 = dba.createCluster('Cluster1') 
MySQL localhost:3308 ssl JS > cluster1 = dba.getCluster()

Output:

MySQL localhost:3308 ssl JS > cluster1.status()
{
    "clusterName": "Cluster1", 
    "defaultReplicaSet": {
        "name": "default", 
        "primary": "127.0.0.1:3308", 
        "ssl": "REQUIRED", 
        "status": "OK_NO_TOLERANCE", 
        "statusText": "Cluster is NOT tolerant to any failures.", 
        "topology": {
            "127.0.0.1:3308": {
                "address": "127.0.0.1:3308", 
                "memberRole": "PRIMARY", 
                "mode": "R/W", 
                "readReplicas": {}, 
                "replicationLag": "applier_queue_applied", 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.31"
            }
        }, 
        "topologyMode": "Single-Primary"
    }, 
    "groupInformationSourceMember": "127.0.0.1:3308"
}

  • Here, we have successfully bootstrapped the first node. Next, the other nodes will join the cluster using the CLONE Plugin.
MySQL localhost:3308 ssl JS > cluster1.addInstance("iroot@localhost:3309",{password:'Iroot@1234'})

Output:

* Waiting for clone to finish...

NOTE: 127.0.0.1:3309 is being cloned from 127.0.0.1:3308

** Stage DROP DATA: Completed 

** Clone Transfer 

    FILE COPY  ############################################################  100%  Completed

    PAGE COPY  ############################################################  100%  Completed

    REDO COPY  ############################################################  100%  Completed

NOTE: 127.0.0.1:3309 is shutting down...

* Waiting for server restart... ready 

* 127.0.0.1:3309 has restarted, waiting for clone to finish...

** Stage RESTART: Completed

* Clone process has finished: 73.66 MB transferred in about 1 second (~73.66 MB/s)

State recovery already finished for '127.0.0.1:3309'

The instance '127.0.0.1:3309' was successfully added to the cluster.

 

MySQL localhost:3308 ssl JS > cluster1.addInstance("iroot@localhost:3310",{password:'Iroot@1234'})

Output:

* Waiting for clone to finish...

NOTE: 127.0.0.1:3310 is being cloned from 127.0.0.1:3309

** Stage DROP DATA: Completed 

** Clone Transfer 

    FILE COPY  ############################################################  100%  Completed

    PAGE COPY  ############################################################  100%  Completed

    REDO COPY  ############################################################  100%  Completed

NOTE: 127.0.0.1:3310 is shutting down...

* Waiting for server restart... ready 

* 127.0.0.1:3310 has restarted, waiting for clone to finish...

** Stage RESTART: Completed

* Clone process has finished: 73.66 MB transferred in about 1 second (~73.66 MB/s)

State recovery already finished for '127.0.0.1:3310'

The instance '127.0.0.1:3310' was successfully added to the cluster.

  • At this stage, our first cluster is ready with all three nodes.
MySQL localhost:3308 ssl JS > cluster1.status()

Output:

{

    "clusterName": "Cluster1", 

    "defaultReplicaSet": {

        "name": "default", 

        "primary": "127.0.0.1:3308", 

        "ssl": "REQUIRED", 

        "status": "OK", 

        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

        "topology": {

            "127.0.0.1:3308": {

                "address": "127.0.0.1:3308", 

                "memberRole": "PRIMARY", 

                "mode": "R/W", 

                "readReplicas": {}, 

                "replicationLag": "applier_queue_applied", 

                "role": "HA", 

                "status": "ONLINE", 

                "version": "8.0.31"

            }, 

            "127.0.0.1:3309": {

                "address": "127.0.0.1:3309", 

                "memberRole": "SECONDARY", 

                "mode": "R/O", 

                "readReplicas": {}, 

                "replicationLag": "applier_queue_applied", 

                "role": "HA", 

                "status": "ONLINE", 

                "version": "8.0.31"

            }, 

            "127.0.0.1:3310": {

                "address": "127.0.0.1:3310", 

                "memberRole": "SECONDARY", 

                "mode": "R/O", 

                "readReplicas": {}, 

                "replicationLag": "applier_queue_applied", 

                "role": "HA", 

                "status": "ONLINE", 

                "version": "8.0.31"

            }

        }, 

        "topologyMode": "Single-Primary"

    }, 

    "groupInformationSourceMember": "127.0.0.1:3308"

}

Let’s now proceed with the second cluster (“cluster2”) setup

  • Deploying the sandboxes via MySqlShell.
MySQL JS > dba.deploySandboxInstance(3311) 
MySQL JS > dba.deploySandboxInstance(3312) 
MySQL JS > dba.deploySandboxInstance(3313)

  •  Similarly, perform some pre-checks as we did for “cluster1” nodes.
# connecting to the concerned nodes. 

MySQL  JS > shell.connect('root@localhost:3311') 
MySQL  JS > shell.connect('root@localhost:3312')
MySQL  JS > shell.connect('root@localhost:3313')

# The below commands will check if satisfying the Innodb cluster requirements (group replication settings) and fix the missing requirements automatically. Here, we have configured a new user "iroot" for cluster deployment. 

MySQL  localhost:3308 ssl  JS > dba.checkInstanceConfiguration('root@localhost:3311')
MySQL  localhost:3308 ssl  JS > dba.configureInstance('root@127.0.0.1:3311',{clusterAdmin: 'iroot', clusterAdminPassword: 'Iroot@1234'})

MySQL  localhost:3308 ssl  JS > dba.checkInstanceConfiguration('root@localhost:3312')
MySQL  localhost:3308 ssl  JS > dba.configureInstance('root@127.0.0.1:3312',{clusterAdmin: 'iroot', clusterAdminPassword: 'Iroot@1234'})

MySQL  localhost:3308 ssl  JS > dba.checkInstanceConfiguration('root@localhost:3313')
MySQL  localhost:3308 ssl  JS > dba.configureInstance('root@127.0.0.1:3313',{clusterAdmin: 'iroot', clusterAdminPassword: 'Iroot@1234'})

  •  Next, we will create the ClusterSet topology by triggering the sync on the node (127.0.0.1:3311) by existing cluster1 nodes. Node (127.0.0.1:3311) will be the Primary node for cluster2, and the rest of other nodes will join this node by Clone/Incremental process.
1) First, connect to “cluster1” node.

MySQL localhost:3308 ssl JS > c iroot@127.0.0.1:3308
MySQL 127.0.0.1:3308 ssl JS > cluster1 = dba.getCluster()

2) Here, “cluster1” join the ClusterSet topology,

MySQL 127.0.0.1:3308 ssl JS > myclusterset = cluster1.createClusterSet('firstclusterset')

Output:

ClusterSet successfully created. Use ClusterSet.createReplicaCluster() to add Replica Clusters to it.
<ClusterSet:firstclusterset>`

3) Verifying the status.

MySQL 127.0.0.1:3308 ssl JS > myclusterset.status({extended: 1})

Output:

{

    "clusters": {

        "Cluster1": {

            "clusterRole": "PRIMARY", 

            "globalStatus": "OK", 

            "primary": "127.0.0.1:3308", 

            "status": "OK", 

            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

            "topology": {

                "127.0.0.1:3308": {

                    "address": "127.0.0.1:3308", 

                    "memberRole": "PRIMARY", 

                    "mode": "R/W", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3309": {

                    "address": "127.0.0.1:3309", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3310": {

                    "address": "127.0.0.1:3310", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }

            }, 

            "transactionSet": "39c28b63-285a-11ee-a411-5254004d77d3:1-4,59d8e60a-285d-11ee-bb44-5254004d77d3:1-85,59d8f3a6-285d-11ee-bb44-5254004d77d3:1-5"

        }

    }, 

    "domainName": "firstclusterset", 

    "globalPrimaryInstance": "127.0.0.1:3308", 

    "metadataServer": "127.0.0.1:3308", 

    "primaryCluster": "Cluster1", 

    "status": "HEALTHY", 

    "statusText": "All Clusters available."

}

 4) Now, Node (“127.0.0.1:3311″) will sync with the existing “cluster1” with Async process.

MySQL  127.0.0.1:3308 ssl  JS > c iroot@127.0.0.1:3311
MySQL  127.0.0.1:3311 ssl  JS > cluster2 = myclusterset.createReplicaCluster("127.0.0.1:3311", "cluster2", {recoveryProgress: 1, timeout: 10})

Output:

... Replica Cluster 'cluster2' successfully created on ClusterSet 'firstclusterset'. ...

5) Next, the other nodes join the “cluster2” with the clone process.

MySQL  127.0.0.1:3311 ssl  JS > cluster2.addInstance("iroot@127.0.0.1:3312",{password:'Iroot@1234'})
MySQL  127.0.0.1:3311 ssl  JS > cluster2.addInstance("iroot@127.0.0.1:3313",{password:'Iroot@1234'})

6) Finally, checking the status of our clusterset environment.

MySQL  127.0.0.1:3311 ssl  JS > myclusterset = dba.getClusterSet()
MySQL  127.0.0.1:3311 ssl  JS > myclusterset.status({extended: 1})

Output:

{

    "clusters": {

        "Cluster1": {

            "clusterRole": "PRIMARY", 

            "globalStatus": "OK", 

            "primary": "127.0.0.1:3308", 

            "status": "OK", 

            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

            "topology": {

                "127.0.0.1:3308": {

                    "address": "127.0.0.1:3308", 

                    "memberRole": "PRIMARY", 

                    "mode": "R/W", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3309": {

                    "address": "127.0.0.1:3309", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3310": {

                    "address": "127.0.0.1:3310", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }

            }, 

            "transactionSet": "39c28b63-285a-11ee-a411-5254004d77d3:1-4,59d8e60a-285d-11ee-bb44-5254004d77d3:1-124,59d8f3a6-285d-11ee-bb44-5254004d77d3:1-5"

        }, 

        "cluster2": {

            "clusterRole": "REPLICA", 

            "clusterSetReplication": {

                "applierStatus": "APPLIED_ALL", 

                "applierThreadState": "Waiting for an event from Coordinator", 

                "applierWorkerThreads": 4, 

                "receiver": "127.0.0.1:3311", 

                "receiverStatus": "ON", 

                "receiverThreadState": "Waiting for source to send event", 

                "source": "127.0.0.1:3308"

            }, 

            "clusterSetReplicationStatus": "OK", 

            "globalStatus": "OK", 

            "status": "OK", 

            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

            "topology": {

                "127.0.0.1:3311": {

                    "address": "127.0.0.1:3311", 

                    "memberRole": "PRIMARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3312": {

                    "address": "127.0.0.1:3312", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3313": {

                    "address": "127.0.0.1:3313", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }

            }, 

            "transactionSet": "2e71122e-2862-11ee-b81c-5254004d77d3:1-5,39c28b63-285a-11ee-a411-5254004d77d3:1-4,59d8e60a-285d-11ee-bb44-5254004d77d3:1-124,59d8f3a6-285d-11ee-bb44-5254004d77d3:1-5", 

            "transactionSetConsistencyStatus": "OK", 

            "transactionSetErrantGtidSet": "", 

            "transactionSetMissingGtidSet": ""

        }

    }, 

    "domainName": "firstclusterset", 

    "globalPrimaryInstance": "127.0.0.1:3308", 

    "metadataServer": "127.0.0.1:3308", 

    "primaryCluster": "Cluster1", 

    "status": "HEALTHY", 

    "statusText": "All Clusters available."

Here, the ClusterSet topology is ready now with all six nodes.

In the next phase, we will bootstrap MySQLRouter with our newly created ClusterSet environment:

  • First, we will generate a dedicated user for MySQLRouter monitoring/management.
MySQL  127.0.0.1:3311 ssl  JS > c iroot@localhost:3308
MySQL  localhost:3308 ssl  JS > cluster1 = dba.getCluster();
MySQL  localhost:3308 ssl  JS > cluster1.setupRouterAccount('router_usr')

Output:

Missing the password for new account router_usr@%. Please provide one.
Password for new account: **********
Confirm password: **********
Creating user router_usr@%.
Account router_usr@% was successfully created.

  • Bootstrap the router with the user (“router_usr) and router name (“Router1”).
[vagrant@localhost ~]$ sudo mysqlrouter --bootstrap iroot@127.0.0.1:3308 --account=router_usr --name='Router1' --user root --force

We are using –-force here because without –-force mysqlrouter won’t recognize the clusterset. This will reconfigure the existing clusterset.

Here, we will see some useful information that later on is required to connect to a database or manage the services.

# MySQL Router 'Router1' configured for the ClusterSet 'firstclusterset'

After this MySQL Router has been started with the generated configuration

    $ /etc/init.d/mysqlrouter restart

or

    $ systemctl start mysqlrouter

or

    $ mysqlrouter -c /etc/mysqlrouter/mysqlrouter.conf

ClusterSet 'firstclusterset' can be reached by connecting to:

## MySQL Classic protocol

- Read/Write Connections: localhost:6446

- Read/Only Connections:  localhost:6447

## MySQL X protocol

- Read/Write Connections: localhost:6448

- Read/Only Connections:  localhost:6449

  • Finally, start the mysqlrouter service:
sudo mysqlrouter -c /etc/mysqlrouter/mysqlrouter.conf &

Validating the connection route

  •  Connect to the router port “6446” and create some demo table/data:
shell> mysql -h 127.0.0.1 -u root -pRoot@1234 -P 6446 -e "create database sbtest;use sbtest;create table sbtest1 (id int(10) not null auto_increment primary key, user varchar(50));insert into sbtest1(user) values('test');"

  •  Connect to the router port “6447” for reading purposes. Here, the connection will be, by default, balanced among the number of nodes of the Primary Cluster(cluster1).
[vagrant@localhost ~]$ mysql -h 127.0.0.1 -u root -pRoot@1234 -P 6447 -e "use sbtest;select * from sbtest1;select @@server_id;"
+----+------+
| id | user |
+----+------+
|  1 | test |
+----+------+
+-------------+
| @@server_id |
+-------------+
|   194452202 |
+-------------+

[vagrant@localhost ~]$ mysql -h 127.0.0.1 -u root -pRoot@1234 -P 6447 -e "use sbtest;select * from sbtest1;select @@server_id;"
+----+------+
| id | user |
+----+------+
|  1 | test |
+----+------+
+-------------+
| @@server_id |
+-------------+
|  2376678236 |
+-------------+

[vagrant@localhost ~]$ mysql -h 127.0.0.1 -u root -pRoot@1234 -P 6447 -e "use sbtest;select * from sbtest1;select @@server_id;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+------+
| id | user |
+----+------+
|  1 | test |
+----+------+
+-------------+
| @@server_id |
+-------------+
|   194452202 |
+-------------+

So, by default, all the connections will route to the default “Primary” Cluster, which, in our case, is “Clustrer1”; however, we can change the primary component based on the requirement.

Changing ClusterSet topology

MySQL  localhost:3308 ssl  JS > myclusterset=dba.getClusterSet()
MySQL  localhost:3308 ssl  JS > myclusterset.status({extended:1})

Output:

<ClusterSet:firstclusterset>

{

    "clusters": {

        "Cluster1": {

            "clusterRole": "PRIMARY", 

            "globalStatus": "OK", 

            "primary": "127.0.0.1:3308", 

            "status": "OK", 

            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

            "topology": {

                "127.0.0.1:3308": {

                    "address": "127.0.0.1:3308", 

                    "memberRole": "PRIMARY", 

                    "mode": "R/W", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3309": {

                    "address": "127.0.0.1:3309", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3310": {

                    "address": "127.0.0.1:3310", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }

            }, 

            "transactionSet": "39c28b63-285a-11ee-a411-5254004d77d3:1-4,59d8e60a-285d-11ee-bb44-5254004d77d3:1-143,59d8f3a6-285d-11ee-bb44-5254004d77d3:1-5"

        }, 

        "cluster2": {

            "clusterRole": "REPLICA", 

            "clusterSetReplication": {

                "applierStatus": "APPLIED_ALL", 

                "applierThreadState": "Waiting for an event from Coordinator", 

                "applierWorkerThreads": 4, 

                "receiver": "127.0.0.1:3311", 

                "receiverStatus": "ON", 

                "receiverThreadState": "Waiting for source to send event", 

                "source": "127.0.0.1:3308"

            }, 

            "clusterSetReplicationStatus": "OK", 

            "globalStatus": "OK", 

            "status": "OK", 

            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

            "topology": {

                "127.0.0.1:3311": {

                    "address": "127.0.0.1:3311", 

                    "memberRole": "PRIMARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3312": {

                    "address": "127.0.0.1:3312", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3313": {

                    "address": "127.0.0.1:3313", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }

            }, 

            "transactionSet": "2e71122e-2862-11ee-b81c-5254004d77d3:1-5,39c28b63-285a-11ee-a411-5254004d77d3:1-4,59d8e60a-285d-11ee-bb44-5254004d77d3:1-143,59d8f3a6-285d-11ee-bb44-5254004d77d3:1-5", 

            "transactionSetConsistencyStatus": "OK", 

            "transactionSetErrantGtidSet": "", 

            "transactionSetMissingGtidSet": ""

        }

    }, 

    "domainName": "firstclusterset", 

    "globalPrimaryInstance": "127.0.0.1:3308", 

    "metadataServer": "127.0.0.1:3308", 

    "primaryCluster": "Cluster1", 

    "status": "HEALTHY", 

    "statusText": "All Clusters available."

}

  • Changing the Primary cluster from “cluster1” to “cluster2:
MySQL localhost:3308 ssl JS > myclusterset.setPrimaryCluster('cluster2')

Output:

Switching the primary cluster of the clusterset to 'cluster2'

* Verifying clusterset status

** Checking cluster cluster2

  Cluster 'cluster2' is available

** Checking cluster Cluster1

  Cluster 'Cluster1' is available

* Reconciling 5 internally generated GTIDs

* Refreshing replication account of demoted cluster

* Synchronizing transaction backlog at 127.0.0.1:3311

** Transactions replicated  ############################################################  100% 

* Updating metadata

* Updating topology

** Changing replication source of 127.0.0.1:3309 to 127.0.0.1:3311

** Changing replication source of 127.0.0.1:3310 to 127.0.0.1:3311

** Changing replication source of 127.0.0.1:3308 to 127.0.0.1:3311

* Acquiring locks in replicaset instances

** Pre-synchronizing SECONDARIES

** Acquiring global lock at PRIMARY

** Acquiring global lock at SECONDARIES

* Synchronizing remaining transactions at promoted primary

** Transactions replicated  ############################################################  100% 

* Updating replica clusters

Cluster 'cluster2' was promoted to PRIMARY of the clusterset. The PRIMARY instance is '127.0.0.1:3311'

  • If we see the output again, we can observe that  “clusterRole:PRIMARY” is shifted to “cluster2”.
<span class="s1">My</span><span class="s2">SQL </span><span class="s3"> localhost:3308 ssl </span><span class="s4"> JS </span><span class="s5">&gt; </span><span class="s6">myclusterset.status({extended:1})</span>

Output:

{

    "clusters": {

        "Cluster1": {

            "clusterRole": "REPLICA", 

            "clusterSetReplication": {

                "applierStatus": "APPLIED_ALL", 

                "applierThreadState": "Waiting for an event from Coordinator", 

                "applierWorkerThreads": 4, 

                "receiver": "127.0.0.1:3308", 

                "receiverStatus": "ON", 

                "receiverThreadState": "Waiting for source to send event", 

                "source": "127.0.0.1:3311"

            }, 

            "clusterSetReplicationStatus": "OK", 

            "globalStatus": "OK", 

            "status": "OK", 

            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

            "topology": {

                "127.0.0.1:3308": {

                    "address": "127.0.0.1:3308", 

                    "memberRole": "PRIMARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3309": {

                    "address": "127.0.0.1:3309", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3310": {

                    "address": "127.0.0.1:3310", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }

            }, 

            "transactionSet": "2e71122e-2862-11ee-b81c-5254004d77d3:1-5,39c28b63-285a-11ee-a411-5254004d77d3:1-4,59d8e60a-285d-11ee-bb44-5254004d77d3:1-145,59d8f3a6-285d-11ee-bb44-5254004d77d3:1-5", 

            "transactionSetConsistencyStatus": "OK", 

            "transactionSetErrantGtidSet": "", 

            "transactionSetMissingGtidSet": ""

        }, 

        "cluster2": {

            "clusterRole": "PRIMARY", 

            "globalStatus": "OK", 

            "primary": "127.0.0.1:3311", 

            "status": "OK", 

            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 

            "topology": {

                "127.0.0.1:3311": {

                    "address": "127.0.0.1:3311", 

                    "memberRole": "PRIMARY", 

                    "mode": "R/W", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3312": {

                    "address": "127.0.0.1:3312", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }, 

                "127.0.0.1:3313": {

                    "address": "127.0.0.1:3313", 

                    "memberRole": "SECONDARY", 

                    "mode": "R/O", 

                    "replicationLagFromImmediateSource": "", 

                    "replicationLagFromOriginalSource": "", 

                    "status": "ONLINE", 

                    "version": "8.0.31"

                }

            }, 

            "transactionSet": "2e71122e-2862-11ee-b81c-5254004d77d3:1-5,39c28b63-285a-11ee-a411-5254004d77d3:1-4,59d8e60a-285d-11ee-bb44-5254004d77d3:1-145,59d8f3a6-285d-11ee-bb44-5254004d77d3:1-5"

        }

    }, 

    "domainName": "firstclusterset", 

    "globalPrimaryInstance": "127.0.0.1:3311", 

    "metadataServer": "127.0.0.1:3311", 

    "primaryCluster": "cluster2", 

    "status": "HEALTHY", 

    "statusText": "All Clusters available."

}

So, we have changed the Primary component from cluster1 to cluster2, but the routing is still set for cluster1. In order to send traffic to cluster2, we also have to change the routing option.

MySQL localhost:3308 ssl JS > myclusterset.listRouters()

Output:

{     "domainName": "firstclusterset",     "routers": {         "localhost.localdomain::Router1": {             "hostname": "localhost.localdomain",             "lastCheckIn": "2023-07-22 02:47:42",             "roPort": "6447",             "roXPort": "6449",             "rwPort": "6446",             "rwXPort": "6448",             "targetCluster": "primary",             "version": "8.0.32"         },

  • Changing the connection target from “cluster1” to “cluster2”:
MySQL localhost:3308 ssl JS > myclusterset.setRoutingOption('localhost.localdomain::Router1', 'target_cluster', 'cluster2')

MySQL localhost:3308 ssl JS > myclusterset.listRouters()

Output:

MySQL localhost:3308 ssl JS > myclusterset.listRouters()
{

    "domainName": "firstclusterset", 

    "routers": {

        "localhost.localdomain::Router1": {

            "hostname": "localhost.localdomain", 

            "lastCheckIn": "2023-07-22 02:47:42", 

            "roPort": "6447", 

            "roXPort": "6449", 

            "rwPort": "6446", 

            "rwXPort": "6448", 

            "targetCluster": "cluster2", 

            "version": "8.0.32"

        }

 Verifying the routing policy in the existing clusterset

MySQL localhost:3308 ssl JS > myclusterset.routingOptions()

Output:

{

    "domainName": "firstclusterset", 

    "global": {

        "invalidated_cluster_policy": "drop_all", 

        "stats_updates_frequency": 0, 

        "target_cluster": "primary"

    }, 

    "routers": {

        "localhost.localdomain::Router1": {

            "target_cluster": "cluster2"

        }

          }

}

There are situations when Primary clusters are not available or reachable. The immediate solution in some situations would be to perform an emergency failover in order to avoid the application block out.

An emergency failover basically switches to a selected replica cluster from the primary InnoDB Cluster for the InnoDB ClusterSet deployment. During an emergency failover process, data consistency is not assured due to async replication and other network factors, so for safety, the original primary cluster is marked as invalidated during the failover process.

So if by any chance the original primary cluster remains online, it should be shut down. Later, the invalidated primary cluster can join the clusterset via rejoin/repair process.

Perform emergency failover

myclusterset.forcePrimaryCluster("cluster2")
myclusterset.setRoutingOption('localhost::Route1', 'target_cluster', 'cluster2')

Summary

With the help of ClusterSet implementation, deploying DR support over different regions is no more a complex challenge. MySQLShell and InnoDB cluster tackles all the configurations and syncing process behind the scene. The disaster recovery and failure time can be minimized with the help of the Admin APIs/MySQLShell commands.

There is one caveat with the clusterset functioning. It does not support high availability/auto-promotion of the new primary if the existing one goes down. We must take care of the same with some manual intervention or via some internal automated process.

Percona Distribution for MySQL is the most complete, stable, scalable, and secure open source MySQL solution available, delivering enterprise-grade database environments for your most critical business applications… and it’s free to use!

 

Try Percona Distribution for MySQL today!

Jul
11
2022
--

Percona Operator for MySQL Supports Group Replication

Percona Operator for MySQL Supports Group Replication

Percona Operator for MySQL Supports Group ReplicationThere are two Operators at Percona to deploy MySQL on Kubernetes:

We wrote a blog post in the past explaining the thought process and reasoning behind creating the new Operator for MySQL. The goal for us is to provide production-grade solutions to run MySQL in Kubernetes and support various replication configurations:

  • Synchronous replication
    • with Percona XtraDB Cluster
    • with Group Replication
  • Asynchronous replication

With the latest 0.2.0 release of Percona Operator for MySQL (based on Percona Server for MySQL), we have added Group Replication support. In this blog post, we will briefly review the design of our implementation and see how to set it up. 

Design

This is a high-level design of running MySQL cluster with Group Replication:

MySQL cluster with Group Replication

MySQL Router acts as an entry point for all requests and routes the traffic to the nodes. 

This is a deeper look at how the Operator deploys these components in Kubernetes:
kubernetes deployment

Going from right to left:

  1. StatefulSet to deploy a cluster of MySQL nodes with Group Replication configured. Each node has its storage attached to it.
  2. Deployment object for stateless MySQL Router. 
  3. Deployment is exposed with a Service. We use various TCP ports here:
    1. MySQL Protocol ports
      1. 6446 – read/write, routing traffic to Primary node
      2. 6447 – read-only, load-balancing the traffic across Replicas 
    2. MySQL X Protocol – can be useful for CRUD operations, ex. asynchronous calls. Ports follow the same logic:
      1. 6448 – read/write
      2. 6449 – read-only 

Action

Prerequisites: you need a Kubernetes cluster. Minikube would do.

The files used in this blog post can be found in this Github repo.

Deploy the Operator

kubectl apply --server-side -f https://raw.githubusercontent.com/spron-in/blog-data/master/ps-operator-gr-demo/bundle.yaml

Note

–server-side

flag, without it you will get the error:

The CustomResourceDefinition "perconaservermysqls.ps.percona.com" is invalid: metadata.annotations: Too long: must have at most 262144 bytes

Our Operator follows OpenAPIv3 schema to have proper validation. This unfortunately increases the size of our Custom Resource Definition manifest and as a result, requires us to use

–server-side

flag.

Deploy the Cluster

We are ready to deploy the cluster now:

kubectl apply -f https://raw.githubusercontent.com/spron-in/blog-data/master/ps-operator-gr-demo/cr.yaml

I created this Custom Resource manifest specifically for this demo. Important to note variables:

  1. Line 10:
    clusterType: group-replication

    – instructs Operator that this is going to be a cluster with Group Replication.

  2. Lines 31-47: are all about MySQL Router. Once Group Replication is enabled, the Operator will automatically deploy the router. 

Get the status

The best way to see if the cluster is ready is to check the Custom Resource state:

$ kubectl get ps
NAME         REPLICATION         ENDPOINT        STATE   AGE
my-cluster   group-replication   35.223.42.238   ready   18m

As you can see, it is

ready

. You can also see

initializing

if the cluster is still not ready or

error

if something went wrong.

Here you can also see the endpoint where you can connect to. In our case, it is a public IP-address of the load balancer. As described in the design section above, there are multiple ports exposed:

$ kubectl get service my-cluster-router
NAME                TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)                                                       AGE
my-cluster-router   LoadBalancer   10.20.22.90   35.223.42.238   6446:30852/TCP,6447:31694/TCP,6448:31515/TCP,6449:31686/TCP   18h

Connect to the Cluster

To connect we will need the user first. By default, there is a root user with a randomly generated password. The password is stored in the Secret object. You can always fetch the password with the following command:

$ kubectl get secrets my-cluster-secrets -ojson | jq -r .data.root | base64 -d
SomeRandomPassword

I’m going to use port 6446, which would grant me read/write access and lead me directly to the Primary node through MySQL Router:

mysql -u root -p -h 35.223.42.238 --port 6446
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 156329
Server version: 8.0.28-19 Percona Server (GPL), Release 19, Revision 31e88966cd3

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

Group Replication in action

Let’s see if Group Replication really works. 

List the members of the cluster by running the following command:

$ mysql -u root -p -P 6446 -h 35.223.42.238 -e 'SELECT member_host, member_state, member_role FROM performance_schema.replication_group_members;'
+-------------------------------------------+--------------+-------------+
| member_host                               | member_state | member_role |
+-------------------------------------------+--------------+-------------+
| my-cluster-mysql-0.my-cluster-mysql.mysql | ONLINE       | PRIMARY     |
| my-cluster-mysql-1.my-cluster-mysql.mysql | ONLINE       | SECONDARY   |
| my-cluster-mysql-2.my-cluster-mysql.mysql | ONLINE       | SECONDARY   |
+-------------------------------------------+--------------+-------------+

Now we will delete one Pod (MySQL node), which also happens to have a Primary role, and see what happens:

$ kubectl delete pod my-cluster-mysql-0
pod "my-cluster-mysql-0" deleted


$ mysql -u root -p -P 6446 -h 35.223.42.238 -e 'SELECT member_host, member_state, member_role FROM performance_schema.replication_group_members;'
+-------------------------------------------+--------------+-------------+
| member_host                               | member_state | member_role |
+-------------------------------------------+--------------+-------------+
| my-cluster-mysql-1.my-cluster-mysql.mysql | ONLINE       | PRIMARY     |
| my-cluster-mysql-2.my-cluster-mysql.mysql | ONLINE       | SECONDARY   |
+-------------------------------------------+--------------+-------------+

One node is gone as expected.

my-cluster-mysql-1

node got promoted to a Primary role. I’m still using port 6446 and the same host to connect to the database, which indicates that MySQL Router is doing its job.

After some time Kubernetes will recreate the Pod and the node will join the cluster again automatically:

$ mysql -u root -p -P 6446 -h 35.223.42.238 -e 'SELECT member_host, member_state, member_role FROM performance_schema.replication_group_members;'
+-------------------------------------------+--------------+-------------+
| member_host                               | member_state | member_role |
+-------------------------------------------+--------------+-------------+
| my-cluster-mysql-0.my-cluster-mysql.mysql | RECOVERING   | SECONDARY   |
| my-cluster-mysql-1.my-cluster-mysql.mysql | ONLINE       | PRIMARY     |
| my-cluster-mysql-2.my-cluster-mysql.mysql | ONLINE       | SECONDARY   |
+-------------------------------------------+--------------+-------------+

The recovery phase might take some time, depending on the data size and amount of the changes, but eventually, it will come back ONLINE:

$ mysql -u root -p -P 6446 -h 35.223.42.238 -e 'SELECT member_host, member_state, member_role FROM performance_schema.replication_group_members;'
+-------------------------------------------+--------------+-------------+
| member_host                               | member_state | member_role |
+-------------------------------------------+--------------+-------------+
| my-cluster-mysql-0.my-cluster-mysql.mysql | ONLINE       | SECONDARY   |
| my-cluster-mysql-1.my-cluster-mysql.mysql | ONLINE       | PRIMARY     |
| my-cluster-mysql-2.my-cluster-mysql.mysql | ONLINE       | SECONDARY   |
+-------------------------------------------+--------------+-------------+

What’s coming up next?

Some exciting capabilities and features that we are going to ship pretty soon:

  • Backup and restore support for clusters with Group Replication
    • We have backups and restores in the Operator, but they currently do not work with Group Replication
  • Monitoring of MySQL Router in Percona Monitoring and Management (PMM)
    • Even though the Operator integrates nicely with PMM, it is possible to monitor MySQL nodes only, but not MySQL Router.
  • Automated Upgrades of MySQL and database components in the Operator
    • We have it in all other Operators and it is just logical to add it here

Percona is an open source company and we value our community and contributors. You are greatly encouraged to contribute to Percona Software. Please read our Contributions guide and visit our community webpage.

Aug
08
2016
--

Docker Images for MySQL Group Replication 5.7.14

MySQL Group Replication

MySQL Group ReplicationIn this post, I will point you to Docker images for MySQL Group Replication testing.

There is a new release of MySQL Group Replication plugin for MySQL 5.7.14. It’s a “beta” plugin and it is probably the last (or at lease one of the final pre-release packages) before Group Replication goes GA (during Oracle OpenWorld 2016, in our best guess).

Since it is close to GA, it would be great to get a better understanding of this new technology. Unfortunately, MySQL Group Replication installation process isn’t very user-friendly.

Or, to put it another way, totally un-user-friendly! It consists of a mere “50 easy steps” – by which I think they mean “easy” to mess up.

Matt Lord, in his post http://mysqlhighavailability.com/mysql-group-replication-a-quick-start-guide/, acknowledges: “getting a working MySQL service consisting of 3 Group Replication members is not an easy “point and click” or automated single command style operation.”

I’m not providing a review of MySQL Group Replication 5.7.14 yet – I need to play around with it a lot more. To make this process easier for myself, and hopefully more helpful to you, I’ve prepared Docker images for the testing of MySQL Group Replication.

Docker Images

To start the first node, run:

docker run -d --net=cluster1 --name=node1  perconalab/mysql-group-replication --group_replication_bootstrap_group=ON

To join all following nodes:

docker run -d --net=cluster1 --name=node2  perconalab/mysql-group-replication --group_replication_group_seeds='node1:6606'

Of course, you need to have Docker Network running:

docker network create cluster1

I hope this will make the testing process easier!

Powered by WordPress | Theme: Aeros 2.0 by TheBuckmaker.com