EXT文件系统误删除数据恢复指南

 

 

我们在管理数据库和系统的时候,经常需要做rm 删除文件的操作。由于Linux是没有回收站的,rm删除了文件或者目录以后,数据是无法从Windows所谓的回收站中找到并恢复的。这样的话,数据被误删除了以后,想要恢复我们一般需要从备份中,或者找数据恢复公司来恢复数据。但是,在某些比较特殊的情况下,使用了以下方法,我们还是可以找回部分数据的。

这里我们主要介绍两种数据恢复的方法。第一种是针对文件在文件系统中已经被删除了,但是,打开这个文件的进程还存在。第二种针对文件在文件系统中已经被删除了,目前也没有任何进程打开着这个文件,但是文件在删除以后没有其他对文件系统的变更操作。

 

  1. 从/proc文件系统恢复数据

在Linux系统中,文件被删除了,只要打开文件的进程没有被关闭,那么恭喜你,这个文件重新恢复出来的可能性非常大。因为Linux操作系统在删除文件时,会判断打开这个文件的所有进程是否都已经关闭,如果还有一个进程没有关闭,那么这个文件的空间将不会释放。只有所有打开这个文件的进程都关闭以后,这个文件的空间才会释放。这也是为什么在Linux下有时候我们删除文件,文件的空间无法释放掉的原因。

这种情况下,我们可以尝试从/proc文件系统中将文件恢复出来。

/proc 文件系统是一种内核和内核模块用来向进程 (process) 发送信息的机制 (所以叫做 /proc)。通过这个伪文件系统让你可以和内核内部数据结构进行交互。你可以获取对应进程的有用信息,在运行中 (on the fly) 通过改变内核参数修改部分设置。它与其他文件系统不同,/proc 是存在于内存之中而不是硬盘上。

接下来我们模拟一下数据误删除的过程,来看看在进程没有关闭的情况下,怎么从/proc中恢复数据。

首先,我们有一个echo_red.sh的文件,我们在会话session 1查看一下这个文件的内容。

此时,在另外一个会话session 2中有一个进程在修改这个文件:

然后这个文件在会话session 1中被我们“误删除”掉了:

 

Session 1

Session 2

[root@test1 /home/woqu]

#ll

总用量 4

-rw-r–r– 1 root 93 10月 16 17:49 echo_red.sh

 

[root@test1 /home/woqu]

#cat echo_red.sh

echo_red()

