Mysql Innodb中的Linux native非同步I/O(一) 記憶體結構的初始化

gaopengtttt發表於2017-11-24

水平有限,有誤請指出

一、前言

在5.7中Innodb非同步I/O的記憶體結構發生了一些變化特別是非同步I/O陣列和以前的結構體不同變為了類叫做AIO類但是換湯不換藥只是將一些方法進行了封裝,而非同步i/o實際的請求放到了Slot結構體它們分別對應了5.6 os_aio_array_t和os_aio_slot_t,這裡不準備詳細介紹每一個屬性的含義,因為在核心月報中淘寶已經給出,5.7基本也是一樣的連線如下:
http://mysql.taobao.org/monthly/2017/07/10/
同時我這裡也是討論關於Linux native的部分對於innodb自己模擬的非同步I/O不做分析,因為用得不多,並且自己能力也有限。但是這裡還是需要明確幾個概念


本文簡書地址
http://www.jianshu.com/p/dd7e1e560af0


二、幾個基本概念

  • 什麼是Linux native I/O
    參考我的文章:
    http://blog.itpub.net/7728585/viewspace-2147684/
    或者參考其他文章
  • MYSQL中的非同步I/O執行緒
    我以前一直搞不清楚這幾個執行緒的作用,為了搞清楚這個我才決定好好學習一下非同步I/O
    如下面的引數設定
mysql> show variables like '%io_threads%';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_read_io_threads  | 2     |
| innodb_write_io_threads | 2     |
+-------------------------+-------+ 

我在本資料中實際設定了2個read 非同步i/o執行緒和2個write非同步I/O執行緒此外都包含一個log和ibuf非同步I/O執行緒在資料庫中我們也可以查詢到這6個非同步I/O執行緒

mysql> select a.thd_id,b.THREAD_OS_ID,a.user ,a.conn_id,b.TYPE,a.source,a.program_name from sys.processlist a,performance_schema.threads b where b.thread_id=a.thd_id and  user  like '%io%';
+--------+--------------+------------------------+---------+------------+--------------------+--------------+
| thd_id | THREAD_OS_ID | user                   | conn_id | TYPE       | source             | program_name |
+--------+--------------+------------------------+---------+------------+--------------------+--------------+
|      3 |        14059 | innodb/io_ibuf_thread  |    NULL | BACKGROUND | NULL               | NULL         |
|      4 |        14060 | innodb/io_log_thread   |    NULL | BACKGROUND | sync0debug.cc:1296 | NULL         |
|      5 |        14061 | innodb/io_read_thread  |    NULL | BACKGROUND | NULL               | NULL         |
|      6 |        14062 | innodb/io_read_thread  |    NULL | BACKGROUND | NULL               | NULL         |
|      7 |        14063 | innodb/io_write_thread |    NULL | BACKGROUND | sync0debug.cc:1296 | NULL         |
|      8 |        14064 | innodb/io_write_thread |    NULL | BACKGROUND | NULL               | NULL         |
+--------+--------------+------------------------+---------+------------+--------------------+--------------+ 
  • AIO類、執行緒、Slot的關係

首先一個AIO類對應了一個型別的非同步,比如ibuf/log/read/write都對應一個AIO類,並且在類的最後用一個類的靜態全域性成員進行指向如下:

 /** Insert buffer */
   static AIO*     s_ibuf;
   /** Redo log */
   static AIO*     s_log;
   /** Reads */
   static AIO*     s_reads;
   /** Writes */
   static AIO*     s_writes; 

而我們的非同步I/O執行緒實際上有6個也就是s_reads包含了2個執行緒/s_writes包含了2個執行緒,那麼執行緒引入了一個叫做local segment的概念,實際上每一個執行緒對應了一個local segment,而在AIO下面掛的就是一個Slot的vertor陣列,陣列的大小和每種型別的執行緒個數(local segment)和每個執行緒最大的Slot有關,看原始碼中對最大的Slot的定義如下:

