『叶问』#41,三节点的MGR集群,有两个节点宕机后还能正常工作吗

每周学点MGR知识。

1. 三节点的MGR集群,有两个节点宕机后还能正常工作吗

要看具体是哪种情况。

如果两个节点是正常关闭的话,则会向MGR集群发送退出信号,这种情况下,这两个节点属于正常退出,最后仅剩的节点会被提升为Primary角色,还可以正常工作,允许对其进行读写,只是此时没有可用性冗余了。当其他节点再次启动并加入集群后,又能恢复正常服务。

如果是因为网络故障,或者mysqld进程发生oom、或被误杀、或其他原因退出了,则这些节点会被标识为 UNREACHABLE 状态,等待直到 group_replication_member_expel_timeout 时长(单位:秒)后这个节点才会正式退出集群。在这种情况下,一旦超过多数派节点处于 UNREACHABLE 状态时,则整个集群不可用,无法提供读写服务。这种情况下,需要把剩下的节点重启MGR服务才能恢复。

正常情况下,不要把 group_replication_member_expel_timeout 值调整太大,并且MGR的事务一致性级别尽量不要选择 AFTER 模式,以防出现整个集群服务不可用的问题,详细参见这篇文章:为什么MGR一致性模式不推荐AFTER

2. MGR可以像主从复制那样只启动两个节点吗

MGR在初始化启动时,是可以只启动两个节点,甚至只有一个节点,但是这样就失去MGR的意义了。因为只要少于三个节点,就没办法进行多数派投票,当发生网络故障等情况时,无法投票确认哪些节点该被踢出集群。

MySQL为什么”错误”选择代价更大的索引

MySQL优化器索引选择迷思。

高鹏(八怪)对本文亦有贡献。

1. 问题描述

群友提出问题,表里有两个列c1、c2,分别为INT、VARCHAR类型,且分别创建了unique key。

SQL查询的条件是 WHERE c1 = ? AND c2 = ?,用EXPLAIN查看执行计划,发现优化器优先选择了VARCHAR类型的c2列索引。

他表示很不理解,难道不应该选择看起来代价更小的INT类型的c1列吗?

2. 问题复现

创建测试表t1:

[root@yejr.run]> CREATE TABLE `t1` (
  `c1` int NOT NULL AUTO_INCREMENT,
  `c2` int unsigned NOT NULL,
  `c3` varchar(20) NOT NULL,
  `c4` varchar(20) NOT NULL,
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k3` (`c3`),
  UNIQUE KEY `k2` (`c2`)
) ENGINE=InnoDB;

利用 mysql_random_data_load 写入一万行数据:

mysql_random_data_load -h127.0.0.1 -uX -pX yejr t1 10000

查看执行计划:

[root@yejr.run]> EXPLAIN SELECT * FROM t1 WHERE
 c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: const
possible_keys: k3,k2
          key: k3
      key_len: 82
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL

可以看到优化器的确选择了 k3 索引,而非”预期”的 k2 索引,这是为什么呢?

3. 问题分析

其实原因很简单粗暴:优化器认为这两个索引选择的代价都是一样的,只是优先选中排在前面的那个索引而已

再建一个相同的表 t2,只不过把 k2、k3 的索引创建顺序对调下:

[root@yejr.run]> CREATE TABLE `t2` (
  `c1` int NOT NULL AUTO_INCREMENT,
  `c2` int unsigned NOT NULL,
  `c3` varchar(20) NOT NULL,
  `c4` varchar(20) NOT NULL,
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k2` (`c2`),
  UNIQUE KEY `k3` (`c3`)
) ENGINE=InnoDB;

再查看执行计划:

[root@yejr.run]> EXPLAIN SELECT * FROM t2 WHERE
 c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: const
possible_keys: k2,k3
          key: k2
      key_len: 4
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL

我们利用 EXPLAIN ANALYZE 来查看下两次执行计划的代价对比:

-- 查看t1表执行计划代价
[root@yejr.run]> EXPLAIN ANALYZE SELECT * FROM t1 WHERE
  c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
EXPLAIN: -> Rows fetched before execution  (cost=0.00..0.00 rows=1) (actual time=0.000..0.000 rows=1 loops=1)

-- 查看t2表执行计划代价
[root@yejr.run]> EXPLAIN ANALYZE SELECT * FROM t2 WHERE  c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
EXPLAIN: -> Rows fetched before execution  (cost=0.00..0.00 rows=1) (actual time=0.000..0.000 rows=1 loops=1)

可以看到,很明显代价都是一样的。

再利用 OPTIMIZE_TRACE 查看执行计划,也能看到两个SQL的代价是一样的:

...
          {
            "rows_estimation": [
              {
                "table": "`t1`",
                "rows": 1,
                "cost": 1,
                "table_type": "const",
                "empty": false
              }
            ]
          },
...

所以,优化器认为选择哪个索引都是一样的,就看哪个索引排序更靠前。

从执行SELECT时的debug trace结果也能佐证:

