MySQL InnoDB Cluster环境搭建和简单测试

InnoDB Cluster初印象

记得MySQL Group Replicatioin 刚开始的时候,MySQL界很是轰动,等待了多年,终于有了官方的这个高可用解决方案。你要说还有一些方案补充,比如MySQL Cluster,MySQL Proxy,这些的使用率个人感觉还是不高,也就是经受的考验还不够,原因有很多,就不赘述了。

不久,我和一个MySQL DBA有了下面的一个基本对话。

我: MySQL GR GA之后,里面的自动切换功能确实很赞,能够做到读写分离,原本MHA的方案现在MGR也可以做了。

MySQL DBA:如果数据库发生了故障,这个自动切换的过程,其实对于应用不是透明的,因为读写节点相当于漂移到了另外一台服务器上,除非再做个中间件。

我:单纯MGR目前还做不了这个,它目前只是保证数据库层面的这种切换和高可用。

MySQL DBA:所以说MGR的企业级应用还是需要一些辅助,这样才算是一个完整的解决方案。

不久,MySQL InnoDB Cluster推出,我觉得这个方案想比原来的MGR更进一步,说实话,我很看好这个方案,尽管目前愿意真正去用的用户确实不多。

如果你看一下官方的这个架构图,就会发现,MGR本身就是Innodb Cluster的一部分,还有两个组件,MySQL Shell,MySQL Router,这三板斧就是InnoDB Cluster的一个核心组件,而正如我之前所说,可以看到MySQL的一个格局和定位,他正在很努力去解决以前诟病的问题。

未分类

安装前先保证Python满足要求

要安装InnoDB Cluster,环境的一个基本要求就是Python,我看了下,很多默认的系统版本是2.6,而它的最低要求是2.7及以上,所以还是需要提前准备下这个部分。

如果你的系统是Python 2.6版本的,可以考虑升级到2.7,参考如下的方法。

下载安装包,部署

wget http://python.org/ftp/python/2.7/Python-2.7.tar.bz2     --no-check-certificate
./configure
make all
make install
make clean
make distclean

查看Python的版本

# /usr/local/bin/python2.7 -V
Python 2.7

做基本的环境设置,替换旧的Python

mv /usr/bin/python /usr/bin/python2.6
ln -s /usr/local/bin/python2.7 /usr/bin/python

sandbox安装部署InnoDB Cluster

搭建InnoDB Cluster显而易见需要多台服务器,而如果在一台服务器上练习测试,也是全然没有问题,如果想更快更方便的测试模拟,还可以使用sandbox来做,首先你得有sandbox,接着InnoDB Cluster的三大组件是MGR,MySQL Shell,MySQL Router,所以你可以从官网直接下载下来。

然后我们开启安装之旅。

使用MySQL Shell的命令mysqlsh开始部署,创建一个端口为3310的实例

mysql-js> dba.deploySandboxInstance(3310)
A new MySQL sandbox instance will be created on this host in
/root/mysql-sandboxes/3310

输入密码之后,一个3310端口的MySQL服务就启动了。

Please enter a MySQL root password for the new instance:
Deploying new MySQL instance...
Instance localhost:3310 successfully deployed and started.
Use shell.connect('root@localhost:3310'); to connect to the instance.

接着创建另外两个节点 3320,3330

dba.deploySandboxInstance(3320)
dba.deploySandboxInstance(3330)

我们切换到3310的MySQL实例,准备开始创建Cluster

mysql-js>  connect root@localhost:3310
Creating a Session to 'root@localhost:3310'
Enter password:
Closing old connection...
Classic Session successfully established. No default schema selected.

定义一个Cluster变量,节点1就开启了Cluster创建之旅,可以从下面的信息看出,至少需要3个节点

mysql-js>  var cluster = dba.createCluster('testCluster')
A new InnoDB cluster will be created on instance 'root@localhost:3310'.
Creating InnoDB cluster 'testCluster' on 'root@localhost:3310'...
Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to  one server failure.

接着把另外两个节点加入进来,先加入端口为3320的节点

mysql-js> cluster.addInstance('root@localhost:3320')
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.
Please provide the password for 'root@localhost:3320':
Adding instance to the cluster ...加入端口为3330的节点,日志和节点2相似。

mysql-js> cluster.addInstance('root@localhost:3330')

这个时候Cluster就创建好了。

这个时候,我们再配置一下MySQL Router,创建个软链接,保证能够正常调用。

# ln -s /home/innodb_cluster/mysql-router-2.1.3-linux-glibc2.12-x86-64bit/bin/mysqlrouter   /usr/bin/mysqlroute
# which mysqlroute
/usr/bin/mysqlroute

配置MySQL Router的启动节点为端口3310的实例

# mysqlrouter –bootstrap root@localhost:3310 –user=mysql

这个时候还是要输入密码,成功之后,这个绑定就打通了。

Please enter MySQL password for root:
Bootstrapping system MySQL Router instance...
MySQL Router  has now been configured for the InnoDB cluster 'testCluster'.
The following connection information can be used to connect to the cluster.
Classic MySQL protocol connections to cluster 'testCluster':
- Read/Write Connections: localhost:6446
- Read/Only Connections: localhost:6447
X protocol connections to cluster 'testCluster':
- Read/Write Connections: localhost:64460
- Read/Only Connections: localhost:64470

可以从上面的日志看出来,分配的读写端口是6446,只读端口是6447,还有x协议连接的端口为64460,64470

启动MySQL Router

# mysqlrouter &
[1] 2913

如果对MySQL Router还有些疑问,可以看看安装目录下,会生成下面的配置文件,我们就看里面的.conf文件,里面的一部分内容如下:

[routing:testCluster_default_rw]
bind_address=0.0.0.0
bind_port=6446
destinations=metadata-cache://testCluster/default?role=PRIMARY
mode=read-write
protocol=classic

验证测试

我们尝试使用6446来连接登录,这个时候就通过MySQL Shell开启了连接入口,MySQL Router做了转接,连接到了里面的读写节点3310

# mysqlsh --uri root@localhost:6446
Creating a Session to 'root@localhost:6446'
Enter password:
Classic Session successfully established. No default schema selected.
Welcome to MySQL Shell 1.0.9

切换到sql模式,查看端口就知道是哪个节点了。

mysql-js> sql
Switching to SQL mode... Commands end with ;
mysql-sql> select @@port;
+--------+
| @@port |
+--------+
|   3310 |
+--------+
1 row in set (0.00 sec)

如果切换为脚本模式查看实例的状态,可以使用里面定义的API来做,输出都是JSON串。

mysql-js> dba.configureLocalInstance('[email protected]:3310')
Please provide the password for '[email protected]:3310':
Detected as sandbox instance.
Validating MySQL configuration file at: /root/mysql-sandboxes/3310/my.cnf
Validating instance...
The instance '127.0.0.1:3310' is valid for Cluster usage
You can now use it in an InnoDB Cluster.
{
"status": "ok"
}如果查看Cluster的信息,可以看到下面的读写节点,只读节点的状态信息

mysql-js> dba.getCluster()
<Cluster:testCluster>

得到Cluster的信息

var cluster = dba.getCluster()
mysql-js> cluster.status()
{
"clusterName": "testCluster",
"defaultReplicaSet": {
"name": "default",
"primary": "localhost:3310",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"localhost:3310": {
"address": "localhost:3310",
"mode": "R/W",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
},
"localhost:3320": {
"address": "localhost:3320",
"mode": "R/O",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
},
"localhost:3330": {
"address": "localhost:3330",
"mode": "R/O",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
}
}
}
}

也可以使用describe得到一些基本的信息

mysql-js> cluster.describe();
{
"clusterName": "testCluster",
"defaultReplicaSet": {
"instances": [
{
"host": "localhost:3310",
"label": "localhost:3310",
"role": "HA"
},
{
"host": "localhost:3320",
"label": "localhost:3320",
"role": "HA"
},
{
"host": "localhost:3330",
"label": "localhost:3330",
"role": "HA"
}
],
"name": "default"
}
}

切换测试

当然光看不练还是假把式,我们切换一下,看看好使不?

模拟一个节点出现问题,可以使用killSandboxInstance方法。

mysql-js> dba.killSandboxInstance(3310)
The MySQL sandbox instance on this host in
/root/mysql-sandboxes/3310 will be killed
Killing MySQL instance...
Instance localhost:3310 successfully killed.

节点被清理了,没有任何进程存在。

# ps -ef|grep mysql|grep 3310
#

我们还是使用6446的端口来统一连接,这个时候就切换到了端口3320的MySQL服务

# mysqlsh --uri root@localhost:6446
mysql-js> sql
Switching to SQL mode... Commands end with ;
mysql-sql> select @@port;
+--------+
| @@port |
+--------+
|   3320 |
+--------+
1 row in set (0.00 sec)

所以切换的部分没有问题,我们再次把“迷失”的节点启动起来。

# mysqlsh --uri root@localhost:6446
mysql-js> dba.startSandboxInstance(3310)
The MySQL sandbox instance on this host in
/root/mysql-sandboxes/3310 will be started
Starting MySQL instance...
Instance localhost:3310 successfully started.

这个时候再次查看Cluster的状态,3320就是主了,3310就是只读节点了。

mysql-js> dba.getCluster()
<Cluster:testCluster>

把节点2纳入到Cluster中

mysql-js> cluster.rejoinInstance('root@localhost:3310')
Rejoining the instance to the InnoDB cluster. Depending on the original
problem that made the instance unavailable, the rejoin operation might not be
successful and further manual steps will be needed to fix the underlying
problem.
Please monitor the output of the rejoin operation and take necessary action if
the instance cannot rejoin.
Please provide the password for 'root@localhost:3310':
Rejoining instance to the cluster ...
The instance 'root@localhost:3310' was successfully rejoined on the cluster.
The instance 'localhost:3310' was successfully added to the MySQL Cluster.
mysql-js>

可以想象如果是一个生产系统,这么多的日志,这个过程真是让人纠结。
最后来一个切换后的Cluster状态

mysql-js> cluster.status()
{
"clusterName": "testCluster",
"defaultReplicaSet": {
"name": "default",
"primary": "localhost:3320",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"localhost:3310": {
"address": "localhost:3310",
"mode": "R/O",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
},
"localhost:3320": {
"address": "localhost:3320",
"mode": "R/W",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
},
"localhost:3330": {
"address": "localhost:3330",
"mode": "R/O",
"readReplicas": {},
"role": "HA",
"status": "ONLINE"
}
}
}
}

zabbix配置percona的插件获取更详细的mysql监控信息

一、前言

前段时间用了下zabbix+percona的插件监控MySql,感觉不错,内置了190多个监控项,40多个图形展示。对MySql的监控比较全面,自己导入模版之后稍微根据自己的需求进行修改就可以使用。

https://www.percona.com/doc/percona-monitoring-plugins/1.1/zabbix/index.html

二、部署

部署前提:你已经在MySql数据库添加安装好了zabbix-agent环境,并且在zabbix-server端添加agent

1、安装percona仓库

rpm -ivh http://www.percona.com/downloads/percona-release/redhat/0.1-3/percona-release-0.1-3.noarch.rpm

2、部署监控环境

Percona的监控插件是php编写的。通过php连接MySql来获取相关数据,所以需要你在本地安装php环境和php-MySql环境

yum -y install php php-mysql

3、安装percona的zabbix模版

yum -y install percona-zabbix-templates

安装完成生成如下文件

# rpm -ql percona-zabbix-templates
/var/lib/zabbix/percona
/var/lib/zabbix/percona/scripts
/var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh
/var/lib/zabbix/percona/scripts/ss_get_mysql_stats.php
/var/lib/zabbix/percona/templates
/var/lib/zabbix/percona/templates/userparameter_percona_mysql.conf
/var/lib/zabbix/percona/templates/zabbix_agent_template_percona_mysql_server_ht_2.0.9-sver1.1.6.xml

可以看到他的组成部分有一个shell脚本,一个php脚本,一个zabbix的配置文件,和一个Zabbix 模版的xml。下面我们需要引用配置文件,并在Zabbix上来导入该模版。

4、复制用户自定义配置项到zabbix配置目录

cp /var/lib/zabbix/percona/templates/userparameter_percona_mysql.conf /etc/zabbix/zabbix_agentd.d/

5、让php可以连接到MySql上

授权监控用户

MariaDB [(none)]> grant select,process,replication client on *.* to zabbix@'192.168.10.31' identified by 'zabbixpass';
MariaDB [(none)]> flush privileges;

修改php文件

vim /var/lib/zabbix/percona/scripts/ss_get_mysql_stats.php
$mysql_user = 'zabbix';
$mysql_pass = 'zabbixpass';

修改监控脚本HOST选项,为MySql服务器IP地址

# vim /var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh
HOST=192.168.10.31

6、测试监控脚本是否可用

# /var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh gg
405647

7、导入zabbix模版

将/var/lib/zabbix/percona/templates/zabbix_agent_template_percona_mysql_server_ht_2.0.9-sver1.1.6.xml下载到本地,然后在zabbix上导入

未分类

下面你就可以给你的MySQL数据库指定监控模板了,由于篇幅原因就不在截图。

三、监控结果展示

未分类

未分类

未分类

未分类

未分类

Zabbix 3使用Percona Monitoring Plugins监控MySQL

因为Zabbix自带的MySQL监控没有提供可以直接使用的Key,所以一般不采用,业界的同学们都使用Percona Monitoring Plugins 监控MySQL的方式。

1、MySQL客户端需要的系统环境

percona监控zabbix的脚本是使用php写的,所以需要准备好php运行环境,这里直接用yum安装就可以满足要求了:

$ yum install zabbix-agent php php-mysql
$ php -v
PHP 7.0.9 (cli) (built: Jul 20 2016 18:08:08) ( NTS )
$ mysql --version
mysql  Ver 14.14 Distrib 5.5.51, for Linux (x86_64) using readline 5.1

2、安装percona-zabbix-templates

$ yum install https://www.percona.com/downloads/percona-monitoring-plugins/1.1.6/percona-zabbix-templates-1.1.6-1.noarch.rpm

也可配置Percona yum仓库后安装,建议配置percona的yum源,因为作为一个DBA你肯定会用到percona toolkit套件。

$ yum install https://www.percona.com/redir/downloads/percona-release/redhat/latest/percona-release-0.1-3.noarch.rpm
$ yum install percona-zabbix-templates

3、percona-zabbix-templates配置文件介绍

$ rpm -ql percona-zabbix-templates
# 监控获取MySQL状态的;
/var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh

# 配置连接数据库用户名密码的,用shell来调用PHP;
/var/lib/zabbix/percona/scripts/ss_get_mysql_stats.php

# Zabbix-agent端监控MySQL的配置文件;
/var/lib/zabbix/percona/templates/userparameter_percona_mysql.conf

# Zabbix模板文件;
/var/lib/zabbix/percona/templates/zabbix_agent_template_percona_mysql_server_ht_2.0.9-sver1.1.6.xml

4、配置zabbix-agent

把配置文件复制到zabbix-agent目录下。

$ cp /var/lib/zabbix/percona/templates/userparameter_percona_mysql.conf /etc/zabbix/zabbix_agentd.d/

默认zabbix-agent配置文件已经把/etc/zabbix/zabbix_agentd.d/目录include进去了。

$ grep Include /etc/zabbix/zabbix_agentd.conf | grep -v "^#"
Include=/etc/zabbix/zabbix_agentd.d/

5、重新启动zabbix-agent即可

# CentOS6
$ service zabbix-agent restart

# CentOS7
$ systemctl restart zabbix-agent

6、zabbix-agent上配置MySQL连接

