前言
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_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。
總結
本文詳細介紹了InnoDB中IO子系統的實現以及使用需要注意的點。InnoDB日志使用同步IO,數據使用異步IO,異步IO的寫盤順序也不是先進先出的模式,這些點都需要注意。Simulate aio雖然有比較大的學習價值,但是在現代操作系統中,推薦使用Native aio。
?以上就是MySQL · 引擎特性 · InnoDB IO子系統的詳情介紹的內容,更多相關內容請關注PHP中文網(www.php.cn)!