首頁  >  文章  >  資料庫  >  MySQL · 引擎特性 · InnoDB IO子系統的詳細介紹

MySQL · 引擎特性 · InnoDB IO子系統的詳細介紹

黄舟
黄舟原創
2017-03-06 11:59:231215瀏覽

前言

InnoDB做為一款成熟的跨平台資料庫引擎,其實現了一套高效易用的IO接口,包括同步異步IO,IO合併等。本文簡單介紹一下其內部實現,主要的程式碼集中在os0file.cc這個檔案中。本文的分析預設是基於MySQL 5.6,CentOS 6,gcc 4.8,其他版本的資訊會另行指出。

基礎知識

WAL技術 : 日誌先行技術,基本上所有的資料庫,都使用了這個技術。簡單的說,就是需要寫資料塊的時候,資料庫前台線程把對應的日誌先寫(批量順序寫)到磁碟上,然後就告訴客戶端操作成功,至於真正寫資料塊的操作(離散隨機寫)則放到後台IO線程。使用了這個技術,雖然多了一個磁碟寫入操作,但由於日誌是批次順序寫,效率很高,所以客戶端很快就能得到對應。此外,如果在真正的資料區塊落盤之前,資料庫奔潰,重啟時候,資料庫可以使用日誌來做崩潰恢復,不會導致資料遺失。
資料預讀: 與資料區塊A「相鄰」的資料區塊B和C在A被讀取的時候,B和C也會有很大的機率被讀取,所以可以在讀取B的時候,提前把他們讀到記憶體中,這就是資料預讀技術。這裡說的相鄰有兩種意義,一種是物理上的相鄰,一種是邏輯上的相鄰。底層資料檔中相鄰,叫做物理上相鄰。如果資料檔案中不相鄰,但是邏輯上相鄰(id=1的資料和id=2的數據,邏輯上相鄰,但是物理上不一定相鄰,可能存在同一個檔案中不同的位置),則叫邏輯相鄰。
檔案開啟模式 : Open系統呼叫常見的模式主要三種:O_DIRECT,O_SYNC以及default模式。 O_DIRECT模式表示後續對文件的操作不使用文件系統的緩存,用戶態直接操作設備文件,繞過了內核的緩存和優化,從另一個角度來說,使用O_DIRECT模式進行寫文件,如果返回成功,數據就真的落盤了(不考慮磁碟自帶的快取),使用O_DIRECT模式進行讀取文件,每次讀取操作是真的從磁碟中讀取,不會從檔案系統的快取中讀取。 O_SYNC表示使用作業系統緩存,檔案的讀寫都經過內核,但是這個模式也保證每次寫資料後,資料一定落盤。 default模式與O_SYNC模式類似,只是寫資料後不保證資料一定落盤,資料有可能還在檔案系統中,當主機宕機,資料有可能遺失。
此外,寫入操作不僅需要修改或增加的資料落盤,還需要檔案元資訊落盤,只有兩部分都落盤了,才能保證資料不丟。 O_DIRECT模式不保證檔案元資訊落盤(但大部分檔案系統都保證,Bug #45892),因此如果不做其他操作,用O_DIRECT寫檔案後,也存在遺失的風險。 O_SYNC則保證資料和元資訊都會落盤。 default模式兩種資料都不保證。
呼叫函數fsync後,能保證資料和日誌都會落盤,因此使用O_DIRECT和default模式開啟的文件,寫完數據,需要呼叫fsync函數。
同步IO : 我們常用的read/write函數(Linux上)就是這類IO,特徵是,在函數執行的時候,呼叫者會等待函數執行完成,而且沒有訊息通知機制,因為函數回傳了,就表示操作完成了,後續直接檢查回傳值就知道操作是否成功。這類IO操作,程式設計比較簡單,在同一個執行緒就能完成所有操作,但是需要調用者等待,在資料庫系統中,比較適合急需某些資料的時候調用,例如WAL中日誌必須在返回客戶端前落盤,則進行一次同步IO操作。
非同步IO : 在資料庫中,後台刷資料塊的IO線程,基本上都使用了非同步IO。資料庫前台線程只需要把刷塊請求提交到非同步IO的佇列中即可返回做其他事情,而後台線程IO線程,則定期檢查這些提交的請求是否已經完成,如果完成再做一些後續處理工作。同時非同步IO由於常常是一批一批的請求提交,如果不同請求存取同一個檔案且偏移量連續,則可以合併成一個IO請求。例如,第一個請求讀取檔案1,偏移量100開始的200位元組數據,第二個請求讀取檔案1,偏移量300開始的100位元組數據,則這兩個請求可以合併為讀取檔案1,偏移量100開始的300位元組資料。資料預讀中的邏輯預讀也常常使用非同步IO技術。
目前Linux上的非同步IO庫,需要檔案使用O_DIRECT模式打開,且資料區塊存放的記憶體位址、檔案讀寫的偏移量和讀寫的資料量必須是檔案系統邏輯區塊大小的整數倍,檔案系統邏輯區塊大小可以使用類似sudo blockdev --getss /dev/sda5的語句查詢。如果上述三者不是檔案系統邏輯區塊大小的整數倍,則在呼叫讀寫函數時候會報錯EINVAL,但是如果檔案不使用O_DIRECT打開,則程式依然可以運行,只是退化成同步IO,阻塞在io_submit函數調用上。

InnoDB常規IO操作以及同步IO

在InnoDB中,如果系統有pread/pwrite函數(os_file_read_funcos_file_write_func),則使用它們來讀取寫,否則使用lseek+read/write方案。這個就是InnoDB同步IO。查看pread/pwrite文件可知,這兩個函數不會改變文件句柄的偏移量且線程安全,所以多線程環境下推薦使用,而lseek+read/write方案則需要自己使用互斥鎖保護,在高並發情況下,頻繁的陷入內核態,對效能有一定影響。

在InnoDB中,使用open系統呼叫開啟檔案(os_file_create_func),模式方面除了O_RDONLY(唯讀),O_RDWR(讀寫),O_CREAT(建立檔案)外,也使用了O_EXCL(保證是這個執行緒建立此檔案)和O_TRUNC(清空檔案)。預設(資料庫不設定為唯讀模式),所有檔案都以O_RDWR模式開啟。 innodb_flush_method這個參數比較重要,重點介紹一下:

  • 如果innodb_flush_method設定了O_DSYNC,日誌檔案(ib_logfileXXX)使用O_SYNC打開,因此寫完資料不需要呼叫函數fsync刷盤,資料fsync刷盤,資料檔案(ibd)使用default模式打開,因此寫完資料需要呼叫fsync刷盤。

  • 如果innodb_flush_method設定了O_DIRECT,日誌檔案(ib_logfileXXX)使用default模式打開,寫完資料需要呼叫fsync函式刷盤,資料檔(ibd)使用O_DIRECT模式打開,寫完資料需要呼叫fsync函數刷盤。

  • 如果innodb_flush_method設定了fsync或不設置,資料檔案和日誌檔案都使用default模式打開,寫完資料都需要使用fsync來刷盤。

  • 如果innodb_flush_method設定為O_DIRECT_NO_FSYNC,檔案開啟方式與O_DIRECT模式類似,差異是,資料檔案寫完後,不呼叫fsync函式來刷盤,主要針對O_DIRECT能保證檔案的元資料也落盤的檔案系統。
    InnoDB目前還不支援使用O_DIRECT模式開啟日誌文件,也不支援使用O_SYNC模式開啟資料檔。
    注意,如果使用linux native aio(詳見下一節),innodb_flush_method一定要配置成O_DI​​RECT,否則會退化成同步IO(錯誤日誌中不會有任務提示)。

InnoDB使用了檔案系統的檔案鎖定來確保只有一個程序對某個檔案進行讀寫操作(os_file_lock),使用了建議鎖定(Advisory locking ),而不是強制鎖(Mandatory locking),因為強制鎖在不少系統上有bug,包括linux。在非唯讀模式下,所有檔案開啟後,都用檔案鎖鎖住。

InnoDB中目錄的建立使用遞歸的方式(os_file_create_subdirs_if_neededos_file_create_directory)。例如,需要建立/a/b/c/這個目錄,先建立c,然後b,然後a,建立目錄呼叫mkdir函數。此外,建立目錄上層需要呼叫os_file_create_simple_func函數,而不是os_file_create_func,需要注意一下。

InnoDB也需要臨時文件,臨時文件的創建邏輯比較簡單(os_file_create_tmpfile),就是在tmp目錄下成功創建一個文件後直接使用unlink函數釋放掉句柄,這樣當進程結束後(不管是正常結束還是異常結束),這個檔案都會自動釋放。 InnoDB建立暫存文件,首先重複了server層函數mysql_tmpfile的邏輯,後續由於需要呼叫server層的函式來釋放資源,其又呼叫dup函式拷貝了一份句柄。

如果需要取得某個檔案的大小,InnoDB並不是去查檔案的元資料(stat函數),而是使用lseek(file, 0, SEEK_END)的方式取得檔案大小,這樣做的原因是防止元資訊更新延遲導致取得的檔案大小有誤。

InnoDB會預先分配一個大小給所有新建的檔案(包括資料和日誌檔案),預先分配的檔案內容全部置為零(os_file_set_size),當檔案寫滿時,再進行擴充。此外,在日誌檔案建立時,即install_db階段,會以100MB的間隔在錯誤日誌中輸出分配進度。

整體來說,常規IO操作和同步IO相對比較簡單,但是在InnoDB中,資料檔的寫入基本上都用了非同步IO。

InnoDB非同步IO

由於MySQL誕生在Linux native aio之前,所以在MySQL非同步IO的程式碼中,有兩種​​實作非同步IO的方案。
第一種是原始的Simulated aio,InnoDB在Linux native air被import進來之前以及某些不支援air的系統上,自己模擬了一條aio的機制。非同步讀寫請求提交時,僅僅把它放入一個佇列中,然後就返回,程式可以去做其他事情。後台有若干非同步io處理執行緒(innobase_read_io_threads和innobase_write_io_threads這兩個參數控制)不斷從這個佇列中取出請求,然後使用同步IO的方式完成讀寫請求以及讀寫完成後的工作。
另外一種就是Native aio。目前在linux上使用io_submit,io_getevents等函數完成(不使用glibc aio,這也是模擬的)。提交請求使用io_submit, 等待請求使用io_getevents。另外,window平台上也有自己對應的aio,這裡就不介紹了,如果使用了window的技術棧,資料庫應該會選用sqlserver。目前,其他平台(Linux和window之外)都只能使用Simulate aio。

先介紹一些通用的函數和結構,接下來分別詳細介紹Simulate alo和Linux上的Native aio。
在os0file.cc中定義了全域數組,類型為os_aio_array_t,這些數組就是Simulate aio用來快取讀寫請求的佇列,數組的每一個元素是os_aio_slot_t#類型,裡面記錄了每個IO請求的類型,檔案的fd,偏移量,需要讀取的資料量,IO請求發起的時間,IO請求是否已經完成等。另外,Linux native io中的struct iocb也在os_aio_slot_t。在數組結構os_aio_slot_t中,記錄了一些統計信息,例如有多少資料元素(os_aio_slot_t)已經被使用了,是否為空,是否為滿等。這樣的全域數組一共有5個,分別用來保存資料檔讀取非同步請求(os_aio_read_array),資料檔寫非同步請求(os_aio_write_array),日誌檔案寫非同步請求(os_aio_log_array),insert buffer寫非同步請求(os_aio_ibuf_array),資料檔案同步讀寫請求(os_aio_sync_array)。日誌檔案的資料塊寫入是同步IO,但是這裡為什麼還要給日誌寫入一個非同步請求佇列(os_aio_log_array)呢?原因是,InnoDB日誌檔案的日誌頭中,需要記錄checkpoint的訊息,目前checkpoint訊息的讀寫還是用非同步IO來實現的,因為不是很緊急。在window平台中,如果對特定檔案使用了非同步IO,就這個檔案就不能使用同步IO了,所以引入了資料檔案同步讀寫請求佇列(os_aio_sync_array)。日誌檔案不需要讀取非同步請求佇列,因為只有在做奔潰復原的時候日誌才需要被讀取,而做崩潰復原的時候,資料庫還不可用,因此完全沒必要搞成非同步讀取模式。這裡有一點要注意,不管變數innobase_read_io_threads和innobase_write_io_threads兩個參數是多少,os_aio_read_arrayos_aio_write_array都只有一個,只不過資料中的os_aio_slot_t#對應增加,在linux中,變數加1,元素數量增加256。例如,innobase_read_io_threads=4,則os_aio_read_array數組被分成了四個部分,每個部分256個元素,每個部分都有自己獨立的鎖、信號量以及統計變量,用來模擬4個線程,innobase_write_io_threads類似。從這裡我們也可以看出,每個非同步read/write執行緒能快取的讀寫請求是有上限的,即為256,如果超過這個數,後續的非同步請求需要等待。 256可以理解為InnoDB層對非同步IO並發數的控制,而在檔案系統層和磁碟層面也有長度限制,分別使用cat /sys/block/sda/queue/nr_requestscat /sys/block/sdb/queue/nr_requests查詢。
os_aio_init在InnoDB啟動的時候調用,用來初始化各種結構,包括上述的全域數組,還有Simulate aio中用的鎖和互斥量。 os_aio_free則釋放對應的結構。 os_aio_print_XXX系列的函數用來輸出aio子系統的狀態,主要用在show engine innodb status語句。

Simulate aio

Simulate aio相對Native aio來說,由於InnoDB本身實作了一套模擬機制,相對較複雜。

  • 入口函數為

    os_aio_func,在debug模式下,會校驗參數,例如資料區塊存放的記憶體位址、檔案讀寫的偏移量和讀寫的資料量是否為OS_FILE_LOG_BLOCK_SIZE的整數倍,但是沒有檢驗檔案開啟模式是否用了O_DIRECT,因為Simulate aio最後都是使用同步IO,沒有必要一定要用O_DIRECT開啟檔案。

  • 校驗通過後,就呼叫os_aio_array_reserve_slot,作用是把這個IO請求分配到某一個後台io處理線程(innobase_xxxx_io_threads分配的,但其實是在同一個全局數組中)中,並把io請求的相關資訊記錄下來,方便後台io執行緒處理。如果IO請求類型相同,請求同一個檔案且偏移量比較接近(預設情況下,偏移量差異在1M內),則InnoDB會把這兩個請求分配到同一個io執行緒中,方便在後續步驟中IO合併。

  • 提交IO請求後,需要喚醒後台io處理線程,因為如果後台執行緒偵測到沒有IO請求,會進入等待狀態(os_event_wait)。

  • 至此,函數返回,程式可以去乾其他事情了,後續的IO處理交給後台執行緒了。
    介紹一下後台IO執行緒怎麼處理的。

  • InnoDB啟動時,後台IO執行緒會被啟動(io_handler_thread)。其會呼叫os_aio_simulated_handle從全域數組中取出IO請求,然後用同步IO處理,結束後,需要做收尾工作,例如,如果是寫請求的話,則需要在buffer pool中把對應的數據頁從髒頁清單移除。

  • os_aio_simulated_handle首先需要從陣列中挑選出某個IO請求來執行,挑選演算法並不是簡單的先進先出,其挑選所有請求中offset最小的請求先處理,這樣做是為了後續的IO合併比較方便計算。但這也容易導致某些offset特別大的孤立請求長時間沒有被執行到,也就是餓死,為了解決這個問題,在挑選IO請求之前,InnoDB會先做一次遍歷,如果發現有請求是2s前推送過來的(也就是等待了2s),但是還沒有被執行,就優先執行最老的請求,防止這些請求被餓死,如果有兩個請求等待時間相同,則選擇offset小的請求。

  • os_aio_simulated_handle接下來要做的工作就是進行IO合併,例如,讀取請求1請求的是file1,offset100開始的200字節,讀取請求2請求的是file1,offset300開始的100字節,則這兩個請求可以合併為一個請求:file1,offset100開始的300字節,IO返回後,再把資料拷貝到原始請求的buffer中就可以了。寫請求也類似,在寫入操作之前先把需要寫的資料拷貝到一個臨時空間,然後一次寫完。注意,只有在offset連續的情況下IO才會合併,有間斷或重疊都不會合併,一模一樣的IO請求也不會合併,所以這裡可以算是一個可最佳化的點。

  • os_aio_simulated_handle如果發現現在沒有IO請求,就會進入等待狀態,等待被喚醒

##綜上所述,可以看出IO請求是一個一個的push的對立面,每push進一個後台線程就拿去處理,如果後台線程優先級比較高的話,IO合併效果可能比較差,為了解決這個問題,Simulate aio提供類似組提交的功能,即一組IO請求提交後,才喚醒後台線程,讓其統一進行處理,這樣IO合併的效果會比較好。但這個依然有點小問題,如果後台執行緒比較繁忙的話,其就不會進入等待狀態,也就是說只要請求進入了佇列,就會被處理。這個問題在下面的Native aio中可以解決。

整體來說,InnoDB實作的這套模擬機制還是比較安全可靠的,如果平台不支援Native aio則使用這套機制來讀寫資料檔。

Linux native aio

如果系統安裝了libaio函式庫且在設定檔裡面設定了innodb_use_native_aio=on則啟動時候會使用Native aio。

  • 入口函數依然為

    os_aio_func,在debug模式下,依然會檢查傳入的參數,同樣不會檢查檔案是否以O_DIRECT模式打開,這算是一個有點風險的點,如果使用者不知道linux native aio需要使用O_DIRECT模式開啟檔案才能發揮aio的優勢,那麼效能就不會達到預期。建議在此處做一下檢查,有問題輸出到錯誤日誌。

  • 檢查通過之後,與Simulated aio一樣,呼叫

    os_aio_array_reserve_slot,把IO請求分配給後台線程,分配演算法也考慮了後續的IO合併,與Simulated aio一樣。不同之處,主要是需要用IO請求的參數初始化iocb這個結構。 IO請求的相關資訊除了需要初始化iocb外,也需要在全域數組的slot中記錄一份,主要是為了在os_aio_print_XXX系列函數中統計方便。

  • 呼叫io_submit提交請求。

  • 至此,函數返回,程式可以去乾其他事情了,後續的IO處理交給後台執行緒了。

    接下來是後台IO線程。

  • 與Simulate aio類似,後台IO執行緒也是在InnoDB啟動時候啟動。如果是Linux native aio,後續會呼叫os_aio_linux_handle這個函數。這個函數的作用與os_aio_simulated_handle類似,但是底層實作相對比較簡單,其只呼叫io_getevents函數等待IO請求完成。超時時間為0.5s,也就是說如果即使0.5內沒有IO請求完成,函數也會返回,繼續調用io_getevents等待,當然在等待前會判斷伺服器是否處於關閉狀態,如果是則退出。

在分發IO線程時,盡量把相鄰的IO放在一個線程內,這個與Simulate aio類似,但是後續的IO合併操作,Simulate aio是自己實現,Native aio則交給核心完成了,因此程式碼比較簡單。
還要一個差別是,當沒有IO請求的時候,Simulate aio會進入等待狀態,而Native aio則會每0.5秒醒來一次,做一些檢查工作,然後繼續等待。因此,當有新的請求來時,Simulated aio需要用戶執行緒喚醒,而Native aio則不需要。此外,在伺服器關閉時,Simulate aio也需要喚醒,Native aio則不需要。

可以發現,Native aio與Simulate aio類似,請求也是一個一個提交,然後一個一個處理,這樣會導致IO合併效果比較差。 Facebook團隊提交了一個Native aio的群組提交優化:把IO請求首先緩存,等IO請求都到了之後,再調用io_submit函數,一口氣提交先前的所有請求(io_submit可以一次提交多個請求),這樣內核就比較方便做IO優化。 Simulate aio在IO線程壓力大的情況下,組提交優化會失效,而Native aio則不會。注意,群組提交優化,不能一口氣提交太多,如果超過了aio等待佇列長度,會強制發起一次io_submit。

總結

本文詳細介紹了InnoDB中IO子系統的實作以及使用需要注意的點。 InnoDB日誌使用同步IO,資料使用非同步IO,非同步IO的寫盤順序也不是先進先出的模式,這些點都需要注意。 Simulate aio雖然有比較大的學習價值,但在現代作業系統中,建議使用Native aio。

以上就是MySQL · 引擎特性 · InnoDB IO子系統的詳細介紹的內容,更多相關內容請關注PHP中文網(www.php.cn)!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn