天天看點

Shell多線程備份資料庫

作者:阿銘linux
Shell多線程備份資料庫

【一天一文檔】-- 關注我,持續學習Linux運維!

Shell這麼簡單的腳本語言有多線程這一說嗎?答案是有的。隻不過它實作起來稍微有點難了解罷了,因為它借助了命名管道實作。所謂多線程就是原本由一個程序完成的事情現在由多個線程去完成。假如一個程序需要10小時完成的事情,現在配置設定10個線程,給他們分工,然後同時去做這件事情,最終可能就需要1小時。

本案例具體需求是這樣的:

1)公司的業務量比較大,有100個資料庫需要全量備份,而每個資料庫的資料量高達幾十GB,(注意,每一個庫都為一個獨立的執行個體,即有着獨立的ip:port)。

2)預估每一個庫的備份時間在30分鐘左右

3)要求在5小時内備份完成

提示:要想在5小時内完成100個資料庫的備份,需要使用shell腳本的多線程功能,一次性開10個線程同時并發備份10個資料庫。

阿銘Linux

知識點一:使用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程序,都會擁有三個标準的檔案描述符,來對應三種不同的流:

檔案描述符 名稱
标準輸入
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           

這個腳本有點複雜,需要琢磨一會兒。