8 * OS_AIO_N_PENDING_IOS_PER_THREAD 

其中巨集定義OS_AIO_N_PENDING_IOS_PER_THREAD=32

那麼對於s_ibuf和s_log因為只有一個執行緒(local segment)那麼就有256個Slot,而s_reads和s_writes當前我的資料庫各有2個執行緒(local segment)那麼就有2*256=512個Slot.

  • global segment
    這個概念主要和模擬的非同步I/O有關,如果我當前有6個非同步I/O執行緒那麼global segment就是6,因為在進行初始化呼叫AIO::start的時候其編號總是固定的及0和1對應瞭然後是read和write執行緒個數,那麼由global segment到local segment的換算也變得簡單了可以參考AIO::get_segment_no_from_slot.

如果沒有顯示指定本文所有segment均指local segment



三、記憶體結構的初始化

整個記憶體結構的初始化是從由innobase_start_or_create_for_mysql呼叫的下面程式碼開始的如下:

點選(此處)摺疊或開啟

  1. if (!os_aio_init(srv_n_read_io_threads,
  2.             srv_n_write_io_threads,
  3.             SRV_MAX_N_PENDING_SYNC_IOS)) {

  4.        ib::error() << "Cannot initialize AIO sub-system";

  5.        return(srv_init_abort(DB_ERROR));
  6.    }
實際上就是呼叫了os_aio_init,接下來我們來進行逐層的分析

1、os_aio_init 由innobase_start_or_create_for_mysql ()調入

本資料庫呼叫棧幀:


點選(此處)摺疊或開啟

  1. #0 os_aio_init (n_readers=2, n_writers=2, n_slots_sync=100) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6734
  2. #1 0x0000000001b8dde1 in innobase_start_or_create_for_mysql () at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/srv/srv0start.cc:1792
原始碼及註釋如下:

點選(此處)摺疊或開啟

  1. bool
  2. os_aio_init(
  3.    ulint n_readers,
  4.    ulint n_writers,
  5.    ulint n_slots_sync)
  6. {
  7.    /* Maximum number of pending aio operations allowed per segment */
  8.    ulint limit = 8 * OS_AIO_N_PENDING_IOS_PER_THREAD; //這裡我們發現了limit的定義也就是
  9.                                                         //一個執行緒(local segments)包含的slot個數及256
  10. .....
  11.    return(AIO::start(limit, n_readers, n_writers, n_slots_sync));
  12. }

這個函式的主要功能就是呼叫AIO::start下面我們進行學習。

2、 AIO::start 由os_aio_init調入

本資料庫呼叫棧幀:


點選(此處)摺疊或開啟

  1. #1 0x0000000001a7db18 in AIO::start (n_per_seg=256, n_readers=2, n_writers=2, n_slots_sync=100)
  2.    at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6610
  3. #2 0x0000000001a7e289 in os_aio_init (n_readers=2, n_writers=2, n_slots_sync=100) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6762
原始碼及註釋如下:

點選(此處)摺疊或開啟

  1. bool
  2. AIO::start(
  3.    ulint n_per_seg, //每個segment的slot數量
  4.    ulint n_readers, //多少個非同步讀I/O執行緒
  5.    ulint n_writers, //多少個非同步寫I/O執行緒
  6.    ulint n_slots_sync)
  7. {
  8. #if defined(LINUX_NATIVE_AIO)
  9.    /* Check if native aio is supported on this system and tmpfs */
  10.    //這裡根據引數innodb_use_native_aio設定和是否支援native aio測試進行綜合判斷,即便
  11.    //引數設定為ON,但是不支援libaio也會將srv_use_native_aio設定為FLASE
  12.    if (srv_use_native_aio && !is_linux_native_aio_supported()) {

  13.        ib::warn() << "Linux Native AIO disabled.";//這裡出現一個經常看到警告資訊native aio不可用

  14.        srv_use_native_aio = FALSE;
  15.        
  16.    }
  17. #endif /* LINUX_NATIVE_AIO */

  18.    srv_reset_io_thread_op_info(); /* 重置執行緒狀態設定為not start */

  19.  /* 這裡開始初始化AIO read執行緒 我這裡設定是 nreaders=2個read執行緒 n_per_seg=每個執行緒 256個slot */
  20.    s_reads = create(
  21.        LATCH_ID_OS_AIO_READ_MUTEX, n_readers * n_per_seg, n_readers);

  22.    if (s_reads == NULL) {
  23.        return(false);
  24.    }
  25.  
  26.  /* 這裡進行只讀檢測 如果是隻讀則log 和 ibuf 非同步執行緒不啟用
  27.     由邏輯srv_read_only_mode ? 0 : 2 進行控制
  28.     下面主要開始設定執行緒的名字如果沒有log和ibuf則
  29.     readers從下標0開始否則從下標2開始 */
  30.  Array of English strings describing the current state of an i/o handler thread */
  31. const char* srv_io_thread_op_info[SRV_MAX_N_IO_THREADS];
  32. const char* srv_io_thread_function[SRV_MAX_N_IO_THREADS];
  33.     */
  34.    ulint start = srv_read_only_mode ? 0 : 2;
  35.    ulint n_segs = n_readers + start;
  36.    
  37.    /* 0 is the ibuf segment and 1 is the redo log segment. */
  38.    for (ulint i = start; i < n_segs; ++i) {
  39.        ut_a(i < SRV_MAX_N_IO_THREADS);
  40.        srv_io_thread_function[i] = "read thread";
  41.    }

  42.    ulint n_segments = n_readers;


  43. /* 如果沒有設定只讀,這裡開始不會初始化ibuf和log aio結構 */

  44.    if (!srv_read_only_mode) {

  45.        s_ibuf = create(LATCH_ID_OS_AIO_IBUF_MUTEX, n_per_seg, 1);

  46.        if (s_ibuf == NULL) {
  47.            return(false);
  48.        }

  49.        ++n_segments;

  50.        srv_io_thread_function[0] = "insert buffer thread";

  51.        s_log = create(LATCH_ID_OS_AIO_LOG_MUTEX, n_per_seg, 1);

  52.        if (s_log == NULL) {
  53.            return(false);
  54.        }

  55.        ++n_segments;

  56.        srv_io_thread_function[1] = "log thread";

  57.    } else {
  58.        s_ibuf = s_log = NULL;
  59.    }

  60. /* 依然是一樣的方式初始化write thread AIO結構 */

  61.    s_writes = create(
  62.        LATCH_ID_OS_AIO_WRITE_MUTEX, n_writers * n_per_seg, n_writers);

  63.    if (s_writes == NULL) {
  64.        return(false);
  65.    }

  66.    n_segments += n_writers; //這裡我們得到最終的GLOBAL SEGMENT = 2+1+1+2 = 6

  67.    for (ulint i = start + n_readers; i < n_segments; ++i) {
  68.        ut_a(i < SRV_MAX_N_IO_THREADS);
  69.        srv_io_thread_function[i] = "write thread";
  70.    }

  71.    ut_ad(n_segments >= static_cast<ulint>(srv_read_only_mode ? 2 : 4));

  72.    s_sync = create(LATCH_ID_OS_AIO_SYNC_MUTEX, n_slots_sync, 1);/*這個執行緒功能還需要看看n_slots_sync=100 */

  73.    if (s_sync == NULL) {

  74.        return(false);
  75.    }

  76.    os_aio_n_segments = n_segments; //=6

  77.    os_aio_validate();
  78.  //開始分配event,他是cond和mutex的封裝
  79.    os_aio_segment_wait_events = static_cast<os_event_t*>(
  80.        ut_zalloc_nokey(
  81.            n_segments * sizeof *os_aio_segment_wait_events)); //這裡分配n_segments 個數的條件變數記憶體這裡是6個
  82. /*
  83.   Array of events used in simulated AIO
  84. static os_event_t* os_aio_segment_wait_events = NULL;
  85.  最後指標給了這樣一個內部全域性靜態變數
  86. */


  87.    if (os_aio_segment_wait_events == NULL) {

  88.        return(false);
  89.    }

  90. /*
  91.    對其進行初始化這裡我們明確的看出每一個SEGMENTS 也就是每一個執行緒都對應一個條件變數和MUTEX
  92.    他的具體作用和模擬非同步I/O有關如AIO::wake_simulated_handler_thread呼叫
  93. */
  94.    for (ulint i = 0; i < n_segments; ++i) {
  95.        os_aio_segment_wait_events[i] = os_event_create(0);
  96.    }

  97.    os_last_printout = ut_time();

  98.    return(true);
  99. }

