概述:
在生產環境中,當開啟insert buffer時(參數innodb_change_buffering=all),部分實例偶爾會出現“UNABLE TO PURGE A RECORD”錯誤。這個其實是一個老bug,從官方bug系統來看,也有其它用戶遇到過類似的問題,並且官方在今年2月份已經對該bug進行了修復。
insert buffer:
在描述問題的產生原因和解決方案之前,首先來簡單說說insert buffer。簡單來說,insert buffer是二級索引操作的一個緩存。在進行二級索引操作時,若請求的page不在buffer pool中,則將二級索引操作進行緩存,待下次讀取該page時,將page與buffer中的操作合並,通過這種方式,可以減少在更新(INSERT,DELETE,UPDATE等)操作時磁盤的隨機讀,提高更新的響應時間。
最早insert buffer僅支持insert 操作的buffer,所以這種buffer叫insert buffer,到mysql5.6時,不僅支持insert操作的buffer操作,還可以支持update,delete,purge等操作的buffer功能,而insert buffer也改稱changing buffer,後面我簡稱ibuf。通過參數innodb_change_buffering設置,用戶可以靈活控制二級索引操作的buffer開啟與否。ibuf僅支持普通二級索引,對於唯一索引和主鍵索引不起作用。我們知道,innodb表是索引組織表,每個表由一個聚簇索引和若干個二級索引組成。若使用ibuf,則對於每個二級索引的更新操作,都會對應產生一個ibuf操作符,下表列出了ibuf操作符與更新語句的對應關系:
操作
Insert buffer操作符
說明
Insert
IBUF_OP_INSERT
Delete
IBUF_OP_DELETE_MARK
Delete語句實際是對刪除記錄打標
Purge
IBUF_OP_DELETE
Purge真正物理上清理打刪除標的記錄
Update
IBUF_OP_DELETE_MARK
IBUF_OP_INSERT
二級索引的update由Delete+Insert組成。
purge:
上節提到的purge其實並非用戶發起的更新語句,而是innodb存儲引擎實現MVCC(多版本並發控制)時,需要的一個後台清理操作。Innodb的多版本實現機制(利用回滾段保存歷史版本信息,並通過記錄中的ROLLPTR與回滾段記錄進行關聯)在刪除或更新記錄時,並非真正物理刪除,而僅僅是在記錄上打上刪除標記,通過後台線程進行清理,這個過程就是purge過程。當然,purge還有另外一個作用是清理回滾段,回收空間,以便空間可以重復利用。有關purge和mvcc的實現,有機會在單獨整理一篇文章。
問題產生的原因:
回到問題本身,我們通過一個簡單的場景來復現問題。假設存在表t(id int, c1 varchar(100),primary key(id),key(c1)),表中包含一條記錄(1,a);並且假設表t的page都不在buffer pool中,以便可以使用ibuf。通過下表的操作序列,可以復現問題。
操作序列
更新語句
Ibuf操作
1【DELETE】
delete from t where id=1;
IBUF_OP_DELETE_MARK
2【INSERT】
insert into t values(1,’a’);
IBUF_OP_INSERT
3【PURGE】
IBUF_OP_DELETE
在讀取page的過程中,會進行合並操作,合並操作的順序是以對應page在ibuf中的操作順序來進行的,在執行到第3步:IBUF_OP_DELETE時,發現待刪除的記錄並沒有打刪除標記(第2步插入了相同主鍵+二級索引的記錄,將刪除標記清理),認為異常,導致拋錯。由於purge操作是由單獨的線程在後台執行,因此執行更新語句的操作與purge操作並沒有嚴格的先後順序,如果上述操作的順序變為1->3->2則不會復現問題。
重要流程:
從上節分析來看,產生問題的主要原因是purge操作和更新操作沒有嚴格的同步,導致purge可能清理到未打刪除標記的記錄。
purge流程:
函數調用關系:
srv_do_purge->trx_purge->que_thr_step->row_purge_step
->row_purge->row_purge_record_func->row_purge_del_mark
->row_purge_remove_sec_if_poss-> row_purge_remove_sec_if_poss_leaf
purge二級索引流程:
判斷是否使用ibuf調用關系:
row_purge_remove_sec_if_poss_leaf->row_search_index_entry->btr_pcur_open_func->btr_cur_search_to_nth_level->ibuf_should_try
更新操作與purge操作在ibuf中的協同:
purge線程獲取page時,若page不在buffer中,將page設置watch標記,然後執行ibuf_insert將purge操作緩存。更新操作(insert,delete,update等)也調用ibuf_insert操作進行buffer,首先會判斷page是否有watch標記,若存在,則認為可能 與purge動作沖突,不能使用ibuf。此時,會去從磁盤讀取page,在讀取page過程中會將purge操作進行合並,後續進行更新操作則不會存在問題。通過watch標記來達到更新操作和purge操作協同使用ibuf的目的,避免上述提到的問題。
到這裡,大家可能會有一個疑問,假設不使用ibuf,正常的更新和purge操作同樣是在不同的線程,也有可能出現(DELETE,INSERT,PURGE)序列,為啥就沒有問題呢?因為在purge二級索引時,還會調用row_purge_poss_sec函數,確認記錄是否可以purge(二級索引記錄對應的聚集索引沒有delete mark或者trx_id比purge view還新時,不能purge),從而避免上述問題。
解決方法:
同樣在purge二級索引過程中,btr_cur_search_to_nth_level,首先調用buf_page_get_gen函數進行watch設置,然後調用row_purge_poss_sec判斷記錄是否可以purge,若用戶已經re-insert,則此時purge動作忽略;否則,表示還沒有insert記錄進來,繼續執行調用ibuf_insert接口進行緩存操作。另一方面,更新操作,insert在使用ibuf時,會判斷是否有watch標記,但程序邏輯在返回時,將標記丟了,導致出現問題。因此只要保證更新操作真正使用ibuf前,檢查沒有purge同時使用ibuf,則可以避免問題發生。
詳細解法可以參考:
https://github.com/mysql/mysql-server/commit/ec369cb4f363161dfbbbd662b20763b54808b7d1
參考文檔:
http://mysqllover.com/?p=1264
https://bugs.mysql.com/bug.php?id=73767
http://hedengcheng.com/?p=94
http://mysql.taobao.org/monthly/2015/04/01/