{

    # echo a message with red color

    echo -e “\e[1;31m$@\e[m"

    return 0

}

 

 

 

[root@test1 /home/woqu]

#cat >echo_red.sh

echo_red()

{

    # echo a message with red color

    echo -e “\e[1;31m$@\e[m"

    return 0

}

 

[root@test1 /home/woqu]

#rm -f echo_red.sh

 

[root@test1 /home/woqu]

#ll

总用量 0

 

 

 

此时,我们发现文件被“误删除”了,需要恢复数据,那么我们需要怎么做列?

l   磁盘备份

发现误删除以后,我们需要立刻停止对该分区的写操作。

在恢复之前,如果可能的话,建议通过dd命令将磁盘整个备份起来,以避免操作的时候损坏了磁盘上相关数据。

 

l   确定进程号和文件句柄号

首先,我们需要确定打开这个文件的进程号,以及进程打开这个文件的文件号。最直接的办法就是lsof |grep -i delete:

[root@test1 /home/woqu]

#lsof |grep -i delete

cat       11791  root    1w      REG              253,0       94    1048589 /home/woqu/echo_red.sh (deleted)

这里一共有9列,各列列名如下:

COMMAND     PID  USER   FD      TYPE             DEVICE SIZE/OFF       NODE NAME

也就是说,打开这个文件的进程是11791,而/home/woqu/echo_red.sh对应该进程的文件句柄是1w。也就是说文件句柄号是1。

l   恢复误删除文件

然后,我们就可以直接将这个文件的内容拷贝出来:

 [root@test1 /root]

#cp /proc/11791/fd/1 echo_red.sh

 

[root@test1 /root]

#cat echo_red.sh

echo_red()

{

    # echo a message with red color

    echo -e “\e[1;31m$@\e[m"

    return 0

}

如上所示,数据文件恢复出来了,内容也是一模一样的。

 

  1. Extundelete工具恢复

对于使用ext3,ext4文件系统的Linux系统有一个比较好的工具可以用于数据恢复,那就是extundelete。当然其他的文件系统当然也有类似的恢复工具。

由于大部分Linux发行版都是以ext3,ext4作为默认文件系统的,我们这里以extundelete为例演示数据删除以后恢复的相关步骤。

老规矩,首先我们需要制造一个“误删除”的现场。

现在我们的/home/mysql下有多个目录,其中一个目录为script:

[root@test1 /home/mysql]

#ll

total 28

drwxr-xr-x 2 mysql 4096 Jul 21 14:42 bin

drwxr-xr-x 2 mysql 4096 Oct 12 17:52 conf

drwxr-xr-x 3 mysql 4096 Sep 26 14:57 data

drwxr-xr-x 4 mysql 4096 Oct 16 15:24 program

drwxr-xr-x 2 root  4096 Oct 16 18:16 script

drwxr-xr-x 4 mysql 4096 Oct 16 15:25 source

drwxr-xr-x 7 mysql 4096 May 31 11:27 thirdparty

这个script目录下有一些文件,如下:

[root@test1 /home/mysql]

#tree script/

script/

├── get_mysql_fdflag.sh

├── mysqlreport.sh

└── test_o_direct.c

由于某种原因,/home/mysql/script被误删除了。

[root@test1 /home/mysql]

#rm -fr script/

 

l   磁盘备份

发现误删除以后,我们需要立刻停止对该分区的写操作,避免inode被重用。

接下来就需要用extundelete工具对它进行恢复。在恢复之前如果可能的话,建议通过dd命令将磁盘整个备份起来,以避免操作的时候损坏了磁盘上相关数据。万一extundelete或者类似的工具无法恢复数据,这些数据交给专业的硬盘恢复公司也更容易找回数据一些。

 

l   umount分区

做完了备份,我们首先做的第一步,需要将误删除数据的磁盘分区首先umount掉,这也是避免该分区的数据被损坏的一个步骤。在我们的模拟环境,我们需要:

[root@test1 /root]

#umount /home/

 

l   安装extundelete

如果你机器上并没有安装extundelete的话,首先,你需要把这个工具安装好。目前最新的extundelete版本是0.2.4,安装方法如下:

yum -y install e2fsprogs*

wget  http://nchc.dl.sourceforge.net/project/extundelete/extundelete/0.2.4/extundelete-0.2.4.tar.bz2

tar xjf extundelete-0.2.4.tar.bz2

cd extundelete-0.2.4/

./configure

make

make install

 

l   查找误删除文件

通过extundelete可以查看哪些文件被删除了。在我们的模拟场景下,可以这样使用extundelete –inode 2 /dev/VolGroup/home查看/home分区下各个文件和目录的详细信息。这里/dev/VolGroup/home指的是/home对应的分区。对于ext系列的文件系统,编号为2的inode中包含了该分区下的各个文件和目录信息。输出信息如下:

[root@test1 /root]

#extundelete –inode 2 /dev/VolGroup/home

NOTICE: Extended attributes are not restored.

Loading filesystem metadata … 400 groups loaded.

Group: 0

Contents of inode 2:

0000 | ed 41 00 00 00 10 00 00 87 99 5e 52 87 99 5e 52 | .A……..^R..^R

0010 | 87 99 5e 52 00 00 00 00 00 00 05 00 08 00 00 00 | ..^R…………

0020 | 00 00 00 00 05 00 00 00 21 24 00 00 00 00 00 00 | ……..!$……

0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0080 | 1c 00 00 00 74 63 29 04 74 63 29 04 b8 23 27 8a | ….tc).tc)..#’.

0090 | e0 3e 2d 52 00 00 00 00 00 00 00 00 00 00 02 ea | .>-R…………

00a0 | 07 06 3c 00 00 00 00 00 21 00 00 00 00 00 00 00 | ..<…..!…….

00b0 | 73 65 6c 69 6e 75 78 00 00 00 00 00 00 00 00 00 | selinux………

00c0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

00d0 | 00 00 00 00 00 00 00 00 00 00 00 00 73 79 73 74 | …………syst

00e0 | 65 6d 5f 75 3a 6f 62 6a 65 63 74 5f 72 3a 68 6f | em_u:object_r:ho

00f0 | 6d 65 5f 72 6f 6f 74 5f 74 3a 73 30 00 00 00 00 | me_root_t:s0….

 

Inode is Allocated

File mode: 16877

Low 16 bits of Owner Uid: 0

Size in bytes: 4096

Access time: 1381931399

Creation time: 1381931399

Modification time: 1381931399

Deletion Time: 0

Low 16 bits of Group Id: 0

Links count: 5

Blocks count: 8

File flags: 0

File version (for NFS): 0

File ACL: 0

Directory ACL: 0

Fragment address: 0

Direct blocks: 9249, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Indirect block: 0

Double indirect block: 0

Triple indirect block: 0

 

File name                                       | Inode number | Deleted status

.                                                 2

..                                                2

lost+found                                        11

mysql                                             262145

cdrom.repo                                        12

woqu                                              2883585

我们这里最关心的还是mysql目录的信息。这里我们知道mysql的Inode为262145。于是我们可以再次用extundelete –inode 来查看mysql目录的详细信息:

[root@test1 /root]

#extundelete –inode 262145 /dev/VolGroup/home

NOTICE: Extended attributes are not restored.

Loading filesystem metadata … 400 groups loaded.

Group: 32

Contents of inode 262145:

0000 | c0 41 59 02 00 10 00 00 71 9a 5e 52 a8 99 5e 52 | .AY…..q.^R..^R

0010 | a8 99 5e 52 00 00 00 00 59 02 0c 00 08 00 00 00 | ..^R….Y…….

0020 | 00 00 08 00 54 00 00 00 0a f3 01 00 04 00 00 00 | ….T………..

0030 | 00 00 00 00 00 00 00 00 01 00 00 00 20 20 10 00 | …………  ..

0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0060 | 00 00 00 00 fc 9e be d7 00 00 00 00 00 00 00 00 | …………….

0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

0080 | 1c 00 00 00 98 8a f7 bb 98 8a f7 bb 84 eb 44 c0 | …………..D.

0090 | ae be 3e 52 b4 1d 94 e3 00 00 00 00 00 00 00 00 | ..>R…………

00a0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

00b0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

00c0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

00d0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

00e0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

00f0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….

 

Inode is Allocated

File mode: 16832

Low 16 bits of Owner Uid: 601

Size in bytes: 4096

Access time: 1381931633

Creation time: 1381931432

Modification time: 1381931432

Deletion Time: 0

Low 16 bits of Group Id: 601

Links count: 12

Blocks count: 8

File flags: 524288

File version (for NFS): 3619593980

File ACL: 0

Directory ACL: 0

Fragment address: 0

Direct blocks: 127754, 4, 0, 0, 1, 1056800, 0, 0, 0, 0, 0, 0

Indirect block: 0

Double indirect block: 0

Triple indirect block: 0

 

File name                                       | Inode number | Deleted status

.                                                 262145

..                                                2

.mozilla                                          262146

.bash_profile                                     262152

.gnome2                                           262150

.emacs                                            262195

.bash_logout                                      262194

.bashrc                                           262149

bin                                               262154

conf                                              262155

data                                              262156

script                                            404044     Deleted

thirdparty                                        262158

program                                           264107

.viminfo                                          262765

.bash_history                                     262193

.bzr.log                                          262153

.mysql_history                                    273588

source                                            402793

.ssh                                              414601

这里我们误删除的script目录在这里被标记为Deleted状态了。

 

l   恢复误删除数据

extundelete可以通过–restore-inode将指定inode对应的文件恢复出来,也可以使用–restore-all将所有状态为已经Deleted的文件和目录恢复回来。restore-inode主要用于恢复单个文件;restore-all用于恢复所有的文件目录。另外,还有–restore-file,–restore-files,–restore-directory来恢复指定目录或者文件。

另外,如果你知道删除的时间,那么可以指定–after或者–before来指定误删除的时间。

恢复数据的时候,extundelete将在当前目录下新建RECOVERED_FILES文件夹,并把恢复出来的数据文件或者目录存放在该目录中。

比如,我们使用–restore-inode恢复数据,恢复264111号inode文件如下:

[root@test1 /root/RECOVERED_FILES]

#extundelete –restore-inode 264111 /dev/VolGroup/home

NOTICE: Extended attributes are not restored.

Loading filesystem metadata … 400 groups loaded.

Loading journal descriptors … 31810 descriptors loaded.

 

[root@test1 /root/RECOVERED_FILES]

#ll file. 264111

-rw-r–r– 1 root 43816 10月 16 15:42 file.264111

如上,它恢复出来的文件会被重命名为file.$Inode_no(这里是file.264111)放在RECOVERED_FILES目录中。需要完全恢复数据的话,只需要将文件拷贝回原目录,并重命名。

 

使用restore-all恢复的话,目录名和文件名都会恢复回来,你可以在当前目录的RECOVERED_FILES目录下找到对应的文件和目录如下:

[root@test1 /root/RECOVERED_FILES]

#ll mysql/

total 16

drwxr-xr-x 4 root 4096 Oct 16 15:42 script

你只需要将script拷贝到原目录就好了。

 

  1. 终极解决方案

当然,以上的两个方法都是万不得已才使用的。最好的DBA和SA永远不是四处奔忙的救火队员。最好的办法是先做好预防工作,在发生之前尽量保证不出问题,而rm误删除文件的预防就是对重要数据进行备份以及rm -i。

alias rm=’rm -i –’

做了别名以后,删除数据的时候,rm命令就会提示你,文件是否确定要删除:

[root@test1 /root/RECOVERED_FILES/mysql/script]

#rm sock

rm:是否删除普通文件 ”sock”?

其他避免误删除等故障的方法可以参考《远离故障的十大原则》。当然,最重要的还是日常对这种不可逆操作的谨慎和小心,并及时做好备份。

 

MVCC read view的问题

之前写了一篇文章以为对MVCC的大致原理有些了解了。今天看了《高性能MySQL》的时候,深究了一下read view的问题,发现还是蛮有意思的。

 

特别画了一张图来确认一下。

本文是上一篇MySQL事务和MVCC简介的后续,建议先了解上一篇文章以后再阅读本文。

上一篇文章简单描述了MVCC的相关情况,但是没有详细说,read view是什么结构,并且它到底是怎么工作的。

比如,我们在show engine innodb status可以看到如下内容:

  1.   —TRANSACTION 0 600, ACTIVE 4 sec, process no 3396, OS thread id 1148250464, thread declared inside InnoDB 442
  2.   mysql tables in use 1, locked 0
  3.   MySQL thread id 8079, query id 728899 localhost baron Sending data
  4.   select sql_calc_found_rows * from b limit 5
  5.   Trx read view will not see trx with id>= 0 601, sees <0 596

要理解这个,我们首先要知道:

read view其实就是一个保存事务ID的list列表。记录的是本事务执行时,MySQL还有哪些事务在执行。

Read Repeatable(下文和图中用RR表示)对应的是在每个事务启动的时候创建 一个Read View。

Read Commit(下文和图中用RC表示)对应的是每次执行SQL statement时候创建 一个Read View。

 

根据show engine innodb status的输出是说看到这个事务的id是600。

对这个事务来说,trx id为596以下的所有事务修改的行数据,这个事务都可以看到,

trx id在601以上的事务修改的数据,这个事务都不应该读取到。

596到601号事务,一共5个事务修改的数据无法确定是否能够读取。read view应该为这5个事务id集合的子集。

 

如果线程的隔离级别是RR:

按照show engine innodb status的输出,600号事务在事务启动的时候,MySQL告诉它:

596之前的所有事务都已经提交了(Trx read view will not see trx with id>= 0 601, sees <0 596),

由于事务本身是600号,那么对应的601号事务因为是在它后面启动的,600号事务肯定无法提供读取到数据(Trx read view will not see trx with id>= 0 601, sees <0 596)。

read view表示的是事务开始时MySQL还有哪些事务在执行,就应该为{596,597,598,599}集合的子集,假设为{596,598},

根据read view,Innodb在读取数据的时候需要判断该行数据的修改事务号,判断的方法为:

a) 如果行数据的修改事务号小于596,由于在事务启动的时候596之前的所有线程都已经提交了,那么该行数据可读。

b) 如果行数据的修改事务号大于601,那么该行数据肯定不可读。如果事务号为600(即自己),本事务未提交,当然也是不可读的。

为了保证在事务内任何时间读取的数据都是一致的,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

c) 如果行数据的修改事务号在read view里面{596,599},说明是该事务(600号)开始时没有提交的数据修改,

为了保证在事务内任何时间读取的数据都是一致的,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

d)如果不在read view里面,即事务id号在{597,598}中,说明修改行数据是该事务(600号)开始时已经提交的数据修改,那么该行数据可读。

mvcc_readview

图1 MySQL read view 示意图

如图1。这个事务的行修改数据在{[0~595],597,598}是可读区间,{596,599,600,[601~ +infinity]}是不可读区间。

 

 

如果线程的隔离级别是RC,线程开始的时候,RC事务并不会做read view,此时开始的SQL跟上面RR的情况可能是一样的。

但是过了一段时间如果601事务提交了,同样的查询,在RC下面提交,对应的show engine innodb status的信息可能稍微有点不同:

  1.   —TRANSACTION 0 600, ACTIVE 4 sec, process no 3396, OS thread id 1148250464, thread declared inside InnoDB 442
  2.   mysql tables in use 1, locked 0
  3.   MySQL thread id 8079, query id 728899 localhost baron Sending data
  4.   select sql_calc_found_rows * from b limit 5
  5.   Trx read view will not see trx with id>= 0 602, sees <0 596

按照输出,600号事务在语句“select sql_calc_found_rows * from b limit 5”发起的时候,MySQL告诉它:

596之前的所有事务都已经提交了(Trx read view will not see trx with id>= 0 601, sees <0 596),

对应的,602号线程以及它之后的所有线程都还未提交(Trx read view will not see trx with id>= 0 602, sees <0 596)。

read view表示的是语句开始时MySQL还有哪些事务在执行(注意,这里跟RR为事务开始的时候的read view不同了),

