天天看點

MySQL8.0 新特性:Partial Update of LOB Column寫入logical diff

Update history:

  1. Update: 2018/7/28 MySQL8.0.12 https://dev.mysql.com/worklog/task/?id=11328

InnoDB: Undo logging is now supported for small updates to large object (LOB) data, which improves performance of LOB updates that are 100 bytes in size or less. Previously, LOB updates were a minimum of one LOB page in size, which is less than optimal for updates that might only modify a few bytes. This enhancement builds upon support added in MySQL 8.0.4 for partial fetch and update of LOB data.

MySQL8.0對json進行了比較完善的支援, 我們知道json具有比較特殊的存儲格式,通常存在多個key value鍵值對,對于類似更新操作通常不會更新整個json列,而是某些鍵值。

對于某些複雜的應用,json列的資料可能會變的非常龐大,這時候一個突出的問題是:innodb并不識别json類型,對它而言這些存儲統一都是LOB類型,而在之前的版本中Innodb處理LOB更新的方式是标記删除舊記錄,并插入新記錄,顯然這會帶來一些存儲上的開銷(盡管Purge線程會去背景清理),而寫入的redo log和Binlog的量也會偏高,對于超大列,可能會嚴重影響到性能。為了解決這個問題,MySQL8.0引入了LOB列部分更新的政策。

官方部落格有幾篇文章介紹的非常清楚,感興趣的可以直接跳過本文,直接閱讀官方部落格:

1: partial update of json values 2: introduces lob index for faster update 3: MVCC of Large Objects 4: MySQL 8.0: Optimizing Small Partial Update of LOB in InnoDB

以及相關的開發worklog:

WL#8963: Support for partial update of JSON in the optimizer WL#8985: InnoDB: Refactor compressed BLOB code to facilitate partial fetch/update WL#9141: InnoDB: Refactor uncompressed BLOB code to facilitate partial fetch/update WL#9263: InnoDB: Enable partial access of LOB using multiple zlib streams WL#8960: InnoDB: Partial Fetch and Update of BLOB WL#10570: Provide logical diffs for partial update of JSON values WL#2955: RBR replication of partial JSON updates

本文僅僅是筆者在了解該特性時做的一些簡單的筆記,,記錄的主要目的是用于以後如果涉及到相關的工作可以快速展開,是以比較淩亂

目前partial update需要通過JSON_SET, 或者JSON_REPLACE等特定接口來進行json列的更新,并且不是所有的更新都能夠滿足條件:

  • 沒有增加新的元素
  • 空間足夠大,可以容納替換的新值
    • 但類似資料長度(10 =>更新成7=>更新成9)是允許的

下面以json_set更新json列為例來看看相關的關鍵堆棧

檢查是否支援partial update

如上所述,需要指定的json函數接口才能進行partial update

mysql_execute_command
    |--> Sql_cmd_dml::execute
             |--> Sql_cmd_dml::prepare
                         |--> Sql_cmd_update::prepare_inner
                                     |---> prepare_partial_update
                                           |-->Item_json_func::supports_partial_update           

這裡隻是做預檢查,對于json列的更新如果全部是通過json_set/replace/remove進行的,則将其标記為候選partial update的列(

TABLE::mark_column_for_partial_update

), 存儲在bitmap結構

TABLE::m_partial_update_columns

設定partial update

入口函數:

TABLE::setup_partial_update()

在滿足某些條件時,需要設定logical diff(用于記錄partial update列的binlog,降低binlog存儲開銷):

  • binlog_row_value_options設定為"partial_json"
  • binlog 打開
  • log_bin_use_v1_row_events關閉
  • 使用row format

然後建立Partial_update_info對象(

Table::m_partial_update_info

), 用于存儲partial update執行過程中的狀态

  • Table::m_enabled_logical_diff_columns
  • TABLE::m_binary_diff_vectors
  • TABLE::m_logical_diff_vectors

建立更新向量

當讀入一行記錄後,就需要根據sql語句來建構後鏡像,而對于partial update所涉及的json列,會做特殊處理:

