天天看點

InnoDB mutex 實作

InnoDB mutex

InnoDB 中的mutex 和 rw_lock 在早期的版本都是通過系統提供的cas, tas 語義自己進行實作, 并沒有使用pthread_mutex_t, pthread_rwlock_t, 這樣實作的好處在于便于統計, 以及為了性能考慮, 還有解決早期作業系統的一些限制.

大概是原理是:

在mutex_enter 之後, 在spin 的次數超過 innodb_sync_spin_loops=30 每次最多 innodb_spin_wait_delay=6如果還沒有拿到Mutex, 會主動yield() 這個線程, 然後wait 在自己實作的wait array 進行等待.

這裡每次spin 時候, 等待的時候執行的是ut_delay, 在ut_delay 中是執行 "pause" 指定, 當innodb_spin_wait_delay = 6 的時候, 在當年100MHz Pentium cpu, 這個時間最大是1us.

wait array 也是InnoDB 實作的一種cond_wait 的實作, 為什麼要自己實作?

早期的MySQL 需要wait array 是因為作業系統無法提供超過100000 event, 是以wait array 在使用者态去進行這些event 維護, 但是到了MySQL 5.0.30 以後, 大部分作業系統已經能夠處理100000 event, 那麼現在之是以還需要 wait array, 主要是為了統計.

在wait array 的實作裡面其實有一把大wait array mutex, 是一個pthread_mutex_t, 然後在wait array 裡面的每一個wait cell 中, 包含了os_event_t, wait 的時候調用了os_event_wait_low(), 然後在 os_event_t 裡面也包含了一個mutex, 是以在一次wait 裡面就有可能調用了兩次pthread_mutex_t 的wait.

并且在os_event_t 喚醒的機制中是直接通過pthread_cond_boradcast(), 當有大量線程等待在一個event 的時候, 會造成很多無謂的喚醒.

大緻代碼實作:

void enter(uint32_t max_spins, uint32_t max_delay, const char *filename,
             uint32_t line) UNIV_NOTHROW {
    // 在try_lock 中通過 TAS 比較是否m_lock_word = LOCKED
    // TAS(&m_lock_word, MUTEX_STATE_LOCKED) == MUTEX_STATE_UNLOCKED
    // 在InnoDB 自己實作的mutex 中, 使用m_lock_word = 0, 1, 2 分别來比較unlock,
    // lock, wait 狀态
    // 在InnoDB 自己實作的rw_lock 中, 同樣使用 m_lock_word 來标記狀态,
    // 不過rw_lock 記錄的狀态就不止lock, unlock, 需要記錄有多少read 等待,
    // 多少write 等待等待, 不過大體都一樣
    if (!try_lock()) {
      // 如果try_lock 失敗, 就進入spin 然後同時try_lock 的邏輯
      spin_and_try_lock(max_spins, max_delay, filename, line);
    }
  }

  void spin_and_try_lock(uint32_t max_spins, uint32_t max_delay,
                         const char *filename, uint32_t line) UNIV_NOTHROW {
    for (;;) {
      /* If the lock was free then try and acquire it. */

      // is_free 的邏輯很簡單, 每spin 一次, 就檢查一下這個lock 是否可以獲得,
      // 如果不可以獲得, 那麼就delay (0, max_delay] 的時間
      if (is_free(max_spins, max_delay, n_spins)) {
......
      }
        // 如果嘗試了max_spins 次, 那麼就将目前cpu 時間片讓出
      os_thread_yield();

      // 最後進入到wait 邏輯, 這個wait 是基于InnoDB 自己實作的wait array 來實作
      if (wait(filename, line, 4)) {
        n_spins += 4;

  }
           

Mark 在這邊文章中說, 現有的mutex 實作會導緻cpu 利用過高, 差不多比使用pthread mutex 高16%, 并且上下文切換也會更高

https://www.facebook.com/notes/mysql-at-facebook/green-mutexes/10151060544265933/

主要的原因是:

  1. 因為Mutex 的喚醒在os_event 裡面, os_event 實作中, 如果需要執行喚醒操作, 那麼需要執行pthread_cond_boradcast() 操作, 需要把所有等待的pthread 都喚醒, 而不是隻喚醒一個.

Innam 在底下回複: 當然隻喚醒一個也并不能完全解決問題, 如果使用 pthread_cond_signal, 那麼等待的線程就是一個一個的被喚醒, 那麼所有等待的線程執行的時間就是串行的

在目前InnoDB 的實作中, 如果使用pthread_cond_boradcase 會讓所有的線程都喚醒, 然後其中的一個線程獲得mutex, 但是其他線程并不會因為拿不到mutex馬上進入wait, 而是依然會通過spin 一段時間再進入wait,這樣就可以減少一些無謂的wait.

是以這裡官方到現在一直也都沒有改.

  1. 在wait array 的實作中, 需要有一個全局的pthread_mutex_t 保護 sync array,
  2. 在預設的配置中, innodb_spin_wait_delay=6 是ut_delay 執行1us, innodb_sync_spin_loops=30 會執行30次, 那麼每次mutex 有可能都需要spin 30us, 這個太暴力了

後來sunny 在這個文章裡面回複,

https://www.facebook.com/notes/mysql-at-facebook/green-mutexes-part-2/10151061901390933

sunny 的回複很有意思:

It is indeed an interesting problem. I think different parts of the code have different requirements. Therefore, I've designed and implemented something that allows using the mutex type that best suits the sub-system. e.g., mutexes that are held very briefly like the page mutexes can be pure spin locks, this also makes them space efficient. I've also gotten rid of the distinction between OS "fast" mutexes and InnoDB mutexes. We can use any type of mutexes in any part of the code. We can also add new mutexe types. I've also been experimenting with Futexes, implemented a mutex type for Linux which uses Futexes to schedule the next thread instead of the sync array

這裡主要核心觀點有兩個

  1. 不同場景需要的mutex 是不一樣的, 比如buffer pool 上面的page 的mutex 希望的就是一直spin. 有些mutex 其實則是希望立刻就進入等待, 隻用使用這些mutex 的使用者知道接下來哪一個政策更合适
  2. 作業系統提供了futex 可能比InnoDB 自己通過wait array 的實作方式, 對于通知機制而言會做的更好.

是以就有了這個worklog:

worklog:

https://dev.mysql.com/worklog/task/?id=6044

總結了現有的 mutex 實作存在的問題

  1. 隻有自己實作的ib_mutex_t, 并沒有支援futex 的實作
  2. 所有的ib_mutex_t 的行為都是一樣的, 通過兩個變量 innodb_spin_wait_delay(控制在Test 失敗以後, 最多會delay 的時間), innodb_sync_spin_loops(控制spin 的次數). 不可以對某一個單獨的ib_mutex_t 設定單獨的wait + loop 次數
  3. 所有的ib_mutex_t 由兩個全局的變量控制, 因為mutex 在嘗試了innodb_sync_spin_loops 次以後, 會等待在一個wait array 裡面的一個wait cell 上, 所有的wait cell 都會注冊到一個叫wait array 的隊列中進行等待, 然後等

現在在 InnoDB 8.0 的代碼中總共實作了4種mutex 的實作方式, 2種的政策

  1. TTASFutexMutex 是spin + futex 的實作, 在mutex_enter 之後, 會首先spin 然後在futex 進行wait
  2. TTASMutex 全spin 方式實作, 在spin 的次數超過 innodb_sync_spin_loops=30 每次最多 innodb_spin_wait_delay=6us 以後, 會主動yield() 這個線程, 然後通過TAS(test and set 進行判斷) 是否可以獲得
  3. OSTrackMutex, 在系統自帶的mutex 上進行封裝, 增加統計計數等等
  4. TTASEevntMutex, InnoDB 一直使用的自己實作的Mutex, 如上文所說使用spin + event 的實作.
#ifdef HAVE_IB_LINUX_FUTEX
UT_MUTEX_TYPE(TTASFutexMutex, GenericPolicy, FutexMutex)
UT_MUTEX_TYPE(TTASFutexMutex, BlockMutexPolicy, BlockFutexMutex)
#endif /* HAVE_IB_LINUX_FUTEX */

UT_MUTEX_TYPE(TTASMutex, GenericPolicy, SpinMutex)
UT_MUTEX_TYPE(TTASMutex, BlockMutexPolicy, BlockSpinMutex)

UT_MUTEX_TYPE(OSTrackMutex, GenericPolicy, SysMutex)
UT_MUTEX_TYPE(OSTrackMutex, BlockMutexPolicy, BlockSysMutex)

UT_MUTEX_TYPE(TTASEventMutex, GenericPolicy, SyncArrayMutex)
UT_MUTEX_TYPE(TTASEventMutex, BlockMutexPolicy, BlockSyncArrayMutex)           

同時在8.0 的實作中定義了兩種政策, GenericPolicy, BlockMutexPolicy. 這兩種政策主要的差別在于在show engine innodb mutex 的時候不同的統計方式.

BlockMutexPolicy 用于統計所有buffer pool 使用的mutex, 是以該Mutex 特别多, 如果每一個bp 單獨統計, 浪費大量的記憶體空間, 是以所有bp mutex 都在一起統計, 事實上buffer pool 的rw_lock 也是一樣

GenericPolicy 用于除了buffer pool mutex 以外的其他地方

使用方式

目前InnoDB 裡面都是使用 TTASEventMutex

隻不過buffer pool 的mutex 使用的是 BlockMutexPolicy, 而且他的mutex 使用的是 GenericPolicy, 不過從目前的代碼來看, 也隻是統計的差別而已

問題

但是從目前來看, 并沒有實作sunny 說的, 不同場景使用不同的mutex, Buffer pool 使用 TTASMutex 實作, 其他mutex 使用 TTASEventMutex, 并且新加入的 TTASFutexMutex, 也就是spin + futex 的實作方式其實也不是預設使用的.

主要的Lock 函數, 這裡mutex 和 rwlock 等等都一樣,

也是先通過執行 max_spins 次的嘗試, 每一次都判斷is_locked() 如果還是被locked(), 那麼進入delay max_delay 的時長. 如果最後超過了max_spins 次的嘗試, 仍然還是lock 的, 那麼才将目前的thread 讓出, 執行yield() 邏輯.

/** Spin and try to acquire the lock.
  @param[in]    max_spins    max spins
  @param[in]    max_delay    max delay per spin
  @return number spins before acquire */
  uint32_t ttas(uint32_t max_spins, uint32_t max_delay) UNIV_NOTHROW {
    uint32_t i = 0;
    const uint32_t step = max_spins;

    std::atomic_thread_fence(std::memory_order_acquire);

    do {
      while (is_locked()) {
        ut_delay(ut_rnd_interval(0, max_delay));

        ++i;

        if (i >= max_spins) {
          max_spins += step;
          os_thread_yield();

          break;
        }
      }

    } while (!try_lock());

    return (i);
  }           
https://bugs.mysql.com/bug.php?id=52806&fbclid=IwAR2AVU1eeVogINFJQEFaKqafzW4HfMjS3_tLv3H7P2DcP7JY20ndW6rLCsA http://www.tocker.ca/what-is-a-mutex-anyway.html http://mysql.taobao.org/monthly/2017/01/01/

繼續閱讀