在一个事务里面,每个SQL执行的时候,它的read view都可能是不同的。有可能事务启动的时候的sql的read view为{596,598},

这个语句执行的时候,601事务提交了,read view为{596,598}。

注意,601号事务虽然在600事务后启动,此时已经提交了行数据修改,它修改的数据,600号线程也可以读到。

根据read view,InnoDB在读取数据的时候需要判断该行数据的修改事务号,判断的方法为:

a) 如果行数据的修改事务号小于596,由于在语句启动的时候596之前的所有线程都已经提交了,那么该行数据可读。

b) 如果行数据的修改事务号大于等于602,那么该行数据肯定不可读。如果事务号为600(即自己),本事务未提交,当然也是不可读的。

为了保证读到的是Commited的数据,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

c) 如果行数据的修改事务号在read view里面{596,599},说明是该语句开始时没有提交的数据修改,

为了保证读到的是Commited的数据,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

d)如果不在read view里面,即事务id号在{597,598}中,说明修改行数据是该语句开始时已经提交的数据修改,那么该行数据可读。

 

如图1。这个语句的修改行数据的事务id在{[0~595],597,598,601}是可读区间,{596,599,600,[602~ +infinity]}是不可读区间。

 

整体来说,这篇文章描述了在Read Readrepeatable和Read Commit环境下,MySQL根据Read View读取数据的方法,来保证可重复读和只读到已经提交的数据。

MYSQL数据丢失讨论

 

目录

 

MySQL数据丢失讨论……………………………………………………………………………………… 1

目录……………………………………………………………………………………………………………….. 3

1.    概述…………………………………………………………………………………………………………. 4

2.    问题定义………………………………………………………………………………………………….. 4

3.    InnoDB事务数据丢失………………………………………………………………………………. 4

3.1.   InnoDB事务基本原理…………………………………………………………………………….. 4

3.2.   InnoDB事务崩溃恢复基本原理……………………………………………………………….. 5

3.3.   InnoDB redo日志…………………………………………………………………………………… 5

3.4.   innodb_flush_log_at_trx_commit……………………………………………………………… 5

4.    数据库复制导致数据丢失…………………………………………………………………………. 6

4.1.   MySQL复制原理简介……………………………………………………………………………… 6

4.2.   sync_binlog…………………………………………………………………………………………… 6

5.    MySQL和InnoDB协同……………………………………………………………………………… 7

5.1.   两段式事务提交……………………………………………………………………………………. 7

5.2.   innodb_support_xa…………………………………………………………………………………. 7

 

 

1.  概述

很多企业选择MySQL都会担心它的数据丢失问题,从而选择Oracle,但是其实并不十分清楚什么情况下,各种原因导致MySQL会丢失部分数据。本文不讨论Oracle和MySQL的优劣,仅仅关注MySQL丢失数据的几种情况。希望能够抛砖引玉,让各位MySQL大牛们梳理出MySQL最安全或者性价比合适的适合各种应用场景的方案。

2.  问题定义

一般我们希望把一系列的数据作为一个原子操作,这样的话,这一系列操作,要么提交,要么全部回滚掉。

当我们提交一个事务,数据库要么告诉我们事务提交成功了,要么告诉我们提交失败。

数据库为了效率等原因,数据只保存在内存中,没有真正的写入到磁盘上去。如果数据库响应为“提交成功”,但是由于数据库挂掉,操作系统,数据库主机等任何问题导致这次“提交成功”的事务对数据库的修改没有生效,那么我们认为这个事务的数据丢失了。这个对银行或者支付宝这种业务场景来说是不能接受的。所以,保证数据不丢失也是数据库选择的一个重要衡量指标

mysql的架构和普通的数据库架构最大的差异在于它使用插件式的存储引擎。数据的存取由存储引擎负责。要了解MySQL数据丢失的问题就需要从MySQL server层和InnoDB目前最流行的支持事务的存储引擎分别来分析了。

 

3.  InnoDB事务数据丢失

首先,我们来看一下InnoDB事务数据丢失的情况。

3.1. InnoDB事务基本原理

InnoDB的事务提交需要写入undo log,redo log,以及真正的数据页。专业的介绍可以参考丁奇云华的两篇文章。我们这里通俗一点简单介绍一下。

InnoDB跟Oracle非常类似,使用日志先行的策略,将数据的变更在内存中完成,并且将事务记录成redo,转换为顺序IO高效的提交事务。这里日志先行,也就是说,日志记录到数据库以后,对应的事务就可以返回给用户,表示事务完成。但是实际上,这个数据可能还只在内存中修改完成,并没有刷到磁盘上去,俗称“还没有落地”。内存是易失的,如果在数据“落地”之前,机器挂了,那么这部分数据就丢失了。而数据库怎么保证这些数据还是能够找回来列?否则,用户提交了一个事务,数据库响应请求并回应为事务“提交成功”,数据库重启以后,这部分修改数据的却回到了事务提交之前的状态。

3.2. InnoDB事务崩溃恢复基本原理

InnoDB和Oracle都是利用redo来保证数据一致性的。如果你有从数据库新建一直到数据库挂掉的所有redo,那么你可以将数据完完整整的重新build出来。但是这样的话,速度肯定很慢。所以一般每隔一段时间,数据库会做一个checkpoint的操作,做checkpoint的目的就是为了让在该时刻之前的所有数据都”落地”。这样的话,数据库挂了,内存中的数据丢了,不用从最原始的位置开始恢复,而只需要从最新的checkpoint来恢复。将已经提交的所有事务变更到具体的数据块中,将那些未提交的事务回滚掉。

3.3. InnoDB redo日志

这样的话,保证事务的redo日志刷到磁盘就成了事务数据是否丢失的关键。而InnoDB为了保证日志的刷写的高效,使用了内存的log buffer,另外,由于InnoDB大部分情况下使用的是文件系统,(linux文件系统本身也是有buffer的)而不是直接使用物理块设备,这样的话就有两种丢失日志的可能性:日志保存在log_buffer中,机器挂了,对应的事务数据就丢失了;日志从log buffer刷到了linux文件系统的buffer,机器挂掉了,对应的事务数据就丢失了。当然,文件系统的缓存刷新到硬件设备,还有可能被raid卡的缓存,甚至是磁盘本身的缓存保留,而不是真正的写到磁盘介质上去了。这个就不在我们这次讨论的范围内了。

InnoDB的日志你还可以参考这篇文章

3.4. innodb_flush_log_at_trx_commit

所以InnoDB有一个特别的参数用于设置这两个缓存的刷新: innodb_flush_log_at_trx_commit。

默认,innodb_flush_log_at_trx_commit=1,表示在每次事务提交的时候,都把log buffer刷到文件系统中去,并且调用文件系统的“flush”操作将缓存刷新到磁盘上去。这样的话,数据库对IO的要求就非常高了,如果底层的硬件提供的IOPS比较差,那么MySQL数据库的并发很快就会由于硬件IO的问题而无法提升。

为了提高效率,保证并发,牺牲一定的数据一致性。innodb_flush_log_at_trx_commit还可以设置为0和2。

innodb_flush_log_at_trx_commit=0时,每隔一秒把log buffer刷到文件系统中去,并且调用文件系统的“flush”操作将缓存刷新到磁盘上去。这样的话,可能丢失1秒的事务数据。

innodb_flush_log_at_trx_commit=2时,在每次事务提交的时候会把log buffer刷到文件系统中去,但是每隔一秒调用文件系统的“flush”操作将缓存刷新到磁盘上去。如果只是MySQL数据库挂掉了,由于文件系统没有问题,那么对应的事务数据并没有丢失。只有在数据库所在的主机操作系统损坏或者突然掉电的情况下,数据库的事务数据可能丢失1秒之类的事务数据。这样的好处就是,减少了事务数据丢失的概率,而对底层硬件的IO要求也没有那么高(log buffer写到文件系统中,一般只是从log buffer的内存转移的文件系统的内存缓存中,对底层IO没有压力)。MySQL 5.6.6以后,这个“1秒”的刷新还可以用innodb_flush_log_at_timeout 来控制刷新间隔。

在大部分应用环境中,应用对数据的一致性要求并没有那么高,所以很多MySQL DBA会设置innodb_flush_log_at_trx_commit=2,这样的话,数据库就存在丢失最多1秒的事务数据的风险。

引用应元的一个图如下:

innodb_flush_log_at_commit

4.  数据库复制导致数据丢失

MySQL相比其他数据库更适用于互联网的其中一个重要特性就是MySQL的复制。对于互联网这种需要提供7*24小时不间断的服务的要求,MySQL提供异步的数据同步机制。利用这种复制同步机制,当数据库主库无法提供服务时,应用可以快速切换到跟它保持同步的一个备库中去。备库继续为应用提供服务,从而不影响应用的可用性。

这里有一个关键的问题,就是应用切换到备库访问,备库的数据需要跟主库的数据一致才能保证不丢失数据。由于目前MySQL还没有提供全同步的主备复制解决方案所以这里也是可能存在数据丢失的情况。

目前MySQL提供两种主备同步的方式:异步(asynchronous)和半同步(Semi-sync)

4.1. MySQL复制原理简介

