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