Sql_cmd_update::update_single_table
 |--> fill_record_n_invoke_before_triggers
      |-->fill_record
             |--> Item::save_in_field
                     |--> Item_func::save_possibly_as_json
                             |--> Item_func_json_set_replace::val_json
                                     |--> Json_wrapper::attempt_binary_update
                                             |--> json_binary::Value::update_in_shadow
                                                    |--> TABLE::add_binary_diff
                                               

json_wrapper::attempt_binary_update

: 做必要的資料類型檢查(是否符合partial update的條件)後,計算需要的空間,檢查是否有足夠的空閑空間

Value::has_space()

來替換成新值。

Value::update_in_shadow

: 進一步将變化的資料存儲到binary diff對象中(

TABLE::add_binary_diff

),每個

Binary_diff

對象包含了要修改對象的偏移量,長度以及一個指向新資料的const指針

如下例,摘自函數

Value::update_in_shadow

的注釋,這裡提取出來,以便于了解json binary的格式,以及如何産生Binary Diff

建立測試表:

root@test 10:00:45>create table t (a int primary key, b json);
Query OK, 0 rows affected (0.02 sec)

root@test 10:01:06>insert into t values (1, '[ "abc", "def" ]');
Query OK, 1 row affected (0.07 sec)
           

json資料的存儲格式如下:

0x02 - type: small JSON array
        0x02 - number of elements (low byte)
        0x00 - number of elements (high byte)
        0x12 - number of bytes (low byte)
        0x00 - number of bytes (high byte)
        0x0C - type of element 0 (string)
        0x0A - offset of element 0 (low byte)
        0x00 - offset of element 0 (high byte)
        0x0C - type of element 1 (string)
        0x0E - offset of element 1 (low byte)
        0x00 - offset of element 1 (high byte)
        0x03 - length of element 0
        'a'
        'b'  - content of element 0
        'c'
        0x03 - length of element 1
        'd'
        'e'  - content of element 1
        'f'           

更新json列的'abc'為'XY', 則空出一個位元組出來:

root@test 10:01:39>UPDATE t SET b = JSON_SET(b, '$[0]', 'XY');
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
           

此時的存儲格式為:

0x02 - type: small JSON array
              0x02 - number of elements (low byte)
              0x00 - number of elements (high byte)
              0x12 - number of bytes (low byte)
              0x00 - number of bytes (high byte)
              0x0C - type of element 0 (string)
              0x0A - offset of element 0 (low byte)
              0x00 - offset of element 0 (high byte)
              0x0C - type of element 1 (string)
              0x0E - offset of element 1 (low byte)
              0x00 - offset of element 1 (high byte)
CHANGED       0x02 - length of element 0
CHANGED           'X'
CHANGED           'Y'  - content of element 0
      (free)  'c'
              0x03 - length of element 1
              'd'
              'e'  - content of element 1
              'f'

           

此處隻影響到一個element,是以 隻有一個binary diff

再執行更新:

UPDATE t SET j = JSON_SET(j, '$[1]', 'XYZW')           

第二個element從3個位元組更新成4個位元組,顯然原地沒有足夠的空間,但可以利用其一個element的剩餘空間

0x02 - type: small JSON array
              0x02 - number of elements (low byte)
              0x00 - number of elements (high byte)
              0x12 - number of bytes (low byte)
              0x00 - number of bytes (high byte)
              0x0C - type of element 0 (string)
              0x0A - offset of element 0 (low byte)
              0x00 - offset of element 0 (high byte)
              0x0C - type of element 1 (string)
 CHANGED 0x0D - offset of element 1 (low byte)
              0x00 - offset of element 1 (high byte)
              0x02 - length of element 0
              'X'  - content of element 0
              'Y'  - content of element 0
CHANGED         0x04 - length of element 1
CHANGED         'X'
CHANGED         'Y'
CHANGED         'Z'  - content of element 1
CHANGED         'W'           

這裡會産生兩個binary diff,一個更新offset, 一個更新資料