這裡我們可以看到實際上還是呼叫核心函式AIO::create,在AIO::create呼叫成果後就將各個型別的AIO物件的指標賦予給了幾個類靜態全域性指標用於後面呼叫

3、AIO::create 由AIO::start 調入

本資料庫呼叫棧幀:

點選(此處)摺疊或開啟

  1. #0 AIO::create (id=LATCH_ID_OS_AIO_READ_MUTEX, n=512, n_segments=2) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6529
  2. #1 0x0000000001a7db18 in AIO::start (n_per_seg=256, n_readers=2, n_writers=2, n_slots_sync=100) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6610
原始碼及註釋如下:

點選(此處)摺疊或開啟

  1. AIO*
  2. AIO::create(
  3.    latch_id_t id,
  4.    ulint n, //某個型別AIO物件應該包含的SLOT數量
  5.    ulint n_segments) //執行緒數量(segment)
  6. {
  7.    if ((n % n_segments)) { //這裡先做了一個保障校驗n是否是n_segments的倍數

  8.        ib::error()
  9.            << "Maximum number of AIO operations must be "
  10.            << "divisible by number of segments";

  11.        return(NULL);
  12.    }
  13.    AIO* array = UT_NEW_NOKEY(AIO(id, n, n_segments)); //功能1、呼叫建構函式AIO(id, n, n_segments)
  14.    if (array != NULL && array->init() != DB_SUCCESS) { //功能2、呼叫array->init()

  15.        UT_DELETE(array);

  16.        array = NULL;
  17.    }
  18.    return(array);
  19. }

我們發現本函式有2個主要功能

  • 1、呼叫建構函式AIO(id, n, n_segments)
  • 2、呼叫array->init()
    所以我們需要分別討論

首先來看AIO(id, n, n_segments)

4、AIO::AIO 由AIO::create 調入


本資料庫呼叫棧幀:

點選(此處)摺疊或開啟

  1. #0 AIO::AIO (this=0x32ea658, id=LATCH_ID_OS_AIO_READ_MUTEX, n=512, segments=2) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6396
  2. #1 0x0000000001a7d862 in AIO::create (id=LATCH_ID_OS_AIO_READ_MUTEX, n=512, n_segments=2)
  3.    at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6538

原始碼及註釋如下:

點選(此處)摺疊或開啟

  1. AIO::AIO(
  2.    latch_id_t id,
  3.    ulint n, //某個型別AIO物件應該包含的SLOT數量
  4.    ulint segments)//執行緒數量(segment)
  5.    :
  6.    m_slots(n), //定義出多少個slot這裡是512個因為我有2個io_read執行緒每個執行緒256個slot,分配記憶體
  7.    m_n_segments(segments), //多少個segments 我是2個io_read也就是2個
  8.    m_n_reserved(),//設定為0
  9.    m_aio_ctx(),//設定為NULL
  10.    m_events(m_slots.size()) //完成events陣列大小設定為slot的個數
  11. {
  12. ....
  13.    mutex_create(id, &m_mutex); //根據傳入的ID建立mutex 本MUTEX 保護多個執行緒同時使用本陣列

  14.    m_not_full = os_event_create("aio_not_full");//建立所謂的event,在這個event中封裝了條件變數cond和mutex
  15.    m_is_empty = os_event_create("aio_is_empty");//建立所謂的event,在這個event中封裝了條件變數cond和mutex

  16.    memset(&m_slots[0], 0x0, sizeof(m_slots[0]) * m_slots.size());//將整個slot記憶體空間全部清0
  17. #ifdef LINUX_NATIVE_AIO
  18.    memset(&m_events[0], 0x0, sizeof(m_events[0]) * m_events.size());//將整個events記憶體空間全部清0,他就是io_getevents呼叫需要的
  19. #endif /* LINUX_NATIVE_AIO */

  20.    os_event_set(m_is_empty); //通過brocast喚醒,所有堵塞在m_is_empty上的執行緒進行處理
  21. }

