案例十二、shell多线程备份数据库

shell脚本多线程,实现起来有点难理解,因为它借助了命名管道实现。所谓多线程就是原本由一个进程完成的事情现在由多个线程去完成。假如一个进程需要10小时完成的事情,现在分配10个线程,给他们分工,然后同时去做这件事情,最终可能就需要1小时。

本案例具体需求如下:

1)公司的业务量比较大,有100个数据库需要全量备份,而每个数据库的数据量高达几十GB,(注意,每一个库都为一个独立的实例,即有着独立的IP:port)。

2)预估每一个库的备份时间在30分钟左右。

3)要求在5小时内备份完成。


提示:要想在5小时内完成100个数据库的备份,需要使用shell脚本的多线程功能,一次性开10个线程同时并发备份10个数据库。


知识点一:使用xtrabackup备份MySQL数据库

mysqldump对于导出几个G的数据库或几个表,还是不错的。一旦数据量达到几十上百G,无论是对原库的压力还是导出的性能,mysqldump就不太行了。Percona-Xtrabackup备份工具,实现MySQL在线热备工作的不二选择,可进行全量,增量,单表备份和还原。


xtrabackup命令只支持InnoDB和XtraDB存储引擎的数据库非阻塞地备份,而innobackupex通过Perl封装了一层xtrabackup,对Myisam的备份通过加表读锁的方式实现。


在CentOS7上这样安装Percona-Xtrabackup:

# rpm -ivh https://www.percona.com/downloads/percona-release/redhat/0.1-3/percona-release-0.1-3.noarch.rpm  //安装yum源
# yum install -y percona-xtrabackup-24  //yum安装2.4版本

用xtrabackup做全量备份的命令是:

# innobackupex --default-file=/etc/my.cnf --host=10.100.100.100 --port=3333 --user=bakuser --password=your_pass /data/backup/mysql

说明:在执行该备份操作之前,需要先创建一个用户bakuser(用户名自定义),并授予reload,lock tables,replication client,process,super等权限。备份数据将会放到/data/backup/mysql目录里面,自动生成一个以当前日期、时间为名字的目录,例如2019-08-10_09_56_12。


知识点二:文件描述符

文件描述符(缩写fd)在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。每一个Unix进程,都会拥有三个标准的文件描述符,来对应三种不同的流:

文件描述符        名称
0               标准输入
1               标准正确输出
2               标准错误输出

除了上面三个标准的描述符外,还可以在进程中去自定义其他的数字作为文件描述符。每一个文件描述符会对应打开一个文件,同时,不同的文件描述符也可以对应同一个打开文件;同一个文件可以被不同的进程打开,也可以被同一个进程多次打开。


写一个测试脚本,/tmp/test.sh,内容:

#!/bin/bash
echo "该进程的pid为$$"
exec 1>/tmp/test.log 2>&1
ls -l /proc/$$/fd/

执行该脚本,然后查看/tmp/test.log

# cat test.log
总用量 0
lrwx------. 1 root root 64 8月  10 10:24 0 -> /dev/pts/1
l-wx------. 1 root root 64 8月  10 10:24 1 -> /tmp/test.log
l-wx------. 1 root root 64 8月  10 10:24 2 -> /tmp/test.log
lr-x------. 1 root root 64 8月  10 10:24 255 -> /tmp/test.sh

说明:exec将脚本后续指令的正确和错误输出重定向到了/tmp/test.log,所以查看该文件时就会看到上述内容。关于exec命令,再看一个直观的例子:

[root@wbs 12.sh]# exec > /tmp/test
[root@wbs 12.sh]# echo "123123"
[root@wbs 12.sh]# echo $PWD
[root@wbs 12.sh]# lalala
-bash: lalala: 未找到命令
[root@wbs 12.sh]# exec > /dev/tty
[root@wbs 12.sh]# cat /tmp/test
123123
/root/20shell/12.sh

说明:通过上面的例子,可以发现,当执行exec后,其后面的命令的标准正确输出全部写入到了/tmp/test文件中,而错误的还是在当前终端上显示,要想退出这个设置,需要重新定义exec的标准输出为/dev/tty。


知识点三:命令管道

前面在shell脚本中多次用过管道符号“|”,这个叫做匿名管道,而这里提到的管道叫命名管道,功能和匿名管道基本上是一样的。

命名管道,英文名First In First Out,简称FIFO。命名管道特点:

1)在文件系统中,FIFO拥有名称,并且是以设备特殊文件的形式存在的;

2)任何进程都可以通过FIFO共享数据;

3)除非FIFO两端同时有读和写的进程,否则FIFO的数据流通将会阻塞;

4)匿名管道是由shell自动创建的,存在于内核中,而FIFO则是由程序创建的(比如mkfifo命令),存在于文件系统中;

5)匿名管道是单向的字节流,而FIFO是双向的字节流。


用mkfifo命令创建一个命名管道:

# screen
# mkfifo 123.fifo
# echo "121212" > 123.fifo  //此时被阻塞,因为我们只是在管道里写入了内容,并没有其他进程读这个内容。
ctrl+a d  //退出该screen
# cat 123.fifo  //此时可以看到121212内容,然后再进入screen去看刚才的echo那条命令已经结束了。

可以把命名管道和文件描述符结合起来:

[root@wbs ~]# mkfifo test.fifo
[root@wbs ~]# exec 100<>test.fifo  //这样可以把fd100的读和写全部指定到test.fifo中
[root@wbs ~]# ls -l /dev/fd/100  //可以看到fd100已经指向到了/root/test.fifo
lrwx------. 1 root root 64 8月  10 11:41 /dev/fd/100 -> /root/test.fifo


知识点四:read命令

在shell脚本中,read命令使用还是比较多的,最典型的用法是,和用户交互,如下:

# read -p "Please input a number:" n
Please input a number:3
# echo $n
3

如果不使用-p选项,也可以这样使用:

# read name
1234
# echo $name
1234

read的-u选项后面可以跟fd,如下:

# read -u 10 a  //这样会把fd 10里面的字符串赋值给a

注意,这里的fd 10就是前面定义的test.fifo,如果你的fd 10里还没有任何的内容写入,那么你执行上面这条命令会卡着不动。因为fd 10是一个命名管道文件,只有写入了东西,read才会读到,否则就一直卡着,等待写入内容。当然,这个命名管道文件可以写入多行,先储存起来,然后等着read去读。

# echo "123" >& 10
# echo "456" >& 10  //连续在fd10中写入两次内容
# read -u 10 a  //第一次读取fd10里的第一行
# echo $a
123
# read -u 10 a  //第二次读取fd10里的第二行
# echo $a
456


知识点五:wait命令

wait命令:等待的意思,即等待那些在没有完成的任务(主要是后台的任务),直到所有任务完成后,才会继续执行wait以后的指令,常用于shell脚本中。以下是关于wait指令的示例:

# sleep 5 &
# wait   //此时会卡死不动,直到上面的后台指令执行完,才会有反应。


知识点六:结合命名管道和read实现多线程

命名管道有两个很明显的特点:

1)先进先出,比如上例中我们给fd10写入了两行内容,则第一次read第一行,第二次read第二行。

2)有内容read则执行,没有则阻塞,例如上例中,read完两次后,如果你再执行一次read,则它会一直卡着,直到我们再次写入新的内容它才会read到。


利用这两个特点,就可以实现shell的多线程了,先看个例子:

#!/bin/bash
#创建命名管道123.fifo文件
mkfifo 123.fifo
#将命名管道123.fifo和文件描述符1000绑定,即fd1000的输入输出都是在123.fifo中
exec 1000<>123.fifo

#连续向fd1000中写入两次空行
echo >&1000
echo >&1000

#循环10次
for i in `seq 1 10`
do
    #每循环一次,读一次fd1000中的内容,即空行,只有读到空行了,才会执行{ }内的指令
    #每次循环都需要打印当前的时间,休眠1秒,然后再次向fd1000中写入空行,这样后续的read就有内容了
    #read指令不仅可以赋值,也可以跟一个函数,用{ }括起来,函数中是多条指令
    read -u1000
    {
        date +%T
        echo $i
        sleep 1
        echo >&1000
    } &  //丢到后台去,这样10次很快就循环完,只不过这些任务是在后台跑着。由于我们一开始就向fd1000里写入了两个空行,所以read会一次性读到两行。
done
#等待所有后台任务执行完成
wait
#删除fd1000
exec 1000>&-
#删除命名管道
rm -f 123.fifo

执行结果:

08:54:11
1
08:54:11
2
08:54:12
3
08:54:12
4
08:54:13
5
08:54:13
6
08:54:14
08:54:14
7
8
08:54:15
9
08:54:15
10

可以看到,原本需要10秒完成的事情,现在5秒就完成了,这说明并发量为2,即两个线程同时执行任务,要想5个线程,那么在一开始的时候,直接向fd1000写入5个空行即可。


本案例参考脚本

#!/bin/bash
#多线程备份数据库
#作者:
#日期:
#版本:v1.0

##假设100个库的库名、host、port以及配置文件路径存到了一个文件里,文件名字为/tmp/databases.list
##格式:db1 10.10.10.2 3308 /data/mysql/db1/my.cnf
##备份数据库使用xtrabackup(由于涉及到myisam,命令为inoobackupex)

exec &> /tmp/mysql_bak.log

if ! which innobackupex &>/dev/nll
then
    echo "安装xtrabackup工具"
    rpm -ivh http://www.percona.com/downloads/percona-release/redhat/0.1-3/percona-release-0.1-3.noarch.rpm  && \ 
    yum install -y percona-xtrabackup-24
    if [ $? -ne 0 ]
    then
        echo "安装xtrabackup工具出错,请检查。"
        exit 1
    fi
fi

bakdir=/data/backup/mysql
bakuser=vyNctM
bakpass=99omeaBHh

function bak_data {
    db_name=$1
    db_host=$2
    db_port=$3
    cnf=$4
    [ -d $bakdir/$db_name ] || mkdir -p $bakdir/$db_name
    innobackupex --defaults-file=$4  --host=$2  --port=$3 --user=$bakuser --password=$bakpass  $bakdir/$1
        if [ $? -ne 0 ]
        then
            echo "备份数据库$1出现问题。"
        fi
}

fifofile=/tmp/$$
mkfifo $fifofile
exec 1000<>$fifofile


thread=10
for ((i=0;i<$thread;i++))
do
    echo >&1000
done

cat /tmp/databases.list | while read line
do
    read -u1000
    {
        bak_data `echo $line`
        echo >&1000
    } &
done

wait
exec 1000>&-
rm -f $fifofile


上一篇:Linux


下一篇:linux – 关闭不需要的文件描述符