我們再執行一條update,将字元串修改成整數,這種情況下,原來存儲字元串offset的位置被更改成了整數,而原來字元串占用的空間變成Unused狀态。這裡隻

UPDATE t SET b= JSON_SET(b, '$[1]', 456)
           
0x02 - type: small JSON array
        0x02 - number of elements (low byte)
        0x00 - number of elements (high byte)
        0x12 - number of bytes (low byte)
        0x00 - number of bytes (high byte)
        0x0C - type of element 0 (string)
        0x0A - offset of element 0 (low byte)
        0x00 - offset of element 0 (high byte)
CHANGED 0x05 - type of element 1 (int16)
CHANGED 0xC8 - value of element 1 (low byte)
CHANGED 0x01 - value of element 1 (high byte)
        0x02 - length of element 0
        'X'  - content of element 0
        'Y'  - content of element 0
(free)  0x04 - length of element 1
(free)  'X' 
(free)  'Y'
(free)  'Z'  - content of element 1
(free)  'W           

類型從string變成int16,使用之前offset的字段記錄int值,而原來string的空間則變成空閑狀态, 這裡産生一個binary diff。

我們再來看看另外一個相似的函數

Value::remove_in_shadow

,即通過json_remove從列上移除一個字段,以下樣例同樣摘自函數的注釋:

json列的值為

{ "a": "x", "b": "y", "c": "z" }

存儲格式:

              0x00 - type: JSONB_TYPE_SMALL_OBJECT
              0x03 - number of elements (low byte)
              0x00 - number of elements (high byte)
              0x22 - number of bytes (low byte)
              0x00 - number of bytes (high byte)
              0x19 - offset of key "a" (high byte)
              0x00 - offset of key "a" (low byte)
              0x01 - length of key "a" (high byte)
              0x00 - length of key "a" (low byte)
              0x1a - offset of key "b" (high byte)
              0x00 - offset of key "b" (low byte)
              0x01 - length of key "b" (high byte)
              0x00 - length of key "b" (low byte)
              0x1b - offset of key "c" (high byte)
              0x00 - offset of key "c" (low byte)
              0x01 - length of key "c" (high byte)
              0x00 - length of key "c" (low byte)
              0x0c - type of value "a": JSONB_TYPE_STRING
              0x1c - offset of value "a" (high byte)
              0x00 - offset of value "a" (low byte)
              0x0c - type of value "b": JSONB_TYPE_STRING
              0x1e - offset of value "b" (high byte)
              0x00 - offset of value "b" (low byte)
              0x0c - type of value "c": JSONB_TYPE_STRING
              0x20 - offset of value "c" (high byte)
              0x00 - offset of value "c" (low byte)
              0x61 - first key  ('a')
              0x62 - second key ('b')
              0x63 - third key  ('c')
              0x01 - length of value "a"
              0x78 - contents of value "a" ('x')
              0x01 - length of value "b"
              0x79 - contents of value "b" ('y')
              0x01 - length of value "c"
              0x7a - contents of value "c" ('z')           

将其中的成員$.b移除掉:

UPDATE t SET j = JSON_REMOVE(j, '$.b');

格式為:

              0x00 - type: JSONB_TYPE_SMALL_OBJECT
CHANGED 0x02 - number of elements (low byte)
              0x00 - number of elements (high byte)
              0x22 - number of bytes (low byte)
              0x00 - number of bytes (high byte)
              0x19 - offset of key "a" (high byte)
              0x00 - offset of key "a" (low byte)
              0x01 - length of key "a" (high byte)
              0x00 - length of key "a" (low byte)
