技術人生系列——MySQL:8.0中關于file_system的重大改進和8.0.23存在的問題
日期:2022-07-25
最近一個和file_system有關的問題,因此將這部分數據結構和重要的函數簡單看了一下,好在數據結構不多,成員變量也不多。但是這部分的知識和IO子系統聯系太緊密了,因為IO子系統不熟,所以難免有錯誤。但是也為學習IO子系統做了一層鋪墊。并且不管錯誤與否必須要記錄一下否則很容易忘記,現記錄如下(包含代碼,文章稍顯混亂),代碼來自8.0.23。
僅供參考。
一、前言
在我們數據庫啟動的時候,實際上很多表都是沒有打開的,當然除了一些redo/undo等系統空間會常駐打開,其他的用戶的innodb表示沒有打開,當遇到真正的IO操作后才會打開文件,這些文件會存在于file_system中,主要包含的就是fil_space_t結構,一個fil_space_t對于單表或者分區表的一個分區來講就對應一個fil_node_t,但是對于redo或者undo這些來講一個fil_space_t對應了多個file_node_t,因為有多個相關文件,真正的io handler存儲在fil_node_t上。在5.7中打開的文件都整體存在于一個file_system下,但是最近遇到了一個問題,稍微看了下8.0發現做了拆分,將一個file_system查分為 69個(8.0.19是65個)這個是個硬編碼。這樣如果我們需要打開文件或者關閉文件那么拆分后鎖的代價就更小了,其中每一個拆分出來的結構叫做shard(內部結構叫做Fil_shard)。
二、總體示意圖
注意這里我們只討論一個fil_space_t對應一個fil_node_t的情況,也就是普通表(分區表的分區),這往往也是和用戶關系較大的。
三、file_system(Fil_system)的重點變量
圖中包含另一些重點的數據結構,其中 file_system中包含:
-
m_shards:這是一個vecoter數組,包含了69個shard。
-
m_max_n_open:這個是我們的參數Innodb_open_files設置的值。
-
m_max_assigned_id:是我們當前分配的最大的space_id。
四、shard的重點變量
-
m_id:當前shard的序號,從0開始
-
m_spaces:一個std map結構,在數據庫初始化的時候就建立好了,其主要是space_id和fil_space_t做的一個map結構
-
m_names:一個std map結構,在數據庫初始化的時候就建立好了,其主要是space_name和fil_space_t做的一個map結構
m_spaces和m_names變量的初始化可以查看函數Fil_shard::space_add,如下:
void Fil_shard::space_add(fil_space_t *space) { ut_ad(mutex_owned()); { auto it = m_spaces.insert(Spaces::value_type(space->id, space)); ut_a(it.second); } { auto name = space->name; auto it = m_names.insert(Names::value_type(name, space)); ut_a(it.second); }}
我們通常在進行物理IO的時候需要打開文件,這個時候拿到的一般為page的space_id,這樣通過Fil_shard::get_space_by_id就快速拿了fil_space_t的結構,而不用去遍歷鏈表。
但是需要注意的一點是,我們首先還需要判定是的這個space_id到底到哪個shard上建立或者到哪個shard上查找,是需要定位到相關的shard上的,這個是通過space_id取余來做到的,函數 Fil_system::shard_by_id(FIL_system 就是我們的file_system的數據結構)函數完成這個功能,計算取余如下:
Fil_shard *shard_by_id(space_id_t space_id) const MY_ATTRIBUTE((warn_unused_result)) {#ifndef UNIV_HOTBACKUP if (space_id == dict_sys_t::s_log_space_first_id) { return m_shards[REDO_SHARD]; } else if (fsp_is_undo_tablespace(space_id)) { const size_t limit = space_id % UNDO_SHARDS; return m_shards[UNDO_SHARDS_START + limit]; } ut_ad(m_shards.size() == MAX_SHARDS); return m_shards[space_id % UNDO_SHARDS_START]; //取余,將各個space分布到不同的shard上#else /* !UNIV_HOTBACKUP */ ut_ad(m_shards.size() == 1); return m_shards[0];#endif /* !UNIV_HOTBACKUP */ }
因為我們的undo,redo會獨占末尾的幾個shard,因此這里用了宏來減去了這部分然后取余,相關宏定義如下。
/** Maximum number of shards supported. */static const size_t MAX_SHARDS = 69; //69個shard/** The redo log is in its own shard. */static const size_t REDO_SHARD = MAX_SHARDS - 1; //末尾第1個為redo的shard/** Number of undo shards to reserve. */static const size_t UNDO_SHARDS = 4;//倒數第2到第5為undo的shard/** The UNDO logs have their own shards (4). */static const size_t UNDO_SHARDS_START = REDO_SHARD - UNDO_SHARDS;#else /* !UNIV_HOTBACKUP */
-
m_LRU:這個玩意是一個最重要的數據結構了,如果一個文件剛剛打開或者io complete讀操作就會將其以頭插法加入到這個鏈表,這就形成了一個LRU鏈表,如果超過參數Innodb_open_files的設置就會在這里面進行尾部淘汰。打開文件函數如下:
Fil_shard::prepare_file_for_io 調入為物理IO準備好文件:void Fil_shard::file_opened(fil_node_t *file) { ut_ad(m_id == REDO_SHARD || mutex_owned()); if (Fil_system::space_belongs_in_LRU(file->space)) { /* Put the file to the LRU list */ UT_LIST_ADD_FIRST(m_LRU, file); //頭插法 } ++s_n_open; file->is_open = true; fil_n_file_opened = s_n_open;}
IO COMPLETE如下:
void Fil_shard::complete_io(fil_node_t *file, const IORequest &type) { ut_ad(m_id == REDO_SHARD || mutex_owned()); ut_a(file->n_pending > 0); --file->n_pending; ut_ad(type.validate()); if (type.is_write()) { //如果是寫操作 ut_ad(!srv_read_only_mode || fsp_is_system_temporary(file->space->id)); write_completed(file); //需要加入flush 而不是 LRU 鏈表 } if (file->n_pending == 0 && Fil_system::space_belongs_in_LRU(file->space)) { //是否為 tablespace /* The file must be put back to the LRU list */ UT_LIST_ADD_FIRST(m_LRU, file); //如果是讀操作直接加入 LRU鏈表 }}
當調用Fil_shard::close_file關閉文件時候會從LRU的末尾去掉,見后文分析。這個結構就是我們最為熟悉的file system(下shard)的LRU鏈表,當然如果數據庫當時打開了很多文件超過了參數Innodb_open_files的設置 ,并且做了大量的IO操作,那么這個LRU鏈表可能找不到相應的能夠關閉的文件。
-
m_unflushed_spaces:如其名,主要是進行寫操作的IO可能涉及到需要進行flush data file,因此都放在這個鏈表里面。加入調用也是Fil_shard::write_completed ,但是需要判定是否文件打開用的SRV_UNIX_O_DIRECT_NO_FSYNC,當然一般都不是的,如下:
Fil_shard::write_completed ->add_to_unflushed_list(file->space); //寫入完成,還沒有flush UT_LIST_ADD_FIRST(m_unflushed_spaces, space);//頭插法
去掉時機:
Fil_shard::space_flush ->remove_from_unflushed_list(space) UT_LIST_REMOVE(m_unflushed_spaces, space);//尾部去除
看起來就是space進行flush刷盤過后。那么需要注意的打開的文件并不一定在m_unflushed_spaces或者m_LRU,如果正在進行讀IO可能兩個鏈表中都沒有這個打開的文件,但是問題不大,因為本來就不能淘汰。
-
static原子變量s_n_open:顯然這個屬性不是某個shard特有的,是全部shard一起持有的,那么它代表的實際上就是當前打開的innodb文件總數,使用原子變量避免加鎖,這樣在比較是否超過最大打開數的時候就可以通過file_system的m_max_n_open(也就是參數Innodb_open_files)和其比較即可。
-
static原子變量s_open_slot:顯然這個屬性不是某個shard特有的,是全部shard一起持有的,它用處是保護(The number of open file descriptors is a shard resource),使用一個原子變量的比較/交換操作compare_exchange_weak并且附加while循環,來保證共享資源file descriptors ,這是無鎖化編程一種方式。
四、關于文件的關閉
如果文件沒有保存在file_system中,進行物理IO的時候需要打開它(Fil_shard::do_io),也就是Fil_shard::prepare_file_for_io調用準備打開文件之前,需要當前打開的innodb文件數量是否大于了參數Innodb_open_files的設置,如果大于了那么就需要做淘汰文件出來,代碼在Fil_shard::mutex_acquire_and_get_space中,當然Fil_shard::mutex_acquire_and_get_space的調用在Fil_shard::prepare_file_for_io調用之前,且都在Fil_shard::do_io下面。這里有一個重點,就是參數Innodb_open_files設置是總的大小,因此不管從哪個shard中淘汰一個,那么這個文件就能打開,而不是一定要在本space所在的shard中關閉,比如我們在shard 4中關閉了一個文件,我需要打開的文件映射到了shard 10,那么打開文件總是還是相等的,沒有問題。這也是我開始疑惑的地方。來看看主要流程,也是很簡單:
Fil_shard::mutex_acquire_and_get_space ->Fil_system::close_file_in_all_LRU(循環每個shard) ->Fil_shard::close_files_in_LRU(判斷LRU上是否有可以close文件) ->Fil_shard::close_file(關閉文件)Fil_shard::mutex_acquire_and_get_space //mutex只是本shard的mutex本函數有1個嵌套循環for (size_t i = 0; i < 3; ++i){ //嘗試清理3次,每次都必須清理出一個文件(壓力過大剛清理又滿了?) while (fil_system->m_max_n_open <= s_n_open && //如果s_n_open static變量大于了fil_system設置m_max_n_open(也就是參數Innodb_open_files設置的大小) !fil_system->close_file_in_all_LRU(i > 1)) { //根據LRU 關閉 打開的文件如果清理嘗試來到第2次開始輸出note級別的日志,如果清理失敗可能繼續 if (ut_time_monotonic() - start_time >= PRINT_INTERVAL_SECS) { //如果本次清理時間大于了PRINT_INTERVAL_SECS 設置就報警 start_time = ut_time_monotonic(); ib::warn(ER_IB_MSG_279) << "Trying to close a file for " << start_time - begin_time << " seconds" << ". Configuration only allows for " << fil_system->m_max_n_open << " open files."; } }Fil_system::close_file_in_all_LRU函數bool Fil_system::close_file_in_all_LRU(bool print_info) { for (auto shard : m_shards) {//m_shards 為一個 數組循環每個shard shard->mutex_acquire(); if (print_info) { ib::info(ER_IB_MSG_277, shard->id(), ulonglong{UT_LIST_GET_LEN(shard->m_LRU)}); //如果是第2次嘗試會打印日志 } bool success = shard->close_files_in_LRU(print_info); shard->mutex_release(); if (success) { //如果成功關閉了一個文件success為true return true; //返回成功 } } return false;//返回失敗}Fil_shard::close_files_in_LRU函數bool Fil_shard::close_files_in_LRU(bool print_info) { for (auto file = UT_LIST_GET_LAST(m_LRU); file != nullptr; file = UT_LIST_GET_PREV(LRU, file)) { //循環LRU鏈表,如果鏈表沒有元素即可返回 if (file->modification_counter == file->flush_counter && file->n_pending_flushes == 0 && file->in_use == 0) {//繼續額外的判斷,尚不清楚為什么。 close_file(file, true); //關閉1個文件,并且會從LRU中取下來 return true; //關閉一個則成功 } if (!print_info) {//如果不需要打印日志,也就是是第1次嘗試清理,不需要打印日志 continue; }...//關閉失敗的note日志省略 return false;//如果本shard沒有可以關閉的則返回flase
這里的流程如果出現文件較多,并且同時打開進行IO文件較多(顯著的就是包含大量分區的分區表)的情況超過了參數Innodb_open_files的大小情況,可能出現日志:
也就是ER_IB_MSG_277的note日志輸出,第一個為shard的id,第二個0代表是LRU元素的個數,但是從上面的邏輯來看,雖然循環次數比較多,但是每次循環的元素很少或者為0個元素,因此代價也就不那么高了。另外從這個算法來看因為是從頭遍歷shard 1-69 個shard,如果要淘汰打開的文件,理論上前面的shard相關的fil_space_t(可簡單理解為表或者分區)更容易淘汰和關閉掉。此處代碼來自8.0.23,不知道過后是否會改進(隨機算法取shard是不是更好?),測試情況下我設置參數Innodb_open_files為400,建立了2000個表,全部打開后,發現前面很多shard的m_LRU為空如下:
(gdb) p ((Fil_shard*)(0x7fffe0044c90))->m_LRU$15 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e0f90))->m_LRU$16 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02ea480))->m_LRU$17 = {count = 17, start = 0x7fffe3267240, end = 0x7fffe2a9f1f0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e19d0))->m_LRU$18 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e1f30))->m_LRU$19 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e49c0))->m_LRU$20 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e6ec0))->m_LRU$21 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e7170))->m_LRU$22 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e5480))->m_LRU$23 = {count = 0, start = 0x0, end = 0x0, node = &fil_node_t::LRU, init = 51966}//前面好多shard的m_LRU長度都是0//到這里才開m_LRU有值(gdb) p ((Fil_shard*)(0x7fffe02e7ee0))->m_LRU$24 = {count = 1, start = 0x7fffe3279530, end = 0x7fffe3279530, node = &fil_node_t::LRU, init = 51966}$25 = {count = 18, start = 0x7fffe327b290, end = 0x7fffe2a9c670, node = &fil_node_t::LRU, init = 51966}(gdb) p ((Fil_shard*)(0x7fffe02e8190))->m_LRU$26 = {count = 17, start = 0x7fffe3279ae0, end = 0x7fffe30974d0, node = &fil_node_t::LRU, init = 51966}
我大概數了一下,就是后面20來個shard才有值,差不多一個有17 18個表,這樣算起來差不多是參數Innodb_open_files設置的400大小。這樣分布不均勻直接導致的問題就是拆分效果大打折扣。
隨后查看8.0.28的代碼似乎進行更改,具體的提交就不去查了,看來官方是發現了這個問題:
bool Fil_system::close_file_in_all_LRU() { const auto n_shards = m_shards.size(); const auto index = m_next_shard_to_close_from_LRU++;//原子變量 for (size_t i = 0; i < n_shards; ++i) { auto shard = m_shards[(index + i) % n_shards]; //跳躍性查找,不在數循序的 if (shard->id() == REDO_SHARD) { /* Redo shard don't ever have any spaces on LRU. It is not guarded by a mutex so we can't continue the execution of the for block. */ continue; } shard->mutex_acquire(); bool success = shard->close_files_in_LRU(); shard->mutex_release(); if (success) { return true; } } return false;}
五、最后
最后我們來看看如果debug這些元素其實很簡單,因為file_system是一個全局變量,直接就能拿到比如,我想查看這69個shard如下:
(gdb) p fil_system->m_shards$2 = std::vector of length 69, capacity 128 = {0x7fffe0044c90, 0x7fffe02e0f90, 0x7fffe02e1120, 0x7fffe02e13d0, 0x7fffe02e1680, 0x7fffe02e19d0, 0x7fffe02e1c80, 0x7fffe02e1f30, 0x7fffe02e21e0, 0x7fffe02e25b0, 0x7fffe02e2860, 0x7fffe02e2b10, 0x7fffe02e2dc0, 0x7fffe02e3070, 0x7fffe02e3320, 0x7fffe02e35d0, 0x7fffe02e3880, 0x7fffe02e3cd0, 0x7fffe02e3f00, 0x7fffe02e41b0, 0x7fffe02e4460, 0x7fffe02e4710, 0x7fffe02e49c0, 0x7fffe02e4c70, 0x7fffe02e4f20, 0x7fffe02e51d0, 0x7fffe02e5480, 0x7fffe02e5730, 0x7fffe02e59e0, 0x7fffe02e5c90, 0x7fffe02e5f40, 0x7fffe02e61f0, 0x7fffe02e64a0, 0x7fffe02e3b30, 0x7fffe02e6c10, 0x7fffe02e6ec0, 0x7fffe02e7170, 0x7fffe02e7420, 0x7fffe02e76d0, 0x7fffe02e7980, 0x7fffe02e7c30, 0x7fffe02e7ee0, 0x7fffe02e8190, 0x7fffe02e8440, 0x7fffe02e86f0, 0x7fffe02e89a0, 0x7fffe02e8c50, 0x7fffe02e8f00, 0x7fffe02e91b0, 0x7fffe02e9460, 0x7fffe02e9710, 0x7fffe02e99c0, 0x7fffe02e9c70, 0x7fffe02e9f20, 0x7fffe02ea1d0, 0x7fffe02ea480, 0x7fffe02ea730, 0x7fffe02ea9e0, 0x7fffe02eac90, 0x7fffe02eaf40, 0x7fffe02eb1f0, 0x7fffe02eb4a0, 0x7fffe02eb750, 0x7fffe02eba00, 0x7fffe02ebcb0, 0x7fffe02e6750, 0x7fffe02ec700, 0x7fffe02ec910, 0x7fffe02ecbc0}(gdb)
如果我想看每個shard里面包含哪些表的fil_space_t如下即可:
(gdb) p ((Fil_shard*)(0x7fffe0044c90))->m_names$6 = std::unordered_map with 18 elements = {[0x7fffe32513c0 "t10/t883"] = 0x7fffe32511e0, [0x7fffe323a2d0 "t10/t819"] = 0x7fffe323a0f0, [0x7fffe3223630 "t10/t755"] = 0x7fffe3223450, [0x7fffe320cc60 "t10/t691"] = 0x7fffe320ca80, [0x7fffe31c51b0 "t10/t499"] = 0x7fffe31c4fd0, [0x7fffe309e2c0 "testpri/t1"] = 0x7fffe309e0e0, [0x7fffe31954b0 "t10/t371"] = 0x7fffe31952d0, [0x7fffe308c620 "t10/ERPDB_TEST"] = 0x7fffe308c440, [0x7fffe31dcca0 "t10/t563"] = 0x7fffe31dcac0, [0x7fffe3104ee0 "t10/t115"] = 0x7fffe3121620, [0x7fffe043eb40 "innodb_system"] = 0x7fffe0437280, [0x7fffe31511c0 "t10/t243"] = 0x7fffe3150fe0, [0x7fffe317e750 "t10/t307"] = 0x7fffe317e570, [0x7fffe326a450 "t10/t947"] = 0x7fffe326a270, [0x7fffe31f4c10 "t10/t627"] = 0x7fffe31f4a30, [0x7fffe2b7bee0 "t10/t51"] = 0x7fffe310b9e0, [0x7fffe3139a90 "t10/t179"] = 0x7fffe31398b0, [0x7fffe31adc40 "t10/t435"] = 0x7fffe31ada60}(這里就是names和fil_space_t的map映射,當然fil_space_t是指針類型)(gdb) p ((Fil_shard*)(0x7fffe0044c90))->m_id$7 = 0(這里是share id 我取的第1個元素 1 id就是0)
也可以查看shard的其他元素,當然可以繼續debug各個fil_space_t,fil_node_t 數據結構的數據。
另外除了8.0.28代碼看到的問題修復,還有2個和這部分相關的BUG供參考如下,當然這幾個BUG沒仔細看,在新版(8.0.27)都修復了:
-
InnoDB: “Too many open files” errors were encountered when creating a large number of tables. (Bug #32634620)
-
InnoDB: An excessive number of notes were written to the error log when the innodb_open_files limit was temporarily exceeded. (Bug #33343690)
其實說了這么多和貌似和我們運維相關的只有1個variable和1個status如下,略顯尷尬:
-
variable Innodb_open_files:innodb能夠打開的最大文件,自適應算法可參考官方文檔,體現在Fil_system::m_max_n_open上。
-
status Innodb_num_open_files:顯然這個就是Innodb當前打開的文件數量,和static原子變量shard::s_n_open是一個值( fil_n_file_opened = s_n_open;)。
文章來源于MySQL學習,作者高鵬(八怪)
《深入理解MySQL主從原理》作者
中亦科技數據庫團隊MySQL二線工程師
十余年數據庫運維經驗\擅長故障診斷,性能調優