久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

MySQL的InnoDB IO子系統知識點有哪些

184次閱讀
沒有評論

共計 8583 個字符,預計需要花費 22 分鐘才能閱讀完成。

本篇內容介紹了“MySQL 的 InnoDB IO 子系統知識點有哪些”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓丸趣 TV 小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

基礎知識

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_func 和 os_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 刷盤,數據文件 (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_DIRECT,否則會退化成同步 IO(錯誤日志中不會有任務提示)。

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

InnoDB 中目錄的創建使用遞歸的方式(os_file_create_subdirs_if_needed 和 os_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_array 和 os_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_requests 和 cat /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。

“MySQL 的 InnoDB IO 子系統知識點有哪些”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注丸趣 TV 網站,丸趣 TV 小編將為大家輸出更多高質量的實用文章!

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-07-18發表,共計8583字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 凉城县| 米泉市| 镶黄旗| 五常市| 麻栗坡县| 南开区| 武胜县| 盐边县| 比如县| 泗水县| 吉安县| 通海县| 沂水县| 马边| 郧西县| 和林格尔县| 南召县| 乐东| 桐城市| 沙雅县| 通州市| 静宁县| 花莲县| 常熟市| 宁化县| 惠安县| 金湖县| 古交市| 牡丹江市| 仙桃市| 随州市| 淳安县| 清苑县| 迭部县| 卓尼县| 金堂县| 驻马店市| 东阿县| 清徐县| 汝阳县| 沿河|