影響范圍: 從 8.0.29 版本開始, 在read heavy 場景, 性能可能有 5%~10% 的性能回退
MySQL 官方在8.0.29 里面加了instant add/drop column 能力, 能夠實現 instant add 或者 drop cloumn 到表的任意位置. PolarDB 在這基礎上增加了可以 Instant 修改列的能力, 具體可以看我們的月報
instant DDL 核心觀點只有一個: don’t touch any row but update the metadata only, 也就是僅僅去修改 Data Dictionary(DD) 信息, 而不去修改數據信息,這樣才有可能做到 Instant.
具體的做法就是給每一個行增加了row_version, 然后DD 本身就是多版本, 不同的數據信息用不同的DD 信息去解析.
首先一個record 是否有row_version 信息添加到了Record info bits 里面.
info bits 包含有deleted flag, min record 等等信息, 后來在8.0.13 的時候增加record 是否有Instant ADD column 信息. 在 8.0.29 版本中增加了record 是否有 row_version 信息.
以上是這個 issue 背景, Instant add/drop column 的原理, 但是原因在哪里呢?
從Markus 提交上來的Flamegraph 可以看到, 在 8.0.33 里面 rec_get_offsets/cmp_dtuple_rec/rec_get_nth_field 等等相比 8.0.28 占比明顯增多了. 整個 row_serch_mvcc 的調用開銷也增加了.
核心原因由于數據record 增加了 row_version 信息, 導致在執行數據解析的函數 rec_get_offsets/rec_get_nth_field 等函數中增加了很多額外的判斷, 并且官方把很多 inline function 改成了 non-inline.
為了驗證想法, 我們做了 3 個地方的修改, 具體可以看 Issue 上面的代碼提交:
1. 將一些 non-inline function 改回inline function
從 inline => non-inline. 修改的函數如下:
8.0.27
rec_get_nth_field => inline
rec_get_nth_field_offs => inline
rec_init_offsets_comp_ordinary => inline
rec_offs_nth_extern => inline
8.2.0
rec_get_nth_field => non-inline
rec_get_nth_field_offs => non-inline
rec_init_offsets_comp_ordinary => non-inline
rec_offs_nth_extern => non-inline
我們測試下來在 oltp_read_only 場景里面, 將這些 non-inline 函數改成 inline 以后, 性能可以有 3~5% 左右的提升空間. 具體改動代碼可以在 issue 里面獲得.
2. 簡化get_rec_insert_state 邏輯
8.0.29 增加了 get_rec_insert_state 函數, 需要判斷當前 record 是來自哪一個版本升級上來的, 從而使用合適的 DD 代碼邏輯進行解析. 如果是包含有 row_version 版本, 還需要判斷是否帶有 version 信息, 如果沒有 version 信息, 是不是8.0.12 instant add column 版本等等, 這里的邏輯非?,嵥?
所以 REC_INSERT_STATE 的狀態非常多.
enum REC_INSERT_STATE { /* Record was inserted before first instant add done in the earlier implementation. */ INSERTED_BEFORE_INSTANT_ADD_OLD_IMPLEMENTATION, /* Record was inserted after first instant add done in the earlier implementation. */ INSERTED_AFTER_INSTANT_ADD_OLD_IMPLEMENTATION, /* Record was inserted after upgrade but before first instant add done in the new implementation. */ INSERTED_AFTER_UPGRADE_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION, /* Record was inserted before first instant add/drop done in the new implementation. */ INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION, /* Record was inserted after first instant add/drop done in the new implementation. */ INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION, /* Record belongs to table with no verison no instant */ // 如果index 上面沒有做過instant add 或者 最新的row_version 版本Instant add/drop INSERTED_INTO_TABLE_WITH_NO_INSTANT_NO_VERSION, NONE };
具體獲得 insert_state 代碼:
static inline enum REC_INSERT_STATE get_rec_insert_state( const dict_index_t *index, const rec_t *rec, bool temp) { ut_ad(dict_table_is_comp(index->table) || temp); if (!index->has_instant_cols_or_row_versions()) { return INSERTED_INTO_TABLE_WITH_NO_INSTANT_NO_VERSION; } /* Position just before info-bits where version will be there if any */ const byte *v_ptr = (byte *)rec - ((temp ? REC_N_TMP_EXTRA_BYTES : REC_N_NEW_EXTRA_BYTES) + 1); const bool is_versioned = (temp) ? rec_new_temp_is_versioned(rec) : rec_new_is_versioned(rec); // 如果有versioned 以后, 這里可以看到version 值是保存在Info bits 和 null field bitmap 中間的1 byte, 如下圖 const uint8_t version = (is_versioned) ? (uint8_t)(*v_ptr) : UINT8_UNDEFINED; const bool is_instant = (temp) ? rec_get_instant_flag_new_temp(rec) : rec_get_instant_flag_new(rec); // 說明一個Record 不能同時被instalt add 和 row_version 版本instant add/drop 處理過 // 應該以后默認的新版本是row_version 版本 instant add/drop, 老的要被淘汰 if (is_versioned && is_instant) { ib::error() << "Record has both instant and version bit set in Table '" << index->table_name << "', Index '" << index->name() << "'. This indicates that the table may be corrupt. Please " "run CHECK TABLE before proceeding."; } enum REC_INSERT_STATE rec_insert_state = REC_INSERT_STATE::NONE; if (is_versioned) { ut_a(is_valid_row_version(version)); if (version == 0) { ut_ad(index->has_instant_cols()); // is_versioned 說明record 有row_version, 如果version = 0, 說明是row_version DD 之前插入, 然后row_version DD 做過以后, 又升級了實例, 所以給這些row_version 設置成0 rec_insert_state = INSERTED_AFTER_UPGRADE_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION; } else { // 最正常的record, row_version DD 之后插入的, 有自己的row_version 版本 ut_ad(index->has_row_versions()); rec_insert_state = INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION; } } else if (is_instant) { // 到這里說明record 上面沒有row_version DD 標記, 只有instant add 標記 // 說明這個Record 是Instant add 之后插入的record, 并且沒有做過row_version DD ut_ad(index->table->has_instant_cols()); rec_insert_state = INSERTED_AFTER_INSTANT_ADD_OLD_IMPLEMENTATION; } else if (index->table->has_instant_cols()) { // 到這里說明record 上面 沒有row_version DD 和 instant add 標記, 但是這個index 上面有instant add 標記 // 說明這個record 是instant add 之前就插入的 rec_insert_state = INSERTED_BEFORE_INSTANT_ADD_OLD_IMPLEMENTATION; } else { // record 上面沒有row_version DD, 也沒用instant add 標記, 并且index 上面也沒用instant add // 那么這個Record 是在row_version DD 以及 instant add 做過之前就插入的 rec_insert_state = INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION; } ut_ad(rec_insert_state != REC_INSERT_STATE::NONE); return rec_insert_state; }
這里雖然 inline enum REC_INSERT_STATE get_rec_insert_state 定義的是 inline, 但是其實這個只是代碼給編譯器的定義, 具體函數是否 Inline 其實是編譯器自己決定的, 最后其實具體運行的時候該函數并沒有 inline, 因為可以從Flamegraph 看到, 說明這個函數是有符號表的信息的, 因此肯定不是 inline 的
3. 將 swatch case 改成 if/else, 并且給編譯器提示likely 執行的 branch
最后我們發現 switch case 對于有些明顯的分支預測并不友好, 通過 if/else 可以手動調整哪些 branch 更有可能執行, 從而優化編譯器的選擇.
審核編輯:湯梓紅
-
編譯器
+關注
關注
1文章
1634瀏覽量
49146 -
MySQL
+關注
關注
1文章
814瀏覽量
26603
原文標題:#issue 111538 MySQL 8.0 instant add/drop column 性能回退問題
文章出處:【微信號:inf_storage,微信公眾號:數據庫和存儲】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論