Shell這么簡單的腳本語言有多線程這一說嗎?答案是有的。只不過它實(shí)現(xiàn)起來稍微有點(diǎn)難理解罷了,因?yàn)樗柚嗣艿缹?shí)現(xiàn)。所謂多線程就是原本由一個進(jìn)程完成的事情現(xiàn)在由多個線程去完成。假如一個進(jìn)程需要10小時完成的事情,現(xiàn)在分配10個線程,給他們分工,然后同時去做這件事情,最終可能就需要1小時。
本案例具體需求是這樣的:
1)公司的業(yè)務(wù)量比較大,有100個數(shù)據(jù)庫需要全量備份,而每個數(shù)據(jù)庫的數(shù)據(jù)量高達(dá)幾十GB,(注意,每一個庫都為一個獨(dú)立的實(shí)例,即有著獨(dú)立的ip:port)。
2)預(yù)估每一個庫的備份時間在30分鐘左右
3)要求在5小時內(nèi)備份完成
提示:要想在5小時內(nèi)完成100個數(shù)據(jù)庫的備份,需要使用shell腳本的多線程功能,一次性開10個線程同時并發(fā)備份10個數(shù)據(jù)庫。
知識點(diǎn)一:使用xtrabackup備份MySQL數(shù)據(jù)庫
Mysqldump對于導(dǎo)出幾個G的數(shù)據(jù)庫或幾個表,還是不錯的,速度并不慢。一旦數(shù)據(jù)量達(dá)到幾十上百G,無論是對原庫的壓力還是導(dǎo)出的性能,mysqldump就力不從心了。Percona-Xtrabackup備份工具,是實(shí)現(xiàn)MySQL在線熱備工作的不二選擇,可進(jìn)行全量、增量、單表備份和還原。
Xtrabackup官網(wǎng)下載地址:https://www.percona.com/downloads/Percona-XtraBackup-LATEST/,由于我的系統(tǒng)是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
?
因?yàn)槭嵌M(jìn)制包,解壓后可直接使用,將包解壓到/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
?
說明:在執(zhí)行該備份操作之前,需要先創(chuàng)建一個用戶bakuser(用戶名自定義),并授予reload, lock tables, replication client, process, super等權(quán)限。備份數(shù)據(jù)將會放到/data/backup/mysql/20221210目錄里面。
知識點(diǎn)二:文件描述符
文件描述符(縮寫fd)在形式上是一個非負(fù)整數(shù)。實(shí)際上,它是一個索引值,指向內(nèi)核為每一個進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進(jìn)程返回一個文件描述符。每一個unix進(jìn)程,都會擁有三個標(biāo)準(zhǔn)的文件描述符,來對應(yīng)三種不同的流:
文件描述符 | 名稱 |
0 | 標(biāo)準(zhǔn)輸入 |
1 | 標(biāo)準(zhǔn)正確輸出 |
2 | 標(biāo)準(zhǔn)錯誤輸出 |
除了上面三個標(biāo)準(zhǔn)的描述符外,我們還可以在進(jìn)程中去自定義其他的數(shù)字作為文件描述符。每一個文件描述符會對應(yīng)一個打開文件,同時,不同的文件描述符也可以對應(yīng)同一個打開文件;同一個文件可以被不同的進(jìn)程打開,也可以被同一個進(jìn)程多次打開。
我們可以寫一個測試腳本/tmp/test.sh,內(nèi)容如下:
?
#!/bin/bash echo "該進(jìn)程的pid為$$" exec 1>/tmp/test.log 2>&1 ls -l /proc/$$/fd/
?
執(zhí)行該腳本 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將腳本后續(xù)指令的正確和錯誤輸出重定向到了/tmp/test.log,所以查看該文件就會看到以上內(nèi)容。關(guān)于exec命令,我們再來看一個直觀的例子:
?
# exec > /tmp/test # echo "123123" # echo $PWD # lalala -bash: lalala: 未找到命令 # exec > /dev/tty # cat /tmp/test 123123 /root說明:通過上面的例子,可以發(fā)現(xiàn),當(dāng)執(zhí)行exec后,其后面的命令的標(biāo)準(zhǔn)正確輸出全部寫入到了/tmp/test文件中,而錯誤的還是在當(dāng)前終端上顯示,要想退出這個設(shè)置,需要重新定義exec的標(biāo)準(zhǔn)輸出為/dev/tty。
?
知識點(diǎn)三:命名管道
我們前面在shell腳本中多次用過這個管道符號'|',這個叫做匿名管道,也就是說它并沒有名字,而這里提到的管道叫做命名管道,功能和那個匿名管道基本上是一樣的。命名管道,英文名First In First Out,簡稱FIFO。命名管道有如下特點(diǎn): 1)在文件系統(tǒng)中,F(xiàn)IFO擁有名稱,并且是以設(shè)備特殊文件的形式存在的; 2)任何進(jìn)程都可以通過FIFO共享數(shù)據(jù); 3)除非FIFO兩端同時有讀與寫的進(jìn)程,否則FIFO的數(shù)據(jù)流通將會阻塞; 4)匿名管道是由shell自動創(chuàng)建的,存在于內(nèi)核中,而FIFO則是由程序創(chuàng)建的(比如mkfifo命令),存在于文件系統(tǒng)中; 5)匿名管道是單向的字節(jié)流,而FIFO則是雙向的字節(jié)流; 可以使用mkfifo命令創(chuàng)建一個命名管道:
# screen # mkfifo 123.fifo # echo "121212" > 123.fifo //此時被阻塞,因?yàn)槲覀冎皇窃诠艿览飳懭雰?nèi)容了,并沒有其他的進(jìn)程讀這個內(nèi)容
?
按ctrl+a 再按d,退出該screen
?
# cat 123.fifo //此時可以看到121212內(nèi)容,然后再進(jìn)入screen去看剛才的echo那條命令已經(jīng)結(jié)束了。
?
我們可以把命名管道和文件描述符結(jié)合起來:
?
# mkfifo test.fifo # exec 100<>test.fifo //這樣可以把fd100的讀和寫全部指定到test.fifo中 # ls -l /dev/fd/100 //可以看到fd100已經(jīng)指向到了/root/test.fifo lrwx------.?1?root?root?64?12月?10?10:08?100?->?/root/test.fifo??
?
知識點(diǎn)四:read命令
在shell腳本中,read命令使用還是比較多的,最典型的用法是,和用戶交互,如下:
?
# read -p "Please input a number: " n Please input a number: 5 [root@aming-master ~]# echo $n 5
?
如果不使用-p選項(xiàng),也可以這樣使用:
?
# read name //name為變量名,這樣也是在給name變量賦值 aming # echo $name aming
?
read的-u選項(xiàng)后面可以跟fd,如下:
?
# read -u10 a //這樣會把fd10里面的字符串賦值給a注意,這里的fd10就是前面我們定義的test.fifo,如果你的fd10里還沒有任何的內(nèi)容寫入,那么你執(zhí)行上面這條命令會卡著不動。因?yàn)閒d10是一個命名管道文件,只有寫入了東西,read才會讀到,否則就一直卡著,等待寫入內(nèi)容。當(dāng)然,這個命名管道文件可以寫入多行,先儲存起來,然后等著read去讀。
# echo "123" >&10 # echo "456" >&10 //連續(xù)在fd10中寫入兩次內(nèi)容 # read -u10 a //第一次讀取fd10里的第一行 # echo $a 123 # read -u10 a //第二次讀取fd10里的第二行 # echo $a 456
?
知識點(diǎn)五:wait命令
wait命令顧名思義就是等待的意思,即等待那些在沒有完成的任務(wù)(主要是后臺的任務(wù)),直到所有任務(wù)完成后,才會繼續(xù)執(zhí)行wait以后的指令,常用于shell腳本中。以下是關(guān)于wait指令的示例:
# sleep 5 & # wait //此時會卡死不動,直到上面的后臺指令執(zhí)行完,才會有反應(yīng)。
?
知識點(diǎn)六:結(jié)合命名管道和read實(shí)現(xiàn)多線程
命名管道有兩個很明顯的特點(diǎn):
1)先進(jìn)先出,比如上例中我們給fd10寫入了兩行內(nèi)容,則第一次read第一行,第二次read第二行。
2)有內(nèi)容read則執(zhí)行,沒有則阻塞,例如上例中,read完兩次后,如果你再執(zhí)行一次read,則它就會一直卡著,直到我們再次寫入新的內(nèi)容它才會read到
利用這兩個特點(diǎn),我們就可以實(shí)現(xiàn)shell的多線程了,先看這個例子:
?
#!/bin/bash #創(chuàng)建命名管道123.fifo文件 mkfifo 123.fifo #將命名管道123.fifo和文件描述符1000綁定,即fd1000的輸入輸出都是在123.fifo中 exec?1000<>123.fifo #連續(xù)向fd1000中寫入兩次空行 echo >&1000 echo >&1000 #循環(huán)10次 for i in `seq 1 10` do #每循環(huán)一次,讀一次fd1000中的內(nèi)容,即空行,只有讀到空行了,才會執(zhí)行{ }內(nèi)的指令 #每次循環(huán)都需要打印當(dāng)前的時間,休眠1秒,然后再次向fd1000中寫入空行,這樣后續(xù)的read就有內(nèi)容了 #read指令不僅可以賦值,也可以跟一個函數(shù),用{ }括起來,函數(shù)中是多條指令 read -u1000 { date +%T echo $i sleep 1 echo >&1000 } & #丟到后臺去,這樣10次很快就循環(huán)完,只不過這些任務(wù)是在后臺跑著。由于我們一開始就向fd1000里寫入了兩個空行,所以read會一次性讀到兩行。 done #等待所有后臺任務(wù)執(zhí)行完成 wait #刪除fd1000 exec 1000>&- #刪除命名管道 rm -f 123.fifo
?
執(zhí)行腳本結(jié)果如下:
?
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秒完成的事情,現(xiàn)在需要5秒就搞定了,這說明并發(fā)量為2,即兩個線程同時執(zhí)行任務(wù)。要想5個線程,那么在一開始的時候,直接向fd1000寫入5個空行即可。
?
本案例參考腳本
?
#!/bin/bash #多線程備份數(shù)據(jù)庫 #作者:阿銘 #日期:2022-12-10 #版本:v1.5 ##假設(shè)100個庫的庫名、host、port以及配置文件路徑存到了一個文件里,文件名字為/tmp/databases.list ##格式:db1 10.10.10.2 3308 /data/mysql/db1/my.cnf ##備份數(shù)據(jù)庫使用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 "備份數(shù)據(jù)庫$1出現(xiàn)問題。" 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
?
這個腳本有點(diǎn)復(fù)雜,需要琢磨一會兒。
審核編輯:湯梓紅
?
評論