InnoDB作爲一款成熟的跨平臺數據庫引擎,其實現了一套高效易用的IO接口,包括同步異步IO,IO合併等。本文簡單介紹一下其內部實現,主要的代碼集中在os0file.cc這個文件中。本文的分析默認基於MySQL 5.6,CentOS 6,gcc 4.8,其餘版本的信息會另行指出。mysql
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函數調用上。linux
在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這個參數比較重要,重點介紹一下:sql
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。服務器
因爲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相對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合併。os_event_wait
)。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則使用這套機制來讀寫數據文件。
若是系統安裝了libaio庫且在配置文件裏面設置了innodb_use_native_aio=on則啓動時候會使用Native aio。
os_aio_func
,在debug模式下,依然會檢查傳入的參數,一樣不會檢查文件是否以O_DIRECT模式打開,這算是一個有點風險的點,若是用戶不知道linux native aio須要使用O_DIRECT模式打開文件才能發揮出aio的優點,那麼性能就不會達到預期。建議在此處作一下檢查,有問題輸出到錯誤日誌。os_aio_array_reserve_slot
,把IO請求分配給後臺線程,分配算法也考慮了後續的IO合併,與Simulated aio同樣。不一樣之處,主要是須要用IO請求的參數初始化iocb這個結構。IO請求的相關信息除了須要初始化iocb外,也須要在全局數組的slot中記錄一份,主要是爲了在os_aio_print_XXX
系列函數中統計方便。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。