經過本函式我們發現在AIO這個結構體中的成員基本都進行了初始化
m_slots/m_n_segments/m_n_reserved/m_aio_ctx/m_events/m_mutex/m_not_full/m_is_empty
只是這些某些還沒有意義比如m_aio_ctx

接下來我們看第二個功能 AIO::init()

5、AIO::init() 由AIO::create調入

本資料庫呼叫棧幀:

點選(此處)摺疊或開啟

  1. #0 AIO::init (this=0x32ea658) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6493
  2. #1 0x0000000001a7d8a2 in AIO::create (id=LATCH_ID_OS_AIO_READ_MUTEX, n=512, n_segments=2)
  3.    at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6540

原始碼及註釋如下:

點選(此處)摺疊或開啟

  1. /** Initialise the array */
  2. dberr_t
  3. AIO::init()
  4. {
  5.    ut_a(!m_slots.empty());//這裡斷言是否為空,不可能為空除非遇到故障

  6. #ifdef _WIN32
  7.    ut_a(m_handles == NULL);

  8.    m_handles = UT_NEW_NOKEY(Handles(m_slots.size()));
  9. #endif /* _WIN32 */

  10.    if (srv_use_native_aio) { //這個並非引數設定而是前面說的引數設定和innodb檢測是否支援native aio的綜合考慮
  11. #ifdef LINUX_NATIVE_AIO
  12.        dberr_t err = init_linux_native_aio();//功能1 如果開啟了innodb_use_native_aio引數並且支援native aio進行呼叫init_linux_native_aio()

  13.        if (err != DB_SUCCESS) {
  14.            return(err);
  15.        }

  16. #endif /* LINUX_NATIVE_AIO */
  17.    }

  18.    return(init_slots()); //功能2 呼叫init_slots()初始化slot記憶體結構

我們發現本函式有2個主要功能

  • 1、如果開啟了innodb_use_native_aio引數並且支援native aio進行呼叫init_linux_native_aio()
  • 2、呼叫init_slots()初始化slot記憶體結構

下面我們先看看AIO::init_linux_native_aio()呼叫

6、AIO::init_linux_native_aio() 由 AIO::init調入

本資料庫呼叫棧幀:

點選(此處)摺疊或開啟

  1. #0 AIO::init_linux_native_aio (this=0x32ea658) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6460
  2. #1 0x0000000001a7d701 in AIO::init (this=0x32ea658) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6503
原始碼及註釋如下:

點選(此處)摺疊或開啟

  1. /** Initialise the Linux Native AIO interface */
  2. dberr_t
  3. AIO::init_linux_native_aio()
  4. {
  5.    /* Initialize the io_context array. One io_context
  6.    per segment in the array. */

  7.    ut_a(m_aio_ctx == NULL);

  8.    m_aio_ctx = static_cast<io_context**>(
  9.        ut_zalloc_nokey(m_n_segments * sizeof(*m_aio_ctx)));//到這裡了我們知道AIO類的屬性已經進行了初始化m_n_segments就是本AIO物件包含的執行緒數量
  10.                                                            //ibuf和redo非同步為1個,write和read非同步執行緒本系統為2個這由引數控制,這裡為io_context_t
  11.                                                            //也就是一個執行緒對應一個io_context_t這是Linux native aio必須的

  12.    if (m_aio_ctx == NULL) {
  13.        return(DB_OUT_OF_MEMORY);
  14.    }

  15.    io_context** ctx = m_aio_ctx;
  16.    ulint max_events = slots_per_segment(); //這裡返回每個執行緒最大的event個數用於初始化io_context_t結構體就是return(m_slots.size() / m_n_segments)
  17.                                                        //及256個

  18.    for (ulint i = 0; i < m_n_segments; ++i, ++ctx) { //進行初始化對每個執行緒的io_context_t呼叫io_steup進行初始化其佇列最大event個數為
  19.                                                        //return(m_slots.size() / m_n_segments); 也就是256

  20.        if (!linux_create_io_ctx(max_events, ctx)) { //linux_create_io_ctx 主要功能就是初始化io_context_t
  21.            /* If something bad happened during aio setup
  22.            we should call it a day and return right away.
  23.            We don't care about any leaks because a failure
  24.            to initialize the io subsystem means that the
  25.            server (or atleast the innodb storage engine)
  26.            is not going to startup. */
  27.            return(DB_IO_ERROR);
  28.        }
  29.    }

  30.    return(DB_SUCCESS); //最後返回成功,這樣io_context_t也就是AIO結構體中的m_aio_ctx得到了初始化
  31. }

到這裡io_context_t已經分配,接下來呼叫AIO::init_slots()我們進行分析


7、AIO::init_slots() 由 AIO::init調入

本資料庫呼叫棧幀:


點選(此處)摺疊或開啟

  1. #0 AIO::init_slots (this=0x32ea658) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6421
  2. #1 0x0000000001a7d71b in AIO::init (this=0x32ea658) at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/os/os0file.cc:6512

原始碼及註釋如下:


點選(此處)摺疊或開啟

  1. /** Initialise the slots */
  2. dberr_t
  3. AIO::init_slots()
  4. {
  5.    for (ulint i = 0; i < m_slots.size(); ++i) {
  6.        Slot& slot = m_slots[i]; //使用引用指向第i個元素

  7.        slot.pos = static_cast<uint16_t>(i); //分別初始化為0到m_slots.size()也就是每個slot進行了編號編號為pos

  8.        slot.is_reserved = false; //初始化

  9. #ifdef WIN_ASYNC_IO //下面是WINDOW的處理不分析

  10.        slot.handle = CreateEvent(NULL, TRUE, FALSE, NULL);

  11.        OVERLAPPED* over = &slot.control;

  12.        over->hEvent = slot.handle;

  13.        (*m_handles)[i] = over->hEvent;

  14. #elif defined(LINUX_NATIVE_AIO)

  15.        slot.ret = 0; //ret設定0

  16.        slot.n_bytes = 0; //n_bytes設定為0

  17.        memset(&slot.control, 0x0, sizeof(slot.control)); //這裡對iocb結構進行清0操作,其實在AIO::AIO中已經清0了因為iocb並不是指標而是實際的記憶體空間

  18. #endif /* WIN_ASYNC_IO */
  19.    }

  20.    return(DB_SUCCESS);
  21. }


到這裡Linux native 非同步I/O的iocb已經分配個數為256*segment 並且初始化0完成。

四、初始化完成後記憶體圖

如下一張簡圖表示了初始化完成後的記憶體圖:


123.png


五、後記

初始化完成後接下來就是如何呼叫了,這個還需要仔細的分析。再開一篇文章進行分析。
那麼最後寫一下如果要用Linux native AIO 需要滿足的條件

  • 1、innodb_use_native_aio = ON
  • 2、libaio安裝了
  • 3、innodb_flush_method O_DIRECT

缺一不可,這些條件很容易達到

作者微信:


微信.jpg





來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/7728585/viewspace-2147756/,如需轉載,請註明出處,否則將追究法律責任。

相關文章