本篇文章給大家分享的是有關(guān)如何解決熱點(diǎn)更新導致的雪崩效應,小編覺(jué)得挺實(shí)用的,因此分享給大家學(xué)習,希望大家閱讀完這篇文章后可以有所收獲,話(huà)不多說(shuō),跟著(zhù)小編一起來(lái)看看吧。
PartⅠ 案例分析
這個(gè)故障的場(chǎng)景比較簡(jiǎn)單,當時(shí)業(yè)務(wù)出現了大量的請求失敗,幾乎處于不可用狀態(tài)。同時(shí)對應的數據庫也存在大量的CPU使用率高的告警。
1. 登上數據庫,通過(guò)show processlist 查看到的現場(chǎng)截圖如下:
2. MySQL 版本為5.7,數據庫表結構如下:
CREATE TABLE `docid_generator` (`id` int(4) NOT NULL AUTO_INCREMENT,PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2
3. 業(yè)務(wù)請求Session如下:
connectupdate docid_generator set id=last_insert_id(id+1); select last_insert_id() exit
通過(guò)初步排查,了解到:表中只有一個(gè)字段、一行記錄,該段業(yè)務(wù)邏輯是通過(guò)mysql中last_insert_id(expr)函數特性實(shí)現id分配功能;按照processlist執行耗時(shí)倒序查看,耗時(shí)最長(cháng)的sql也是該類(lèi)update請求;innodb status可以看到大量的事務(wù)在等待該條記錄的X鎖;update的X鎖使得請求只能串行進(jìn)行,導致響應很慢,可是最先到來(lái)的一批update請求是什么原因卡住了呢?
通過(guò)pref分析,顯示lock_deadlock_recursive函數占據了cpu recycle事件的近50%時(shí)間。該函數是通過(guò)深度優(yōu)先算法進(jìn)行遞歸調用,檢測是否滿(mǎn)足死鎖條件,再進(jìn)行最小代價(jià)的事務(wù)回滾。
查看information_schema中innodb_trx事務(wù)鎖等待隊列,發(fā)現已經(jīng)有6100+條鎖等待信息。
通過(guò)查閱文檔發(fā)現,InnoDB監控器輸出的最近死鎖檢信息中包含“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION”,表示處于等待的事務(wù)列表長(cháng)度已達到限制200。超過(guò)200個(gè)事務(wù)的等待列表被視為死鎖,并且將回滾嘗試檢查等待列表的事務(wù)。如果鎖定線(xiàn)程必須查看等待列表上的事務(wù)擁有的超過(guò)1,000,000個(gè)鎖,則也可能發(fā)生相同的錯誤。
每個(gè)請求維護自己的鎖隊列,在這個(gè)案例中,業(yè)務(wù)的并發(fā)為200個(gè),因為單條記錄X鎖,只能串行執行,按照先后順序依次維護自己的鎖隊列,極限情況記錄阻塞的鎖隊列長(cháng)度為(1+199)*200/2!所以這一階段耗時(shí)較長(cháng)。
知道耗時(shí)長(cháng)的原因就好辦了。因為業(yè)務(wù)場(chǎng)景是單一的id分配,只有一條記錄,邏輯上不會(huì )出現死鎖情況,所以完全可以關(guān)閉死鎖檢測功能。很幸運,5.7版本innodb_deadlock_detect可以關(guān)閉死鎖檢測。關(guān)閉后,我們再次200并發(fā)測試,從原來(lái)的10s降低到0.2s,性能提升50倍。
分析到這里,相信大家對這個(gè)故障案例也一定有了比較深刻的了解。在之前到的介紹里為了不打斷故障分析的連貫性,略過(guò)了一些數據庫概念的介紹,下面挑選幾個(gè)給大家詳細介紹下。
“死鎖”可以理解為兩個(gè)或兩個(gè)以上的線(xiàn)程在執行過(guò)程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現象,若無(wú)外力作用,它們都將無(wú)法推進(jìn)下去。此時(shí)稱(chēng)系統處于死鎖狀態(tài)或系統產(chǎn)生了死鎖,這些永遠在互相等待的進(jìn)程稱(chēng)為死鎖進(jìn)程。
在數據庫中我們可以形象的理解為:
如上圖所示,事務(wù)A在等待事務(wù)B釋放id=2的鎖,事務(wù)B在等待事務(wù)A釋放id=1的鎖。
這種情況就是死鎖,發(fā)生死鎖有兩種方法解決:
1)直接進(jìn)入等待,直到超時(shí)。這個(gè)超時(shí)時(shí)間可以通過(guò)參數innodb_lock_wait_timeout來(lái)設置
2)發(fā)起死鎖檢測,發(fā)現死鎖后,主動(dòng)回滾死鎖鏈條中的某一個(gè)事務(wù),讓其他事務(wù)得以執行。將參數innodb_deadlock_detect設置為on,表示開(kāi)啟這個(gè)邏輯。
innodb_deadlock_detect=on,該選項使用了禁用MySQL的死鎖檢測功能的。在高并發(fā)系統上,當許多線(xiàn)程等待同一個(gè)鎖時(shí),死鎖檢測可能導致速度減慢。當發(fā)生死鎖時(shí),如果禁用了死鎖檢測則可能會(huì )更有效,這樣可以依賴(lài)innodb_lock_wait_timeout的設置進(jìn)行事務(wù)回滾。
MySQL默認情況下是開(kāi)啟了死鎖檢測的,InnoDB自動(dòng)檢測發(fā)送死鎖的事務(wù),并回滾其中的一個(gè)事務(wù)或所有導致死鎖的事務(wù)。InnoDB會(huì )在導致死鎖的事務(wù)中選擇一個(gè)權重比較小的事務(wù)來(lái)回滾,這個(gè)權重值可能是由該事務(wù)insert, updated, deleted的行數決定的。
如果innodb_table_locks = 1(默認值)并且autocommit = 0,則InnoDB能感知到表鎖的存在,并且上層的MySQL層知道行級鎖。否則,InnoDB無(wú)法檢測到由MySQL LOCK TABLES語(yǔ)句設置的表鎖或由除InnoDB之外的存儲引擎設置的鎖定的死鎖。通過(guò)設置innodb_lock_wait_timeout系統變量的值來(lái)解決這些情況。
如果電商業(yè)務(wù)在大促和秒殺場(chǎng)景、在線(xiàn)教育業(yè)務(wù)在報名和簽到、游戲業(yè)務(wù)開(kāi)服等高并發(fā)場(chǎng)景中遇到了類(lèi)似的熱點(diǎn)更新故障,相信大家一定不會(huì )有太多時(shí)間理性的梳理和挖掘問(wèn)題的根因,在較短做出最合理優(yōu)化方案的難度也較大。而此時(shí)用戶(hù)或者業(yè)務(wù)方對數據庫的要求必然是不管用什么方法,先讓業(yè)務(wù)跑起來(lái)(恢復)再說(shuō)。
那么對于熱點(diǎn)更新類(lèi)的故障,DBA常用的應急預案:重啟、切換、kill(不論是使用pt-kill還是自己的kill腳本,顯然都很難解決,而且會(huì )加劇阻塞)、權限控制(極可能誤傷一些正常的核心業(yè)務(wù)邏輯,導致業(yè)務(wù)依然失?。?,大概率無(wú)法完成業(yè)務(wù)恢復。即使有損降低如果不依靠業(yè)務(wù)側介入都很難完成。
騰訊智能管家DBbrain,為了防止在熱點(diǎn)更新時(shí),用戶(hù)數據庫不被大壓力打掛,提供了“SQL限流”和“熱點(diǎn)數據防護”這兩大功能,幫助用戶(hù)可以在數據庫端實(shí)現切實(shí)有效的降級和防護,保障用戶(hù)核心業(yè)務(wù)能正常運行。
DBbrain提供了“SQL限流”功能,能夠幫助用戶(hù)在數據庫側實(shí)現優(yōu)雅的臨時(shí)降級。通過(guò)在SQL進(jìn)入數據庫內核之前拒絕的方式,能解決更多高并發(fā)故障中,通過(guò)kill無(wú)法快速恢復的場(chǎng)景,除了上文介紹的“熱點(diǎn)更新引發(fā)死鎖檢測阻塞的場(chǎng)景”之外,還適用于:
某類(lèi)SQL并發(fā)急劇上升,影響正常業(yè)務(wù),比如緩存穿透或者異常調用,造成原來(lái)并發(fā)不大的SQL語(yǔ)句突然上升。
有數據傾斜SQL,影響正常業(yè)務(wù),比如大促時(shí)拉取某個(gè)特別大的數據,造成整體系統繁忙。
未創(chuàng )建索引SQL,影響正常業(yè)務(wù),比如新上線(xiàn)SQL調用量特別大,又沒(méi)有創(chuàng )建索引,造成整體系統繁忙。
用戶(hù)可以通過(guò)在DBbrain控制臺中,設置目標SQL的特性。
SQL類(lèi)型:select、update、delete、insert、replace
最大并發(fā)數:同一時(shí)刻并發(fā)數超過(guò)設置的閾值的SQL將被拒絕
限流時(shí)間:支持設定規則持續時(shí)間,超時(shí)后不再生效
SQL關(guān)鍵詞:關(guān)鍵字的匹配是無(wú)序的,匹配時(shí)遍歷關(guān)鍵字,看SQL中是否有這個(gè)關(guān)鍵字,有幾個(gè)關(guān)鍵字就匹配幾遍
DBbrain會(huì )根據SQL樣本的關(guān)鍵字自動(dòng)拒絕請求,保證業(yè)務(wù)核心服務(wù)的正常運行,并且統計在開(kāi)啟“SQL限流”時(shí)間段內被拒絕的SQL請求數量。
DBbrain針對于秒殺場(chǎng)景,大幅度優(yōu)化對于單行數據的update操作的性能。當開(kāi)啟熱點(diǎn)更新自動(dòng)探測時(shí),系統會(huì )自動(dòng)探測是否有單行的熱點(diǎn)更新(同一數據行上面等待的行鎖數量超過(guò)32個(gè)后續的事務(wù)就會(huì )開(kāi)始等待),如果有,則會(huì )讓大量的并發(fā)update排隊執行,以減少大量行鎖或觸發(fā)大量死鎖檢測造成的并發(fā)性能下降。
DBbrain提供的“熱點(diǎn)更新保護”功能,支持自動(dòng)結束和手動(dòng)關(guān)閉兩種模式,設置自動(dòng)結束時(shí)間可實(shí)現靈活控制。
在上面的案例中,5.7.15以上的版本可以通過(guò)關(guān)閉死鎖檢測方式提升性能,也可以通過(guò) 騰訊云數據庫智能管家DBbrain提供的“SQL限流”和“熱點(diǎn)更新保護”來(lái)緩解大量熱點(diǎn)更新對數據庫帶來(lái)的負載壓力。那么接下來(lái)的章節將從業(yè)務(wù)實(shí)現的角度分享一些啟發(fā)建議。
3.1)基于MySQL實(shí)現
表結構如下:
CREATE TABLE `id_allocate` (`id` bigint NOT NULL AUTO_INCREMENT,business_tag varchar(20) not null,PRIMARY KEY (`id`),UNIQUE KEY `name` (business_tag)) ENGINE=InnoDB AUTO_INCREMENT=2;
3.1.1)類(lèi)似上文例子,通過(guò)mysql last_insert_id(expr)函數方法:
請求邏輯:
connectupdate id_allocate set id=last_insert_id(id+1) where business_tag='test1'; select last_insert_id() exit注意點(diǎn):5.7以上關(guān)閉死鎖檢測innodb_deadlock_detect;
3.1.2)通過(guò)mysql auto_increment字段,去掉business_tag字段,只保留id字段,請求邏輯:
connectinsert into id_allocate value(null); select last_insert_id() exit
注意點(diǎn):數據量會(huì )持續增大,可以定期低峰刪除或者創(chuàng )建為分區表,定期刪除歷史數據
純依賴(lài)MySQL實(shí)現,第一種方法更簡(jiǎn)單易用。高可用上,常見(jiàn)的思路是存在2個(gè)MySQL實(shí)例中,設置自增的步長(cháng)和起始值,比如兩個(gè)數據庫,設置auto-increment-increment=2,分別設置auto-increment-offset為1和2,業(yè)務(wù)請求這兩個(gè)DB依次獲取到1,3,5,7和2,4,6,8。該方法可避免單MySQL故障的影響,但同時(shí)系統的嚴格單調遞增也變成了趨勢遞增(若單機故障,可能還有id變小的情況)。
利用redis的incr和incrby方式,能支撐的qps更高。同樣若擔心高可用問(wèn)題,可以設置兩個(gè)key分別存儲在兩個(gè)redis實(shí)例上,通過(guò)控制初始值和incrby的offset來(lái)保障。這里顯著(zhù)的弊端是 redis數據不能持久化,但目前騰訊云redis支持了主備同步、雙機房容災和備份功能,對于項目開(kāi)發(fā)緊急,性能要求高的場(chǎng)景也可以嘗試使用。
表結構:
CREATE TABLE `id_allocate` (`id` bigint NOT NULL AUTO_INCREMENT,business_tag varchar(20) not null,max_id bigint not null,step int not null,PRIMARY KEY (`id`),UNIQUE KEY `name` (business_tag)) ENGINE=InnoDB AUTO_INCREMENT=2;
business_tag標識業(yè)務(wù);
max_id標識目前分配出去的最大id;
step標識每次idallocate-server訪(fǎng)問(wèn)數據庫時(shí)候一次拉走的id區間大小。
實(shí)現思路:第三方通過(guò)調用idallocate-server服務(wù)獲取id 。idallocate-server內存至少包含三個(gè)值:當前的mid,最大能發(fā)的id1,最大能發(fā)的id2;id2和id1相差一個(gè)step。初始時(shí)候,idallocate-server服務(wù)從數據庫中更新兩次,分別得到初始值mid、id1和id2:
beginselect max_id from id_allocate where business_tag='test1' for update; #得到midupdate id_allocate set max_id=max_id+step where business_tag='test1';select max_id from id_allocate where business_tag='test1';#得到id1commit
beginupdate id_allocate set max_id=max_id+step where business_tag='test1';select max_id from id_allocate where business_tag='test1';#得到id2commit
隨著(zhù)第三方請求idallocate-server獲取id,mid一直增大,當達到id1的90%時(shí)候,需檢測id2是否已經(jīng)存在,若不存在則訪(fǎng)問(wèn)數據庫進(jìn)行獲取。若存在則mid達到id1大小后,分配id2部分,當mid達到id2的90%時(shí)候,需檢測id1是否存在。依次循環(huán)保證idallocate-server內存中至少有一個(gè)step大小的buffer號段存在。
上述方案中:
1. 可用性:idallocate-server服務(wù)可以橫向擴展,避免單點(diǎn);MySQL層面可以通過(guò)主備集群半同步或者強一致性同步來(lái)保證,且短時(shí)間內MySQL故障也不會(huì )影響服務(wù)。
2. 性能:將更新MySQL的請求降低為純MySQL id分配 方式的 1/step(沒(méi)step個(gè)id大小 更新一次db),降低數據庫的壓力;同時(shí)通過(guò)id2和id1雙號段的設計,避免了當單獨id1分配完全,需等待idallocate-server實(shí)時(shí)去db更新獲取最新數據 這種延時(shí)毛刺
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。
Copyright ? 2009-2022 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 特網(wǎng)科技 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 百度云 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站