掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
在我測(cè)試過程中,使用的是自動(dòng)提交,一條語句為一個(gè)事務(wù),開8個(gè)線程的話大概是單線程復(fù)制的5倍(共有20個(gè)表),性能應(yīng)該還是不錯(cuò)的,多線程下QPS可以達(dá)到32000,單線程差不多6500,但是這是把double write關(guān)了的情況,如果打開了double write,那么一開始的QPS也差不多是32000,但做幾分鐘之后,這個(gè)數(shù)字一直在減小,那個(gè)感覺啊真是不好,怎么老是一直減少呢,等到跟上來了,一直看著它減少到15000,這個(gè)很不好,相當(dāng)于是2倍的提升,這個(gè)看上去完全是因?yàn)閐ouble write的影響,因?yàn)橹皇切薷牧诉@么一個(gè)參數(shù)而出現(xiàn)的兩個(gè)不同的結(jié)果,但是查遍了網(wǎng)上也都說double write的影響只會(huì)是5-10%,那么就奇怪了,我這個(gè)的影響明顯是50%以上啊,難道是兩次寫就是50%?不對(duì)的,因?yàn)閐ouble write本來就是連續(xù)寫的。肯定是哪里有其它的問題。

超過十年行業(yè)經(jīng)驗(yàn),技術(shù)領(lǐng)先,服務(wù)至上的經(jīng)營(yíng)模式,全靠網(wǎng)絡(luò)和口碑獲得客戶,為自己降低成本,也就是為客戶降低成本。到目前業(yè)務(wù)范圍包括了:成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì),成都網(wǎng)站推廣,成都網(wǎng)站優(yōu)化,整體網(wǎng)絡(luò)托管,成都小程序開發(fā),微信開發(fā),App定制開發(fā),同時(shí)也可以讓客戶的網(wǎng)站和網(wǎng)絡(luò)營(yíng)銷和我們一樣獲得訂單和生意!
然后在無奈之下,在測(cè)試時(shí),通過pstack工具看MYSQL運(yùn)行時(shí)慢到底是什么樣的堆棧,到底是在等啥?什么影響了它的性能,然后看到很多時(shí)候堆棧都是這樣的:
- Thread 4 (Thread 0x7fdadd357700 (LWP 9800)):
- #1 0x00000000008d3007 in os_event_wait_low ()
- #2 0x00000000008230ae in sync_array_wait_event ()
- #3 0x0000000000823f46 in mutex_spin_wait ()
- #4 0x00000000008674df in buf_flush_buffered_writes ()
- #5 0x0000000000868b97 in buf_flush_batch ()
- #6 0x000000000086a6df in buf_flush_list ()
- #7 0x00000000008c31b2 in log_check_margins ()
- #8 0x00000000008eba6a in row_ins_index_entry_low ()
- #9 0x00000000008efd9e in row_ins_step ()
- #10 0x0000000000803be9 in row_insert_for_mysql ()
- #11 0x00000000007f2d6c in ha_innobase::write_row(unsigned char*) ()
- #12 0x000000000068c760 in handler::ha_write_row(unsigned char*) ()
- #13 0x000000000055a2ed in write_record(THD*, TABLE*, st_copy_info*) ()
- Thread 3 (Thread 0x7fdadd316700 (LWP 9801)):
- #1 0x00000000008d3007 in os_event_wait_low ()
- #2 0x00000000008230ae in sync_array_wait_event ()
- #3 0x0000000000823f46 in mutex_spin_wait ()
- #4 0x00000000008674df in buf_flush_buffered_writes ()
- #5 0x0000000000868b97 in buf_flush_batch ()
- #6 0x000000000086a6df in buf_flush_list ()
- #7 0x00000000008c31b2 in log_check_margins ()
- #8 0x00000000008eba6a in row_ins_index_entry_low ()
- #9 0x00000000008efd9e in row_ins_step ()
- #10 0x0000000000803be9 in row_insert_for_mysql ()
- #11 0x00000000007f2d6c in ha_innobase::write_row(unsigned char*) ()
- #12 0x000000000068c760 in handler::ha_write_row(unsigned char*) ()
- #13 0x000000000055a2ed in write_record(THD*, TABLE*, st_copy_info*) ()
從上面的堆棧中可以看出,SQL線程很多都是在buf_flush_buffered_writes函數(shù)中等待,而這個(gè)函數(shù)正好是處理double write的函數(shù),所以我重點(diǎn)看了這里,然后一進(jìn)去就明白了是為什么了,看到這個(gè)函數(shù)一開始有一行mutex_enter(&(trx_doublewrite->mutex)),而在函數(shù)退出前有一行mutex_exit(&(trx_doublewrite->mutex)),里面是處理所有double write緩存起來的頁面,也就是前面要刷的頁面,因?yàn)镮NNODB支持多個(gè)BUFFER POOL實(shí)例,這樣可以增大并發(fā)度,頁面可以放在不同的BUFFER POOL中,這樣兩個(gè)BUFFER POOL中的頁面在同時(shí)訪問時(shí)可以互不干擾,那么可想而知,double write緩存的頁面就是來自多個(gè)SQL線程并發(fā)收集起來的,那么很容易想到,問題其實(shí)就里在這里,由多個(gè)線程做檢查點(diǎn),但只有一個(gè)線程會(huì)做double write,這樣產(chǎn)生了瓶頸,導(dǎo)致等待一段時(shí)間后就會(huì)越來越慢,也許就是這個(gè)問題,那后面就看了一下代碼,它的實(shí)現(xiàn)是否允許多個(gè)線程做檢查點(diǎn)呢,主要是看函數(shù)log_free_check(log_check_margins)的實(shí)現(xiàn),因?yàn)檫@個(gè)函數(shù)才是用戶線程調(diào)用的,代碼是這樣的:
- log_free_check(void)
- {
- if (log_sys->check_flush_or_checkpoint)
- log_check_margins();
- }
那就主要是log_sys->check_flush_or_checkpoint有沒有可能多個(gè)線程進(jìn)來了,最后發(fā)現(xiàn)在里面直接就調(diào)log_checkpoint_margin函數(shù)了,而再進(jìn)去里面,就是對(duì)buffer pool中的臟頁面進(jìn)行刷盤了,同時(shí)這里刷盤是刷每一個(gè)buffer pool instance的,而不是分開自己刷自己的,當(dāng)然對(duì)于某一個(gè)buffer pool instance,只會(huì)有一個(gè)線程做,進(jìn)來之后會(huì)找到?jīng)]有任何一個(gè)線程在做刷盤的buffer pool instance來做,所以其實(shí)是并發(fā)處理這多個(gè)buffer pool instance的,那么現(xiàn)在得到的結(jié)論就是經(jīng)常性的多個(gè)線程一起做刷盤操作,而做完刷盤之后,如果打開了double write,則要將所有的buffer pool instance要刷的頁面做double write,上面也看到了,它是一個(gè)mutex,多個(gè)線程一起搶這一個(gè)臨界區(qū),導(dǎo)致系統(tǒng)的并發(fā)度大大的降低,那么現(xiàn)在問題已經(jīng)很明顯,原因也已經(jīng)很明顯,這個(gè)其實(shí)與DOUBLEWRITE沒關(guān)系,那個(gè)5-10%我還是承認(rèn)的,這里只不過是代碼實(shí)現(xiàn)有問題而已。
那么結(jié)論就里說,這其實(shí)是INNODB的一個(gè)BUG,就是多BUFFERPOOL實(shí)例下,DOUBLEWRITE會(huì)導(dǎo)致系統(tǒng)并發(fā)性能大大降低的問題。
那如何解決呢?
首先我已經(jīng)向bugs.mysql.com報(bào)了BUG,鏈接http://bugs.mysql.com/bug.php?id=67808&edit=2,本人英語不好,寫得挺費(fèi)勁。
難道就這樣等它解決嗎?不對(duì),我已經(jīng)等不上了,即使出來了也不是在5.5.27上啊,所以自己解決吧。
這里歸根結(jié)底的問題就是做檢查點(diǎn)函數(shù)log_checkpoint_margin中存在并發(fā),導(dǎo)致DOUBLEWRITE的瓶頸出現(xiàn)了,因?yàn)樵贗NNODB的增刪改操作的一開始,都會(huì)直接先調(diào)用log_free_check這個(gè)函數(shù),出現(xiàn)這樣的問題的概率太高了。
想想,這個(gè)做檢查點(diǎn)需要多個(gè)線程嗎?如果是一個(gè)線程在做是不是就沒有問題了?DOUBLEWRITE的瓶頸也不存在了?確實(shí)是這樣的。
再想想,做檢查點(diǎn)需要多個(gè)線程嗎?只有一個(gè)線程做是不是就夠了?因?yàn)闄z查點(diǎn)歸根結(jié)底是為了給日志讓空間出來,日志一直往2個(gè)(默認(rèn))日志文件中循環(huán)添加,第一個(gè)寫完寫第二個(gè),寫完第二個(gè)再寫第一個(gè),其實(shí)就里一個(gè)圈,不斷的循環(huán),那么這里就必須要保證,向里面寫的數(shù)據(jù)的位置不能走到檢查點(diǎn)的位置的前面去(因?yàn)閿?shù)據(jù)的LSN是新產(chǎn)生的日志的LSN,肯定是要小于檢查點(diǎn)的LSN的,也可以表示為,數(shù)據(jù)的LSN必須要小于檢查點(diǎn)的LSN加上整個(gè)日志組的日志容量),因?yàn)闄z查點(diǎn)LSN前面的日志表明,所有數(shù)據(jù)已經(jīng)都寫入磁盤了,可以扔掉了,那如果大于了,就會(huì)把沒有做檢查點(diǎn)的日志覆蓋掉,這樣會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)誤或者更嚴(yán)重的一些問題。
有了這樣的想法,則這個(gè)問題應(yīng)該不難解決,先在log_sys中加入一個(gè)成員checkpoint_doing,用來表示現(xiàn)在是否有線程正在做檢查點(diǎn),再修改函數(shù)log_check_margins,最前面加上代碼段:
- mutex_enter(&(log_sys->mutex));
- if (log_sys->checkpoint_doing > 0) {
- mutex_exit(&(log_sys->mutex));
- return;
- }
- log_sys->checkpoint_doing++;
- mutex_exit(&(log_sys->mutex));
上面這表示如果有線程已經(jīng)做了,那這里不會(huì)再進(jìn)去,直接就出去了,如果沒有線程在做,那么當(dāng)前線程才做,同時(shí)將標(biāo)志置為正在做。這樣保證了只有一個(gè)用戶線程會(huì)做檢查點(diǎn)。當(dāng)然在修改及判斷這個(gè)checkpoint_doing的時(shí)候必須要對(duì)其進(jìn)行保護(hù),上面代碼中也已經(jīng)有所體現(xiàn)。那么這樣就好了嗎?如果當(dāng)前系統(tǒng)的壓力非常大,那么出去了,而沒有做檢查點(diǎn)檢查,繼續(xù)做寫操作,這樣有可能會(huì)導(dǎo)致新的日志寫的超過了檢查點(diǎn)的位置,導(dǎo)致數(shù)據(jù)覆蓋,所以還需要做一個(gè)修改操作。
因?yàn)樵贗NNODB中寫日志的函數(shù)只有l(wèi)og_write_up_to,并且這只會(huì)有一個(gè)線程寫,那么為了防止這個(gè)問題的話是不是在它寫日志的時(shí)候檢查一下,如果空間不夠了等待或者做一次檢查點(diǎn)后再繼續(xù)做,是不是就沒有問題了?我認(rèn)為確實(shí)是這樣的,那么繼續(xù)修改:
- if (!log_sys->checkpoint_waiting && log_sys->lsn - log_sys->last_checkpoint_lsn > log_sys->max_checkpoint_age)
- {
- mutex_exit(&(log_sys->mutex));
- log_sys->checkpoint_waiting = 1;
- log_check_margins();
- log_sys->checkpoint_waiting = 0;
- goto loop;
- }
這段代碼就加在log_write_up_to函數(shù)中五個(gè)判斷條件之后,能走到這里說明這次的日志要寫入日志文件了,那么這里檢查是最合適的,上面的代碼有一個(gè)條件判斷,最主要是的log_sys->lsn - log_sys->last_checkpoint_lsn > log_sys->max_checkpoint_age,這個(gè)表示的是如果當(dāng)前的最新LSN超過檢查點(diǎn)LSN的數(shù)目已經(jīng)大于最大的做檢查點(diǎn)差值數(shù),則就等待或者做一次檢查點(diǎn),這個(gè)條件與log_checkpoint_margin函數(shù)中判斷是不是要做檢查點(diǎn)的條件是一樣的,這樣的話就保證了這段代碼中調(diào)用了log_check_margins時(shí)要么里面已經(jīng)有人正在做,要么自己肯定能做一次檢查點(diǎn),不然在這里會(huì)產(chǎn)生死循環(huán)。做了之后從而使的log_sys->last_checkpoint_lsn變大,向前走,讓出空間,這樣這次日志就可以寫入進(jìn)去了,那么goto loop可以起到循環(huán)等待的作用。
上面還看到一個(gè)新的成員checkpoint_waiting,這個(gè)是為了防止進(jìn)入死循環(huán)而設(shè)置的,因?yàn)閘og_check_margins里面還會(huì)再調(diào)用log_write_up_to。
那么到現(xiàn)在為止,這個(gè)問題應(yīng)該算是可以了的,接下來就是測(cè)試了,把多線程的SLAVE復(fù)制跑起來,我發(fā)現(xiàn)這個(gè)是一個(gè)非常好的并發(fā)測(cè)試工具,不需要專門寫應(yīng)用來設(shè)置并發(fā)環(huán)境。
測(cè)試的結(jié)果表明,那么問題不復(fù)存在,平均的QPS在打開DOUBLEWRITE時(shí)都是31000,這個(gè)數(shù)字挺好的。問題解決,同時(shí)發(fā)現(xiàn)那個(gè)分支就從來沒有進(jìn)去過,說明用戶線程做了已經(jīng)足夠了,那里只是一個(gè)機(jī)率很小的問題預(yù)防而已。
但是這個(gè)修改現(xiàn)在還沒有辦法去驗(yàn)證,只能由各位先從理論上看看是不是正確吧,我本人認(rèn)為應(yīng)該還是沒什么大問題的,請(qǐng)各位大俠指點(diǎn)!
這里要感謝一下我的好朋友好戰(zhàn)友陳福榮同學(xué),在MYSQL學(xué)習(xí)及實(shí)現(xiàn)方面一直不斷的討論,研究,我們共同進(jìn)步。
原文鏈接:http://www.cnblogs.com/bamboos/archive/2012/12/05/2802997.html
【編輯推薦】

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流