-- 1、 T1表,k3索引在前面
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k3` (`c3`),
  UNIQUE KEY `k2` (`c2`)

T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t1`"
T@2: | | | | | | | | opt: field: "c3"   (C3在前面,因此最后使用k3)
T@2: | | | | | | | | >convert_string
T@2: | | | | | | | | | >alloc_root
T@2: | | | | | | | | | | enter: root: 0x40a8068
T@2: | | | | | | | | | | exit: ptr: 0x4b41ab0
T@2: | | | | | | | | | <alloc_root 304
T@2: | | | | | | | | <convert_string 2610
T@2: | | | | | | | | opt: equals: "'Louise Garrett'" 
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: Key_use: optimize= 0 used_tables=0x0 ref_table_rows= 18446744073709551615 keypart_map= 1
T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t1`"
T@2: | | | | | | | | opt: field: "c2"
T@2: | | | | | | | | opt: equals: "22896242"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: Key_use: optimize= 0 used_tables=0x0 ref_table_rows= 18446744073709551615 keypart_map= 1
T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t1`"
T@2: | | | | | | | | opt: field: "c2"
T@2: | | | | | | | | opt: equals: "22896242"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: ref_optimizer_key_uses: ending struct
T@2: | | | | | | | | opt: (null): ending struct

-- 2、 T2表,k2索引在前面
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k2` (`c2`),
  UNIQUE KEY `k3` (`c3`)

T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t2`"
T@2: | | | | | | | | opt: field: "c2" (C2在前面因此使用k2索引)
T@2: | | | | | | | | opt: equals: "22896242"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: Key_use: optimize= 0 used_tables=0x0 ref_table_rows= 18446744073709551615 keypart_map= 1
T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t2`"
T@2: | | | | | | | | opt: field: "c3"
T@2: | | | | | | | | >convert_string
T@2: | | | | | | | | | >alloc_root
T@2: | | | | | | | | | | enter: root: 0x40a8068
T@2: | | | | | | | | | | exit: ptr: 0x4b41ab0
T@2: | | | | | | | | | <alloc_root 304
T@2: | | | | | | | | <convert_string 2610
T@2: | | | | | | | | opt: equals: "'Louise Garrett'"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: ref_optimizer_key_uses: ending struct
T@2: | | | | | | | | opt: (null): ending struct

4. 问题延伸

到这里,我们不禁有疑问,这两个索引的代价真的是一样吗?

就让我们用 mysqlslap 来做个简单对比测试吧:

-- 测试1:对c2列随机point select
mysqlslap -hlocalhost -uroot -Smysql.sock --no-drop --create-schema X -i 3 --number-of-queries 1000000 -q "set @xid = cast(round(rand()*2147265929) as unsigned); select * from t1 where c2 = @xid" -c 8
...
    Average number of seconds to run all queries: 9.483 seconds
...


-- 测试2:对c3列随机point select
mysqlslap -hlocalhost -uroot -Smysql.sock --no-drop --create-schema X -i 3 --number-of-queries 1000000 -q "set @xid = concat('u',cast(round(rand()*2147265929) as unsigned)); select * from t1 where c3 = @xid" -c 8
...
    Average number of seconds to run all queries: 10.360 seconds
...

可以看到,如果是走 c3 列索引,耗时会比走 c2 列索引多出来约 7% ~ 9%(在我的环境下测试的结果,不同环境、不同数据量可能也不同)。

看来,MySQL优化器还是有必要进一步提高的哟 :)

测试使用版本:GreatSQL 8.0.25(MySQL 5.6.39结果亦是如此)。

Enjoy MySQL :)

『叶问』#40,MySQL进程号、连接ID、查询ID、InnoDB线程与系统线程如何对应

一文快速掌握 MySQL进程号、连接ID、查询ID、InnoDB线程与系统线程的对应关系。

有时候,怀疑某个MySQL内存查询导致CPU或磁盘I/O消耗特别高,但又不确定具体是哪个SQL引起的。

或者当InnoDB引擎内部有semaphore wait时,想知道具体是哪个线程/查询引起的。多说一下,当有semaphore wait事件超过600秒的话,InnoDB会发出crash信号:

InnoDB: ###### Diagnostic info printed to the standard error stream
2020-12-13T09:41:33.810011Z 0 [ERROR] [FATAL] InnoDB: Semaphore wait has lasted > 600 seconds. We intentionally crash the server because it appears to be hung.
2020-12-13 10:41:33 0x7f3d92a4e700 InnoDB: Assertion failure in thread 139902430013184 in file ut0ut.cc line 917
InnoDB: We intentionally generate a memory trap.
InnoDB: Submit a detailed bug report to http://bugs.mysql.com.
InnoDB: If you get repeated assertion failures or crashes, even
InnoDB: immediately after the mysqld startup, there may be
InnoDB: corruption in the InnoDB tablespace. Please refer to
InnoDB: http://dev.mysql.com/doc/refman/8.0/en/forcing-innodb-recovery.html
InnoDB: about forcing recovery.
09:41:33 UTC - mysqld got signal 6 ;

因此也要监控InnoDB的semaphore wait状态,一旦超过阈值,就要尽快报警并分析出问题原因,及时杀掉或停止引起等待的查询请求。

不过本文想讨论的是,MySQL的进程ID、内部查询ID、内部线程ID,和操作系统层的进程ID、线程如何对应起来。

1、操作系统进程ID

MySQL是一个单进程多线程的服务程序,用 ps -ef|grep mysqld 就能看到其系统进程ID了。另外,当 my.cnf 配置文件中增加一行 innodb_status_file = 1 时,也会生成带有系统进程ID的innodb status 文件

[root@yejr.run]# ps -ef | grep mysqld
mysql    38801     1  0 Jun13 ?        00:03:30 /usr/local/GreatSQL-8.0.22/bin/mysqld --defaults-file=/mysql/data06/my.cnf

[root@yejr.run]# ls -la innodb_status.38801
-rw-r----- 1 mysql mysql 4906 Jun 14 14:26 innodb_status.38801

文件 innodb_status.pid 的作用是每隔15秒左右输出innodb引擎各种状态信息,和执行 SHOW ENGINE INNODB STATUS 的作用相同。二者的区别在于,前者(文件输出方式)的输出内容长度不受限制,而后者(命令行输出)则最多只显示1MB内容,更多的会被截断。所以务必设置 innodb_status_file = 1 选项。

Standard Monitor output is limited to 1MB when produced using the SHOW ENGINE INNODB STATUS statement. This limit does not apply to output written to server standard error output (stderr).

2、系统线程和MySQL连接ID、查询ID等的关系

从MySQL 5.7开始,performance_schema.threads 表增加 THREAD_OS_ID 列,用于记录MySQL内部线程对应的系统线程ID。

创建一个新连接,并执行下面的SQL:

[root@yejr.run]# mysql -S./mysql.sock -uroot -p mymgr
...
# 查到MySQL的连接ID是25(PROCESSLIST_ID = 25)
[root@yejr.run] [none]> show processlist;
+----+------+-----------+-------+---------+------+-------+------------------+-----------+---------------+
| Id | User | Host      | db    | Command | Time | State | Info             | Rows_sent | Rows_examined |
+----+------+-----------+-------+---------+------+-------+------------------+-----------+---------------+
| 25 | root | localhost | mymgr | Query   |    0 | init  | show processlist |         0 |             0 |
+----+------+-----------+-------+---------+------+-------+------------------+-----------+---------------+

[root@yejr.run] [mymgr]> begin; select *,sleep(1000) from t1 for update;
...  <-- 这个SQL会运行很长时间,方便我们观察

新开一个窗口,查看 pfs.threads 表:

[root@yejr.run] [performance_schema]> SELECT * FROM threads WHERE PROCESSLIST_ID=25\G
*************************** 1. row ***************************
          THREAD_ID: 65  <-- MySQL内部线程ID,也是PFS的内部计数器
               NAME: thread/sql/one_connection
               TYPE: FOREGROUND
     PROCESSLIST_ID: 25  <-- MySQL连接ID
   PROCESSLIST_USER: root
   PROCESSLIST_HOST: localhost
     PROCESSLIST_DB: mymgr
PROCESSLIST_COMMAND: Query
   PROCESSLIST_TIME: 246
  PROCESSLIST_STATE: User sleep
   PROCESSLIST_INFO: select *,sleep(1000) from t1 for update  <-- 正在运行的SQL
   PARENT_THREAD_ID: NULL
               ROLE: NULL
       INSTRUMENTED: YES
            HISTORY: YES
    CONNECTION_TYPE: Socket
       THREAD_OS_ID: 58412  <-- 对应操作系统的线程ID
     RESOURCE_GROUP: USR_default

运行 ps -Lef 查看对应的系统线程

[root@yejr.run]# ps -Lef | grep 58412  <-- 上面查询pfs.threads看到 THREAD_OS_ID 列的值
mysql    38801     1 58412  0   40 14:46 ?        00:00:00 /usr/local/GreatSQL-8.0.22/bin/mysqld --defaults-file=/mysql/data06/my.cnf

如果设置了 general_log=1,那么也能看到 general_log 里有这样的记录:

[root@yejr.run]# cat yejr.run.log

/usr/local/GreatSQL-8.0.22/bin/mysqld, Version: 8.0.22-13 (Source distribution). started with:
Tcp port: 6001  Unix socket: mysql.sock
#几个列分别是:时间、连接ID、请求类型、详细的SQL
Time                 Id Command    Argument
...
2021-06-14T14:46:47.474393+08:00    25 Query select *,sleep(1000) from t1 for update  <-- 可以看到连接ID是25
...

再查询 pfs.events_statements_current 表:

[root@yejr.run] [performance_schema]> SELECT * FROM events_statements_current WHERE THREAD_ID = 65\G
              THREAD_ID: 65
               EVENT_ID: 8
           END_EVENT_ID: NULL
             EVENT_NAME: statement/sql/select
                 SOURCE: init_net_server_extension.cc:95
            TIMER_START: 82217983305961000
              TIMER_END: 82559992238886000
             TIMER_WAIT: 342008932925000
              LOCK_TIME: 206000000
               SQL_TEXT: select *,sleep(1000) from t1 for update
                 DIGEST: 4d7f4182dff4abc484010b73024b4afb35075e1b6592d14ce895a2b8764b8f46
            DIGEST_TEXT: SELECT * , `sleep` (?) FROM `t1` FOR UPDATE
         CURRENT_SCHEMA: mymgr
...
            SELECT_SCAN: 1
...
       NESTING_EVENT_ID: 6
     NESTING_EVENT_TYPE: TRANSACTION
    NESTING_EVENT_LEVEL: 0
           STATEMENT_ID: 87  <-- 查询ID,每次查询该ID值都会增加

执行 SHOW ENGINE INNODB STATUS\G 查看事务状态:

...
# 事务ID=9322,运行时长=252秒
---TRANSACTION 9322, ACTIVE 252 sec
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 1 row lock(s)
# MySQL连接ID=25,OS线程句柄 = 140442126739200(后面再介绍),查询ID=87(对应上面的 STATEMENT_ID)
MySQL thread id 25, OS thread handle 140442126739200, query id 87 localhost root User sleep
select *,sleep(1000) from t1 for update
...

3、OS thread handle和操作系统线程ID的对应关系

首先,OS thread handle 140442126739200 (OS thread handle是进程内部用于识别各个线程的内部ID),这里是个十进制的数值,需要先转成十六进制(有时候可能会直接用十六进制表示,这个问题也有人提出了 MDEV-17237):

[root@yejr.run] [performance_schema]> select lower(conv(140442126739200, 10, 16));
+--------------------------------------+
| lower(conv(140442126739200, 10, 16)) |
+--------------------------------------+
| 7fbb3b136700                         |
+--------------------------------------+

再利用 pstack 查询该句柄和操作系统线程ID的关联:

[root@yejr.run]# pstack `pidof mysqld` | grep 7fbb3b136700
Thread 2 (Thread 0x7fbb3b136700 (LWP 58412)):

可以看到 LWP = 58412,对应上面的 THREAD_OS_ID 值,LWP是Light-Weight Processes的缩写(轻量级进程)。用 pidstat 也能看到这个LWP:

[root@yejr.run]# pidstat -t -p 38801 | grep 58031
03:45:02 PM  1000         -     58031    0.00    0.00    0.00    0.00     2  |__mysqld

【特别提醒】运行pstack会短暂阻塞mysqld进程,所以请切勿在业务高峰期执行,除非万不得已

有时候可能会看到类似下面的 innodb status:

SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 15143
--Thread 140585128785664 has waited at buf0flu.cc line 1209 for 237 seconds the semaphore:
SX-lock on RW-latch at 0x7fdb1fbe3f80 created in file buf0buf.cc line 1460
a writer (thread id 140584786024192) has reserved it in mode SX
number of readers 0, waiters flag 1, lock_word: 10000000
Last time read locked in file row0sel.cc line 3758
Last time write locked in file /export/home/pb2/build/sb_0-34537258-1560179931.8/mysql-5.7.27/storage/innobase/fsp/fsp0fsp.cc line 167

在上面这段信息中,线程 Thread 140585128785664buf0flu.cc 代码1209行这里等待了237秒,想要请求 SX-lock 的 RW-latch,被另一个线程 thread id 140584786024192 给阻塞了,它持有的是 SX latch。

这时候,就可以利用pstack反查对应的LWP,再反查出PROCESSLIST_ID,以及正在执行的SQL。pfs.events_statements_current 中只能查到当前执行的SQL,可以通过 pfs.events_statements_history 查看最近执行过的SQL。

Enjoy MySQL :)

延伸阅读

  • 15.17.3 InnoDB Standard Monitor and Lock Monitor Output, https://dev.mysql.com/doc/refman/8.0/en/innodb-standard-monitor.html
  • 27.12.21.6 The threads Table, https://dev.mysql.com/doc/refman/8.0/en/performance-schema-threads-table.html
  • 27.12.6.1 The events_statements_current Table, https://dev.mysql.com/doc/refman/8.0/en/performance-schema-events-statements-current-table.html
  • 27.12.6.2 The events_statements_history Table, https://dev.mysql.com/doc/refman/8.0/en/performance-schema-events-statements-history-table.html

『叶问』#39,都有哪些情况可能导致MGR服务无法启动

『叶问』#39,都有哪些情况可能导致MGR服务无法启动

1. 都有哪些情况可能导致MGR服务无法启动

简单整理了下,大概有以下原因可能导致MGR服务无法启动:

  1. 网络原因,例如网络本来就不通,或被防火墙拦住。防火墙通常至少有两道,操作系统默认的firewall策略,以及云主机被默认的安全策略。
  2. 第一个启动的节点没先做初始引导操作(group_replication_bootstrap_group=ON)。
  3. 没有正确配置group_name,所有节点的 group_replication_group_name 值要一致才可以。
  4. 没正确配置 group_replication_group_name,常见于新手。要为MGR服务专门新开一个服务端口,常用33061端口,但新手可能会照样写成3306端口。
  5. 通常,我们会在各MGR节点的 hosts 文件里加上所有节点的hostname。这是为了防止本地节点使用的hostname和MGR收到的hostname不一致,这种情况下,可以在每个本地节点设置 report-host,主动上报hostname即可解决。
  6. 没设置正确的allowlist。有可能加入MGR各节点的IP不在默认的allowlist中,可参考这篇文章:MySQL Group Replication集群对IP地址的限制导致的一些问题与解决办法
  7. 个别节点的本地事务更多,例如误操作写入数据,也会无法加入MGR,这种情况需要重建本地节点。
  8. 个别节点的本地事务缺失太多,且加入MGR时无法自动完成恢复,这种情况比较少见,需要手动执行clone复制数据,或者其他类似操作。

更多MGR相关问题请移步到这篇文章:GreatSQL MGR FAQ

2. MySQL中怎么知道一个表的创建时间

来自群友的问题:有地方可以查看到mysql第一次初始化启动的时间吗?

这个问题可以换个角度来思考,即:怎么查的实例中MySQL元数据表初始化创建时间。

至少有两种方法可用:
方法一,查询MySQL日志。
如果MySQL实例自从初始化后的日志一直留存着的话,自然可以查到当时的时间。

方法二, 查询MySQL元数据表。

执行类似下面的命令,即可查得该实例初始化的时间:

[root@yejr.run]> SELECT TABLE_NAME, CREATE_TIME, UPDATE_TIME, CHECK_TIME
 FROM information_schema.TABLES where
  table_schema='mysql' order by create_time limit 2;
+--------------------+---------------------+---------------------+------------+
| TABLE_NAME         | CREATE_TIME         | UPDATE_TIME         | CHECK_TIME |
+--------------------+---------------------+---------------------+------------+
| innodb_table_stats | 2020-02-17 08:21:19 | 2021-11-18 21:50:17 | NULL       |
| innodb_index_stats | 2020-02-17 08:21:19 | 2021-11-18 21:50:17 | NULL       |
+--------------------+---------------------+---------------------+------------+

P.S,提这个问题好像没啥意义吧。。。

GreatSQL MGR FAQ

关于GreatSQL及MGR的FAQ,持续更新中。

Last Update: 2021.12.14。

0. GreatSQL简介

GreatSQL是由万里数据库维护的MySQL分支,开源、免费。GreatSQL基于Percona Server,在其基础上进一步提升MGR(MySQL Group Replication)的性能及可靠性。此外,GreatSQL合并了华为鲲鹏计算团队贡献的Patch,实现了InnoDB并行查询特性,以及对InnoDB事务锁的优化。

GreatSQL可以作为MySQL或Percona Server的可选替代方案,用于线上生产环境。

GreatSQL完全免费并兼容MySQL或Percona Server。

1. GreatSQL的特色有哪些

相对于MySQL官方社区版,GreatSQL有以下几个优势:

  • InnoDB性能更好
    • 支持InnoDB并行查询,TPC-H测试中平均提升聚合分析型SQL性能15倍,最高提升40多倍。
    • 优化InnoDB事务锁,tps性能可提升约10%。
  • MGR更可靠、稳定,性能也更好。
    • MGR中引入地理标签特性,主要用于解决多机房数据同步的问题。
    • MGR中优化了流控算法,运行更加平稳。
    • 解决磁盘空间爆满时导致MGR集群阻塞的问题。
    • 解决MGR多主模式下或切主时可能导致丢数据的问题。
    • 解决节点异常退出MGR集群时导致性能抖动的问题。
    • MGR节点异常状态判断更完善。
    • 重新设计MGR事务认证队列清理算法,不复存在每隔60秒性能抖动的问题。
    • 修复了recovery过程中长时间等待的问题。
    • 修复了传输大数据可能导致逻辑判断死循环问题。
    • 修复了多数派节点不同类型异常退出集群导致的视图更新的问题。

无论是更可靠的MGR还是性能更好的InnoDB,都值得将当前的MySQL或Percona Server升级到GreatSQL。

关于GreatSQL的优势可阅读下面几篇文章:

2. GreatSQL在哪里可以下载

二进制包、RPM包

二进制包下载地址:https://gitee.com/GreatSQL/GreatSQL/releases

目前提供CentOS 7、CentOS 8两种操作系统,以及X86和ARM两种不同架构下的二进制包、RPM包。

minimal 关键字的安装包是对二进制文件进行strip后,所以文件尺寸较小,功能上没本质区别,仅是不支持gdb debug功能,可以放心使用。

源码

可以直接用git clone的方式下载GreatSQL源码,例如:

# 可从gitee下载
$ git clone https://gitee.com/GreatSQL/GreatSQL.git

# 或从github下载
$ git clone https://github.com/GreatSQL/GreatSQL.git

Ansible安装包

GreatSQL提供Ansible一键安装包,可在gitee或github下载:

  • https://gitee.com/GreatSQL/GreatSQL-Ansible/releases
  • https://github.com/GreatSQL/GreatSQL-Ansible/releases

Docker镜像

GreatSQL提供Docker镜像,可直接从docker hub拉取:

# 直接下载最新版本
$ docker pull docker.io/greatsql/greatsql

# 或自行指定版本
$ docker pull docker.io/greatsql/greatsql:8.0.25

# 或指定ARM版本
$ docker pull docker.io/greatsql/greatsql:8.0.25-aarch64

3. 使用GreatSQL遇到问题时找谁

使用GreatSQL过程中如果遇到问题,可将问题细节整理清楚后,联系GreatSQL社区寻求帮助。

扫码添加GreatSQL社区助手
GreatSQL社区微信小助手

或扫码加入GreatSQL社区QQ群(533341697)
GreatSQL社区QQ群

此外,我们已经在B站发布MGR相关系列视频,可以前往学习,视频链接:https://space.bilibili.com/1363850082

4. GreatSQL版本计划是怎样的

GreatSQL不计划每个小版本都跟随,暂定奇数版本跟随方式,即 8.0.25、8.0.27、8.0.29 … 以此类推。

未来若有版本计划变更我们再更新。

5. GreatSQL支持读写分离吗

可以利用MySQL Router来实现读写分离。

6. 可以使用MySQL Shell来管理GreatSQL吗

是可以的,最好采用相同版本号的MySQL Shell即可。

7. 使用MGR有什么限制吗

下面是关于MGR使用的一些限制:

  • 所有表必须是InnoDB引擎。可以创建非InnoDB引擎表,但无法写入数据,在利用Clone构建新节点时也会报错。
  • 所有表都必须要有主键。同上,能创建没有主键的表,但无法写入数据,在利用Clone构建新节点时也会报错。
  • 不要使用大事务,默认地,事务超过150MB会报错,最大可支持2GB的事务(在GreatSQL未来的版本中,会增加对大事务的支持,提高大事务上限)。
  • 如果是从旧版本进行升级,则不能选择 MINIMAL 模式升级,建议选择 AUTO 模式,即 upgrade=AUTO
  • 由于MGR的事务认证线程不支持 gap lock,因此建议把所有节点的事务隔离级别都改成 READ COMMITTED。基于相同的原因,MGR集群中也不要使用 table lockname lock(即 GET_LOCK() 函数 )。
  • 在多主(multi-primary)模式下不支持串行(SERIALIZABLE)隔离级别。
  • 不支持在不同的MGR节点上,对同一个表分别执行DML和DDL,可能会造成数据丢失或节点报错退出。
  • 在多主(multi-primary)模式下不支持多层级联外键表。另外,为了避免因为使用外键造成MGR报错,建议设置 group_replication_enforce_update_everywhere_checks=ON
  • 在多主(multi-primary)模式下,如果多个节点都执行 SELECT ... FOR UPDATE 后提交事务会造成死锁。
  • 不支持复制过滤(Replication Filters)设置。

看起来限制有点多,但绝大多数时候并不影响正常的业务使用。

此外,想要启用MGR还有几个要求:
– 每个节点都要启用binlog。
– 每个节点都要转存binlog,即设置 log_slave_updates=1
– binlog format务必是row模式,即 binlog_format=ROW
– 每个节点的 server_idserver_uuid 不能相同。
– 在8.0.20之前,要求 binlog_checksum=NONE,但是从8.0.20后,可以设置 binlog_checksum=CRC32
– 要求启用 GTID,即设置 gtid_mode=ON
– 要求 master_info_repository=TABLErelay_log_info_repository=TABLE,不过从MySQL 8.0.23开始,这两个选项已经默认设置TABLE,因此无需再单独设置。
– 所有节点上的表名大小写参数 lower_case_table_names 设置要求一致。
– 最好在局域网内部署MGR,而不要跨公网,网络延迟太大的话,会导致MGR性能很差或很容易出错。
– 建议启用writeset模式,即设置以下几个参数
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = N,N>0,可以设置为逻辑CPU数的2倍
binlog_transaction_dependency_tracking = WRITESET
slave_preserve_commit_order = 1
slave_checkpoint_period = 2

8. MGR最多可支持多少个节点

MGR最多可支持9个节点,无论是单主还是多主模式。

9. MGR可以设置为自启动吗

设置参数 group_replication_bootstrap_group=ON 即可。但是当MGR第一个节点初始化启动时,或者整个MGR集群都关闭再重启时,第一个节点都必须先采用引导模式 group_replication_bootstrap_group=ON

10. MGR支持读负载均衡吗

支持的。可以在MGR集群的前端挂载MySQL Router,即可实现读负载均衡。

11. MGR支持写负载均衡吗

不支持。由于MGR采用shared nothing模式,每个节点都存储全量数据,因此所有写入每个节点都要再应用一次。

12. MGR相对传统主从复制是不是会更耗CPU、内存和带宽等资源

一定程度上来说,是的。因为MGR需要在多个节点间进行事务冲突检测,不过这方面的开销有限,总体来说也还好。

13. 为什么启动MGR后,多了个33061端口

当启用MGR服务后,MySQL会监听33061端口,该端口用于MGR节点间的通信。因此当服务器间有防火墙策略时,记得针对该端口开放。

当然了,可自行定义该端口,例如 group_replication_local_address=192.168.0.1:33062

14. 部署MGR时,务必对所有节点都设置hostname吗

这个不是必须的。

之所以要在每个节点上都加上各节点的hostname对照表,是因为在MGR节点间通信过程中,可能收到的主机名和本地实际配置的不一致。

这种情况下,也可以在每个节点上自行设置 report_hostreport_port 来解决这个问题。

15. 可以跨公网部署MGR吗

可以的,但非常不推荐。

此外,由于MGR默认的allowlist不包含公网地址,因此需要将公网地址加进去,例如:

group_replication_ip_allowlist='192.0.2.0/24, 114.114.114.0/24'

顺便提醒下,MGR默认的allowlist范围(group_replication_ip_allowlist=AUTOMATIC)是以下几个

IPv4 (as defined in RFC 1918)
10/8 prefix       (10.0.0.0 - 10.255.255.255) - Class A
172.16/12 prefix  (172.16.0.0 - 172.31.255.255) - Class B
192.168/16 prefix (192.168.0.0 - 192.168.255.255) - Class C

IPv6 (as defined in RFC 4193 and RFC 5156)
fc00:/7 prefix    - unique-local addresses
fe80::/10 prefix  - link-local unicast addresses

127.0.0.1 - localhost for IPv4
::1       - localhost for IPv6

有时候docker容器的IP地址不在上述范围中,也会导致MGR服务无法启动。

16. 怎么查看MGR当前是单主还是多主模式

执行下面的命令:

[root@GreatSQL]> SELECT * FROM performance_schema.replication_group_members;
+---------------------------+-----------...-+-------------+--------------+-------------+----------------+
| CHANNEL_NAME              | MEMBER_ID ... | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION |
+---------------------------+-----------...-+-------------+--------------+-------------+----------------+
| group_replication_applier | 4ebd3504-1... |        3306 | ONLINE       | SECONDARY   | 8.0.25         |
| group_replication_applier | 549b92bf-1... |        3307 | ONLINE       | SECONDARY   | 8.0.25         |
| group_replication_applier | 5596116c-1... |        3308 | ONLINE       | SECONDARY   | 8.0.25         |
| group_replication_applier | ed5fe7ba-3... |        3309 | ONLINE       | PRIMARY     | 8.0.25         |
+---------------------------+-----------...-+-------------+--------------+-------------+----------------+

如果只看到一个节点的 MEMBER_ROLE 值为 PRIMARY,则表示这是单主模式。如果看到所有节点上该状态值均为 PRIMARY,则表示这是多主模式。

另外,也可以通过查询MySQL选项值来确认:

[root@GreatSQL]# mysqladmin var|grep -i group_replication_single_primary_mode
| group_replication_single_primary_mode        | ON

值为 ON,这表示采用单主模式。如果该值为 OFF,则表示采用多主模式。

在MySQL Shell中也可以查看状态来确认:

MySQL  GreatSQL:3306 ssl  JS > var c=dba.getCluster()
MySQL  GreatSQL:3306 ssl  JS > c.describe() /* 或者 c.status() */
...
        "topologyMode": "Single-Primary"
...

P.S,强烈建议采用单主模式,遇到bug或其他问题的概率更低,运行MGR更稳定可靠。

17. 怎么切换单主或多主

在MySQL客户端命令行模式下,执行下面的命令即可:

-- 从单主切换为多主
[root@GreatSQL]> SELECT group_replication_switch_to_multi_primary_mode();
+--------------------------------------------------+
| group_replication_switch_to_multi_primary_mode() |
+--------------------------------------------------+
| Mode switched to multi-primary successfully.     |
+--------------------------------------------------+

-- 从多主切换为单主
[root@GreatSQL]> SELECT group_replication_switch_to_single_primary_mode();
+---------------------------------------------------+
| group_replication_switch_to_single_primary_mode() |
+---------------------------------------------------+
| Mode switched to single-primary successfully.     |
+---------------------------------------------------+

注意: 切换时会重新选主,新的主节点有可能不是切换之前的那个,这时可以运行下面的命令来重新指定:

[root@GreatSQL]> SELECT group_replication_set_as_primary('ed5fe7ba-37c2-11ec-8e12-70b5e873a570');
+--------------------------------------------------------------------------+
| group_replication_set_as_primary('ed5fe7ba-37c2-11ec-8e12-70b5e873a570') |
+--------------------------------------------------------------------------+
| Primary server switched to: ed5fe7ba-37c2-11ec-8e12-70b5e873a570         |
+--------------------------------------------------------------------------+

也可以通过MySQL Shell来操作:

MySQL  GreatSQL:3306 ssl  JS > var c=dba.getCluster()
> c.switchToMultiPrimaryMode()  /*切换为多主模式*/
Switching cluster 'MGR27' to Multi-Primary mode...

Instance 'GreatSQL:3306' was switched from SECONDARY to PRIMARY.
Instance 'GreatSQL:3307' was switched from SECONDARY to PRIMARY.
Instance 'GreatSQL:3308' was switched from SECONDARY to PRIMARY.
Instance 'GreatSQL:3309' remains PRIMARY.

The cluster successfully switched to Multi-Primary mode.

> c.switchToSinglePrimaryMode()  /*切换为单主模式*/
Switching cluster 'MGR27' to Single-Primary mode...

Instance 'GreatSQL:3306' remains PRIMARY.
Instance 'GreatSQL:3307' was switched from PRIMARY to SECONDARY.
Instance 'GreatSQL:3308' was switched from PRIMARY to SECONDARY.
Instance 'GreatSQL:3309' was switched from PRIMARY to SECONDARY.

WARNING: The cluster internal session is not the primary member anymore. For cluster management operations please obtain a fresh cluster handle using dba.getCluster().

WARNING: Existing connections that expected a R/W connection must be disconnected, i.e. instances that became SECONDARY.

The cluster successfully switched to Single-Primary mode.

> c.setPrimaryInstance('GreatSQL:3309');  /*重新设置主节点*/
Setting instance 'GreatSQL:3309' as the primary instance of cluster 'MGR27'...

Instance 'GreatSQL:3306' was switched from PRIMARY to SECONDARY.
Instance 'GreatSQL:3307' remains SECONDARY.
Instance 'GreatSQL:3308' remains SECONDARY.
Instance 'GreatSQL:3309' was switched from SECONDARY to PRIMARY.

The instance 'GreatSQL:3309' was successfully elected as primary.

P.S,强烈建议采用单主模式,遇到bug或其他问题的概率更低,运行MGR更稳定可靠。

18. 怎么查看MGR从节点是否有延迟

首先,可以执行下面的命令查看当前除了 PRIMARY 节点外,其他节点的 trx_tobe_appliedtrx_tobe_verified 值是否较大:

[root@GreatSQL]> SELECT MEMBER_ID AS id, COUNT_TRANSACTIONS_IN_QUEUE AS trx_tobe_verified, COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE AS trx_tobe_applied, COUNT_TRANSACTIONS_CHECKED AS trx_chkd, COUNT_TRANSACTIONS_REMOTE_APPLIED AS trx_done, COUNT_TRANSACTIONS_LOCAL_PROPOSED AS proposed FROM performance_schema.replication_group_member_stats;
+--------------------------------------+-------------------+------------------+----------+----------+----------+
| id                                   | trx_tobe_verified | trx_tobe_applied | trx_chkd | trx_done | proposed |
+--------------------------------------+-------------------+------------------+----------+----------+----------+
| 4ebd3504-11d9-11ec-8f92-70b5e873a570 |                 0 |                0 |   422248 |        6 |   422248 |
| 549b92bf-11d9-11ec-88e1-70b5e873a570 |                 0 |           238391 |   422079 |   183692 |        0 |
| 5596116c-11d9-11ec-8624-70b5e873a570 |              2936 |           238519 |   422115 |   183598 |        0 |
| ed5fe7ba-37c2-11ec-8e12-70b5e873a570 |              2976 |           238123 |   422167 |   184044 |        0 |
+--------------------------------------+-------------------+------------------+----------+----------+----------+

其中,trx_tobe_applied 的值表示等待被apply的事务队列大小,trx_tobe_verified 表示等待被认证的事务队列大小,这二者任何一个值大于0,都表示当前有一定程度的延迟。

另外,也可以查看接收到的事务和已执行完的事务之间的差距来判断:

[root@GreatSQL]> SELECT RECEIVED_TRANSACTION_SET FROM performance_schema.replication_connection_status WHERE  channel_name = 'group_replication_applier' UNION ALL SELECT variable_value FROM performance_schema.global_variables WHERE  variable_name = 'gtid_executed'\G
*************************** 1. row ***************************
RECEIVED_TRANSACTION_SET: 6cfb873b-573f-11ec-814a-d08e7908bcb1:1-3124520
*************************** 2. row ***************************
RECEIVED_TRANSACTION_SET: 6cfb873b-573f-11ec-814a-d08e7908bcb1:1-3078139

可以看到,接收到的事务 GTID 已经到了 3124520,而本地只执行到 3078139,二者的差距是 46381。可以顺便持续关注这个差值的变化情况,估算出本地节点是否能追平延迟,还是会加大延迟。

19. MySQL Router支持单机多实例部署吗

是的,支持。
在MySQL Router初始化部署时,添加 --name--directory 及端口号等参数即可,例如:

-- 部署第一个实例
root@GreatSQL# mysqlrouter --bootstrap mymgr@192.168.1.1:3306 --name=MGR1 --directory=/etc/mysqlrouter/MGR1  --user=mysqlrouter --conf-base-port=6446 --https-port=8443

-- 部署第二个实例
root@GreatSQL# mysqlrouter --bootstrap mymgr@192.168.1.1:4306 --name=MGR2 --directory=/etc/mysqlrouter/MGR2  --user=mysqlrouter --conf-base-port=7446 --https-port=9443

然后每个实例用各自目录下的 start.shstop.sh 脚本启停即可。

关于MySQL Router多实例部署的方法,可以参考这篇参考文档:《叶问》38期,MGR整个集群挂掉后,如何才能自动选主,不用手动干预