MySQL复制的原理简介如下:MySQL主库在事务提交时写binlog,并通过sync_binlog参数来控制binlog刷新到磁盘“落地”。而备库通过IO线程从主库拉取binlog,并记录到本地的relay log中;由本地的SQL线程再将relay log中的数据应用到本地数据库中。

异步的方式下,几个线程都是独立的,相互不依赖。

而在半同步的情况下,主库的事务提交需要保证至少有一个备库的IO线程已经拉到了数据,这样保证了至少有一个备库有最新的事务数据,避免了数据丢失。这里称为半同步,是因为主库并不要求SQL线程已经执行完成了这个事务。

半同步在MySQL 5.5才开始提供,并且可能引起并发和效率的一系列问题,比如只有一个备库,备库挂掉了,那么主库在事务提交10秒(rpl_semi_sync_master_timeout控制)后,才会继续,之后变成传统的异步方式。所以目前在生产环境下使用半同步的比较少。

在异步方式下,如何保证数据尽量不丢失就成了主要问题。这个问题其实就是如何保证数据库的binlog不丢失,尽快将binlog落地,这样就算数据库挂掉了,我们还可以通过binlog来将丢失的部分数据手工同步到备库上去(MHA会自动抽取缺失的部分补全备库)。

图示如下:

mysql_replication

4.2. sync_binlog

这个问题就跟上一个innodb_flush_log_at_trx_commit的问题类似了。MySQL提供一个sync_binlog参数来控制数据库的binlog刷到磁盘上去。虽然binlog也有binlog cache,但是MySQL并没有控制binlog cache同步到文件系统缓存的相关考虑。所以我们这里不涉及binlog cache。

默认,sync_binlog=0,表示MySQL不控制binlog的刷新,由文件系统自己控制它的缓存的刷新。

如果sync_binlog>0,表示每sync_binlog次事务提交,MySQL调用文件系统的刷新操作将缓存刷下去。最安全的就是sync_binlog=1了,表示每次事务提交,MySQL都会把binlog刷下去。这样的话,在数据库所在的主机操作系统损坏或者突然掉电的情况下,系统才有可能丢失1个事务的数据。但是binlog虽然是顺序IO,但是设置sync_binlog=1,多个事务同时提交,同样很大的影响MySQL和IO性能。虽然可以通过group commit的补丁缓解,但是刷新的频率过高对IO的影响也非常大。

所以很多MySQL DBA设置的sync_binlog并不是最安全的1,而是100或者是0。这样牺牲一定的一致性,可以获得更高的并发和性能。

5.  MySQL和InnoDB协同

5.1. 两段式事务提交

最后我们需要讨论一下上述两个参数对应的redolog和 binlog协同的问题。这两个log都影响数据丢失,但是他们分别在InnoDB和MySQL server层维护。由于一个事务可能使用两种事务引擎,所以MySQL用两段式事务提交来协调事务提交。我们先简单了解一下两段式事务提交的过程

transaction_xa

第一阶段:

首先,协调者在自身节点的日志中写入一条的日志记录,然后所有参与者发送消息prepare T,询问这些参与者(包括自身),是否能够提交这个事务;

参与者在接受到这个prepare T 消息以后,会根据自身的情况,进行事务的预处理,如果参与者能够提交该事务,则会将日志写入磁盘,并返回给协调者一个ready T信息,同时自身进入预提交状态状态;如果不能提交该事务,则记录日志,并返回一个not commit T信息给协调者,同时撤销在自身上所做的数据库改;

参与者能够推迟发送响应的时间,但最终还是需要发送的。

第二阶段:

协调者会收集所有参与者的意见,如果收到参与者发来的not commit T信息,则标识着该事务不能提交,协调者会将Abort T 记录到日志中,并向所有参与者发送一个Abort T 信息,让所有参与者撤销在自身上所有的预操作;

如果协调者收到所有参与者发来prepare T信息,那么协调者会将Commit T日志写入磁盘,并向所有参与者发送一个Commit T信息,提交该事务。若协调者迟迟未收到某个参与者发来的信息,则认为该参与者发送了一个VOTE_ABORT信息,从而取消该事务的执行。

参与者接收到协调者发来的Abort T信息以后,参与者会终止提交,并将Abort T 记录到日志中;如果参与者收到的是Commit T信息,则会将事务进行提交,并写入记录

一般情况下,两阶段提交机制都能较好的运行,当在事务进行过程中,有参与者宕机时,他重启以后,可以通过询问其他参与者或者协调者,从而知道这个事务到底提交了没有。当然,这一切的前提都是各个参与者在进行每一步操作时,都会事先写入日志。

具体的介绍可以参考《事务和两阶段提交》以及《分布式事务设计-两阶段提交》

 

5.2. innodb_support_xa

innodb_support_xa可以开关InnoDB的xa两段式事务提交。默认情况下,innodb_support_xa=true,支持xa两段式事务提交。此时MySQL首先要求innodb prepare,对应的redolog 将写入log buffer;如果有其他的引擎,其他引擎也需要做事务提交的prepare,然后MySQL server将binlog将写入;并通知各事务引擎真正commit;InnoDB将commit标志写入,完成真正的提交,响应应用程序为提交成功。这个过程中任何出错将导致事务回滚,响应应用程序为提交失败。也就是说,在这种情况下,基本不会出错。

但是由于xa两段式事务提交导致多余flush等操作,性能影响会达到10%,所有为了提高性能,有些DBA会设置innodb_support_xa=false。这样的话,redolog和binlog将无法同步,可能存在事务在主库提交,但是没有记录到binlog的情况。这样也有可能造成事务数据的丢失。

 

综上,我们列举了影响InnoDB数据丢失的参数innodb_flush_log_at_trx_commit,影响MySQL复制数据丢失的sync_binlog,以及由于MySQL和InnoDB需要协调而可能导致数据丢失的参数innodb_support_xa。

InnoDB plugin 1.0手册翻译

之前和团队的同学一起翻译了部分InnoDB Plugin 1.0的手册,但是一直没有最终完稿。

利用周末的时间,我把剩下来的几章翻译完成并放到公司官方网站,希望大家一起来审阅一下。

这里也非常感谢彭立勋,胡中泉,陈良允同学。彭立勋主要翻译完成了1-7章,胡中泉,陈良允对文档进行了审阅。这些翻译文档也是取得了这些同学的同意才发表在公司博客网站的。

下面列出所有翻译文档的链接,请各位大牛批评指正(请直接在沃趣科技博客中评论):

第一章:InnoDB Plugin简介
第二章:在InnoDB存储引擎下快速创建索引
第三章:InnoDB 数据压缩
第四章:InnoDB文件格式管理
第五章:可变长度列(Variable-Length Columns)的存储
第六章:InnoDB的INFORMATION_SCHEMA数据表
第七章:性能优化及可扩展性的提高
第八章.灵活性、易用性、可靠性改进
第九章. 安装InnoDB Plugin
第十章.升级InnoDB Plugin
第十一章.从InnoDB Plugin降级
第十二章. InnoDB Plugin变更历史
附录A.第三方软件
附录 B.在5.1.30及之前版本MySQL使用InnoDB Plugin
附录 C. InnoDB Plugin 1.0参数修改列表

 

另外,由于本人时间有限,本来这些文档需要转换为docbook格式提交给Oracle官方文档的team的,如果哪位同学有兴趣可以一起参与进来。

远离故障的十大原则

 

故障是运维人员永远的痛。相信每一个运维人员的KPI中都有一项:可用性。可用性高就是不出故障,各个公司对可用性和故障评级的标准都不相同,但是避免故障的方法却是殊途同归。我们怎么避免故障,沃趣科技简单列举了以下几条,与大家共勉!
1、变更要有回滚,在同样的环境测试过
2、对破坏性的操作谨慎小心
3、设置好命令提示 
4、备份并验证备份有效性
5、对生产环境存有敬畏之心
6、交接和休假最容易出故障,变更请谨慎
7、搭建报警,及时获得出错信息。搭建性能监控,了解历史,获得趋势,预测未来
8、自动切换需谨慎
9、仔细一点,偏执一点,检查,检查,再检查
10、简单即是美。

 

第1条,变更要有回滚,在同样的环境测试过。也是运维最繁琐,最苦逼的地方,所有的变更都必须有回滚的办法,在同样的环境下测试过。没有做过的东西,总是会在你意想不到的地方给你一次痛击,在阿里巴巴的这么多年运维经验告诉我们,所有没有做过的变更,出错的概率最大。所以我们需要给变更以回滚的可能,在各个步骤可能出错的情况下,考虑回滚到最初状态。优秀的运维人员对不考虑回滚的的操作都是敬而远之的。从某种意义上来说,运维是一门经验的学科,是一门试错的学科。

 

第2条,对破坏性的操作谨慎小心。破坏性的操作有哪些列?对数据库来说有:DROP Table, Drop database, truncate table, delete all data;这些操作做完了以后几乎无法考虑怎么把数据都回滚回去了。就算回滚,代价也是非常大的。你执行这样的语句非常简单,但是回滚恢复数据缺非常困难。linux的命令rm可以-r(recursive)递归的删除某一个目录,-f(force)强制删除,但是你有没有删错过文件。我们遇到过一个文件名中末尾有空格的情况,而有的同事rm -r习惯性的会在文件名后面加*,这样就成了rm -r aa *,所有当前目录的数据都被删除掉了!经过这次故障以后我们给rm做了别名:
alias rm=’rm -i’
这样在删除数据时,rm命令会提示你,是否确认删除该文件。
同样的cp和mv也可以有同样的选项:
alias cp=’cp -i’
alias mv=’mv -i’

 