首先如果你要监控的MySQL不是默认的端口3306(是就忽略此步骤),则需要修改/var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh脚本,小心被坑。修改端口号码文件:

# 这里要先建好这个文件,否则会报rm权限错误;
$ touch /tmp/localhost-mysql_cacti_stats.txt:3307
$ chown -R zabbix.zabbix /tmp/localhost-mysql_cacti_stats.txt:3307

# 修改端口脚本;
$ vim /var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh
CACHEFILE="/tmp/$HOST-mysql_cacti_stats.txt "
换成:
CACHEFILE="/tmp/$HOST-mysql_zabbix_stats.txt:3307"

TIMEFLM=`stat -c %Y /tmp/$HOST-mysql_cacti_stats.txt`
换成:
TIMEFLM=`stat -c %Y /tmp/$HOST-mysql_zabbix_stats.txt:3307`

如果你的MySQL是编译安装的,执行客户端命令需要绝对路径的话,你可能还需要更改脚本中的执行命令语句:

RES=`HOME=~zabbix mysql -e 'SHOW SLAVE STATUSG' | egrep '(Slave_IO_Running|Slave_SQL_Running):' | awk -F: '{print $2}' | tr 'n' ','`
换成:
RES=`/usr/local/mysql/bin/mysql -e 'SHOW SLAVE STATUSG' | egrep '(Slave_IO_Running|Slave_SQL_Running):' | awk -F: '{print $2}' | tr 'n' ','`

(创建监控MySQL的用户和密码。

mysql> GRANT SELECT,PROCESS,SUPER,REPLICATION CLIENT ON *.* TO 'zabbixmoniter'@'localhost' IDENTIFIED BY "123456";
mysql> flush privileges;

然后PHP脚本连接MySQL的用户名和密码。

$ cat /var/lib/zabbix/percona/scripts/ss_get_mysql_stats.php
$mysql_user = 'zabbixmoniter';
$mysql_pass = '123456';
$mysql_port = 3306;

测试脚本

$ bash /var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh gg
908

7、配置Zabbix Server 3.0

先去percona官网:https://www.percona.com/downloads/percona-monitoring-plugins/

下载Percona Monitoring Plugins最新tar.gz源码包。解压里面有cacti、nagios、zabbix不同监控模块,找到zabbix/templates/目录,然后通过Zabbix Web界面 (Configuration -> Templates -> Import) 导入XML模板,注意要另外选择上Screens。最后配置主机关联上Percona MySQL Server Template模板即可。

针对Zabbix server 3.0导入模板文件时可能会报错:

Invalid tag “/zabbix_export/date”:“YYYY-MM-DDThh:mm:ssZ” is expected.

经过核查,这个原因是模版错误,是因为percona官方上1.1.6还是zabbix2.0的版本模版,所以需要跟新上来匹配zabbix3.0的模版规范,而且这个zabbix模版还是直接copy的cacti模版,比较粗糙,用more查看就可以看到里面都是cacti的内容,可见percona官网对zabbix也不是很重视嘛,至少不如cacti那样重视的。

解决:网上有人在原来的zabbix_agent_template_percona_mysql_server_ht_2.0.9-sver1.1.6_1.xml之上进行修改,改完后就可以import导入进去了,(下载地址:http://jaminzhang.github.io/soft-conf/Zabbix/zbx_percona_mysql_template.xml)。

模板导入进去之后,接下来要做的就是对主机进行添加模板了,当然这属于zabbix的知识了,自行看博客zabbix章节。percona这套MySQL监控项很多,需要好好细看才能理解。

8、MySQL从库监控Slave配置

先在mysql slave的agent里面需要check下看是否生效:

$ sh /var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh running-slave                                                                         
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
0

所以需要配置下访问mysql的一些基本参数:

$ cat /etc/my.cnf
[mysql]
user=zabbixmoniter
password=ys_ipowerlong0418
socket=/usr/local/mysql/mysql.sock

再check试试,就可以看到了:

$ sh /var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh running-slave
1

mysql查看连接数命令show processlist详解

经常会遇到一些连接数超标或者mysql链接出现异常(比如表被锁等)导致的报错,此时直接重启不仅影响线上的业务而且指标不治本。

我们处理方式一般是不重启服务的前提下处理异常的表列,同时排查是服务器问题、mysql参数配置问题还是程序、或表本身设计的问题。

比如这里连接数:

[root@21yunwei /]# mysql   -uroot  -p123456  -e  "use 21yunwei;show processlist;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+------+-----------+----------+---------+------+----------+------------------+
| Id | User | Host      | db       | Command | Time | State    | Info             |
+----+------+-----------+----------+---------+------+----------+------------------+
| 27 | root | localhost | 21yunwei | Query   |    0 | starting | show processlist |
+----+------+-----------+----------+---------+------+----------+------------------+

show processlist查看方式:

mysqladmin  -uroot -p123456  processlist;
mysql   -uroot  -p123456  -e  "show processlist;"

说明:

  • 如果是以root用户登录,可以查看所有的进程信息。
  • 如果是以普通用户登录,只可以查看自己对应的数据库的信息。

如下信息取于酷喃博客:

  • 先简单说一下各列的含义和用途。
  • id列,不用说了吧,一个标识,你要kill一个语句的时候很有用。
  • user列,显示单前用户,如果不是root,这个命令就只显示你权限范围内的sql语句。
  • host列,显示这个语句是从哪个ip的哪个端口上发出的。呵呵,可以用来追踪出问题语句的用户。
  • db列,显示这个进程目前连接的是哪个数据库。
  • command列,显示当前连接的执行的命令,一般就是休眠(sleep),查询(query),连接(connect)。
  • time列,此这个状态持续的时间,单位是秒。
  • state列,显示使用当前连接的sql语句的状态,很重要的列,后续会有所有的状态的描述,请注意,state只是语句执行中的某一个状态,一个sql语句,已查询为例,可能需要经过copying to tmp table,Sorting result,Sending data等状态才可以完成。
  • info列,显示具体执行的sql语句,因为长度有限,所以长的sql语句就显示不全,但是一个判断问题语句的重要依据。

这个命令中最关键的就是state列,mysql列出的状态主要有以下几种:

  • Checking table
    正在检查数据表(这是自动的)。
     
  • Closing tables
    正在将表中修改的数据刷新到磁盘中,同时正在关闭已经用完的表。这是一个很快的操作,如果不是这样的话,就应该确认磁盘空间是否已经满了或者磁盘是否正处于重负中。
     
  • Connect Out
    复制从服务器正在连接主服务器。
     
  • Copying to tmp table on disk
    由于临时结果集大于tmp_table_size,正在将临时表从内存存储转为磁盘存储以此节省内存。
     
  • Creating tmp table
    正在创建临时表以存放部分查询结果。
     
  • deleting from main table
    服务器正在执行多表删除中的第一部分,刚删除第一个表。
     
  • deleting from reference tables
    服务器正在执行多表删除中的第二部分,正在删除其他表的记录。
     
  • Flushing tables
    正在执行FLUSH TABLES,等待其他线程关闭数据表。
     
  • Killed
    发送了一个kill请求给某线程,那么这个线程将会检查kill标志位,同时会放弃下一个kill请求。MySQL会在每次的主循环中检查kill标志位,不过有些情况下该线程可能会过一小段才能死掉。如果该线程程被其他线程锁住了,那么kill请求会在锁释放时马上生效。
     
  • Locked
    被其他查询锁住了。
     
  • Sending data
    正在处理SELECT查询的记录,同时正在把结果发送给客户端。
     
  • Sorting for group
    正在为GROUP BY做排序。
     
  • Sorting for order
    正在为ORDER BY做排序。
     
    Opening tables
    这个过程应该会很快,除非受到其他因素的干扰。例如,在执ALTER TABLE或LOCK TABLE语句行完以前,数据表无法被其他线程打开。正尝试打开一个表。
     
  • Removing duplicates
    正在执行一个SELECT DISTINCT方式的查询,但是MySQL无法在前一个阶段优化掉那些重复的记录。因此,MySQL需要再次去掉重复的记录,然后再把结果发送给客户端。
     
  • Reopen table
    获得了对一个表的锁,但是必须在表结构修改之后才能获得这个锁。已经释放锁,关闭数据表,正尝试重新打开数据表。
     
  • Repair by sorting
    修复指令正在排序以创建索引。
     
  • Repair with keycache
    修复指令正在利用索引缓存一个一个地创建新索引。它会比Repair by sorting慢些。
     
  • Searching rows for update
    正在讲符合条件的记录找出来以备更新。它必须在UPDATE要修改相关的记录之前就完成了。
     
  • Sleeping
    正在等待客户端发送新请求.
     
  • System lock
    正在等待取得一个外部的系统锁。如果当前没有运行多个mysqld服务器同时请求同一个表,那么可以通过增加–skip-external-locking参数来禁止外部系统锁。
     
  • Upgrading lock
    INSERT DELAYED正在尝试取得一个锁表以插入新记录。
     
  • Updating
    正在搜索匹配的记录,并且修改它们。
     
  • User Lock
    正在等待GET_LOCK()。
     
  • Waiting for tables
    该线程得到通知,数据表结构已经被修改了,需要重新打开数据表以取得新的结构。然后,为了能的重新打开数据表,必须等到所有其他线程关闭这个表。以下几种情况下会产生这个通知:FLUSH TABLES tbl_name, ALTER TABLE, RENAME TABLE, REPAIR TABLE, ANALYZE TABLE,或OPTIMIZE TABLE。
     
  • waiting for handler insert
    INSERT DELAYED已经处理完了所有待处理的插入操作,正在等待新的请求。

大部分状态对应很快的操作,只要有一个线程保持同一个状态好几秒钟,那么可能是有问题发生了,需要检查一下。

参考资料:

mysql手册所有状态说明,链接如下:http://dev.mysql.com/doc/refman/5.0/en/general-thread-states.html

mysql show processlist详解 http://coolnull.com/1230.html

实例分析MySQL下的四种事务隔离级别

数据库事务有四种隔离级别:

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
  • 提交读(Read Committed):只能读取到已经提交的数据,Oracle等多数数据库默认都是该级别。
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻读。
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。

上面这样的教科书式定义第一次接触事务隔离概念的朋友看了可能会一脸懵逼,下面我们就通过具体的实例来解释四个隔离级别。

首先我们创建一个user表:

CREATE TABLE user (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(255) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE `uniq_name` USING BTREE (name)
) ENGINE=`InnoDB` AUTO_INCREMENT=10 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

读未提交隔离级别

我们先将事务的隔离级别设置为read committed:

mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED       |
+------------------------+
1 row in set (0.00 sec)

在下面我们开了两个终端分别用来模拟事务一和事务二,p.s: 操作一和操作二的意思是按照时间顺序来执行的。

事务1

mysql> start transaction; # 操作1
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(name) values('ziwenxie'); # 操作3
Query OK, 1 row affected (0.05 sec)

事务2

mysql> start transaction; # 操作2
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user; # 操作4
+----+----------+
| id | name     |
+----+----------+
| 10 | ziwenxie |
+----+----------+
1 row in set (0.00 sec)

从上面的执行结果可以和清晰的看出来,在read uncommited级别下面我们在事务一中可能会读取到事务二中没有commit的数据,这就是脏读。

读提交隔离级别

通过设置隔离级别为committed可以解决上面的脏读问题。

mysql> set session transaction isolation level read committed;

事务一

mysql> start transaction; # 操作一
Query OK, 0 rows affected (0.00 sec)


mysql> select * from user; # 操作三
+----+----------+
| id | name     |
+----+----------+
| 10 | ziwenxie |
+----+----------+
1 row in set (0.00 sec)

mysql> select * from user; # 操作五,操作四的修改并没有影响到事务一
+----+----------+
| id | name     |
+----+----------+
| 10 | ziwenxie |
+----+----------+
1 row in set (0.00 sec)

mysql> select * from user; # 操作七

+----+------+
| id | name |
+----+------+
| 10 | lisi |
+----+------+
1 row in set (0.00 sec)

mysql> commit; # 操作八
Query OK, 0 rows affected (0.00 sec)

事务二

mysql> start transaction; # 操作二
Query OK, 0 rows affected (0.00 sec)

mysql> update user set name='lisi' where id=10; # 操作四
Query OK, 1 row affected (0.06 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit; # 操作六
Query OK, 0 rows affected (0.08 sec)

虽然脏读的问题解决了,但是注意在事务一的操作七中,事务二在操作六commit后会造成事务一在同一个transaction中两次读取到的数据不同,这就是不可重复读问题,使用第三个事务隔离级别repeatable read可以解决这个问题。

可重复读隔离级别

MySQL的Innodb存储引擎默认的事务隔离级别就是可重复读隔离级别,所以我们不用进行多余的设置。

事务一

mysql> start tansactoin; # 操作一

mysql> select * from user; # 操作五
+----+----------+
| id | name     |
+----+----------+
| 10 | ziwenxie |
+----+----------+
1 row in set (0.00 sec)

mysql> commit; # 操作六
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user; # 操作七
+----+------+
| id | name |
+----+------+
| 10 | lisi |
+----+------+
1 row in set (0.00 sec)

事务二

mysql> start tansactoin; # 操作二

mysql> update user set name='lisi' where id=10; # 操作三
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit; # 操作四

在事务一的操作五中我们并没有读取到事务二在操作三中的update,只有在commit之后才能读到更新后的数据。

Innodb解决了幻读么

实际上RR级别是可能产生幻读,InnoDB引擎官方称中利用MVCC多版本并发控制解决了这个问题,下面我们验证一下Innodb真的解决了幻读了么?

为了方便展示,我修改了一下上面的user表:

mysql> alter table user add salary int(11);
Query OK, 0 rows affected (0.51 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> delete from user;
Query OK, 1 rows affected (0.07 sec)

mysql> insert into user(name, salary) value('ziwenxie', 88888888);
Query OK, 1 row affected (0.07 sec)

mysql> select * from user;
+----+----------+----------+
| id | name     | salary   |
+----+----------+----------+
| 10 | ziwenxie | 88888888 |
+----+----------+----------+
1 row in set (0.00 sec)

事务一

mysql> start transaction;  # 操作一
Query OK, 0 rows affected (0.00 sec)

mysql> update user set salary='4444'; # 操作六,竟然影响了两行,不是说解决了幻读么?
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0

mysql> select * from user; # 操作七, Innodb并没有完全解决幻读
+----+----------+--------+
| id | name     | salary |
+----+----------+--------+
| 10 | ziwenxie |   4444 |
| 11 | zhangsan |   4444 |
+----+----------+--------+
2 rows in set (0.00 sec)

mysql> commit; # 操作八
Query OK, 0 rows affected (0.04 sec)

事务二

mysql> start transaction; # 操作二
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(name, salary) value('zhangsan', '666666'); # 操作四
Query OK, 1 row affected (0.00 sec)

mysql> commit; # 操作五
Query OK, 0 rows affected (0.04 sec)

从上面的例子可以看出,Innodb并没有如官方所说解决幻读,不过上面这样的场景中也不是很常见不用过多的担心。

串行化隔离级别

所有事务串行执行,最高隔离级别,不会出现幻读性能会很差,实际开发中很少使用到。

配置Vsftpd MySQL虚拟用户验证登录

一、安装所需要包和包组:

在数据库服务器上安装包:

yum –y install mariadb-server mariadb-devel 
systemctl start mariadb.service 
systemctl enable mariadb

在FTP服务器上安装包pam_mysql

centos7:无对应rpm包,需手动编译安装

yum -y groupinstall “Development Tools” 
yum -y install mariadb-devel pam-devel vsftpd 

下载pam_mysql-0.7RC1.tar.gz

ftp://172.16.0.1/pub/Sources/sources/pam/ 
tar xvfpam_mysql-0.7RC1.tar.gz 
cd pam_mysql-0.7RC1/ 
./configure –with-mysql=/usr –with-pam=/usr –with-pam-mods-dir=/lib64/security 
make 
make install

二、在数据库服务器上创建虚拟用户账号

1、建立存储虚拟用户的数据库

示例创建为vsftpd数据库

mysql> CREATE DATABASE vsftpd; 
mysql> SHOW DATABASES; 

ftp服务和mysql不在同一主机:

mysql> GRANT SELECT ON vsftpd.* TO 
vsftpd@’172.16.%.%’ IDENTIFIED BY ‘magedu’; 

ftp服务和mysql在同一主机:

mysql> GRANT SELECT ON vsftpd.* TO 
vsftpd@localhost IDENTIFIED BY ‘magedu’; 
mysql> GRANT SELECT ON vsftpd.* TO 
vsftpd@’127.0.0.1′ IDENTIFIED BY ‘magedu’; 
mysql> FLUSH PRIVILEGES;

2、准备相关表

mysql> USE vsftpd; 
Mysql> SHOW TABLES; 
mysql> CREATE TABLE users ( id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,name CHAR(50) BINARY NOT NULL,password CHAR(48) BINARY NOT NULL); 
mysql>DESC users; 

mysql -uvsftpd -h 172.16.200.200 -pmagedu 
mysql> SHOW DATABASES;

3、添加测试的虚拟用户

根据需要添加所需要的用户,为了安全应该使用PASSWORD函数加密其密码后存储

mysql>DESC users; 
mysql> INSERT INTO users(name,password) values(‘wang’,password(‘magedu’)); 
mysql> INSERT INTO users(name,password) values(‘mage’,password(‘magedu’)); 
mysql> SELECT * FROM users;

三、在FTP服务器上配置vsftpd服务

1、在FTP服务器上建立pam认证所需文件

vi /etc/pam.d/vsftpd.mysql 

添加如下两行

auth required pam_mysql.so user=vsftpd passwd=magedu host=mysqlserver db=vsftpd table=users usercolumn=name passwdcolumn=password crypt=2 
account required pam_mysql.so user=vsftpd passwd=magedu host=mysqlserver db=vsftpd table=users usercolumn=name passwdcolumn=password crypt=2 

注意:参考README文档,选择正确的加密方式crypt是加密方式,0表示不加密,1表示crypt(3)加密,2表示使用mysql password()函数加密,3表示md5加密,4表示sha1加密

配置字段说明

  • auth 表示认证
  • account 验证账号密码正常使用
  • required 表示认证要通过
  • pam_mysql.so模块是默认的相对路径,是相对/lib64/security/路径而言,也可以写绝对路径;后面为给此模块传递的参数
  • user=vsftpd为登录mysql的用户
  • passwd=mageedu 登录mysql的的密码
  • host=localhost 在mysql中定义的允许连接的主机名或ip地址
  • db=vsftpd 连接msyql的哪一个库
  • table=users 连接库里的哪一个表
  • usercolumn=name 当做用户名的字段
  • passwdcolumn=password 当做用户名字段的密码
  • crypt=2 密码的加密方式为mysql password()函数加密

2、建立相应用户和修改vsftpd配置文件

使其适应mysql认证

建立虚拟用户映射的系统用户及对应的目录

useradd -s /sbin/nologin -d /var/ftproot vuser 
chmod 555 /var/ftproot centos7 需除去ftp根目录的写权限 
mkdir /var/ftproot/{upload,pub} 
setfacl –m u:vuser:rwx /var/ftproot/upload 

确保/etc/vsftpd.conf中已经启用了以下选项

anonymous_enable=YES 

添加下面两项

guest_enable=YES 
guest_username=vuser 

修改下面一项,原系统用户无法登录

pam_service_name=vsftpd.mysql

四、启动vsftpd服务

service vsftpd start;systemctl start vsftpd 
chkconfig vsftpd on;systemctl enable vsftpd 

查看端口开启情况

netstat -tnlp |grep :21

五、Selinux相关设置:在FTP服务器上执行

restorecon -R /lib64/security 
setsebool -P ftpd_connect_db 1 
setsebool -P ftp_home_dir 1 
chcon -R -t public_content_rw_t /var/ftproot/

六、测试:利用FTP客户端工具,以虚拟用户登录验证结果

tail /var/log/secure

七、在FTP服务器上配置虚拟用户具有不同的访问权限

vsftpd可以在配置文件目录中为每个用户提供单独的配置文件以定义其ftp服务访问权限,每个虚拟用户的配置文件名同虚拟用户的用户名。配置文件目录可以是任意未使用目录,只需要在vsftpd.conf指定其路径及名称即可

1、配置vsftpd为虚拟用户使用配置文件目录

vim /etc/vsftpd/vsftpd.conf 

添加如下选项

user_config_dir=/etc/vsftpd/vusers_config

2、创建所需要目录

并为虚拟用户提供配置文件

mkdir /etc/vsftpd/vusers_config/ 
cd /etc/vsftpd/vusers_config/ 
touch wangmage

3、配置虚拟用户的访问权限

虚拟用户对vsftpd服务的访问权限是通过匿名用户的相关指令进行的。如果需要让用户wang具有上传文件的权限,可以修改/etc/vsftpd/vusers_config/wang文件,在里面添加如下选项并设置为YES即可,只读则设为NO

注意:需确保对应的映射用户对于文件系统有写权限

anon_upload_enable={YES|NO} 
anon_mkdir_write_enable={YES|NO} 
anon_other_write_enable={YES|NO}

MySQL调优/优化的100 个建议

MySQL是一个强大的开源数据库。随着MySQL上的应用越来越多,MySQL逐渐遇到了瓶颈。这里提供 101 条优化 MySQL 的建议。有些技巧适合特定的安装环境,但是思路是相通的。我已经将它们分成了几类以帮助你理解。

MySQL监控MySQL服务器硬件和OS(操作系统)调优

1、有足够的物理内存,能将整个InnoDB文件加载到内存里 —— 如果访问的文件在内存里,而不是在磁盘上,InnoDB会快很多。

2、全力避免 Swap 操作 — 交换(swapping)是从磁盘读取数据,所以会很慢。

3、使用电池供电的RAM(Battery-Backed RAM)。

4、使用一个高级磁盘阵列 — 最好是 RAID10 或者更高。

5、避免使用RAID5 — 和校验需要确保完整性,开销很高。

6、将你的操作系统和数据分开,不仅仅是逻辑上要分开,物理上也要分开 — 操作系统的读写开销会影响数据库的性能。

7、将临时文件和复制日志与数据文件分开 — 后台的写操作影响数据库从磁盘文件的读写操作。

8、更多的磁盘空间等于更高的速度。

9、磁盘速度越快越好。

10、SAS优于SATA。

11、小磁盘的速度比大磁盘的更快,尤其是在 RAID 中。

12、使用电池供电的缓存 RAID(Battery-Backed Cache RAID)控制器。

13、避免使用软磁盘阵列。

14、考虑使用固态IO卡(不是磁盘)来作为数据分区 — 几乎对所有量级数据,这种卡能够支持 2 GBps 的写操作。

15、在 Linux 系统上,设置 swappiness 的值为0 — 没有理由在数据库服务器上缓存文件,这种方式在Web服务器或桌面应用中用的更多。

16、尽可能使用 noatime 和 nodirtime 来挂载文件系统 — 没有必要为每次访问来更新文件的修改时间。

17、使用 XFS 文件系统 — 一个比ext3更快的、更小的文件系统,拥有更多的日志选项,同时,MySQL在ext3上存在双缓冲区的问题。

18、优化你的 XFS 文件系统日志和缓冲区参数 – -为了获取最大的性能基准。

19、在Linux系统中,使用 NOOP 或 DEADLINE IO 调度器 — CFQ 和 ANTICIPATORY 调度器已经被证明比 NOOP 和 DEADLINE 慢。

20、使用 64 位操作系统 — 有更多的内存能用于寻址和 MySQL 使用。

21、将不用的包和后台程序从服务器上删除 — 减少资源占用。

22、将使用 MySQL 的 host 和 MySQL自身的 host 都配置在一个 host 文件中 — 这样没有 DNS 查找。

23、永远不要强制杀死一个MySQL进程 — 你将损坏数据库,并运行备份。

24、让你的服务器只服务于MySQL — 后台处理程序和其他服务会占用数据库的 CPU 时间。

MySQL 配置

25、使用 innodb_flush_method=O_DIRECT 来避免写的时候出现双缓冲区。

26、避免使用 O_DIRECT 和 EXT3 文件系统 — 这会把所有写入的东西序列化。

27、分配足够 innodb_buffer_pool_size ,来将整个InnoDB 文件加载到内存 — 减少从磁盘上读。

28、不要让 innodb_log_file_size 太大,这样能够更快,也有更多的磁盘空间 — 经常刷新有利降低发生故障时的恢复时间。

29、不要同时使用 innodb_thread_concurrency 和 thread_concurrency 变量 — 这两个值不能兼容。

30、为 max_connections 指定一个小的值 — 太多的连接将耗尽你的RAM,导致整个MySQL服务器被锁定。

31、保持 thread_cache 在一个相对较高的数值,大约是 16 — 防止打开连接时候速度下降。

32、使用 skip-name-resolve — 移除 DNS 查找。

33、如果你的查询重复率比较高,并且你的数据不是经常改变,请使用查询缓存 — 但是,在经常改变的数据上使用查询缓存会对性能有负面影响。

34、增加 temp_table_size — 防止磁盘写。

35、增加 max_heap_table_size — 防止磁盘写。

36、不要将 sort_buffer_size 的值设置的太高 — 可能导致连接很快耗尽所有内存。

37、监控 key_read_requests 和 key_reads,以便确定 key_buffer 的值 — key 的读需求应该比 key_reads 的值更高,否则使用 key_buffer 就没有效率了。

38、设置 innodb_flush_log_at_trx_commit = 0 可以提高性能,但是保持默认值(1)的话,能保证数据的完整性,也能保证复制不会滞后。

39、有一个测试环境,便于测试你的配置,可以经常重启,不会影响生产环境。

MySQL Schema 优化:

40、保证你的数据库的整洁性。

41、归档老数据 — 删除查询中检索或返回的多余的行

42、在数据上加上索引。

43、不要过度使用索引,评估你的查询。

44、压缩 text 和 blob 数据类型 — 为了节省空间,减少从磁盘读数据。

45、UTF 8 和 UTF16 比 latin1 慢。

46、有节制的使用触发器。

47、保持数据最小量的冗余 — 不要复制没必要的数据.

48、使用链接表,而不是扩展行。

49、注意你的数据类型,尽可能的使用最小的。

50、如果其他数据需要经常需要查询,而 blob/text 不需要,则将 blob/text 数据域其他数据分离。

51、经常检查和优化表。

52、经常做重写 InnoDB 表的优化。

53、有时,增加列时,先删除索引,之后在加上索引会更快。

54、为不同的需求选择不同的存储引擎。

55、日志表或审计表使用ARCHIVE存储引擎 — 写的效率更高。

56、将 session 数据存储在 memcache 中,而不是 MySQL 中 — memcache 可以设置自动过期,防止MySQL对临时数据高成本的读写操作。

57、如果字符串的长度是可变的,则使用VARCHAR代替CHAR — 节约空间,因为CHAR是固定长度,而VARCHAR不是(utf8 不受这个影响)。

58、逐步对 schema 做修改 — 一个小的变化将产生的巨大的影响。

59、在开发环境测试所有 schema 变动,而不是在生产环境的镜像上去做。

60、不要随意改变你的配置文件,这可能产生非常大的影响。

61、有时候,少量的配置会更好。

62、质疑使用通用的MySQL配置文件。

查询优化

63、使用慢查询日志,找出执行慢的查询。

64、使用 EXPLAIN 来决定查询功能是否合适。

65、经常测试你的查询,看是否需要做性能优化 — 性能可能会随着时间的变化而变化。

66、避免在整个表上使用count(*) ,它可能会将整个表锁住。

67、保持查询一致,这样后续类似的查询就能使用查询缓存了。

68、如果合适,用 GROUP BY 代替 DISTINCT。

69、在 WHERE、GROUP BY 和 ORDER BY 的列上加上索引。

70、保证索引简单,不要在同一列上加多个索引。

71、有时,MySQL 会选择错误的索引,这种情况使用 USE INDEX。

72、使用 SQL_MODE=STRICT 来检查问题。

73、索引字段少于5个时,UNION 操作用 LIMIT,而不是 OR。

74、使用 INSERT ON DUPLICATE KEY 或 INSERT IGNORE 来代替 UPDATE,避免 UPDATE 前需要先 SELECT。

75、使用索引字段和 ORDER BY 来代替 MAX。

76、避免使用 ORDER BY RAND()。

77、LIMIT M,N 在特定场景下会降低查询效率,有节制使用。

78、使用 UNION 来代替 WHERE 子句中的子查询。

79、对 UPDATE 来说,使用 SHARE MODE 来防止排他锁。

80、重启 MySQL 时,记得预热数据库,确保将数据加载到内存,提高查询效率。

81、使用 DROP TABLE ,然后再 CREATE TABLE ,而不是 DELETE FROM ,以删除表中所有数据。

82、最小化你要查询的数据,只获取你需要的数据,通常来说不要使用 *。

83、考虑持久连接,而不是多次建立连接,已减少资源的消耗。

84、基准查询,包括服务器的负载,有时一个简单的查询会影响其他的查询。

85、当服务器的负载增加时,使用SHOW PROCESSLIST来查看慢的/有问题的查询。

86、在存有生产环境数据副本的开发环境中,测试所有可疑的查询。

MySQL备份过程

87、在二级复制服务器上进行备份。

88、备份过程中停止数据的复制,以防止出现数据依赖和外键约束的不一致。

89、彻底停止MySQL之后,再从数据文件进行备份。

90、如果使用MySQL dump进行备份,请同时备份二进制日志 — 确保复制过程不被中断。

91、不要信任 LVM 快照的备份 — 可能会创建不一致的数据,将来会因此产生问题。

92、为每个表做一个备份,这样更容易实现单表的恢复 — 如果数据与其他表是相互独立的。

93、使用 mysqldump 时,指定 -opt 参数。

94、备份前检测和优化表。

95、临时禁用外键约束,来提高导入的速度。

96、临时禁用唯一性检查,来提高导入的速度。

97、每次备份完后,计算数据库/表数据和索引的大小,监控其增长。

98、使用定时任务(cron)脚本,来监控从库复制的错误和延迟。

99、定期备份数据。

100、定期测试备份的数据。

MySQL主键 数据类型及索引的优化

一、安装

这是同事分享给我的。安装数据库也是一门学问,千万不要被安装的简单性而忽略一些细节。针对于 Win os 服务器而言,MySQL 的安装版可以选则三种不同的服务器类型:

  • Developer Machine(开发机器)
    为 MySQL 分配最少分系统资源

  • Server Machine(服务器)
    为 MySQL 分配一定比例的系统资源

  • Dedicated MySQL Server Machine(专用MySQL服务器)
    为 MySQL 分配所有的系统资源

如果你跟我一样不幸,不仅选择了 Win os 做服务器系统,还选择了 Developer Machine(开发机器),兄弟抱一个,不要哭,重装。发生这些上述不幸的原因已经不重要,需要做的是必须切换成 Dedicated MySQL Server Machine(专用MySQL服务器)。

重装切换之后,你会发现,之前安装的一定是假的 MySQL。

二、主键

主键或者唯一键可以用作某条记录的唯一标志符。主键生成有两种方式:

  • 自增
  • UUID

1、自增

自增方式有个重要因素是“步长”,也就是则增的幅度,在单机模式下,一般步长为1。若是在分布式数据库系统下,步长设为节点的数量,这样一来,就可以避免主键重复的情况。建议预估好节点的数量,步长不可小于节点数。

2、UUID

UUID 可以更有效地避免自增主键带来的烦恼,但是它也有不足之处:

  • UUID 过长,增大数据库总容量,降低性能
  • UUID 无序,插入数据时根据主键寻址费时

针对上述 UUID 的缺点,推特开发了“雪花算法”,并开源。其中心思想是利用时间戳、数据中心码、机器码、序列号组成有规则的 UUID,使其有序降低性能消耗。

我在项目中使用了“自增+步长2”,因为使用了主从,虽然不是分布式,但是双数据源也是两个节点,采用这种方式保险一些。

更多内容请移步我的其他博文:数据库自增长主键与-UUID (http://zhoupq.com/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%87%AA%E5%A2%9E%E9%95%BF%E4%B8%BB%E9%94%AE%E4%B8%8E-UUID/)
  
推荐 MySQL 使用自增ID主键和UUID 作为主键的优劣比较详细过程(从百万到千万表记录测试) (http://blog.csdn.net/mchdba/article/details/52336203)

三、数据类型

1、长度

我一直秉承杀鸡就用杀鸡刀,宰牛采用宰牛刀的原则。一个“tab_NAME” 的数据类型非要整一个 “VARCHAR(500)”,这是浪费,过多的长度分配会造成空间占用太多,最终造成性能下降。有同事说我杞人忧天,一个库才二十几张表,即使每个字段都设成500,也不过如此嘛。从短期的结果上来看,结果没有受到明显影响。但是别忘了,我们是来解决问题的,如果因为我们的操作违反了约定,造成严重后果,那么我们将背负罪过。“量身定做”的好处不言而喻,列的长度亦是如此。

2、NULL

尽量不将列设为 NULL,从业务角度上看,NULL 是错误的,试想,既然是 NULL,哪有何必存在这个列呢?反之,既然存在这个列,那么 NULL 便失去了意义。让我设计表的时候,我都会给列设置一个初始值,“tab_UPDATETIME” 就设置为 “CURRENT_TIMESTAMP”,“tab_STATUS” 就设置为 “1”或者“0”。

从开发维护的角度看,如果不确定列是否为 NULL,那么在 SQL 中,就必须加上 “AND tab_NAME != NULL AND tab_NAME != ‘’”,很容易被忽略,代码越多,出错的概率就越大。

四、索引

好的索引是一颗仙丹,可以让迟缓的查询得到质的提升,否则,就是一碗毒药。

索引我做了三点优化:

  • 勿滥用索引
  • 最左前缀索引
  • 前缀索引

1、勿滥用索引

索引不是越多越好,因为生成索引需要时间,而且索引占表物理空间。表一大,查询速度多少会受影响。我亲眼看到同事建好表时候,无微不至地为每一个字段都建了索引,或者为每一个条件都建了索引,形如条件“a=1 and b =2 and c=3 and d=4”,为其建了“a”、“b”、“c”、“d”四个索引。浪费!低效!这种情况,应当使用“最左前缀索引”。

2、最左前缀索引

在生成联合索引时会碰到最左前缀索引,什么是最左前缀索引呢?就是在联合索引中,从最左边的索引开始匹配,直到遇上“like”、“>”、“>=”、“<”、“<=”等范围匹配时停止,即使后面有“=”都不再匹配。

简单举例:现有字段“a”、“b”、“c”、“d”组成的联合索引“abcd”,SQL 条件部分为:

a=1 and b =2 and c=3 and d=4
a=1 and b =2 and d=4 and c=3
a=1 and b =2 and c>=3 and d=4

  
1 用到索引为“abcd”,2 用到的索引为“abd”, (2 同 1)3 用到的索引为“abc”。条件的顺序很重要。跟自拍一样,脸大的站后面。
  
抱歉,上述第二点同第一点,同样用到的索引为“abcd”。
  
利用 EXPLAIN 工具分析:

// 建表语句略,已知创建了组合索引 (abcd) 
mysql> EXPLAIN SELECT * FROM test t WHERE t.a = 'q' AND t.b = 'w' AND t.c = 'e' AND t.d = 'f';
+----+-------------+-------+------+---------------+------+---------+-------------------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref                     | rows | Extra                    |
+----+-------------+-------+------+---------------+------+---------+-------------------------+------+--------------------------+
|  1 | SIMPLE      | t     | ref  | name          | name | 360     | const,const,const,const |    1 | Using where; Using index |
+----+-------------+-------+------+---------------+------+---------+-------------------------+------+--------------------------+
1 row in set
mysql> EXPLAIN SELECT * FROM test t WHERE t.a = 'q' AND t.b = 'w' AND t.d = 'f' AND t.c = 'e';
+----+-------------+-------+------+---------------+------+---------+-------------------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref                     | rows | Extra                    |
+----+-------------+-------+------+---------------+------+---------+-------------------------+------+--------------------------+
|  1 | SIMPLE      | t     | ref  | name          | name | 360     | const,const,const,const |    1 | Using where; Using index |
+----+-------------+-------+------+---------------+------+---------+-------------------------+------+--------------------------+
1 row in set

3、前缀索引

前缀索引是针对某个字段而言,我们知道 MySQL 中,有一个全文索引,“BOLD”、“TEXT” 是不能建全文索引。想想也能理解,这么长的全文索引得占多少空间,这显然是不现实的。最好的办法是为其创建“左前缀索引”,只取字符串的前面一小段作为索引,具体取多少,决定于多长的字符可以尽可能多的确定唯一记录。“varchar”同样受用。

更多内容请移步我的其他博文:MySQL 高性能索引之前缀索引 (http://zhoupq.com/MySQL-%E9%AB%98%E6%80%A7%E8%83%BD%E7%B4%A2%E5%BC%95%E4%B9%8B%E5%89%8D%E7%BC%80%E7%B4%A2%E5%BC%95/)

五、多表联合查询

不管是 Heibernate 在代码中拼 SQL,还是 MyBatis 在 Mapper.xml 中写 SQL,由于数据库范式的规范,导致为完成某项查询,必须联合多表查询。一个 LEFT JOIN 很常见,三四个 LEFT JOIN 呢?

多个 LEFT JOIN 肯定不行,即使有索引,也很容易造成全表扫描,为了减少该情况发生的概率,我一般会采取两种方法:

  • 反范式
  • 临时表

1、反范式

衡量一个 DBA 的水平有多高,得看他反范式能力有多强。
—— 知乎

比如我要根据 A表 的日期,关联 B表,统计出每个日期下某个属性的数量。我可以在A表中添加一列,用来存储“数量”,虽然违反了范式,但是性能上得到了提升。我觉得这是一笔划算的买卖。

规范化是为了技术服务,而技术是为业务服务。规范化也就是套路,能保证不出错,但是并不能解决特殊问题,特殊问题还需要特殊处理。

2、临时表

当需要联合三张表以上时,轻微的反范式已经不适用了,推荐用临时表,或者物化视图,但是 MySQL 的物化视图实现起来比较困难。事实上,我用的就是临时表,将四张表的部分数据抽离出来,保存在一张临时表中,制定一个“计划”,每天凌晨会自动更新。

好处

  • 加快查询速度

缺点

  • 会在某一时刻(凌晨)数据库IO过高
  • 可能会出现异常,做好事务管理,让其回滚,重新执行,再有问题,就需要人工干预
  • 数据准确性会延迟一天,适合非敏感业务

以上是仅针对数据库做的优化,至于缓存(一级缓存、二级缓存),那属于持久层框架的职责,不在此文记录范围之内。

使用Nginx lua把日志实时存储到mysql

准备材料

  • Nginx.1.12安装包 (下载http://nginx.org/en/download.html)

  • mysql 自行安装

  • Lua 相关包
    LuaJIT (下载 http://luajit.org/download.html)
    ngx_devel_kit (下载 https://github.com/simpl/ngx_devel_kit/tags)
    ngx_lua (下载 https://github.com/openresty/lua-nginx-module/tags)
    pcre (下载 https://sourceforge.net/projects/pcre/files/pcre/)
    lua_mysql (下载 https://github.com/openresty/lua-resty-mysql)

安装

LuaJIT 安装

tar zxf LuaJIT-2.0.5.tar.gz
cd LuaJIT-2.0.5
make
make install PREFIX=/home/myself/lj2

pcre 安装

tar -zxvf pcre-8.32.tar.gz
cd pcre-8.32
make
make install

Nginx 安装

export LUAJIT_LIB=/path/to/luajit/lib
export LUAJIT_INC=/path/to/luajit/include/luajit-2.1
./configure --prefix=/opt/nginx 
     --with-ld-opt="-Wl,-rpath,/path/to/luajit-or-lua/lib" 
     --add-module=/path/to/ngx_devel_kit 
     --add-module=/path/to/lua-nginx-module
make
make install

配置文件

user  root;
worker_processes  2;

events {
worker_connections  1024;
}


http{
lua_package_path "/home/oicq/guomm/nginx_lua/lua-resty-mysql-master/lib/?.lua;;"; --重要
lua_shared_dict logs 10m;

init_worker_by_lua_block {
    local delay = 10
    function put_log_into_mysql(premature)      
            local mysql = require "resty.mysql"
            local db, err = mysql:new()
            if not db then
                ngx.log(ngx.ERR,"failed to instantiate mysql: ", err)
                return
            end

            db:set_timeout(1000)
            local ok, err, errcode, sqlstate = db:connect{
                host = "xxx",
                port = 3306,
                database = "database_name",
                user = "username",
                password = "password",
                charset = "utf8",
            }

            if not ok then
                ngx.log(ngx.ERR,"failed to connect: ", err, ": ", errcode, " ", sqlstate)
                return
            end

            -- get data from shared dict and put them into mysql
            local key = "logs"
            local vals = ""
            local temp_val = ngx.shared.logs:lpop(key)
            while (temp_val ~= nil)
            do
                vals = vals .. ",".. temp_val
                temp_val = ngx.shared.logs:lpop(key)
            end

            if vals ~= "" then
                vals = string.sub(vals, 2,-1)
                local command = ("insert into es_visit_record(access_ip,server_ip,access_time,run_time,es_response_time,request_body_byte,run_state,url,post_data) values "..vals)
                ngx.log(ngx.ERR,"command is ",command)
                local res, err, errcode, sqlstate = db:query(command)
                if not res then
                    ngx.log(ngx.ERR,"insert error: ", err, ": ", errcode, ": ", sqlstate, ".")
                    return
                end
            end

            local ok, err = db:close()
            if not ok then
                ngx.log(ngx.ERR,"failed to close: ", err)
                return
            end
            -- decycle call timer to run put_log_into_mysql method, just like crontab
            local ok, err = ngx.timer.at(delay, put_log_into_mysql);
            if not ok then
                ngx.log(ngx.ERR, "failed to create timer: ", err)
                return
            end
    end

    local ok, err = ngx.timer.at(delay, put_log_into_mysql)
    if not ok then
        ngx.log(ngx.ERR, "failed to create timer: ", err)
        return
    end
}


upstream elasticsearch_servers {
    server xxx max_fails=3 fail_timeout=30s;
    server xxx max_fails=3 fail_timeout=30s;
    server xx max_fails=3 fail_timeout=30s;
}

log_format  porxy  '$remote_addr,$upstream_addr,[$time_local],$request,$request_body,$status,$body_bytes_sent,$request_time,$upstream_response_time';

server {
    listen 9202;
    location / {

        proxy_pass http://elasticsearch_servers;

        log_by_lua_block{

            local currentTime = os.date("%Y-%m-%d %H:%M:%S", os.time())
            currentTime = """ .. currentTime .. """

            local req_body = '-'
            if ngx.var.request_body then
                req_body = ngx.var.request_body
                req_body = string.gsub(req_body,"n","")
                --req_body = string.gsub(req_body,"t","")
            end
            req_body = """ .. req_body .. """

            local req_status = 0
            if ngx.var.status then
                req_status = ngx.var.status
            end

            local req_time = 0
            if ngx.var.request_time then
                req_time = ngx.var.request_time
            end

            local req_req = """ .. ngx.var.request .. """
            local remote_addr = """ .. ngx.var.http_x_forwarded_for .. """
            local server_addr = """ .. ngx.var.upstream_addr .. """
            local myparams = ("("..remote_addr..",".. server_addr..","..currentTime..","..ngx.var.request_time .. ",".. ngx.var.upstream_response_time..","..ngx.var.body_bytes_sent..","..ngx.var.status..","..req_req..","..req_body..")")
            local key = "logs"
            local len,err = ngx.shared.logs:rpush(key, myparams)

            if err then
                ngx.log(ngx.ERR,"failed to put log vals into shared dict")
                return
            end

        }
    }
    access_log logs/es_access.log porxy;
}
}

应用场景和日志文件解析

本配置主要解决Nginx向MySQL中实时插入日志的问题。

1、刚开始的时候看了Nginx和mysql的连接模块。比如说nginx-mysql-module,可以连接mysql。但是插入日志时遇到问题,我们知道nginx的执行过程先是location解析并重写阶段,然后是访问权限控制阶段,接着是内容生成阶段,最后是日志记录阶段。mysql访问阶段属于内容生成阶段,所以代理运行的时间和状态,mysql都无法获取的到。因此,这种通过nginx直连mysql的方式无法达到我们的要求。

2、通过lua脚本在日志生成阶段获取信息,然后将数据插入mysql。nginx有一个限制,无法在log阶段访问socket即无法访问mysql,所以无法在log阶段直接将数据存入mysql。但是可以通过运行包含mysql操作的shell脚本来解决这个问题。但是这个方法有两个弊端:

  • 获取到Nginx代理的结果后,每次都要连接mysql并向其插入数据。当并发量大时,mysql端会出现问题。

  • 不向mysql插入数据,整个时间的消耗大约在0.02-0.04s之间。而向mysql插入数据后,整个时间消耗大约在0.4-0.9之间,消耗的时间是原来的10倍。

3、通过lua + ngx.time.at + lua_mysql + lua.share.dict 解决问题。整个过程如下所示:

  • 在nginx启动阶段,ngx.time.at启动一个延时任务。在任务中,每隔一段时间取出nginx内存共享区的log数据,将数据合并,存入mysql,同时再一个相同的延时任务,递归调用。这样就与crontab命令相似。当定时器到期,定时器中的 Lua 代码是在一个“轻线程”中运行的,它与创造它的原始请求是完全分离的,因此不存在大量线程同时运行的情况。

  • 在日志生成阶段,将数据封装并存入nginx的内存共享区。

Mysql 访问权限的问题

不但访问Mysql的Mysql用户需要有操作对应数据库的权限,还需要调用Mysql命令的用户具有访问mysql的权限。授权命令如下:

GRANT ALL PRIVILEGES ON *.* to root@xxx IDENTIFIED BY 'password';

Mysql 编码类型

总的来说,Mysql的数据库对应三种编码。Mysql客户端显示数据的编码,连接Mysql用的编码(即数据存入mysql时,数据的编码),Mysql存储用的编码(字段,表,数据库三种格式可能不同)。不管Mysql存储用的编码是什么,只要Mysql客户端显示数据的编码和连接Mysql用的编码相同,数据就能通过mysql客户端正确显示。