天天看點

[WorkLog] InnoDB Faster truncate/drop table space

這個系列, 介紹upstream 一些有意思的worklog

問題

在InnoDB 現有的版本裡面, 如果一個table space 被truncated 或者 drop 的時候, 比如有一個連接配接建立了臨時表, 連接配接斷開以後, 對應的臨時表都需要進行drop 操作.

InnoDB 是需要将該tablespace 對應的所有的page 從LRU/FLUSH list 中删除, 如果沒有這個操作, 新的table 的table spaceid 如果重複的話, 那麼就可能通路到髒資料.

為了将這些page 删除, 那麼就需要全部周遊LRU/FLUSH list, 當bp 特别大的時候, 這樣周遊的開銷是很大的, 并且無論這個要删除的table 有多大, 都需要将這些LRU/FLUSH list 全部周遊..

解決方法

解決方法和之前解決undo ACID DDL 的方法類似, 核心思想就是通過引用計數的方法, 對table_space 加reference, 然後後續lazy delete

bp 上的每一個page 都有自己對應的version, 當table space 被drop/rename 的時候, 隻需要對fil_space 的version + 1, 那麼bp 中該fil_space 對應的page 就因為version < fil_space.current_version 而變得無效.

原先由drop/rename tablespace 觸發的space_delete 操作就變的非常的輕量. 後續定期的将這些stable page 删除或者複用即可

不過帶來的額外開銷就是, 每一次通路bp 中的一個page 就需要确認目前page 是否過期.

具體實作

buf_page_t 增加 m_space, m_version.

Additions to buf_page_t {
 ...
 // 指向對應的fil_space_t
 fil_space_t *m_space{};

 // Version number of the page to check for stale pages. This value is
 // "inherited" from the m_space->m_version when we init a page.
 // page 的version number, 在page_init 的時候設定成m_space->m_version
 uint32_t m_version{};
};           

fil_space_t 增加m_version, m_n_ref_count.

m_version 就是目前fil_space_t 的版本号, 每次delete/truncate 就會 + 1

m_n_ref_count: bp 每增加一個page , m_n_ref_count + 1, 隻能等到m_n_ref_count == 0 的時候, 改fil_space 才能被删除, 否則bp 裡面的m_space 指針就會指向空

Additions to  fil_space_t {
 ...
 // Version number of the instance, not persistent. Every time we truncate
 // or delete we bump up the version number.
 lsn_t m_version{};

 // Reference count of how many pages point to this instance. An instance cannot
 // be deleted if the reference count is greater than zero. The only exception
 // is shutdown.
 std::atomic_int m_n_ref_count{};
};           

增加了lazy delete fil_space 以後, 那麼什麼時候将記憶體中的fil_space_t 删除呢?

最後的删除操作在 master_thread 會定期執行, 将之前已經标記删除, 放入到m_deleted_spaces 中的space 一起删除

/ Purge any deleted tablespace pages. /

fil_purge(); => fil_shard.purge()

void purge() {
    mutex_acquire();
    for (auto it = m_deleted_spaces.begin(); it != m_deleted_spaces.end();) {
      auto space = it->second;
      // has_no_references() 說明該fil_space 對應的bp 已經都删除了, 那麼該space 就可以删除
      if (space->has_no_references()) {
        ut_a(space->files.front().n_pending == 0);
        space_free_low(space);
        it = m_deleted_spaces.erase(it);
        ... }
    mutex_release();
  }           

drop/rename tablespace

執行drop/rename tablespace 的時候需要執行 row_drop_tablespace => fil_delete_tablespace => space_delete(space_id, buf_remove)

新增加 buf_remove_t 類型: BUF_REMOVE_NONE. 不需要移除該tablespace 的所有bp.

8.0.23 drop table 的時候, 執行 row_drop_tablespace => fil_delete_tablespace, 之前delete tablespace 的時候, 傳入的是 BUF_REMOVE_ALL_NO_WRITE, 需要将該space 對應的bp 都清理才可以完成操作.

傳入 BUF_REMOVE_NONE 就隻需要将tablespace 标記删除, 放入到 m_deleted_spaces 中, 不需要清理bp, 然後将對應的實體檔案删除即可. 該tablespace 對應bp 中的資料就變成 stale page, 後續會有操作将這些stale page 删除或者複用.

enum buf_remove_t {
  /** Don't remove any pages. */
  BUF_REMOVE_NONE,
  /** Remove all pages from the buffer pool, don't write or sync to disk */
  BUF_REMOVE_ALL_NO_WRITE,
  /** Remove only from the flush list, don't write or sync to disk */
  BUF_REMOVE_FLUSH_NO_WRITE,
  /** Flush dirty pages to disk only don't remove from the buffer pool */
  BUF_REMOVE_FLUSH_WRITE
};           

BUF_REMOVE_ALL_NO_WRITE:

從flush list 和 LRU list 上面都删除, 資料不需要, 并且也不需要刷盤. 從LRU list 上面也都删除開銷是比較大的, 是以更多的時候是使用BUF_REMOVE_FLUSH_NO_WRITE, 隻删flush list, 不删LRU

一般來說truncate table 的時候是執行這個. 在5.6/5.7 裡面, 由于truncate table 了以後, space id 是不會變的, 那麼就必須把這些space 對應的page 都删除, 否則如果新的table 的space id 和老的space id 一緻, 那就通路到髒資料了.

BUF_REMOVE_FLUSH_NO_WRITE:

從flush list 删除删除, 并且不需要刷盤, 直接丢棄掉. 和BUF_REMOVE_ALL_NO_WRITE 相比, 把從LRU list 上面删除的操作放到了背景來做, 因為lru list 的大小是遠遠大于flush list, 删除lru list 的成本是很大的, 是以放在後來執行

一般drop table 是執行這個操作, 讓背景慢慢從lru list 裡面把要drop 的tablespace 删除

BUF_REMOVE_FLUSH_WRITE:

從flush list 上删除, 并且刷髒, 那麼就不需要從LRU list 上删除, 因為LRU list 上也是最新的

常用場景, 執行DDL 以後, DDL 隻需要確定這個DDL 産生的page 必須進行刷髒. 執行刷髒邏輯

BUF_REMOVE_NONE:

隻需要将tablespace 标記删除, 不需要清理bp, 該tablespace 對應bp 中的資料就變成 stale page, 後續會有操作将這些stale page 删除或者複用.

<那麼什麼時候會将這些 stale page 删除呢?

總共有多個場景:

  1. 在正常從bp 中讀取page 的時候, 如果讀取到的page 是 stale, 那麼通過執行 buf_page_free_stale() 将該page 進行删除操作
  2. 在從double write buffer Double_write::write_pages() 到磁盤的時候, 如果這個時候改page 的space file 已經被删除, 那麼這個時候通過 buf_page_free_stale_during_write() 進行删除
  3. 在刷髒操作buf_flush_batch()的時候, 從LRU_list 或者 flush_list 拿取page, 如果發現該page 是stale, 并且沒有io 操作在這個page 上面, 那麼通過 buf_page_free_stale() 進行删除操作
  4. 在single page flush 的時候, 同樣判斷該page 是stale, 那麼通過buf_page_free_stale() 進行删除