第3条,设置好命令提示。让你时刻知道你在操作哪个数据库,让你知道你在哪个目录下。mysql字符客户端允许你设置提示符,默认的提示符就是一个光秃秃的mysql >,为了让你清楚的知道你当前是以哪个用户名,哪个IP(可能是localhost,127.0.0.1或者具体的物理IP),你当前操作的是哪个schema,以及当前的时间,你可以设置数据库的提示符为:prompt=”\\u@\\h : \\d \\r:\\m:\\s> “。它可以直接写在my.cnf的[mysql]下,这样你每次连上MySQL就默认显示如下:
root@127.0.0.1 : woqutech 08:24:36>
具体prompt可以设置哪些提示,你可以参考http://dev.mysql.com/doc/refman/5.6/en/mysql-commands.html中的列表
而linux命令提示符也允许你设置的。有两个地方可以设置。第一个:PS1。这个是每次shell提示你输入命令的信息,默认为:$或者#,只会提示你是超级用户还是普通用户。有经验的运维者会设置export PS1=’\n\e[1;37m[\e[m\e[1;31m\u\e[m\e[1;31m@\e[m\e[1;31m\h\e[m \e[4m`pwd`\e[m\e[1;37m]\e[m\e[1;36m\e[m\n\$'。这样你就可以知道你当前的目录,登录的用户名和主机信息了,示例提示符如下:
[root@woqu-lsv-01 /home/mysql]
#
你可以查看http://www.cyberciti.biz/tips/howto-linux-unix-bash-shell-setup-prompt.html获得具体的PS1设置颜色,设置各个提示内容的介绍。
第二个提示符就是PROMPT_COMMAND。这个是设置你连到具体的数据库以后标签页标题上显示的内容,Windows用户可能会用securtCRT,Mac用户可能会用iTerm2,开多个标签页的话,如果每个标签页的标题上内容一样,我们切来切去就有可能在错误的标签页上做操作,设置了这个以后,这个问题概率就会小很多。比如我们的机器上设置为PROMPT_COMMAND=’echo -ne “\033]0;${USER}@${HOSTNAME%%.*}”; echo -ne “\007″‘对应的标签页如下图

prompt_command

 

 

 

 

第4条,备份并验证备份有效性。是人总会出错,是机器总可能会有突然崩溃的那一天。怎么办-我们需要准备备份。

备份的学问很大。按照不同的纬度可以分为:冷备份和热备份;实时备份和非实时备份;物理备份和逻辑备份。

互联网企业为了提供7*24小时不间断的服务,数据库就需要有实时热备份。在主库出现问题的情况下能够由备库提供服务。备库时候有效,数据是否一致,主库出现问题的时候怎么切换都需要运维人员认真考虑。

是不是有了这些就够了列?不行,应用程序也是人写的,曾经出现过程序一不小心delete语句没有带任何条件,导致一个表中所有的数据都被删除的惨状。所以你除了实时的备份,还需要有非实时的备份,在你的数据出现逻辑错误之后能够从备份数据中恢复出来。现在很多人在研究MySQL模仿oracle的flashback功能,利用binlog来恢复数据。但是这样的话,binlog_format必须设置为row并且对于DDL操作也无法回滚。它是为快速解决部分数据被错误删除的解决方案,但是无法代替非实时备份的作用。

非实时备份有可以分为在线延时备份和离线备份。在线延时备份是搭建数据库的一定时间延迟的热备份,比如MySQL就可以搭建一个延迟一天的slave,一直保持着备库与主库的延迟在一天。可以利用pt-slave-delay工具来实现这个功能。另外,离线备份是目前大家用的比较多的,可以利用mysqldump进行逻辑备份或者xtrabackup进行物理备份。为了空间的原因和快速恢复考虑,你还可以利用xtrabackup进行增量的物理备份。

备份有了,是否就可以高枕无忧了?还是不行。你需要验证备份的有效性。没有一个备份能够保证它备份出来的数据能够100%恢复出正确的数据,特别是物理备份的概率相对来说,更低,xtrabackup备份一个月总有那么几次来大姨妈,不能给你很好的服务。所以,备份并不只是备份,它还包括备份的验证,它如果不能恢复出正确的数据,就只是浪费空间而已。备份的验证最简单的就是找一个空闲的库,来恢复出来,mysql启动以后检查部分数据。如果不需要这么严谨,对于xtrabackup来说,你至少得验证它–apply-log能够恢复上去吧?同样,备库的数据一致性也需要经常检查一下,mysql的replication并不保证100%的数据一致性,你可以去翻翻mysql statement复制的bug列表,有些数据在主备不同的环境上分别执行,数据就会不一样。可以考虑用percona的工具pt-table-checksum来检查主备不一致,用pt-table-sync来同步主备数据。

 

 

第5条,对生产环境存有敬畏之心。这应该是运维者进入行业首先需要具备的素质。但是我们还是需要把它拿出来强调一下。

有机会的话,你可以梳理一下:

  • 你的生产环境上有哪些账户,这些账户是否都确实需要登录到机器上来?这些账户即包括linux用户还包括数据库账户。
  • 你的root用户是否开放给了某些用户,这些用户安全吗?
  • 你的用户密码是否经常修改,是否加密不让具体的操作人员直接看到,密码强度时候足够,密码重试次数达到一定次数是否黑名单;
  • 你的生产环境和线下环境是否隔离,数据库是否和外网隔离?
  • 是否一些工作明明能够在开发库和测试库做,却被放到生产环境上去了。
  • 是否有专门的人负责线上应用的发布,从而避免开发人员直接接触生产环境

这些都是你避免出现csdn密码泄漏,在业界的名声一落千丈的法宝。

 

 

第6条,交接和休假最容易出故障,变更请谨慎。这个是经验之谈。我们在总结故障的情况时,发现在公司部门有变化时,工作交接(不管是休假,工作职责变化还是离职),故障的出现频率会比正常情况下多50%以上。有人说,这是因为机器或者应用是有感情的,舍不得离开的运维者。

我们不谈感情,简单的理性分析一下。公司或者部门难免会做一些调整,变化是世界上唯一不变的事情。而运维人员是一线做事情的人,部门调整或者领导的更换可能导致工作的着重点不同,做事的方式和评测的标准变了,适应过程中难免会出现一些考虑不周到的地方,出故障也是情理之中了。

而工作交接,对运维人来说,其实是一个非常费时费力的事情,你需要把所有平常做的工作都梳理清楚,甚至包括你的一些经意不经意的操作习惯,这样的话,下一个人才可能接手的下来。比如:你可能认为备库正常情况下没有访问,于是让某些并不重要的任务(一个月一次抽取部分数据到线下测试?)直接连备机IP进行操作。下一个人接手,认为备机就是备机,操作起来不会有任何问题,结果下一次任务抽取就是一个故障出来了。再举一个我们遇到了事例吧:同事A出国休假了,休假期间估计联系不上,他留了文档,并告诫说某几个库和表是比较核心和容易出问题的,没有特殊情况最好等他回来再做变更。正好,休假期间,开发人员找到同事B,要求他重置一个字段的某一位(bit),并打包票说这个bit没有用,同事B拒绝,并背上了不配合的骂名。同事A回来吓了一身冷汗,原来这个字段已经被另外一个离职的开发使用了。

所以,运维部门和运维人员对变化需要尽量放平心态;接手别人的工作要一而再,再而三的确认变更方案。请教人并不见得就是能力不行的表现;休假前最好各种可以做好的事情,最好能够准备一份文档,指明在什么情况下怎么做和联系哪些人。在别人放假的时候接手工作,“能拖则拖”,实在需要执行:必须不厌其烦的跟原运维者确认各个操作细节。 

 

 

第7条,搭建报警,及时获得出错信息。搭建性能监控,了解历史,获得趋势,预测未来。运维的最高境界不是故障来了,泰山崩于前而不惊,苍老师勾引你而抗日;而是没有故障,让故障消失在萌芽之中。请给那些默默无闻,每天想着我们的系统还存在哪些隐患,怎么解决,怎么及早发现的运维人员鼓掌。他们是最可爱的人。而他们赖以生存的工具就是报警和监控。Oracle发展了这么多年,awr和相关的性能参数都相对比较全;MySQL现在也已经迎头赶上,配套的工具越来越多。

报警可以让你及时知道系统出现了什么异常。比如slave io报警,在数据库replication异常的时候就会提醒你:IO线程出现了问题,可能是网络问题,主数据库问题等,slave sql报警会提醒你replication的SQL线程出现了问题,可能是主备不一致,slave被停掉了,存储过程在备机有异常或者其他问题。这样你收到报警就可以及时跟进,而不至于主备长时间不一致,主库坏掉了想要切换到备库的时候却不能切换。

性能监控可以让你了解系统的历史性能信息。分析故障发生时的各种现象,确认故障的真正原因;了解变化趋势,发现故障的苗头,及早优化和调整。比如你如果使用了PCI-E的Flash卡,你可以监控logical_written_bytes,logical_read_bytes,physical_written_bytes,physical_read_bytes以便获得flash卡的每秒的逻辑读写和物理读写字节数。对于MySQL你可以监控Com_delete+Com_delete_multi, Com_insert+Com_insert_select,Com_update+Com_update_multi,Com_select来获得每秒的MySQL DML删除,插入,更新和查询的次数。

报警和性能监控其实不不完全独立的,很多性能的监控项也可以报警出来。比如linux的iostat中的await_time可以作为性能监控采集起来获得系统IO响应时间的变化曲线,当该值达到20以上的时候,也可以报警出来,让运维人员跟进是磁盘阵列中坏了一块,还是异常的数据拷贝影响了系统的IO性能等。

nagios和cacti是目前MySQL领域使用最广泛的报警和性能展示系统。percona最新推出percona-monitor-plugins(http://www.percona.com/software/percona-monitoring-plugins)就是基于他们俩的。

 

 

第8条:自动切换需谨慎。现在数据库的HA很多都是进行自动切换的,这样运维人员深夜起来手工切换到备库的机会就会少很多。切换也会快速很多。但是,它带来的副作用也不容忽视。

现在业界使用的HA软件非常多,heartbeat由于很多SA兼作DBA的运维比较熟悉,在MySQL自动切换也是不少的。一般来说,它会通过mysqladmin ping来探测MySQL是否存活,如果发现异常,那么他就会切换VIP和MySQL资源到备库。但是此时备库的数据延迟是否为0,主库crash之后binlog的数据是否全部都同步到备库上去了,备库的read_only是否关闭,这些heartbeat都不管。我们想象一下,主库上应用提交了一笔订单,结果发生了切换,这笔订单没有同步到备库上,卖家也就损失了一个销售单,对客户,对公司都是非常大的影响。

当然,自动切换也不能全盘否定,它能够更快速的将应用切换到新的热备份备库上,应用的不可用时间大大缩短。只是我们要好好利用这一把双刃剑,仔细评估它的影响,降低或者去除副作用,让它为我们服务。

 

 

第9条,仔细一点,偏执一点,检查,检查,再检查。之前我跟一个资深的运维学习线上操作的时候,觉得这家伙有点变态,他在做一个变更的时候,会先提前一两周发送邮件并电话手机的通知相关人;在测试机上写好脚本,召集大家review操作步骤和脚本;测试完成以后拷贝到生产环境;登录对应机器,“打开,关闭,打开,关闭”该脚本;跟相关人员再次确认执行的操作,顺序,时间点,可能的影响和回滚是否都准备好了;执行前还要退出这个机器,然后再登录进去,“打开,关闭”脚本;最后才在后台运行脚本,在另外一个窗口登录着,随时ps和查看结果输出。期间姿势端正,呼吸急促而均匀,眼神凝重。操作的人不觉得累,倒是一边学习的人很累。

当我做到一定程度,我也开始这样了。医学上,这种好像叫做强迫症。唉…,提前通知会让大家都有准备,也避免了临时相关人员过来说这个操作和其他操作有依赖需要调整操作时间的问题; 召集大家review步骤和脚本是为了让大家一起来看看整个过程中还有哪些依赖没有考虑到或者哪些细节没有注意到,三个臭皮匠顶一个诸葛亮在运维来说是金科玉律;“打开,关闭,打开,关闭”是为了一再确认脚本拷贝过来是否正确,目录时候正确,思考在测试环境运行和在生产环境运行有什么不一样的;退出再登录机器是为了确认我登录的机器确实没有错;在后台运行是担心网络突然中断,我的脚本运行到一半怎么办;调整呼吸和端正姿势是为了对这个操作的敬重,对自己工作和运维工作的尊重。

以MySQL 使用flash卡为例吧。flash算是一个比较新的事务,提供的IO比普通磁盘是几个数量级的提升。要想在生产环境使用,首先我们需要对他进行详尽的评估和破坏性测试,设置各种参数,考虑他们在各种场景下使用的配置;24小时不间断的进行半个月读写操作,中途突然掉电;高并发,高吞吐量下的测试;温度湿度极限测试;预留空间释放测试等等。然后我们会尝试在测试库上部署试用,收集和修改各个配置已达到最稳定,最高性能的配置;运行稳定以后我们才考虑在线上备库使用,并且主备要求异构;适当的时机切换为使用新的flahs卡为主库,万一出现了问题,还可以切换回原主机。

这里也跟大家简单介绍一下screen命令,这个命令会在服务器段开启一个session,就算你的网络断掉了,你的脚本也会自动在后台运行。screen -S woqutech可以开启一个woqutech命令的后台session;如果你的网络断掉了,你可以用screen -dr woqutech连上之前的session继续进行操作。IBM的文档库中有一个非常靠谱的文档:http://www.ibm.com/developerworks/cn/linux/l-cn-screen/

 

第10条,简单即是美。最后一条有点禅的意境了。它和Unix的思想不谋而合。我们总是面临着各种诱惑:新的系统架构,新的更智能的命令和工具,最新的硬件平台,功能更全的HA软件等。他们总是以各种各样的方式吸引我们,most exciting,unbelievable,让你欲罢不能。你可以在线下安装,测试,怎么搞都行。但是如果想要在生产环境下使用起来,那就得经过非常详细,非常漫长,各种方式验证其稳定性的过程。

能够使用系统内置命令的话,就不用考虑其他要专门下载安装的软件了;脚本本身就能完成的功能,就没有必要专门找一个功能丰富的软件来做;linux本身自带的字符界面比那些复杂的图形界面要简洁方便;MySQL的一些分区,生僻函数,没有必要的话不要使用。

 

最后祝大家运维的运维工作一帆风顺,多福多寿,不出故障。

参考:http://feedproxy.google.com/~r/iheavy/~3/sRnyFPA0R9E/

淘宝物流MySQL slave数据丢失详细原因

前两天,惊闻淘宝发生了一个非常狗血的事情,备库复制状态一切正常,但是备库的数据DDL可以复制过去,DML都丢失了。导致数据库数据不一致。这样的话,就算你有slave监控,也发现不了主备数据延迟和不一致的问题。

最后淘宝希羽定位到了问题,并提出了解决方案。参考http://hickey.in/?p=146。不过具体原因和为什么会发生这个问题并没有说的那么详细。沃趣科技特别针对这个问题做了深入详细的研究,整理出来,以飨观众。

首先我们需要跟大家解释一下MySQL复制的基本原理。

主库为每一个slave开启一个binlog dump线程,用于把本机记录下所有的变更,发送给备库;备库使用io thread线程接收数据存入relay log中;然后由sql thread线程从relay log中读出来应用到本地。这个是大家都熟知的。我们不详细介绍。但是slave是怎么注册上主库,主库是怎么通知各个binlog dump线程,binlog dump和io thread线程怎么通讯,io thread怎么重连,relay log怎么读取二进制数据翻译成对应的信息应用在slave上;我们就不得而知了。而问题就发生在最后“relay log怎么读取二进制数据翻译成对应的信息应用在slave上”

binlog是二进制数据,必须用mysqlbinlog工具才能打开。所以我们有必要先介绍一下binlog的格式。也就是insert,update,delete等这些数据是怎么以二进制形式记录到binlog文件中去的。binlog文件是按照event来组织的。每个文件前4个字节是fe 62 69 6e,接下来就是各个event了。event有很多种类型。列出如下:

enum Log_event_type {
  UNKNOWN_EVENT= 0,
  START_EVENT_V3= 1,
  QUERY_EVENT= 2,
  STOP_EVENT= 3,
  ROTATE_EVENT= 4,
  INTVAR_EVENT= 5,
  LOAD_EVENT= 6,
  SLAVE_EVENT= 7,
  CREATE_FILE_EVENT= 8,
  APPEND_BLOCK_EVENT= 9,
  EXEC_LOAD_EVENT= 10,
  DELETE_FILE_EVENT= 11,
  NEW_LOAD_EVENT= 12,
  RAND_EVENT= 13,
  USER_VAR_EVENT= 14,
  FORMAT_DESCRIPTION_EVENT= 15,
  XID_EVENT= 16,
  BEGIN_LOAD_QUERY_EVENT= 17,
  EXECUTE_LOAD_QUERY_EVENT= 18,
  TABLE_MAP_EVENT = 19,
  PRE_GA_WRITE_ROWS_EVENT = 20,
  PRE_GA_UPDATE_ROWS_EVENT = 21,
  PRE_GA_DELETE_ROWS_EVENT = 22,
  WRITE_ROWS_EVENT = 23,
  UPDATE_ROWS_EVENT = 24,
  DELETE_ROWS_EVENT = 25,
  INCIDENT_EVENT= 26,
  HEARTBEAT_LOG_EVENT= 27,
  ENUM_END_EVENT
  /* end marker */
};

比如ROTATE_EVENT对应的记录了binlog切换到下一个binlog文件的信息,XID_EVENT记录了一个事务提交的相关信息。Binlog_format可以设置为STATEMENT和ROW的方式。当设置为STATEMENT情况下,DML会记录为原始的SQL,也就是记录在QUERY_EVENT中。而ROW会记录为TABLE_MAP_EVENT+ROW_LOG_EVENT(包括WRITE_ROWS_EVENT,UPDATE_ROWS_EVENT,DELETE_ROWS_EVENT)。使用mysqlbinlog可以看看他们的区别。

STATEMENT方式下,记录为QUERY_EVENT如下图:

statement_event

 

 

 

 

 

ROW方式下,update一条记录如下:

row_simple

 

 

 

 

这样的话我们就无法看到它到底update了什么数据,使用mysqlbinlog -vvv可以让它更详细的翻译给我看:

row_vvv

 

 

 

 

 

 

 

淘宝采用的是ROW方式,有两个好处:第一:更容易解析,DRC或者mysql transfer等淘宝系数据库迁移工具可以精确的解析出数据,进行同步;第二:可以有效避免rand(),uuid()等由于主备环境不一致而导致的问题。

这里还有一个地方需要解释一下,为什么一个update在ROW模式下需要分解成两个event:一个Table_map,一个Update_rows。我们想象一下,一个update如果更新了10000条数据,那么对应的表结构信息是否需要记录10000次列,其实是对同一个表的操作,所以这里binlog只是记录了一个Table_map用于记录表结构相关信息,而后面的Update_rows记录了更新数据的行信息。他们之间是通过table_id来联系的。

淘宝的问题也就出现在这里,这两个事件是通过table_id来联系的,table_id是ulong类型的。刚好这个联系在淘宝的这个环境下就断了。具体的细节要牵涉到部分源码。对源码不感兴趣的同学可以直接跳过这一段:

首先,我们了解一下记录表定义信息的数据结构。它对应的class是Table_map_log_event(对应源码sql/log_event.cc),它保存了本次DML操作对应的:table_id,数据库名,表名,字段数,字段类型等。对应的这些信息都是保存在一个table_mapping的hash数据结构中(sql/rpl_tblmap.cc中)。hash的key就是table_id,hash的值就是TABLE*的数据结构(包含了表的各种信息,包括数据库名,表名,字段数,字段类型等),通过set_table()方法来hash,通过get_table()方法来根据table_id获得对应的表信息。这里table_id是ulong型的。

然后我们了解一下存储变更的具体数据的数据结构。update行的Update_row event对应的class是Update_rows_log_event(对应源码sql/log_event.cc),基类是Rows_log_event(之类分别有:Write_rows_log_event,Update_rows_log_event,Delete_rows_log_event与insert, update, delete一一对应)。一个Row_log_event对应一行数据的变更(插入,更新,删除),它记录的信息包括table_id,哪些字段为空的bitmap,各个字段的具体数据等。这里table_id是ulong型的。

所有的binlog event有一个公共的父类Log_event(对应源码sql/log_event.h),每一个log_event都是通过do_apply_event()方法来将event应用到本地数据库去。

另外,我们必须要介绍一下RPL_TABLE_LIST结构(对应源码sql/rpl_utility.h),它的父类TABLE_LIST(对应源码:sql/table.h)中定义table_id为:

  uint          table_id; /* table id (from binlog) for opened table */   //这里就是最终需要bug fix的地方

这里定义为uint和其他地方定义为ulong不一样!

这里就是最终需要bug fix的地方。问题的原因我们还要继续看。这个RPL_TABLE_LIST是包含在Relay_log_info结构(对应源码sql/rpl_mi.h)中的,它记录了这次变更需要lock的多个表信息。

 

前面提过,每一个event都有一个do_apply_event()方法用于将event应用到本地数据库中去。int Table_map_log_event::do_apply_event(Relay_log_info const *rli)方法(对应源码sql/log_event.cc)中就将ulong型的m_table_id赋值给uint型的table_list->table_id,而table_list作为tables_to_lock存入了公共变量rli中。

table_list->table_id= DBUG_EVALUATE_IF(“inject_tblmap_same_id_maps_diff_table”, 0, m_table_id);

/*
We record in the slave’s information that the table should be
locked by linking the table into the list of tables to lock.
*/
table_list->next_global= table_list->next_local= rli->tables_to_lock;
const_cast<Relay_log_info*>(rli)->tables_to_lock= table_list;
const_cast<Relay_log_info*>(rli)->tables_to_lock_count++;
/* ‘memory’ is freed in clear_tables_to_lock */

Row_log_event类的int Rows_log_event::do_apply_event(Relay_log_info const *rli)方法(对应源码sql/log_event.cc)中:

TABLE_LIST *ptr= rli->tables_to_lock;
for (uint i=0 ; ptr && (i < rli->tables_to_lock_count); ptr= ptr->next_global, i++)
const_cast<Relay_log_info*>(rli)->m_table_map.set_table(ptr->table_id, ptr->table);

利用记录在rli(Relay_log_info结构)中的tables_to_lock获得table_list(RPL_TABLE_LIST结构),而这个结构里面的是已经被截断的uint型的table_id。

当需要对具体的表进行row变更的时候在同样的int Rows_log_event::do_apply_event(Relay_log_info const *rli)方法(对应源码sql/log_event.cc)中:

TABLE*
table=
m_table= const_cast<Relay_log_info*>(rli)->m_table_map.get_table(m_table_id);

DBUG_PRINT(“debug”, (“m_table: 0x%lx, m_table_id: %lu”, (ulong) m_table, m_table_id));

通过ulong型的值去获得hash表中获得表结构信息就无法获取了。也就是说,之前用uint型的ptr->table_id构建出来的key,value的hash对,用ulong型的m_table_id是无法查询到的。

图示如下:

tableid_bug

 

 

 

 

 

 

 

 

 

 

为了举例简单,假设uint 4 bit ulong 8bit,ulong11111111被存到了uint型的数据中(假设被截断为00001111)并存到hash表中去了,那么对应的ulong型的key去查数据时,插到的表定义肯定就是NULL了。这样的话,表结构找不到,备机也就无法同步主库的任何DML数据,也就是淘宝物流库备机与主机不一致的原因了。

 

那么是什么原因导致的这个问题列。淘宝是因为它的table cache设置过小,table_definition_cache为256,table_open_cache为512,而该实例上由于分库分表,表一共有4301个,table cache严重不足。这里很多人对table_id有误解,认为table_id是跟表一起走的,是固定的。其实table_id是表载入table cache时临时分配的,一个不断增长的变量。当table cache不足,flush table又非常多的时候,这个table_id增长的速率非常快,达到uint的上限时,2的32次方以后,就触发了这个bug,导致主备不一致。广大的MySQL使用者都留意一下,你的table_id是不是也非常大了,超过2的32次方,那么你的备机就重搭吧。

解决方案:

1、自己打patch,重编译并替换线上MySQL

2、增加table cache 大小。

3、重启主库让table_id归零。

 

参考:

http://hickey.in/?p=146

http://hatemysql.com/2011/12/14/mysql-show-slave-status/

http://dev.mysql.com/doc/internals/en/binary-log.html

MariaDB 新特性介绍-动态虚拟列

MariaDB为NoSQL的扩展提供的另外一个特性 就是:动态列。对表的每一行都可以有一个“虚拟列”,该列可以用于保存一个对象的不同属性,每个属性在各行之间可以完全不同,“虚拟列”的个数和内容完全有应用程序来控制和解释。

NoSQL的另外一个好处就是,我存储的东西是一个没有结构的东西,而数据库则要求是一个固定列名,确定长度的一个个列。为了满足这个需求,MariaDB引入了动态列的概念。它允许你在每一行上有一个“虚拟列”。这个虚拟列被作为一个额外的字段存在在blob中,并且有一系列函数用于创建,更新,删除,检查,查询这个列。 空说无凭,举例为证。我们先创建一个表t1,用来存储shirt,phone,computer商品的价格,其中dynstr就是用于做虚拟列的:

create table t1 (id int auto_increment primary key,
                 name varchar(40),
                 type enum ("shirt", "phone", "computer"),
                 price decimal(10,2),
                 dynstr mediumblob);

并插入一些数据

insert into t1 (name, type, price, dynstr) values
("Funny shirt", "shirt", 10.0, COLUMN_CREATE(1, "blue", 10, "XL")),
("nokia", "phone", 649, COLUMN_CREATE(1, "black", 2, "touchscreen")),
("htc Desire hd", "phone", 579, COLUMN_CREATE(1, "black", 3, "Android")),
("BM/Lenovo Thinkpad X60s", "computer", 419, COLUMN_CREATE(1, "black", 3, "Linux"));

这里利用了COLUMN_CREATE()函数来创建动态列。插入的四条数据。针对各个不同的商品定义不同的属性。比如1在各个商品中表示的是颜色;3表示的是操作系统;10表示的是大小。每行数据的属性不完全相同,也不要求所有的属性值都需要包含在动态列中。那么,动态列的有哪些属性要怎么查看列:

select id, name, type, price, length(dynstr) as len, column_list(dynstr) as list from t1;
+----+-------------------------+----------+--------+------+------+
| id | name                    | type     | price  | len  | list |
+----+-------------------------+----------+--------+------+------+
|  1 | Funny shirt             | shirt    |  10.00 |   17 | 1,10 |
|  2 | nokia                   | phone    | 649.00 |   27 | 1,2  |
|  3 | htc Desire hd           | phone    | 579.00 |   23 | 1,3  |
|  4 | BM/Lenovo Thinkpad X60s | computer | 419.00 |   21 | 1,3  |
+----+-------------------------+----------+--------+------+------+

COLUMN_LIST()可以列出动态列到底有哪些属性,这样的话我们就可以根据属性过滤

SELECT name FROM t1 WHERE COLUMN_GET(dynstr, 1 as char(10)) = "black";
+-------------------------+
| name                    |
+-------------------------+
| nokia                   |
| htc Desire hd           |
| BM/Lenovo Thinkpad X60s |
+-------------------------+
SELECT name, COLUMN_GET(dynstr, 1 as char(10)) FROM t1 WHERE COLUMN_EXISTS(dynstr, 1);
+-------------------------+--------+
| name                    | colour |
+-------------------------+--------+
| Funny shirt             | blue   |
| nokia                   | black  |
| htc Desire hd           | black  |
| BM/Lenovo Thinkpad X60s | black  |
+-------------------------+--------+

上面的第一条SQL查询了颜色为black的所有产品哪些属性,第二条SQL查询了存在颜色属性的所有行。对应的COLUMN_GET() 函数表示获取对应属性的属性值。COLUMN_EXISTS()函数表示动态列中是否存储了该属性值。我们也注意到,现在动态列的所有属性都是用数字来表示的。

前面都是对动态列创建和查询的例子,我们再看看更新的例子。比如我们需要对类型为‘computer’的数据加4G内存: 

UPDATE t1 set dynstr=COLUMN_ADD(dynstr, 15, "4G ram") where type="computer";
SELECT name, type, price, length(dynstr) as len, column_list(dynstr) as list from t1 where type="computer";
+-------------------------+----------+--------+------+--------+
| name                    | type     | price  | len  | list   |
+-------------------------+----------+--------+------+--------+
| BM/Lenovo Thinkpad X60s | computer | 419.00 |   29 | 1,3,15 |
+-------------------------+----------+--------+------+--------+

COLUMN_ADD()函数会给每个’computer’的行加上新的属性,属性值为‘4G ram’。不过,COLUMN_ADD()并不仅仅是添加,它和MySQL的replace一样,如果发现相同类型的属性,对应的属性值将被覆盖。

动态列现在正在快速发展期,还存在一定的限制。比如:

  • 列必须通过数字来标识;
  • 动态列对象最大只有536870911字节;
  • 创建索引问题;

具体信息参考:http://kb.askmonty.org/en/dynamic-columns/

 

MariaDB 新特性介绍-handlesocket

NoSQL的发展如火如荼,如果MySQL也能提供NoSQL的功能和性能,那你是否介意让MySQL在NoSQL领域来个第二春。接下来我们来看看MariaDB在NoSQL方面的扩展。首先是HandleSocket,它能够绕过SQL解析等步骤支持你直接从InnoDB/XtraDB中获取数据。

现在NoSQL的盛行已经超过了我们的想象,各种NoSQL的产品和软件工具不断涌现,层出不穷。不过,MariaDB也不甘示弱,它引入了HandleSocket作为plugin,使得你可以越过server层,直接操作innoDB或者XtraDB。这样的话,你就可以把MariaDB当成一个NoSQL来使用了。目前支持的客户端包括C++,Perl,PHP,Java,Python,Ruby,JavaScript,Scala,Haskell8种。

从MariaDB5.3.0开始,HandleSocket就随源码和二进制分发包一起发布。但是你需要以root用户通过INSTALL PLUGIN安装这个插件。

MariaDB [(none)]> INSTALL PLUGIN handlersocket SONAME 'handlersocket.so';

安装成功以后,SHOW PROCESSLIST就可以看到HandleSocket的各个工作线程了。对应的客户端应用程序需要连接HandleSocket进行操作的话,需要安装对应的handlesocke client,对应的库参考:http://kb.askmonty.org/en/handlersocket-client-libraries/

Handlesock是作为一个精灵进程跑在mysqld进程中,它能够接受TCP链接,并且执行客户端的请求,但是它并不支持SQL。 由于它不分析SQL,并且会尽量优化进行批量操作,所以它的CPU消耗非常小;另外,协议相对mysql/libmysql会紧凑得多,所以网络上的开销也会小的多。网上有很多对HandleSocket的测试,它比一般的NoSQL产品都会快得多。Handlesocket也存在一些限制:

  • 它对IO Bound类型的应用没有太多性能提升。
  • 如果你需要对表进行操作,由于HandleSocket持有锁,那么对应的HandleSocket的业务访问都必须暂停下来
  • HandleSocket在并发情况下和自增配合有问题,使用HandleSocket的情况下,建议取消自增。

HandleSocket具体信息参考:http://kb.askmonty.org/en/handlersocket/

MariaDB 新特性介绍-进度报告

当你需要对MySQL做一个长时间的操作时,比如alter table,能做的就是等待,再等待,如果MySQL告诉你:它正在做”copy to tmp table”,并且这一步骤已经做完了5.37%,你会不会爱它多一点。没错,这就是MariaDB提供的新特性:进度报告。参考http://t.cn/zlPLwtI

很对DBA相信都做过大表的alter table,我们把命令敲进去以后,MySQL就自顾自的做事情了,如果要一个小时,那么我们看到的MySQL界面展示界面就一直是我们刚刚敲进去命令的样子。到底它需要执行多少时间,现在做什么事情,做这个事情做到了什么程度,我们一无所知。于是,MariaDB为我们引入了进度报告的概念。我们先一睹为快:

MariaDB [test]> alter table my_mail engine=maria;
Stage: 1 of 2 'copy to tmp table'  5.37% of stage done

如上,我们想要修改my_mail表为maria引擎,执行了alter table的命令。MariaDB告诉我它分两步来做,现在正在做第一步:”copy to tmp table”,而这一步它已经完成了5.37%。是不是很人性化?

在MariaDB,我们可以看到一共有多少步stage,目前完成了哪些步stage,目前在这个stage上完成百分之几。进度一目了然,非常友好。对应的在show processlist命令和INFORMATION_SCHEMA.PROCESSLIST表上都进行了配套的修改。这样的话,用户就可以判断目前的进度和整个操作需要进行多少时间。 目前MariaDB对以下几种命令会进行进度报告:

  • ALTER TABLE
  • ADD INDEX
  • DROP INDEX
  • LOAD DATA INFILE (对应的另外一个命令LOAD DATA LOCAL INFILE由于不知道文件大小,所以无效).

而MariaDB默认存储引擎Aria支持其他的几个命令:

  • CHECK TABLE
  • REPAIR TABLE
  • ANALYZE TABLE
  • OPTIMIZE TABLE

另外,进度报告功能是默认支持的,如果需要取消,需要在客户端指定–disable-progress-reports。 具体信息请参考:http://kb.askmonty.org/en/progress-reporting/

MariaDB 新特性介绍-limit 行检查限制

MySQL的分页语法比oracle简单,直接limit就可以了,但是limit 1000000,10将扫描1000010的记录并丢弃一百万的数据,用户翻页获得下10条记录可能需要等待好几分钟。并且数据库的IO资源在此期间受到很大的冲击。MariaDB提供了一种简单的解决办法,在检查的数据行数大于指定值之后就报错退出,节省数据库IO资源。

我们先看看它的语法:

SELECT FROM WHERE [group_clause] [order_clause] LIMIT [[offset,] row_count] ROWS EXAMINED rows_limit;

limit算是MySQL比oracle语法友好的一个非常方便的功能了。但是,对于分页查询来说,如果你limit的offset非常大,那么你的数据库就悲剧了。比如limit 1000000,10。那么MySQL就需要先把1000010条记录找出来,然后丢弃掉前100000条记录。查询这么多记录数据库是一个很大的负担,并且查出来的100000条记录其实也都不是你想要的数据。这也是很多访问量极大的网站取消了你直接跳到某页功能的原因所在。大部分的DBA对这个一筹莫展,部分DBA会说服产品设计师采用记录上一页最大ID,然后where id>? limit 10的办法解决。 我们曾经遇到过在订单库上,很多limit 500000,10的并发使得数据库响应时间变成原来的10倍的情况。导致了非常严重的故障。

MariaDB提供了一个变通的办法,她让你指定一个检查行数限制。如果超过这个检查行数,那么就不要浪费数据库资源了,直接中断查询退出。当然它并不是对所有的Query都有效,它的限制是:它对Delete和Update无效;如果子查询查询的行操作,而你的限制是执行在父查询上,那么它只对父查询有效;如果是组合查询(Union,子查询等),在多个地方有ROWS EXAMINED检查行限制,那么只有最后一个限制有效。 当然,这个只是一个尽量减少对数据库资源浪费的办法,如果没有必要,还是需要尽量从应用层面说服产品负责人避免这种访问。比如:

SELECT * from t1, t2 LIMIT 10 ROWS EXAMINED 10000;

这个查询限制了检查行数不能超过一万。而下面这个查询

SELECT * FROM t1
WHERE c1 IN (SELECT * FROM t2 WHERE c2 > ' ' LIMIT ROWS EXAMINED 0)
LIMIT ROWS EXAMINED 11;

内部的行数限制被覆盖掉了,就只有外围的11行检查限制生效了。具体信息参考:http://kb.askmonty.org/en/limit-rows-examined/