目錄
分場景1、被刪除的數據很小小到只會引起10分鐘內的主從延遲-不建議
分場景2、被刪除的數據不小,但是如果直接delete一定會引起15分鐘以上的主從延遲
最終對於生產mysql的日誌清理策略的best practice
自動監控mysql主從延遲報警shell指令碼-behind_master.sh
使用CentOS的crontab設定監控指令碼每5s執行一次寫法
自動發送告警資訊到企業微信介面(aldi-cupidmq)的python指令碼
本方案適合:無關業務的「日誌數據」,但往往日誌數據是最最佔用我們的整體系統效能的,因此對這樣的日誌,我們是需要進行定期清理的。
如果你要說:業務數據也需要那麼我告訴你,業務數據肯定用的是本方案中的場景2中的分場景2模式(只有這一條路),但是業務數據會暴發到你連本方案都無法覆蓋的那一天的(很快的,如我上一家公司:幾千萬的會員生成業務流水),那麼當本方案都失效時怎麼辦?
答案就是:垂直折分,hash一致演算法,sharding sphere就要用上了,對於這一塊涉及到的面太龐大了因此我需要寫一段時間,當我寫完也會分享給到大家。
開始進入正文:
如果你是單機,如果你是自己在家玩。你的數據庫裡有億級數據,你來一條:
delete from user_behavior_logs;
然後你慢慢等個幾小時,等到你的mysql暴了、硬碟被燒了都沒事。
如果你在公司的生產環境,特別是在具有主從複製、1主多從甚至多主多從的環境下,你來一條delete命令,你知道會發生什麼事嗎?
它的原理其實是:mysql上的delete語句首先會同步到各個從庫上,delete語句會產生redo日誌也會同步在各個從庫上,然後是mysql本身數據的binlog也在同步。三條操作*總mysql庫量*你刪除的數據量產生的:
導致了上面的「主從延遲」這樣的一個問題。
當發生主從延遲時其實是不用怕的,當生產環境讀寫操作頻繁,總會發生一定概率的主從延遲。偶爾在大促季,一天發生個1-2次並且只要在主從延遲發生時,從庫可以在5-10分鐘內追平主庫就不構成任何影響。
但是,如果發生了主從延遲,這個從延尺不斷的在加大時間,超過了20分鐘,30分鐘,往40、60分鐘走時,此時的整個db羣就是:讀業務全部受影響,因爲從還沒執行完自己的任務還要去追主,但是主上不斷的在寫進大量的數據。一般爲了讓從能夠追上主,你就必須「鎖主庫」。
我們都知道,在生產環境下是不能鎖主庫的,一鎖,所有的訂單或者相關的「寫操作」都沒法提交了。
那麼就有人說了:讓從慢慢追主吧。
但是,些時你的整體網站是讀寫分離的,從庫追不平主庫,整體的讀業務又受影響。
這個痛苦啊,此時就會發生著名的「主從延遲土撥鼠之日」,這是一個悖論,即:
眼看着數據庫裡的日誌越來越多、佔用的磁碟越來越大、影響了日常的正常報表、運維工作,再不刪,整體業務要嚴重受到影響。但是呢,當你要去刪,就又出現了嚴重的主從延遲,一樣影響業務。咽不下去也吐不出,活活被憋死!
「一千個觀衆眼中有一千個哈姆雷特」--《殺死比爾說的-哦,不是,是莎士比亞》
可是,我們在生產db上刪除記錄並且又能不影響主從同步的話就只有「一種」方法,我們在說任何方法前先來一個感性的認識,即我們先用「人類」可以懂的語言來描述一下這件事到底該怎麼做。它其實可以分爲兩個場景來做,每個場景有不同的做法:
場景一、當要被刪除的數據量遠大於保留的數據量的場景下的做法
場景二、當要被刪除的數據量遠小於保留的數據量的場景下的做法
下面 下麪,就讓我們來展開這兩個場景吧。
假設我們實際要執行的是下面 下麪這樣的一條sql:
delete from user_behavior_logs where created_datetime between '2016-08-10 17:20:00' and '2019-12-31 17:20:00';
這涉及到在生產的主庫上:
刪除:1700萬條記錄
實際需要保留的數據:30萬條,條件: between '2020-01-01 17:20:00' and '2020-08-10 17:20:00',30萬條數據。
那麼我們的做法爲:
1) 照着要被刪除的table名建立一個完全一模一樣名字帶tmp_字首的table名
2)選取要保留的數據 into tmp_table
3)rename table 原來的table名 to deleted_原來的table名
4)rename table tmp_table to 原來的table名
5)drop table deleted_原來的table名
它化成具體的操作就是以下這麼幾條sql(create table語句省略,因爲這個太簡單了)
insert into tmp_user_behavior_logs
(
ak,gu,ln,st,os,
ss,ip,bruser_behavior_logs,lan,fv,ifj,ifc,brs,cp,pn,pl,chn,sv,ev,et,pt,prn,created_datetime
)
select
ak,gu,ln,st,os,
ss,ip,bruser_behavior_logs,lan,fv,ifj,ifc,brs,cp,pn,pl,chn,sv,ev,et,pt,prn,now()
from user_behavior_logs where created_datetime between '2020-01-01 17:20:00' and '2020-08-10 17:20:00';
rename table user_behavior_logs to deleted_user_behavior_logs;
rename table tmp_user_behavior_logs to user_behavior_logs;
drop table deleted_user_behavior_logs;
以上的操作位於:base 1000萬條記錄,同時使用壓力測試工具不斷的往數據庫中以每5秒進5000條數據的速度插入新數據,master slaver主從情況下,在4c cpu, 8gb ram,非ssd磁碟執行情況:
對於insert into ...select from...語句涉及到30萬數據量的情況下,執行時間爲:16s,執行期間有報主從同步,主從同步一開始值有點高爲70s,這個報警持續了5分鐘左右即消失;
對於rename與drop語句執行只用了1s,執行過程無任何主從同步報警;
結論
就算主從報警,爲完全可接受範圍內。
以上的操作位於:base 1000萬條記錄,同時使用壓力測試工具不斷的往數據庫中以每5秒進5000條數據的速度插入新數據,master slaver主從情況下,在64c cpu, 256gb ram,ssd磁碟執行情況:
對於insert into ...select from...語句涉及到30萬數據量的情況下,執行時間爲:1.3s,執行期間,有報主從同步,主從同步一開始值爲5s,這個報警持續了15s就消失了;
對於rename與drop語句執行只用了1s,執行過程無任何主從同步報警;
結論
就算主從報警也可以忽略不計。
第1步:確定要被刪除的id範圍;
第2步:使用儲存過程,分成小批次刪除,每次刪除的量不要超過(delete+where條件)萬條。刪除後停一下,再刪下一批,全程最好有監控報警隨時看着
以上的操作位於:base 2.1億條記錄,總共:460gb,同時使用壓力測試工具不斷的往數據庫中以每5秒進5000條數據的速度插入新數據,master slaver主從情況下,在4c cpu, 8gb ram,非ssd磁碟執行情況:
對於delete from user_behavior_logs where id between 1 and 5000; 每隔30秒我做一次這樣的delete操作。
實際操作時間爲:3.7s,主從延遲報警持續了:59s即告結束。
結論
就算主從報警,爲完全可接受範圍內。
以上的操作位於:base 1000萬條記錄,每5秒進5000條數據,master slaver主從情況下,在64c cpu, 256gb ram,ssd磁碟執行情況:
對於delete from user_behavior_logs where id between 1 and 5000; 每隔30秒我做一次這樣的delete操作。
實際操作時間爲:1s,主從延遲報警持續了:8s即告結束。
結論
就算主從報警,爲完全可接受範圍內。每次刪除需要少數據量,頻率不能太高,每次刪完當中需要有一個30-60秒的間隔以讓從儘量追上主庫。
假設我們實際要執行的是下面 下麪這樣的一條sql:
delete from user_behavior_logs where created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00';
這涉及到在生產的主庫上:
刪除:170萬條記錄
實際需要保留的數據:9000w條記錄,條件爲created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00';
那麼我們的做法爲:
第一步:mysqldump成一個檔案;
第二步:把dump出去的檔案匯入到一個新的表中去
第三步:使用分場景2中的rename手法來
注意:這個手法只有在非業務時間段即一般在零晨去做這個事情,mysqldump回新表時,會造成不小的主從延遲,來看一下本人的實際操作情況。
以上的操作位於:base 1億條記錄,總共:140gb,同時使用壓力測試工具不斷的往數據庫中以每5秒進5000條數據的速度插入新數據,master slaver主從情況下,在4c cpu, 8gb ram,非ssd磁碟執行情況:
對於delete from user_behavior_logs where created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00'; 要刪除的數據多達:170w條,需要保留的有9900w條。
用mysqldump導出和恢復9900w條記錄總計用了:6小時,造成了嚴重的主從同步,最後不得不鎖主庫,再用mysqldump追平從庫,最後造成整個操作沒法完成。
結論
該操作確實很耗時,在一般機器上很難模擬,也驗證了,這種操作很耗資源。
以上的操作位於:base 1億條記錄,總共:140gb,同時使用壓力測試工具不斷的往數據庫中以每5秒進5000條數據的速度插入新數據,master slaver主從情況下,在64c cpu, 256gb ram,ssd磁碟執行情況:
對於delete from user_behavior_logs where created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00'; 要刪除的數據多達:170w條,需要保留的有9000w條。
用mysqldump導出和恢復9900w條記錄總計用了:3小時,從庫每3分鐘報一次主從同步,連續了3小時直到mysqldump把9900w條記錄匯入了新表才告終目。而後續的rename表名和drop都是秒級,期間無任務報警。
結論
這種手法,只有在非營業時間去做,並且這點時間是完全可以忍受的,但是這種需求只應該每半年或者季度發生一次。
#!/bin/bash
#desc:指令碼
#通過從庫監控Seconds_Behind_Master的值監控延遲情況。
#該值爲null或着超過告警閾值會報錯.
#本腳還通過mysql命令執行情況判定mysql服務可用狀態。
#author:hahaxiao_mk
#date:2018/04/27
#source ~/.bash_profile
#source ~/.bashrc
#----Seconds_Behind_Master的值
v_sbm='NULL'
#----檢測域值,單位s
v_threshold=1
#----機器標示
v_machine_mark=ymkmysql
MYSQL_HOME=/usr/local/mysql
#-----發送告警資訊函數
function f_send_msg()
{
echo "準備發送主從遲告警:${1} ${2}" >> /home/appadmin/behind_master.log
python /home/appadmin/send_alert_msg.py ${1}$2 101 1
#呼叫alert告警${v_java_home_bin}/java -jar /opt/config/inf/alarm.jar 1 $1 $2
}
#-----判定mysql服務狀態
starttime=$(date +%Y-%m-%d\ %H:%M:%S)
v_mysql_status=`mysql -uroot -phaha -P3306 -h10.0.0.1 -e "show slave status\G"|grep Seconds_Behind_Master`
echo "開發庫10.0.0.1於 ${starttime} -> 主從延遲目前爲:${v_mysql_status}" >> /home/appadmin/behind_master.log
if [ $? -eq 1 ]
then
v_err_msg="mysql is not available! "
# f_send_msg ${v_mobile} ${v_err_msg}
echo ${v_err_msg}
f_send_msg ${v_err_msg}
exit
fi
#------判定延遲情況
v_sbm=`echo ${v_mysql_status}|awk -F ":" '{print $2}'`
if [ "${v_sbm}" = " NULL" ]
then
v_err_msg="開發庫10.0.0.1於 ${starttime} -> 發生主從延遲爲: ${v_sbm}!"
# f_send_msg ${v_mobile} ${v_err_msg}
#echo ${v_err_msg}
f_send_msg ${v_err_msg}
elif [ ${v_sbm} -gt ${v_threshold} ]
then
v_err_msg="開發庫10.0.0.1發生主從延遲${v_sbm}s!"
echo ${v_err_msg} >> /home/appadmin/behind_master.log
#f_send_msg ${v_mobile} ${v_err_msg}
#echo ${v_err_msg}
f_send_msg ${v_err_msg}
fi
crontab -e
然後把下面 下麪這一陀複製進去吧(crontab的最小條件爲分鐘,因此要做成秒必須化解成以下的語句,這是一個實用技巧
* * * * * sh /home/appadmin/behind_master.sh
* * * * * sleep 5; sh /home/appadmin/behind_master.sh
* * * * * sleep 10; sh /home/appadmin/behind_master.sh
* * * * * sleep 15; sh /home/appadmin/behind_master.sh
* * * * * sleep 20; sh /home/appadmin/behind_master.sh
* * * * * sleep 25; sh /home/appadmin/behind_master.sh
* * * * * sleep 30; sh /home/appadmin/behind_master.sh
* * * * * sleep 35; sh /home/appadmin/behind_master.sh
* * * * * sleep 40; sh /home/appadmin/behind_master.sh
* * * * * sleep 45; sh /home/appadmin/behind_master.sh
* * * * * sleep 50; sh /home/appadmin/behind_master.sh
#!/usr/bin/python
import re
import requests
import time
import json
import sys
url='http://localhost:9081/alertservice/sendMsg'
if (len(sys.argv)>1):
inputedmsg=sys.argv[1]
msgtype=sys.argv[2]
modelId=sys.argv[3]
print('input message->'+inputedmsg+' input msgtype->'+msgtype+' modelId->'+modelId)
currentTime=time.strftime('%Y/%m/%d %H:%M:%S',time.localtime(time.time()))
print 'current time is ', currentTime
if(msgtype=='101'):
wechatmsg='Issue happened on ' +currentTime +':\n'+ inputedmsg
wechatcontent={"modelId": modelId, "content": wechatmsg}
wechatheaders = {"content-type": "application/json; charset=UTF-8", "type": "101"}
req = requests.post(url, data=json.dumps(wechatcontent),headers=wechatheaders)
print(req.text)
elif(msgtype=='102'):
print('send mail msg')
else:
print('inputed msgtype required 101|102')
else:
print('inputed msg can not be null')