CHANGED 0x1b - offset of key "c" (high byte)
CHANGED 0x00 - offset of key "c" (low byte)
CHANGED 0x01 - length of key "c" (high byte)
CHANGED 0x00 - length of key "c" (low byte)
CHANGED 0x0c - type of value "a": JSONB_TYPE_STRING
CHANGED 0x1c - offset of value "a" (high byte)
CHANGED 0x00 - offset of value "a" (low byte)
CHANGED 0x0c - type of value "c": JSONB_TYPE_STRING
CHANGED 0x20 - offset of value "c" (high byte)
CHANGED 0x00 - offset of value "c" (low byte)
      (free)  0x00
      (free)  0x0c
      (free)  0x1e
      (free)  0x00
      (free)  0x0c
      (free)  0x20
      (free)  0x00
              0x61 - first key  ('a')
      (free)  0x62
              0x63 - third key  ('c')
              0x01 - length of value "a"
              0x78 - contents of value "a" ('x')
      (free)  0x01
      (free)  0x79
              0x01 - length of value "c"
              0x7a - contents of value "c" ('z')
           

這裡會産生兩個binary diff,一個用于更新element個數,一個用于更新offset。

從上面的例子可以看到,每個Binary diff表示了一段連續更新的資料,有幾段連續更新的資料,就有幾個binary diff。 binary diff存儲到

TABLE::m_partial_update_info->m_binary_diff_vectors

中,

寫入logical diff

logical diff 主要用于優化寫binlog

Sql_cmd_update::update_single_table
 |--> fill_record_n_invoke_before_triggers
      |-->fill_record
             |--> Item::save_in_field
                     |--> Item_func::save_possibly_as_json
                             |--> Item_func_json_set_replace::val_json
                                    |-->TABLE::add_logical_diff
                                               

新的LOB存儲格式

相關代碼:

storage/innobase/lob/*, 所有的類和函數定義在namesapce lob下面

從上面的分析可以看到,Server層已經提供了所有修改的偏移量,新資料長度,已經判斷好了資料能夠原地存儲,對于innodb,則須要利用這些資訊來實作partial update 。

在展開這個問題之前,我們先來看下innodb針對json列的新格式。從代碼中可以看到,為了實作partial update, innodb增加了幾種新的資料頁格式:

壓縮表:
FIL_PAGE_TYPE_ZLOB_FIRST
FIL_PAGE_TYPE_ZLOB_DATA
FIL_PAGE_TYPE_ZLOB_INDEX
FIL_PAGE_TYPE_ZLOB_FRAG
FIL_PAGE_TYPE_ZLOB_FRAG_ENTRY

普通表:
FIL_PAGE_TYPE_LOB_INDEX
FIL_PAGE_TYPE_LOB_DATA
FIL_PAGE_TYPE_LOB_FIRST           

我們知道,傳統的LOB列通常是在聚集索引記錄内留一個外部存儲指針,指向lob存儲的page,如果一個page存儲不下,就會産生lob page連結清單。而新的存儲格式,則引入了lob index的概念,也就是為所有的lob page建立索引,格式如下:

ref pointer in cluster record
                         -------
                            |
                    FIL_PAGE_TYPE_LOG_FIRST
                            |
                    FIL_PAGE_TYPE_LOB_INDEX             ----------->   FIL_PAGE_TYPE_LOB_DATA
                            |
                    FIL_PAGE_TYPE_LOB_INDEX             -------------> FIL_PAGE_TYPE_LOB_DATA
                           |
                    ... ....           

Note: 本文隻讨論非壓縮表的場景, 對于壓縮表引入了更加複雜的資料類型,以後有空再在本文補上。

ref Pointer格式如下(和之前相比,增加了版本号)

字段 位元組數 描述
BTR_EXTERN_SPACE_ID 4 space id
BTR_EXTERN_PAGE_NO 第一個 lob page的no
BTR_EXTERN_OFFSET/BTR_EXTERN_VERSION 新的格式記錄version号

第一個FIL_PAGE_TYPE_LOG_FIRST頁面的操作定義在 lob::first_page_t類中格式如下(參考檔案: include/lob0first.h lob/lob0first.cc):

OFFSET_VERSION 1 表示lob的版本号,目前為0,用于以後lob格式改變做版本區分
OFFSET_FLAGS 目前隻使用第一個bit,被設定時表示無法做partial update, 用于通知purge線程某個更新操作産生的老版本LOB可以被完全釋放掉
OFFSET_LOB_VERSION 每個lob page都有個版本号,初始為1,每次更新後遞增
OFFSET_LAST_TRX_ID 6
OFFSET_LAST_UNDO_NO
OFFSET_DATA_LEN 存儲在該page上的資料長度
OFFSET_TRX_ID 建立存儲在該page上的事務id
OFFSET_INDEX_LIST 16 維護lob page連結清單
OFFSET_INDEX_FREE_NODES 維護空閑節點
LOB_PAGE_DATA 存儲資料的起始位置,注意第一個page同時包含了lob index 和lob data,但在第一個lob page中隻包含了10個lob index記錄,每個lob index大小為60位元組

除了第一個lob page外,其他所有的lob page都是通過lob index記錄來指向的,lob index之間連結成連結清單,每個index entry指向一個lob page,

普通Lob Page的格式如下

lob data version,目前為0
資料長度
建立該lob page的事務Id
lob data開始的位置

lob index entry的大小為60位元組,主要包含如下内容(include/lob0index.h lob/lob0index.cc):

偏移量
OFFSET_PREV Pointer to the previous index entry
OFFSET_NEXT Pointer to the next index entry
OFFSET_VERSIONS Pointer to the list of old versions for this index entry
OFFSET_TRXID The creator transaction identifier.
OFFSET_TRXID_MODIFIER The modifier transaction identifier
OFFSET_TRX_UNDO_NO the undo number of creator transaction.
OFFSET_TRX_UNDO_NO_MODIFIER The undo number of modifier transaction.
OFFSET_PAGE_NO The page number of LOB data page
The amount of LOB data it contains in bytes.
The LOB version number to which this index entry belongs.

從index entry的記錄格式我們可以看到 兩個關鍵資訊:

  • 對lob page的修改會産生新的lob page(“lob::replace()”) 和新的lob index entry
  • lob page no及其資料長度,據此我們可以根據修改的資料在json column裡的offset,通過lob index快速的定位到其所在的lob page
  • 每個lob page的版本号: 為了實作mvcc多版本,使用者線程先從undo log中找到對應版本的clust record,找出其中存儲的版本号v1,然後在掃描lob index時,如index entry中記錄的版本号<= v1,則是可見的,如果> v1, 那麼就需要根據OFFSET_VERSIONS連結清單,找到對應版本的index entry,并

    根據這個老的Index entry找到對應的lob page, 如下所示:

EXTERN REF (v2)
|
LOB IDX ENTRY (v1)
|
LOB IDX ENTRY(v2)  -----> LOB IDX ENTRY(v1)
|
LOG IDX ...(v1)           

多版本讀判斷參考函數 'lob::read'

lob更新

lob::update

: 根據binary diff,依次replace

Note: 不是所有的lob資料都需要partial update, 額外的lob index同樣會帶來存儲開銷,是以定義了一個threshold(ref_t::LOB_BIG_THRESHOLD_SIZE),超過2個page才去做partial update; 另外row_format也要確定lob列不存儲列字首到clust index ( ref

btr_store_big_rec_extern_fields

)

寫入binlog

在更新完一行後,對應的變更需要打包到線程的cache中(

THD::binlog_write_row() --> pack_row()

), 這時候要對partial update進行特殊處理,需要設定特定選項:

  • binlog_row_image = MINIMAL;
  • binlog_row_value_options=PARTIAL_JSON

如上例第一個update産生的binlog如下:

UPDATE t SET b = JSON_SET(b, '$[0]', 'XY');

binlog:

'/*!*/;
### UPDATE `test`.`t`
### WHERE
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
### SET
###   @2=JSON_REPLACE(@2, '$[0]', 'XY') /* JSON meta=4 nullable=1 is_null=0 */
           

由于存在主鍵,是以前鏡像隻記錄了主鍵值,而後鏡像也隻記錄了需要更新的列的内容,對于超大Json列,binlog上的開銷也是極小的,考慮到binlog通常會成為性能瓶頸點,預計這一特性會帶來不錯的吞吐量提升