精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

破解gh-ost變更導致MySQL表膨脹之謎

數據庫 MySQL
本文先介紹了一些關于 InnoDB 索引機制和頁溢出、頁分裂方面的知識;介紹了業界通用的 DDL 變更工具流程原理。

一、問題背景

二、索引結構

    1. B+tree

    2. 頁(page)

    3. 溢出頁

    4. 頁面分裂

三、當前DDL變更機制

四、變更后,表為什么膨脹?

    1. 原因說明

    2. 流程復現

    3. 排查過程

五、變更后,統計信息為什么差異巨大?

六、統計信息與慢SQL之間的關聯關系?

七、如何臨時解決該問題?

八、如何長期解決該問題?

九、總結

一、問題背景

業務同學在 OneDBA 平臺進行一次正常 DDL 變更完成后(變更內容跟此次問題無關),發現一些 SQL 開始出現慢查,同時變更后的表比變更前的表存儲空間膨脹了幾乎 100%。經過分析和流程復現完整還原了整個事件,發現了 MySQL 在平衡 B+tree 頁分裂方面遇到單行記錄太大時的一些缺陷,整理分享。

為了能更好的說明問題背后的機制,會進行一些關鍵的“MySQL原理”和“當前DDL變更流程”方面的知識鋪墊,熟悉的同學可以跳過。

本次 DDL 變更后帶來了如下問題:

  • 變更后,表存儲空間膨脹了幾乎 100%;
  • 變更后,表統計信息出現了嚴重偏差;
  • 變更后,部分有排序的 SQL 出現了慢查。

現在來看,表空間膨脹跟統計信息出錯是同一個問題導致,而統計信息出錯間接導致了部分SQL出現了慢查,下面帶著這些問題開始一步步分析找根因。

二、索引結構

B+tree

InnoDB 表是索引組織表,也就是所謂的索引即數據,數據即索引。索引分為聚集索引和二級索引,所有行數據都存儲在聚集索引,二級索引存儲的是字段值和主鍵,但不管哪種索引,其結構都是 B+tree 結構。

一棵 B+tree 分為根頁、非葉子節點和葉子節點,一個簡單的示意圖(from Jeremy Cole)如下:

圖片圖片

由于 InnoDB B+tree 結構高扇區特性,所以每個索引高度基本在 3-5 層之間,層級(Level)從葉子節點的 0 開始編號,沿樹向上遞增。每層的頁面節點之間使用雙向鏈表,前一個指針和后一個指針按key升序排列。

最小存儲單位是頁,每個頁有一個編號,頁內的記錄使用單向鏈表,按 key 升序排列。每個數據頁中有兩個虛擬的行記錄,用來限定記錄的邊界;其中最小值(Infimum)表示小于頁面上任何 key 的值,并且始終是單向鏈表記錄列表中的第一個記錄;最大值(Supremum)表示大于頁面上任何 key 的值,并且始終是單向鏈表記錄列表中的最后一條記錄。這兩個值在頁創建時被建立,并且在任何情況下不會被刪除。

非葉子節點頁包含子頁的最小 key 和子頁號,稱為“節點指針”。

現在我們知道了我們插入的數據最終根據主鍵順序存儲在葉子節點(頁)里面,可以滿足點查和范圍查詢的需求。

頁(page)

默認一個頁 16K 大小,且 InnoDB 規定一個頁最少能夠存儲兩行數據,這里需要注意規定一個頁最少能夠存儲兩行數據是指在空間分配上,并不是說一個頁必須要存兩行,也可以存一行。

怎么實現一個頁必須要能夠存儲兩行記錄呢? 當一條記錄 <8k 時會存儲在當前頁內,反之 >8k 時必須溢出存儲,當前頁只存儲溢出頁面的地址,需 20 個字節(行格式:Dynamic),這樣就能保證一個頁肯定能最少存儲的下兩條記錄。

溢出頁

當一個記錄 >8k 時會循環查找可以溢出存儲的字段,text類字段會優先溢出,沒有就開始挑選 varchar 類字段,總之這是 InnoDB 內部行為,目前無法干預。

建表時無論是使用 text 類型,還是 varchar 類型,當大小 <8k 時都是存儲在當前頁,也就是在 B+tree 結構中,只有 >8k 時才會進行溢出存儲。

頁面分裂

隨著表數據的變化,對記錄的新增、更新、刪除;那么如何在 B+tree 中高效管理動態數據也是一項核心挑戰。

MySQL InnoDB 引擎通過頁面分裂和頁面合并兩大關鍵機制來動態調整存儲結構,不僅能確保數據的邏輯完整性和邏輯順序正確,還能保證數據庫的整體性能。這些機制發生于 InnoDB 的 B+tree 索引結構內部,其具體操作是:

  • 頁面分裂:當已滿的索引頁無法容納新記錄時,創建新頁并重新分配記錄。
  • 頁面合并:當頁內記錄因刪除/更新低于閾值時,與相鄰頁合并以優化空間。

