嵌入式技术
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官网下载地址:https://www.percona.com/downloads/Percona-XtraBackup-LATEST/,由于我的系统是Rocky8,所以在这里,我下载8.0.30版本。
wget https://downloads.percona.com/downloads/Percona-XtraBackup-LATEST/Percona-XtraBackup-8.0.30-23/binary/tarball/percona-xtrabackup-8.0.30-23-Linux-x86_64.glibc2.17.tar.gz
因为是二进制包,解压后可直接使用,将包解压到/usr/local/下
tar zxf percona-xtrabackup-8.0.30-23-Linux-x86_64.glibc2.17.tar.gz -C /usr/local/ ln -s /usr/local/percona-xtrabackup-8.0.30-22-Linux-x86_64.glibc2.17/bin/xtrabackup /usr/bin/
用xtrabackup做全量备份的命令是:
# xtrabackup --defaults-file=/usr/local/mysql/my.cnf --user=bakuser --password=your_pass -S /tmp/mysql.sock --backup --target-dir=/data/backup/mysql/20221210
说明:在执行该备份操作之前,需要先创建一个用户bakuser(用户名自定义),并授予reload, lock tables, replication client, process, super等权限。备份数据将会放到/data/backup/mysql/20221210目录里面。
知识点二:文件描述符
文件描述符(缩写fd)在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。每一个unix进程,都会拥有三个标准的文件描述符,来对应三种不同的流:
文件描述符 | 名称 |
0 | 标准输入 |
1 | 标准正确输出 |
2 | 标准错误输出 |
除了上面三个标准的描述符外,我们还可以在进程中去自定义其他的数字作为文件描述符。每一个文件描述符会对应一个打开文件,同时,不同的文件描述符也可以对应同一个打开文件;同一个文件可以被不同的进程打开,也可以被同一个进程多次打开。
我们可以写一个测试脚本/tmp/test.sh,内容如下:
#!/bin/bash echo "该进程的pid为$$" exec 1>/tmp/test.log 2>&1 ls -l /proc/$$/fd/
执行该脚本 sh /tmp/test.sh,然后查看/tmp/test.log:
# cat /tmp/test.log 总用量 0 lrwx------ 1 root root 64 12月 10 10:06 0 -> /dev/pts/0 l-wx------ 1 root root 64 12月 10 10:06 1 -> /tmp/test.log l-wx------ 1 root root 64 12月 10 10:06 2 -> /tmp/test.log lr-x------ 1 root root 64 12月 10 10:06 255 -> /tmp/test.sh
说明:exec将脚本后续指令的正确和错误输出重定向到了/tmp/test.log,所以查看该文件就会看到以上内容。关于exec命令,我们再来看一个直观的例子:
# exec > /tmp/test # echo "123123" # echo $PWD # lalala -bash: lalala: 未找到命令 # exec > /dev/tty # cat /tmp/test 123123 /root说明:通过上面的例子,可以发现,当执行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那条命令已经结束了。
我们可以把命名管道和文件描述符结合起来:
# mkfifo test.fifo # exec 100<>test.fifo //这样可以把fd100的读和写全部指定到test.fifo中 # ls -l /dev/fd/100 //可以看到fd100已经指向到了/root/test.fifo lrwx------. 1 root root 64 12月 10 10:08 100 -> /root/test.fifo
知识点四:read命令
在shell脚本中,read命令使用还是比较多的,最典型的用法是,和用户交互,如下:
# read -p "Please input a number: " n Please input a number: 5 [root@aming-master ~]# echo $n 5
如果不使用-p选项,也可以这样使用:
# read name //name为变量名,这样也是在给name变量赋值 aming # echo $name aming
read的-u选项后面可以跟fd,如下:
# read -u10 a //这样会把fd10里面的字符串赋值给a注意,这里的fd10就是前面我们定义的test.fifo,如果你的fd10里还没有任何的内容写入,那么你执行上面这条命令会卡着不动。因为fd10是一个命名管道文件,只有写入了东西,read才会读到,否则就一直卡着,等待写入内容。当然,这个命名管道文件可以写入多行,先储存起来,然后等着read去读。
# echo "123" >&10 # echo "456" >&10 //连续在fd10中写入两次内容 # read -u10 a //第一次读取fd10里的第一行 # echo $a 123 # read -u10 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
执行脚本结果如下:
10:12:02 10:12:02 1 2 10:12:03 10:12:03 3 4 10:12:04 5 10:12:04 6 10:12:05 7 10:12:05 8 10:12:06 10:12:06 9 10可以看到,原本需要10秒完成的事情,现在需要5秒就搞定了,这说明并发量为2,即两个线程同时执行任务。要想5个线程,那么在一开始的时候,直接向fd1000写入5个空行即可。
本案例参考脚本
#!/bin/bash #多线程备份数据库 #作者:阿铭 #日期:2022-12-10 #版本:v1.5 ##假设100个库的库名、host、port以及配置文件路径存到了一个文件里,文件名字为/tmp/databases.list ##格式:db1 10.10.10.2 3308 /data/mysql/db1/my.cnf ##备份数据库使用xtrabackup exec &> /tmp/mysql_bak.log if ! which xtrabackup &>/dev/nll then echo "安装xtrabackup工具" wget https://downloads.percona.com/downloads/Percona-XtraBackup-LATEST/Percona-XtraBackup-8.0.30-23/binary/tarball/percona-xtrabackup-8.0.30-23-Linux-x86_64.glibc2.17.tar.gz tar zxf percona-xtrabackup-8.0.30-23-Linux-x86_64.glibc2.17.tar.gz -C /usr/local/ && ln -s /usr/local/percona-xtrabackup-8.0.30-22-Linux-x86_64.glibc2.17/bin/xtrabackup /usr/bin/ if [ $? -ne 0 ] then echo "安装xtrabackup工具出错,请检查。" exit 1 fi fi bakdir=/data/backup/mysql/`date +%F` 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 xtrabackup --defaults-file=$4 --host=$2 --port=$3 --user=$bakuser --password=$bakpass --databases=$1 --backup --target-dir=$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
这个脚本有点复杂,需要琢磨一会儿。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !