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