深入理解上述機制至關重要,因為頁面的分裂與合并將直接影響存儲效率、I/O模式、加鎖行為及整體性能。其中頁面的分裂一般分為兩種:

  • 中間點(mid point)分裂:將原始頁面中50%數據移動到新申請頁面,這是最普通的分裂方法。
  • 插入點(insert point)分裂:判斷本次插入是否遞增 or 遞減,如果判定為順序插入,就在當前插入點進行分裂,這里情況細分較多,大部分情況是直接插入到新申請頁面,也可能會涉及到已存在記錄移動到新頁面,有有些特殊情況下還會直接插入老的頁面(老頁面的記錄被移動到新頁面)。

表空間管理

InnoDB的B+tree是通過多層結構映射在磁盤上的,從它的邏輯存儲結構來看,所有數據都被有邏輯地存放在一個空間中,這個空間就叫做表空間(tablespace)。表空間由段(segment)、區(extent)、頁(page)組成,搞這么多手段的唯一目的就是為了降低IO的隨機性,保證存儲物理上盡可能是順序的。

三、當前DDL變更機制

在整個數據庫平臺(OneDBA)構建過程中,MySQL 結構變更模塊是核心基礎能力,也是研發同學在日常業務迭代過程中使用頻率較高的功能之一,主要圍繞對表加字段、加索引、改屬性等操作,為了減少這些操作對線上數據庫或業務的影響,早期便為 MySQL 結構變更開發了一套基于容器運行的無鎖變更程序,核心采用的是全量數據復制+增量 binlog 回放來進行變更,也是業界通用做法(內部代號:dw-osc,基于 GitHub 開源的 ghost 工具二次開發),主要解決的核心問題:

  • 實現無鎖化的結構變更,變更過程中不會阻擋業務對表的讀寫操作。
  • 實現變更不會導致較大主從數據延遲,避免業務從庫讀取不到數據導致業務故障。
  • 實現同時支持大規模任務變更,使用容器實現使用完即銷毀,無變更任務時不占用資源。

變更工具工作原理簡單描述(重要):

圖片圖片

重點:

簡單理解工具進行 DDL 變更過程中為了保證數據一致性,對于全量數據的復制與 binlog 回放是并行交叉處理,這種機制它有一個特點就是【第三步】會導致新插入的記錄可能會先寫入到表中(主鍵 ID 大的記錄先寫入到了表),然后【第二步】中復制數據后寫入到表中(主鍵 ID 小的記錄后寫入表)。

這里順便說一下當前得物結構變更整體架構:由于變更工具的工作原理需消費大量 binlog 日志保證數據一致性,會導致在變更過程中會有大量的帶寬占用問題,為了消除帶寬占用問題,開發了 Proxy 代理程序,在此基礎之上支持了多云商、多區域本地化變更。

目前整體架構圖如下:

圖片圖片

四、變更后,表為什么膨脹?

原因說明

上面幾個關鍵點鋪墊完了,回到第一個問題,這里先直接說明根本原因,后面會闡述一下排查過程(有同學感興趣所以分享一下,整個過程還是耗費不少時間)。

在『結構變更機制』介紹中,我們發現這種變更機制它有一個特點,就是【第三步】會導致新插入的記錄可能會先寫入到表中(主鍵 ID 大的記錄先寫入到了表),然后【第二步】中復制數據后寫入到表中(主鍵 ID 小的記錄)。這種寫入特性疊加單行記錄過大的時候(業務表單行記錄大小 5k 左右),會碰到 MySQL 頁分裂的一個瑕疵(暫且稱之為瑕疵,或許是一個 Bug),導致了一個頁只存儲了 1 條記錄(16k 的頁只存儲了 5k,浪費 2/3 空間),放大了存儲問題。

流程復現

下面直接復現一下這種現象下導致異常頁分裂的過程:

CREATE TABLE `sbtest` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pad` varchar(12000),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

然后插入兩行 5k 大小的大主鍵記錄(模擬變更時 binlog 回放先插入數據):

insert into sbtest values (10000, repeat('a',5120));
insert into sbtest values (10001, repeat('a',5120));

這里寫了一個小工具打印記錄對應的 page 號和 heap 號。

# ./peng
[pk:10000] page: 3 -> heap: 2
[pk:10001] page: 3 -> heap: 3

可以看到兩條記錄都存在 3 號頁,此時表只有這一個頁。

繼續開始順序插入數據(模擬變更時 copy 全量數據過程),插入 rec-1:

insert into sbtest values (1, repeat('a',5120));
# ./peng
[pk:1] page: 3 -> heap: 4
[pk:10000] page: 3 -> heap: 2
[pk:10001] page: 3 -> heap: 3

插入 rec-2:

insert into sbtest values (2, repeat('a',5120));
# ./peng
[pk:1] page: 4 -> heap: 2
[pk:2] page: 4 -> heap: 3
[pk:10000] page: 5 -> heap: 2
[pk:10001] page: 5 -> heap: 3

可以看到開始分裂了,page 3 被提升為根節點了,同時分裂出兩個葉子節點,各自存了兩條數據。此時已經形成了一棵 2 層高的樹,還是用圖表示吧,比較直觀,如下:

圖片圖片

插入 rec-3:

insert into sbtest values (3, repeat('a',5120));
# ./peng
[pk:1] page: 4 -> heap: 2
[pk:2] page: 4 -> heap: 3
[pk:3] page: 5 -> heap: 4
[pk:10000] page: 5 -> heap: 2
[pk:10001] page: 5 -> heap: 3

示意圖如下:

圖片圖片

插入 rec-4:

insert into sbtest values (4, repeat('a',5120));
# ./peng
[pk:1] page: 4 -> heap: 2
[pk:2] page: 4 -> heap: 3
[pk:3] page: 5 -> heap: 4
[pk:4] page: 5 -> heap: 3
[pk:10000] page: 5 -> heap: 2
[pk:10001] page: 6 -> heap: 2

這里開始分裂一個新頁 page 6,開始出現比較復雜的情況,同時也為后面分裂導致一個頁只有 1 條數據埋下伏筆:

圖片圖片

這里可以看到把 10001 這條記錄從 page 5 上面遷移到了新建的 page 6 上面(老的 page 5 中會刪除 10001 這條記錄,并放入到刪除鏈表中),而把當前插入的 rec-4 插入到了原來的 page 5 上面,這個處理邏輯在代碼中是一個特殊處理,向右分裂時,當插入點頁面前面有大于等于兩條記錄時,會設置分裂記錄為 10001,所以把它遷移到了 page 6,同時會把當前插入記錄插入到原 page 5。具體可以看 btr_page_get_split_rec_to_right 函數。

/* 這里返回true表示將行記錄向右分裂:即分配的新page的hint_page_no為原page+1 */
ibool btr_page_get_split_rec_to_right(
/*============================*/
        btr_cur_t*        cursor,
        rec_t**           split_rec)
{
  page_t*        page;
  rec_t*        insert_point;
  
  // 獲取當前游標頁和insert_point
  page = btr_cur_get_page(cursor);
  insert_point = btr_cur_get_rec(cursor);
  
  /* 使用啟發式方法:如果新的插入操作緊跟在同一頁面上的前一個插入操作之后,
     我們假設這里存在一個順序插入的模式。 */
  
  // PAGE_LAST_INSERT代表上次插入位置,insert_point代表小于等于待插入目標記錄的最大記錄位置
  // 如果PAGE_LAST_INSERT=insert_point意味著本次待插入的記錄是緊接著上次已插入的記錄,
  // 這是一種順序插入模式,一旦判定是順序插入,必然反回true,向右分裂
  if (page_header_get_ptr(page, PAGE_LAST_INSERT) == insert_point) {
    // 1. 獲取當前insert_point的page內的下一條記錄,并判斷是否是supremum記錄
    // 2. 如果不是,繼續判斷當前insert_point的下下條記錄是否是supremum記錄
    // 也就是說,會向后看兩條記錄,這兩條記錄有一條為supremum記錄,
    // split_rec都會被設置為NULL,向右分裂
    rec_t*        next_rec;
    next_rec = page_rec_get_next(insert_point);
    
    if (page_rec_is_supremum(next_rec)) {
    split_at_new:
      /* split_rec為NULL表示從新插入的記錄開始分裂,插入到新頁 */
      *split_rec = nullptr;
    } else {
      rec_t* next_next_rec = page_rec_get_next(next_rec);
      if (page_rec_is_supremum(next_next_rec)) {
        goto split_at_new;
      }
      
      /* 如果不是supremum記錄,則設置拆分記錄為下下條記錄 */


      /* 這樣做的目的是,如果從插入點開始向上有 >= 2 條用戶記錄,
         我們在該頁上保留 1 條記錄,因為這樣后面的順序插入就可以使用
         自適應哈希索引,因為它們只需查看此頁面上的記錄即可對正確的
         搜索位置進行必要的檢查 */
      
      *split_rec = next_next_rec;
    }
    
    return true;
  }
  
  return false;
}

插入 rec-5:

insert into sbtest values (5, repeat('a',5120));
# ./peng
[pk:1] page: 4 -> heap: 2
[pk:2] page: 4 -> heap: 3
[pk:3] page: 5 -> heap: 4
[pk:4] page: 5 -> heap: 3
[pk:5] page: 7 -> heap: 3
[pk:10000] page: 7 -> heap: 2
[pk:10001] page: 6 -> heap: 2

開始分裂一個新頁 page 7,新的組織結構方式如下圖:

圖片圖片

此時是一個正常的插入點右分裂機制,把老的 page 5 中的記錄 10000 都移動到了 page 7,并且新插入的 rec-5 也寫入到了 page 7 中。到此時看上去一切正常,接下來再插入記錄在當前這種結構下就會產生異常。

插入 rec-6:

insert into sbtest values (5, repeat('a',5120));
# ./peng
[pk:1] page: 4 -> heap: 2
[pk:2] page: 4 -> heap: 3
[pk:3] page: 5 -> heap: 4
[pk:4] page: 5 -> heap: 3
[pk:5] page: 7 -> heap: 3
[pk:6] page: 8 -> heap: 3
[pk:10000] page: 8 -> heap: 2
[pk:10001] page: 6 -> heap: 2

圖片圖片

此時也是一個正常的插入點右分裂機制,把老的 page 7 中的記錄 10000 都移動到了 page 8,并且新插入的 rec-6 也寫入到了 page 8 中,但是我們可以發現 page 7 中只有一條孤零零的 rec-5 了,一個頁只存儲了一條記錄。

按照代碼中正常的插入點右分裂機制,繼續插入 rec-7 會導致 rec-6 成為一個單頁、插入 rec-8 又會導致 rec-7 成為一個單頁,一直這樣循環下去。

目前來看就是在插入 rec-4,觸發了一個內部優化策略(具體優化沒太去研究),進行了一些特殊的記錄遷移和插入動作,當然跟記錄過大也有很大關系。

排查過程

有同學對這個問題排查過程比較感興趣,所以這里也整理分享一下,簡化了一些無用信息,僅供參考。

表總行數在 400 百萬,正常情況下的大小在 33G 左右,變更之后的大小在 67G 左右。

  • 首先根據備份恢復了一個數據庫現場出來。
  • 統計了業務表行大小,發現行基本偏大,在 4-7k 之間(一個頁只存了2行,浪費1/3空間)。
  • 分析了變更前后的表數據頁,以及每個頁存儲多少行數據。

a.發現變更之前數據頁大概 200 百萬,變更之后 400 百萬,解釋了存儲翻倍。

b.發現變更之前存儲 1 行的頁基本沒有,變更之后存儲 1 行的頁接近 400 百萬。

基于現在這些信息我們知道了存儲翻倍的根本原因,就是之前一個頁存儲 2 條記錄,現在一個頁只存儲了 1 條記錄,新的問題來了,為什么變更后會存儲 1 條記錄,繼續尋找答案。

  • 我們首先在備份恢復的實例上面進行了一次靜態變更,就是變更期間沒有新的 DML 操作,沒有復現。但說明了一個問題,異常跟增量有關,此時大概知道跟變更過程中的 binlog 回放特性有關【上面說的回放會導致主鍵 ID 大的記錄先寫入表中】。
  • 寫了個工具把 400 百萬數據每條記錄分布在哪個頁里面,以及頁里面的記錄對應的 heap 是什么都記錄到數據庫表中分析,慢長等待跑數據。

圖片圖片

  • 數據分析完后通過分析發現存儲一條數據的頁對應的記錄的 heap 值基本都是 3,正常應該是 2,意味著這些頁并不是一開始就存一條數據,而是產生了頁分裂導致的。
  • 開始繼續再看頁分裂相關的資料和代碼,列出頁分裂的各種情況,結合上面的信息構建了一個復現環境。插入數據頁分裂核心函數。

btr_cur_optimistic_insert:樂觀插入數據,當前頁直接存儲

btr_cur_pessimistic_insert:悲觀插入數據,開始分裂頁

btr_root_raise_and_insert:單獨處理根節點的分裂

btr_page_split_and_insert:分裂普通頁,所有流程都在這個函數

btr_page_get_split_rec_to_right:判斷是否是向右分裂

btr_page_get_split_rec_to_left:判斷是否是向左分裂

heap

heap 是頁里面的一個概念,用來標記記錄在頁里面的相對位置,頁里面的第一條用戶記錄一般是 2,而 0 和 1 默認分配給了最大最小虛擬記錄,在頁面創建的時候就初始化好了,最大最小記錄上面有簡單介紹。

解析 ibd 文件

更快的方式還是應該分析物理 ibd 文件,能夠解析出頁的具體數據,以及被分裂刪除的數據,分裂就是把一個頁里面的部分記錄移動到新的頁,然后刪除老的記錄,但不會真正刪除,而是移動到頁里面的一個刪除鏈表,后面可以復用。

五、變更后,統計信息為什么差異巨大?

表統計信息主要涉及索引基數統計(也就是唯一值的數量),主鍵索引的基數統計也就是表行數,在優化器進行成本估算時有些 SQL 條件會使用索引基數進行抉擇索引選擇(大部分情況是 index dive 方式估算掃描行數)。

InnoDB 統計信息收集算法簡單理解就是采樣葉子節點 N 個頁(默認 20 個頁),掃描統計每個頁的唯一值數量,N 個頁的唯一值數量累加,然后除以N得到單個頁平均唯一值數量,再乘以表的總頁面數量就估算出了索引總的唯一值數量。

但是當一個頁只有 1 條數據的時候統計信息會產生嚴重偏差(上面已經分析出了表膨脹的原因就是一個頁只存儲了 1 條記錄),主要是代碼里面有個優化邏輯,對單個頁的唯一值進行了減 1 操作,具體描述如下注釋。本來一個頁面就只有 1 條記錄,再進行減 1 操作就變成 0 了,根據上面的公式得到的索引總唯一值就偏差非常大了。

static bool dict_stats_analyze_index_for_n_prefix(
    ...
    // 記錄頁唯一key數量
    uint64_t n_diff_on_leaf_page;
    
    // 開始進行dive,獲取n_diff_on_leaf_page的值
    dict_stats_analyze_index_below_cur(pcur.get_btr_cur(), n_prefix,
                                       &n_diff_on_leaf_page, &n_external_pages);
    
    /* 為了避免相鄰兩次dive統計到連續的相同的兩個數據,因此減1進行修正。
    一次是某個頁面的最后一個值,一次是另一個頁面的第一個值。請考慮以下示例:
    Leaf level:
    page: (2,2,2,2,3,3)
    ... 許多頁面類似于 (3,3,3,3,3,3)...
    page: (3,3,3,3,5,5)
    ... 許多頁面類似于 (5,5,5,5,5,5)...
    page: (5,5,5,5,8,8)
    page: (8,8,8,8,9,9)
    我們的算法會(正確地)估計平均每頁有 2 條不同的記錄。
    由于有 4 頁 non-boring 記錄,它會(錯誤地)將不同記錄的數量估計為 8 條
    */ 
    if (n_diff_on_leaf_page > 0) {
      n_diff_on_leaf_page--;
    }
    
    // 更新數據,在所有分析的頁面上發現的不同鍵值數量的累計總和
    n_diff_data->n_diff_all_analyzed_pages += n_diff_on_leaf_page;
)

可以看到PRIMARY主鍵異常情況下統計數據只有 20 萬,表有 400 百萬數據。正常情況下主鍵統計數據有 200 百萬,也與表實際行數差異較大,同樣是因為單個頁面行數太少(正常情況大部分也只有2條數據),再進行減1操作后,導致統計也不準確。

MySQL> select table_name,index_name,stat_value,sample_size from mysql.innodb_index_stats where database_name like 'sbtest' and TABLE_NAME like 'table_1' and stat_name='n_diff_pfx01';
+-------------------+--------------------------------------------+------------+-------------+
| table_name        | index_name                                 | stat_value | sample_size |
+-------------------+--------------------------------------------+------------+-------------+
| table_1           | PRIMARY                                    |     206508 |          20 |
+-------------------+--------------------------------------------+------------+-------------+
11 rows in set (0.00 sec)

優化

為了避免相鄰兩次dive統計到連續的相同的兩個數據,因此減1進行修正。

這里應該是可以優化的,對于主鍵來說是不是可以判斷只有一個字段時不需要進行減1操作,會導致表行數統計非常不準確,畢竟相鄰頁不會數據重疊。

最低限度也需要判斷單個頁只有一條數據時不需要減1操作。

六、統計信息與慢SQL之間的關聯關系?

當前 MySQL 對大部分 SQL 在評估掃描行數時都不再依賴統計信息數據,而是通過一種 index dive 采樣算法實時獲取大概需要掃描的數據,這種方式的缺點就是成本略高,所以也提供有參數來控制某些 SQL 是走 index dive 還是直接使用統計數據。

另外在SQL帶有 order by field limit 時會觸發MySQL內部的一個關于 prefer_ordering_index 的 ORDER BY 優化,在該優化中,會比較使用有序索引和無序索引的代價,誰低用誰。

當時業務有問題的慢 SQL 就是被這個優化干擾了。

# where條件
user_id = ? and biz = ? and is_del = ? and status in (?) ORDER BY modify_time limit 5


# 表索引
idx_modify_time(`modify_time`)
idx_user_biz_del(`user_id`,`biz`, `is_del`)

正常走 idx_user_biz_del 索引為過濾性最好,但需要對 modify_time 字段進行排序。

這個優化機制就是想嘗試走 idx_modify_time 索引,走有序索引想避免排序,然后套了一個公式來預估如果走 idx_modify_time 有序索引大概需要掃描多少行?公式非常簡單直接:表總行數 / 最優索引的掃描行數 * limit。

  • 表總行數:也就是統計信息里面主鍵的 n_rows
  • 最優索引的掃描行數:也就是走 idx_user_biz_del 索引需要掃描的行數
  • limit:也就是 SQL 語句里面的 limit 值

使用有序索引預估的行數對比最優索引的掃描行數來決定使用誰,在這種改變索引的策略下,如果表的總行數估計較低(就是上面主鍵的統計值),會導致更傾向于選擇有序索引。

但一個最重要的因素被 MySQL 忽略了,就是實際業務數據分布并不是按它給的這種公式來,往往需要掃描很多數據才能滿足 limit 值,造成慢 SQL。

七、如何臨時解決該問題?

發現問題后,可控的情況下選擇在低峰期對表執行原生 alter table xxx engine=innodb 語句, MySQL 內部重新整理了表空間數據,相關問題恢復正常。但這個原生 DDL 語句,雖然變更不會產生鎖表,但該語句無法限速,同時也會導致主從數據較大延遲。

為什么原生 DDL 語句可以解決該問題?看兩者在流程上的對比區別。

alter table xxx engine=innodb變更流程

當前工具結構變更流程

  1. 建臨時表:在目標數據庫中創建與原表結構相同的臨時表用于數據拷貝。
  2. 拷貝全量數據:將目標表中的全量數據同步至臨時表。
  3. 增量DML臨時存儲在一個緩沖區內。
  4. 全量數據復制完成后,開始應用增量DML日志。
  5. 切換新舊表:重命名原表作為備份,再用臨時表替換原表。
  6. 變更完成
  1. 創建臨時表:在目標數據庫中創建與原表結構相同的臨時表用于數據拷貝。
  2. 拷貝全量數據:將目標表中的全量數據同步至臨時表。
  3. 解析Binlog并同步增量數據: 將目標表中的增量數據同步至臨時表。
  4. 切換新舊表:重命名原表作為備份,再用臨時表替換原表。
  5. 變更完成

可以看出結構變更唯一不同的就是增量 DML 語句是等全量數據復制完成后才開始應用,所以能修復表空間,沒有導致表膨脹。

八、如何長期解決該問題?

關于業務側的改造這里不做過多說明,我們看看從變更流程上面是否可以避免這個問題。

既然在變更過程中復制全量數據和 binlog 增量數據回放存在交叉并行執行的可能,那么如果我們先執行全量數據復制,然后再進行增量 binlog 回放是不是就可以繞過這個頁分裂問題(就變成了跟 MySQL 原生 DDL 一樣的流程)。

變更工具實際改動如下圖:

圖片圖片

這樣就不存在最大記錄先插入到表中的問題,丟棄的記錄后續全量復制也同樣會把記錄復制到臨時表中。并且這個優化還能解決需要大量回放 binlog 問題,細節可以看看 gh-ost 的 PR-1378。

九、總結

本文先介紹了一些關于 InnoDB 索引機制和頁溢出、頁分裂方面的知識;介紹了業界通用的 DDL 變更工具流程原理。

隨后詳細分析了變更后表空間膨脹問題根因,主要是當前變更流程機制疊加單行記錄過大的時候(業務表單行記錄大小 5k 左右),會碰到 MySQL 頁分裂的一個瑕疵,導致了一個頁只存儲了 1 條記錄(16k 的頁只存儲了 5k,浪費 2/3 空間),導致存儲空間膨脹問題。

最后分析了統計信息出錯的原因和統計信息出錯與慢 SQL 之間的關聯關系,以及解決方案。

責任編輯:武曉燕 來源: 得物技術
相關推薦

2024-08-08 07:38:42

2010-10-09 16:48:04

2024-02-21 15:30:56

2010-03-15 16:06:52

2009-11-10 10:12:55

2023-06-01 19:14:18

2024-10-16 10:26:10

2020-12-08 09:13:51

MySQLDDL變更

2016-03-22 16:51:13

C++泛型膨脹

2015-09-16 10:28:09

2024-04-10 14:27:03

MySQL數據庫

2022-12-26 08:07:03

MySQL批量數據

2017-07-05 14:14:33

MySQL表服務變慢

2024-10-18 15:30:00

2012-03-28 09:48:45

2011-07-06 12:04:53

架構

2010-07-30 13:21:21

2017-08-29 14:30:34

2010-05-27 10:26:22

MySQL服務

2025-04-29 09:10:00

點贊
收藏

51CTO技術棧公眾號

亚洲综合免费视频| 亚洲国产精品小视频| 日韩一区在线视频| www精品久久| 国产一区二区在线播放视频| 老牛影视av一区二区在线观看| 日韩福利电影在线| 亚洲激情小视频| 亚洲五月天综合| 五月天婷婷社区| 亚洲国产精品一区| 亚洲精品一区在线观看| 日韩不卡一二区| 国产又粗又长视频| 99久久www免费| 欧美丝袜丝nylons| 日韩欧美手机在线| 潘金莲一级淫片aaaaaa播放| 日韩三区视频| 欧美日韩国产综合新一区 | 欧美视频二区欧美影视| 国产日韩欧美一区二区三区乱码| 2019国产精品自在线拍国产不卡| 久久精品免费网站| 激情视频在线观看免费| 天天躁日日躁狠狠躁欧美巨大小说 | 日韩欧美视频一区| 中文字幕不卡每日更新1区2区| 国产又粗又猛又黄视频| 国产精品激情电影| 亚洲精品在线三区| 91香蕉视频在线观看视频| 久久国产精品一区| 国产91在线观看| 久久久免费av| 女~淫辱の触手3d动漫| 国产超碰精品| 中文av字幕一区| 成人欧美一区二区三区在线湿哒哒| 中文乱码字幕高清一区二区| av在线亚洲一区| 亚洲在线中文字幕| 精品国产综合区久久久久久| 精品国产免费观看| 国产尤物久久久| 在线不卡中文字幕| 日韩视频在线视频| 男人天堂综合| 久久99久久久久| 欧美激情亚洲精品| 国产特级黄色录像| 亚洲伦理一区二区| 欧美日韩亚洲国产综合| 2022中文字幕| 免费动漫网站在线观看| 99国产欧美另类久久久精品| 国产精品成人aaaaa网站| 色老板免费视频| 欧美日韩一区二区三区在线电影 | 亚洲1区在线观看| 天天做天天摸天天爽国产一区| 欧美高清视频一区二区三区在线观看| 久草热在线观看| 国产综合自拍| 欧美激情在线狂野欧美精品| 久久久久久久国产视频| 国产一区二区三区探花 | 青青草视频播放| 91福利精品在线观看| 尤物在线观看一区| 欧美精品一区二区视频| 韩国三级av在线免费观看| 国产农村妇女毛片精品久久麻豆| 97在线中文字幕| 老熟妇一区二区三区| 视频在线观看国产精品| 欧美日韩国产成人在线| 色欲AV无码精品一区二区久久| 亚洲视频国产| 欧美精品一二三| 亚洲天堂国产视频| 在线观看精品| 精品日本美女福利在线观看| 丰满人妻中伦妇伦精品app| 影音先锋在线播放| 亚洲国产高清在线观看视频| 亚洲一区三区视频在线观看| 人人九九精品| 成人毛片老司机大片| 91精品视频免费看| 18国产免费视频| 国产在线精品一区二区不卡了| 国产精品99导航| 国产精品久久久久久久久久久久久久久久久 | 久久中文字幕一区| 在线国产视频一区| 乱中年女人伦av一区二区| 亚洲欧洲免费视频| 国产精品九九九九九| 色无极亚洲影院| 国产亚洲精品一区二区| 性久久久久久久久久 | 老司机午夜网站| aaa在线观看| 久久一日本道色综合| 国产精品国产三级国产专区53 | 国产亚洲精品高潮| 印度午夜性春猛xxx交| 成人看的羞羞网站| 亚洲图片欧洲图片av| 极品魔鬼身材女神啪啪精品| 国产深夜精品| 欧美性视频精品| 国产精品久久无码一三区| 91尤物视频在线观看| 韩国黄色一级大片| 日本精品裸体写真集在线观看| 欧美丝袜一区二区| 国产在线青青草| 亚洲精品永久免费视频| 色综合久久88色综合天天免费| 午夜精品久久久久久久无码 | 亚洲精品免费视频| 国产三级三级三级看三级| 国产香蕉精品| 亚洲成人激情在线观看| 日本少妇xxxx| 亚洲香蕉视频| 中文字幕亚洲无线码在线一区| 特级西西人体高清大胆| 国产精品99久久| 日韩美女在线观看| 亚洲视频一区二区三区四区| 91麻豆国产在线观看| 人偷久久久久久久偷女厕| 国产精品久久久久一区二区国产 | 欧美aa在线视频| 91精品久久久久久久久| 99热这里只有精品5| 粉嫩在线一区二区三区视频| 中文字幕色一区二区| 成人福利一区二区| 在线看福利67194| 麻豆精品一区二区三区视频| 好吊视频一区二区三区四区| 91精品在线一区| 九七久久人人| 88在线观看91蜜桃国自产| 污污免费在线观看| 成人精品电影| 国产精品露脸av在线| www.好吊色| 久久一区二区视频| av免费在线播放网站| 91嫩草国产线观看亚洲一区二区 | 石原莉奈在线亚洲三区| 欧美日韩一区二区三| 偷拍视频一区二区三区| 亚洲人线精品午夜| 中国一级片黄色一级片黄| 国精产品一区一区三区mba视频| 不卡一卡2卡3卡4卡精品在| 日韩毛片在线一区二区毛片| 五月天激情综合| 日韩乱码人妻无码中文字幕久久| 香蕉久久久久久久av网站| 91精品视频免费| 污污网站在线看| 欧美无砖专区一中文字| 18啪啪污污免费网站| 国产在线看一区| 日韩一级片免费视频| 四虎5151久久欧美毛片| 国产精品久久久久久久久久尿| 91高清在线| 日韩视频永久免费| 国产又粗又长又黄的视频| 国产亚洲综合精品| 色就是色欧美| 97蜜桃久久| 日韩亚洲电影在线| 色网站在线播放| 国产sm精品调教视频网站| 水蜜桃色314在线观看| 97色婷婷成人综合在线观看| 九九久久综合网站| 日韩a在线观看| 欧美精品三级在线观看| 久久久久久久久久综合| 久久久91精品国产一区二区三区| 国产一区二区三区乱码| 蜜桃一区二区三区| 69久久夜色精品国产69| 大乳在线免费观看| 色94色欧美sute亚洲线路一ni| 亚洲视频在线播放免费| 日韩电影在线免费| 日韩一区二区高清视频| 国产精品毛片aⅴ一区二区三区| 中文字幕免费国产精品| 亚洲第九十九页| 亚洲精品午夜久久久| 99日在线视频| 久久综合av| 国产欧美一区二区三区久久人妖| 欧美videosex性欧美黑吊| 欧美一区二区三区男人的天堂| 亚洲欧美精品久久| 91视频免费观看| 丰满少妇一区二区三区专区| 一区二区三区午夜探花| 91在线高清免费观看| 青青草原综合久久大伊人精品| 中文在线不卡视频| 国产成人自拍一区| 中文字幕高清不卡| 亚洲视频 中文字幕| 精品一区二区在线观看| 黄www在线观看| 亚洲性感美女99在线| 成人自拍网站| 国产探花在线观看| 亚洲精品在线电影| 91丨九色丨丰满| 在线观看区一区二| 国产三级aaa| 久久久国产综合精品女国产盗摄| 成人一区二区三区仙踪林| 影院欧美亚洲| 国产成人精品免费看在线播放| 精品视频在线播放一区二区三区| 国产成人av在线播放| 国产理论在线| 国产一区二区三区四区福利| 天天操天天舔天天干| 欧美在线影院一区二区| 日韩久久久久久久久| 久久精品视频一区二区三区| 国产黑丝在线观看| 成人久久视频在线观看| 无码人妻一区二区三区精品视频 | 成人深夜视频在线观看| 欧美体内she精高潮| 国产中文一区二区三区| 亚洲综合激情视频| 精品一二三四区| 91精品视频国产| 亚洲精选国产| 777av视频| 亚洲欧美bt| 亚洲视频在线观看一区二区三区| 久久精品亚洲| 欧美与动交zoz0z| 你懂的国产精品永久在线| 久久久久一区二区| 欧美一级做一级爱a做片性| 欧美激情欧美狂野欧美精品| 久青草国产在线| 亚洲欧美日韩精品久久奇米色影视| 中文字幕一区二区久久人妻| 欧美在线免费观看亚洲| 中文字幕欧美人妻精品一区蜜臀| 欧美无砖砖区免费| 精品久久无码中文字幕| 欧美成人aa大片| 在线播放精品视频| 欧美乱妇一区二区三区不卡视频| 国产精品久久久久久久免费| 日韩女优电影在线观看| 天天干,夜夜操| 在线视频中文亚洲| 超碰在线caoporn| 国产一区二区免费| 永久免费在线观看视频| 日韩精品一二三四区| 国产福利第一视频| 亚洲а∨天堂久久精品9966| 日韩av免费观影| xvideos成人免费中文版| 在线看福利影| 青草成人免费视频| h片在线观看视频免费免费| 2019av中文字幕| 成人综合网站| 国产99视频精品免费视频36| 亚洲成人一品| 一区二区精品国产| 亚洲精选国产| 中文字幕成人免费视频| 成人精品小蝌蚪| 91精品人妻一区二区三区蜜桃2| 播五月开心婷婷综合| avhd101老司机| 中文字幕免费不卡在线| 波多野结衣爱爱视频| 欧美午夜片在线免费观看| 91丨九色丨蝌蚪丨对白| 日韩黄色在线免费观看| 亚洲第一色视频| 一区二区三区 在线观看视| 色yeye免费人成网站在线观看| 日韩av三级在线观看| 亚洲精品aⅴ| 亚洲精品永久www嫩草| 欧美一级精品片在线看| 欧美在线观看视频免费| 快she精品国产999| 少妇性l交大片7724com| 久久亚区不卡日本| 国产黄色小视频网站| 91久久精品一区二区| 狠狠人妻久久久久久综合麻豆 | 三区四区在线视频| 欧美亚洲国产视频小说| 4438全国亚洲精品观看视频| 亚洲午夜精品久久久久久浪潮| 一本色道久久综合| 乱人伦xxxx国语对白| 国产乱妇无码大片在线观看| 爱情岛论坛亚洲自拍| 国产校园另类小说区| 日韩视频免费观看高清| 欧美一区二区三区免费大片| 成人在线观看黄色| 亲爱的老师9免费观看全集电视剧| 国产成人一二片| 女人被男人躁得好爽免费视频| 精品一区免费av| 国精产品视频一二二区| 一本久久a久久精品亚洲| 国产精品国产高清国产| 久久久久国色av免费观看性色| 996久久国产精品线观看| 亚洲午夜精品久久| 久久精品国产久精国产| 成人免费播放视频| 国产精品福利一区二区| 麻豆changesxxx国产| 天天综合色天天| www.久久色| 欧美日韩高清在线观看| 亚洲2区在线| 九九热只有这里有精品| 国产成人免费视| 久久无码人妻精品一区二区三区 | 99综合电影在线视频| 欧美无人区码suv| 国产精品久久久久久久久久免费看 | 成人欧美一区二区三区黑人免费| 综合在线一区| 免费黄色在线播放| 亚洲一区在线观看网站| 中文字幕在线观看视频网站| 亚洲大胆人体在线| 蜜桃麻豆av在线| 91久久精品美女| 一本一道久久综合狠狠老| 四川一级毛毛片| 午夜国产精品影院在线观看| 91在线视频国产| 久久精品久久久久久| 久草免费在线视频| 麻豆传媒一区| 狠久久av成人天堂| 亚洲一区二区三区无码久久| 欧美性20hd另类| 中文字幕日本在线观看| 成人情趣片在线观看免费| 欧美韩国一区| 国产伦精品一区二区三区妓女| 在线观看日产精品| 最爽无遮挡行房视频在线| 精品国产综合| 久久成人av少妇免费| 久久久久久久九九九九| 亚洲九九九在线观看| 色婷婷成人网| 久久久久99精品成人片| 久久久国产精华| 在线视频1卡二卡三卡| 欧美激情中文网| 国产精品免费大片| 亚洲综合伊人久久| 欧美性猛交xxxxx免费看| 9191在线观看| 国偷自产av一区二区三区小尤奈| 99久久九九| 中文字幕人妻熟女在线| 色综合天天综合网天天看片| 久操视频在线| 欧美日韩一区二区三区免费| 韩国精品在线观看| 在线能看的av| 欧美成人精品在线播放| 91国产一区| 精品少妇一区二区三区在线| 国产精品午夜免费| 香蕉视频国产在线| 亚洲va久久久噜噜噜| 亚洲二区三区不卡|