【4.分佈式存儲】-mysql及proxy

概述

文章https://segmentfault.com/a/11... 中介紹了存儲應該考慮的方向。本文詳細介紹其中的mysq,主要是INNODB。總體架構,啓動流程,一條語句的執行過程帶你快速深刻mysql源碼。再從性能(緩存,數據結構),功能(ACID實現,索引)如何實現介紹了mysql中核心點。第二部分爲分佈式,介紹原生mysql的同步過程。第三部分是proxy,由於proxy多數會自研,只介紹proxy應該包含的功能。mysql

關鍵詞:innodb,MVCC,ACID實現,索引,主從同步,proxy

第一章 總體架構/流程

clipboard.png
mysql爲單進程多線程,由於元數據用Innodb保存,啓動後除了mysql的處理鏈接請求/超時等還會啓動Innodb的全部線程。
主流程:linux

主函數在sql/amin.cc中
調用Mysqld.cc中mysqld_main

 1. 首先載入日誌,信號註冊,plugin_register
    (mysql是插件式存儲引擎設計,innodb,myisam等都是插件,在這裏註冊),核心爲mysqld_socket_acceptor->connection_event_loop();

 2. 監聽處理循環poll。
    process_new_connection處理handler有三種:線程池方式只用於商業,一個線程處理全部請求,一個鏈接一個線程(大多數選擇Per_thread_connection_handler)。
 3. 若thread_cache中有空閒直接獲取,不然建立新的用戶線程。進入用戶線程的handle_connection
    3.1 mysql網絡通訊一共有這幾層:`THD` | Protocol | NET | VIO | SOCKET,protocol對數據的協議格式化,NET封裝了net buf讀寫刷到網絡的操做,VIO是對全部鏈接類型網絡操做的一層封裝(TCP/IP, Socket, Name Pipe, SSL, SHARED MEMORY),handle_connection初始化THD(線程),開始do_command   (關於THD,有個很好的圖:http://mysql.taobao.org/monthly/2016/07/04/)
    3.2.do_command=>dispatch_comand=>mysql_parse=》【檢查query_cache有緩存直接返回不然=》】parse_sql=》mysql_execute_cmd判斷insert等調用mysql_insert,每條記錄調用write_record,這個是各個引擎的基類,根據操做表類型調用引擎層的函數=》寫binlog日誌=》提交/回滾。注意你們可能都覺得是有query_cache的。可是從8.0開啓廢棄了query_cache。第二正會講一下
 4. 除了用戶線程和主線程,在啓動時,還建立了timer_notify線程。因爲爲了解決DDL沒法作到atomic等,從MySQL8.0開始取消了FRM文件及其餘server層的元數據文件(frm, par, trn, trg, isl,db.opt),全部的元數據都用InnoDB引擎進行存儲, 另一些諸如權限表之類的系統表也改用InnoDB引擎。所以在加載這些表時,建立了innodb用到的一系列線程。

從插入流程開始

總體流程圖以下:
clipboard.pnggit

必須有這些步驟的緣由:
clipboard.png
[1]爲了快,全部數據先寫入內存,再刷髒
[2]爲了防止數據頁寫過程當中崩潰數據的持久性=》先寫redo保證重啓後能夠恢復。日誌寫不成功不操做,日誌是順序寫,內容少,能夠同步等。(最好是物理重作)。
[3]異常回滾=》物理回滾反解複雜,須要一個邏輯日誌。
基於undo log又實現了MVCC
unlog等也要保證操做持久化原子化。
[4]爲了刪除不每次整理頁,只標記,爲了真正刪除/undo不須要的清除=》purge
[5]flush對一個pageid屢次操做合併在一塊兒減小隨機操做=》二級索引非惟一change buff
[6]Flush過程當中一個頁部分寫成功就崩潰,沒法正確後恢復=》二次寫
[7]爲完整的主鏈路。
[8]爲異步的刷盤鏈路github

詳細步驟:算法

  1. 外層 handle_connection=>do_commannd=>dispatch_command=>mysql_parse=>mysql_execute_commannd=>sql_cmd_dml::execute=> execute_inner while{對每條記錄執行write_record} =>ha_write_row【返回到這裏不出錯記錄binlog】,調用引擎table->file->ha_write_row(table->record[0])
  2. 引擎層:
    row_insert_for_mysql_using_ins_graph開始,有開啓事務的操做,trx_start_low。
    首先,須要分配回滾段,由於會修改數據,就須要找地方把老版本的數據給記錄下來,其次,須要經過全局事務id產生器產生一個事務id,最後,把讀寫事務加入到全局讀寫事務鏈表(trx_sys->rw_trx_list),把事務id加入到活躍讀寫事務數組中(trx_sys->descriptors)
    在InnoDB看來全部的事務在啓動時候都是隻讀狀態,只有接受到修改數據的SQL後(InnoDB接收到才行。由於在start transaction read only模式下,DML/DDL都被Serve層擋掉了)才調用trx_set_rw_mode函數把只讀事務提高爲讀寫事務。
    以後開始事務內部處理。圖中所示,細節不少,先不寫了。

第二章 性能

磁盤,B+樹

  • 準確的B+比B爲什麼更適合?
    區別兩點,一個是B樹搜索是能夠止於非頁節點的,包含數據(包含數據在磁盤中頁的位置),且數據只出如今樹中一次。另外一點是葉子節點有雙向鏈表。第一點使得節點能夠包含更多路(由於不存數據在磁盤中頁的位置,只包含下一層的指針頁位置,B樹這兩個都要包含),層高會更少;只能到頁節點搜索結束,性能穩定。第二點爲了掃描和範圍索引。

    clipboard.png

    clipboard.png

內存buffer

全部數據頁。都走這套。包括undo等sql

name desc
buf_pool_t::page_hash page_hash用於存儲已經或正在讀入內存的page。根據<space_id, page_no>快速查找。當不在page hash時,纔會去嘗試從文件讀取
buf_pool_t::LRU LRU上維持了全部從磁盤讀入的數據頁,該LRU上又在鏈表尾部開始大約3/8處將鏈表劃分爲兩部分,新讀入的page被加入到這個位置;當咱們設置了innodb_old_blocks_time,若兩次訪問page的時間超過該閥值,則將其挪動到LRU頭部;這就避免了相似一次性的全表掃描操做致使buffer pool污染
buf_pool_t::free 存儲了當前空閒可分配的block
buf_pool_t::flush_list 存儲了被修改過的page,根據oldest_modification(即載入內存後第一次修改該page時的Redo LSN)排序
buf_pool_t::flush_rbt 在崩潰恢復階段在flush list上創建的紅黑數,用於將apply redo後的page快速的插入到flush list上,以保證其有序
buf_pool_t::unzip_LRU 壓縮表上解壓後的page被存儲到unzip_LRU。 buf_block_t::frame存儲解壓後的數據,buf_block_t::page->zip.data指向原始壓縮數據。
buf_pool_t::zip_free[BUF_BUDDY_SIZES_MAX] 用於管理壓縮頁產生的空閒碎片page。壓縮頁佔用的內存採用buddy allocator算法進行分配。

page_hash查找。
LRU只是用於淘汰。一份block。指針保存在hash和lru上(全部的數據頁)
flush_list 修改過的block被加到flush_list上,
unzip_LRU 解壓的數據頁被放到unzip_LRU鏈表上。數據庫

  • cache過程:
    當一個線程請求page時,首先根據space id 和page no找到對應的buffer pool instance。而後查詢page hash。若是看到page hash中已經有對應的block了,說明page已經或正在被讀入buffer pool,若是io_fix爲BUF_IO_READ,說明正在進行IO,就經過加X鎖的方式作一次sync(buf_wait_for_read),確保IO完成。

    若是沒有則表示須要從磁盤讀取。在讀盤前首先咱們須要爲即將讀入內存的數據頁分配一個空閒的block。當free list上存在空閒的block時,能夠直接從free list上摘取;若是沒有,就須要從unzip_lru 或者 lru上驅逐page。先unzip lru。再lru是否有可替換page,直接釋放,不然多是髒頁多,再線程在LRU上作髒頁刷新。後臺線程也會按期作髒頁刷新。segmentfault

    一個流程對buffer的操做步驟:
    clipboard.png數組

內存整體流程緩存

clipboard.png

索引:聚簇索引

見上B+樹

二級索引 change buffer

對非惟一二級索引頁,delete_mark,delete,insert順序插入緩衝區,合併減小隨機IO。

  • 物理:ibdata第4個page B+ Tree(key:spaceid,offset,counter)
  • 內存:ibuf,B+樹
  • 內容: space,offset,發生change的數據
  • 寫入:
    1 不會致使空page:delete時只有一條記錄 拒絕
    2 不會致使分裂,insert時檢查IBUF BITMAP標識剩餘空間大小,超出觸發merge 拒絕
  • merge:(在不少狀況都須要把ibuf裏的頁進行合併)
    1.輔助索引頁被讀取到緩衝池時
    2.插入時預估page no空間不足
    3.ibuf空間不足
    4.插入ibuf可能產生ibuf Tree的索引分裂
    5.Master (IDLE ,ACTIVE,SHUTDOWN)
    ……
  • Purge操做和insert在ibuf併發問題
    在purge模式下,用ibuf同時將watch插入到hash table中,若是都在內存裏,會給同一份page加鎖,沒問題,可是要兩個線程都寫入ibuf_insert時,是沒辦法控制順序的(原本就容許這種無序,由於非惟一)。因此須要一個進入後,另外一個就放棄,不能都寫入ibuf。
    在purge模式下,用ibuf同時將watch插入到hash table中,insert就不會再放入ibuf中了
    其餘讀取清除這個buf.

第三章 功能——事務

  • A undolog
  • C(一個事務中間狀態可見性) MVCC
  • I (多個事物之間可見性/操做不干擾) MVCC
  • D redolog

undolog

  • 物理:回滾段,rseg0在ibdata第6個page,1~32臨時表空間,33~128獨立表空間或ibdata,存儲在ibdata,臨時表空間或單獨表空間。每一個表空間能夠包含若干個段。每一個段有1024個控制頁slot和歷史表。每一個slot對應一個undo log對象,有一個undo log header.
  • 內存:全局trx_sys->rseg_array。每一個事務trx->rsegs
  • 內容: 邏輯日誌
    Insert undo日誌記錄插入的惟一鍵值的len和value。
    Update undo日誌在insert undo基礎上,同時記錄了舊記錄事務id,以及被更新字段的舊數據
  • 寫入

    入口函數:btr_cur_ins_lock_and_undo
     a) 從chached_list或分配一個空閒slot建立undo頁
     b) 順序寫undo log header和記錄
     c) 在事務提交階段,加入到history list或釋放【見事務提交】
     Undo log的寫入在一個單獨的mtr中,受redo log的保護,先講一個子事務mtr。Mtr是InnoDB對物理數據文件  操做的最小原子單元,保證持久性,用於管理對Page加鎖、修改、釋放、以及日誌提交到公共buffer等工做。
    開啓時初始化m_impl,好比mlog用於存儲redo log記錄
    提交時須要將本地產生的日誌拷貝到公共緩衝區,將修改的髒頁放到flush list上。
  • 回滾:
    入口函數:row_indo_step
    解析老版本記錄,作逆向操做
  • 事務提交時undolog
    1.入口函數:trx_commit_low-->trx_write_serialisation_history
    2.事務提交總體流程(寫完redo就能夠提交了。)
    clipboard.png
    生成事務no。若是有update類的undo日誌 。加入到purge_queue(清理垃圾),history鏈表(維護歷史版本)
    子事務提交。Redo log寫到公共緩存
    釋放MVCC的readview;insert的undo日誌釋放(可cache重用,不然所有釋放包括page頁)
    刷日誌

    3.在該函數中,須要將該事務包含的Undo都設置爲完成狀態,先設置insert undo,再設置update undo(trx_undo_set_state_at_finish),完成狀態包含三種:

    若是當前的undo log只佔一個page,且佔用的header page大小使用不足其3/4時(TRX_UNDO_PAGE_REUSE_LIMIT),則狀態設置爲TRX_UNDO_CACHED,該undo對象會隨後加入到undo cache list上;
       若是是Insert_undo(undo類型爲TRX_UNDO_INSERT),則狀態設置爲TRX_UNDO_TO_FREE;
       若是不知足a和b,則代表該undo可能須要Purge線程去執行清理操做,狀態設置爲TRX_UNDO_TO_PURGE。

    對於undate undo須要調用trx_undo_update_cleanup進行清理操做。
    注意上面只清理了update_undo,insert_undo直到事務釋放記錄鎖、從讀寫事務鏈表清除、以及關閉read view後才進行,
    這裏的slot,undo page ,history關係:
    每一個rseg控制頁有1024個slot和history。undo page釋放後或者移到history list後,就能夠把slot清空、undo page轉爲cache不釋放則不動slot

  • purge:刪除(更新數據的真正刪除),清除過時undo。入口函數srv_do_purge
    做用: 對於用戶刪除的數據,InnoDB並非馬上刪除,而是標記一下,後臺線程批量的真正刪除。相似的還有InnoDB的二級索引的更新操做,不是直接對索引進行更新,而是標記一下,而後產生一條新的。這個線程就是後臺的Purge線程。此外,清除過時的undo,histroy list,指的是undo不須要被用來構建以前的版本,也不須要用來回滾事務。
    咱們先來分析一下Purge Coordinator的流程。啓動線程後,會進入一個大的循環,循環的終止條件是數據庫關閉。在循環內部,首先是自適應的sleep,而後纔會進入核心Purge邏輯。sleep時間與全局歷史鏈表有關係,若是歷史鏈表沒有增加,且總數小於5000,則進入sleep,等待事務提交的時候被喚醒(srv_purge_coordinator_suspend)。退出循環後,也就是數據庫進入關閉的流程,這個時候就須要依據參數innodb_fast_shutdown來肯定在關閉前是否須要把全部記錄給清除。接下來,介紹一下核心Purge邏輯。 srv_do_purge

    clipboard.png

    1)首先依據當前的系統負載來肯定須要使用的Purge線程數(srv_do_purge),即若是壓力小,只用一個Purge Cooridinator線程就能夠了。若是壓力大,就多喚醒幾個線程一塊兒作清理記錄的操做。若是全局歷史鏈表在增長,或者全局歷史鏈表已經超過innodb_max_purge_lag,則認爲壓力大,須要增長處理的線程數。若是數據庫處於不活躍狀態(srv_check_activity),則減小處理的線程數。
    2)若是歷史鏈表很長,超過innodb_max_purge_lag,則須要從新計算delay時間(不超過innodb_max_purge_lag_delay)。若是計算結果大於0,則在後續的DML中須要先sleep,保證不會太快產生undo(row_mysql_delay_if_needed)。
    3)從全局視圖鏈表中,克隆最老的readview(快照、拿視圖爲了拿事務id.undo日誌中upadte記了事務id),全部在這個readview開啓以前提交的事務所產生的undo都被認爲是能夠清理的。克隆以後,還須要把最老視圖的建立者的id加入到view->descriptors中,由於這個事務修改產生的undo,暫時還不能刪除(read_view_purge_open)。
    4)從undo segment的最小堆中(堆存放每一個段未被purge的最老的undo頁),找出最先提交事務的undolog(trx_purge_get_rseg_with_min_trx_id),若是undolog標記過delete_mark(表示有記錄刪除操做),則把先關undopage信息暫存在purge_sys_t中(trx_purge_read_undo_rec)。
    5)依據purge_sys_t中的信息,讀取出相應的undo,同時把相關信息加入到任務隊列中。同時更新掃描過的指針,方便後續truncate undolog。
    6)循環第4步和第5步,直到爲空,或者接下到view->low_limit_no,即最老視圖建立時已經提交的事務,或者已經解析的page數量超過innodb_purge_batch_size。(把delete和Undopage分別存放,detele給工做線程刪除)
    7)把全部的任務都放入隊列後,就能夠通知全部Purge Worker線程(若是有的話)去執行記錄刪除操做了。刪除記錄的核心邏輯在函數row_purge_record_func中。有兩種狀況,一種是數據記錄被刪除了,那麼須要刪除全部的彙集索引和二級索引(row_purge_del_mark),另一種是二級索引被更新了(老是先刪除+插入新記錄),因此須要去執行清理操做。
    8)在全部提交的任務都已經被執行完後,就能夠調用函數trx_purge_truncate去刪除update undo(insert undo在事務提交後就被清理了)。每一個undo segment分別清理,從本身的histrory list中取出最先的一個undo,進行truncate(trx_purge_truncate_rseg_history)。truncate中,最終會調用fseg_free_page來清理磁盤上的空間。

MVCC

undo+read view 寫時併發讀
ReadView::id 建立該視圖的事務ID;

m_ids 建立ReadView時,活躍的讀寫事務ID數組,有序存儲;記錄trx_id不在m_ids中可見
m_low_limit_id  當前最大事務ID;記錄rx_id>=ReadView::m_low_limit_id,則說明該事務是建立readview以後開啓的,不可見
Rem_up_limit_id ;m_ids 集合中的最小值;記錄trx_id< m_up_limit_id該事務在建立ReadView時已經提交了,可見

二級索引回聚簇索引中。
若不可見,則經過undo構建老版本記錄。

redolog

  • 物理:log文件,ib_logfile 覆蓋寫
  • 內存:log buffer log_sys(記了日誌在磁盤和內存中用到的信息,好比總大小,一些須要刷盤的閾值等)
  • 內容:記錄物理位置spaceid,page,offset上要操做的邏輯日誌
  • 寫入
    每一個子事務的操做都會寫入log(mtr.m_impl.m_log中)
    mlog_open_and_write_index=》memcpy=》mlog_close
  • 提交 子事務提交寫入緩衝區

提交時,準備log內容,提交到公共buffer中,並將對應的髒頁加到flush list上
    Step 1: mtr_t::Command::prepare_write()
        1.若當前mtr的模式爲MTR_LOG_NO_REDO 或者MTR_LOG_NONE,則獲取log_sys->mutex,從函數返回
        2.若當前要寫入的redo log記錄的大小超過log buffer的二分之一,則去擴大log buffer,大小約爲原來的兩倍。
        3.持有log_sys->mutex
        4.調用函數log_margin_checkpoint_age檢查本次寫入:若是本次產生的redo log size的兩倍超過redo log文件capacity,則打印一條錯誤信息;若本次寫入可能覆蓋檢查點,還須要去強制作一次同步*chekpoint*
        5.檢查本次修改的表空間是不是上次checkpoint後第一次修改(fil_names_write_if_was_clean)
         若是space->max_lsn = 0,表示自上次checkpoint後第一次修改該表空間:
            a. 修改space->max_lsn爲當前log_sys->lsn;
            b. 調用fil_names_dirty_and_write將該tablespace加入到fil_system->named_spaces鏈表上;
            c. 調用fil_names_write寫入一條類型爲MLOG_FILE_NAME的日誌,寫入類型、spaceid, page no(0)、文件路徑長度、以及文件路徑名(將本次的表空間和文件信息加入到一個內存鏈表上 (去除恢復中對數據字典的依賴))。
            在mtr日誌末尾追加一個字節的MLOG_MULTI_REC_END類型的標記,表示這是多個日誌類型的mtr。
            若是不是從上一次checkpoint後第一次修改該表,則根據mtr中log的個數,或標識日誌頭最高位爲MLOG_SINGLE_REC_FLAG,或附加一個1字節的MLOG_MULTI_REC_END日誌。
            
    Step 2: 拷貝
       若日誌不夠,log_wait_for_space_after_reserving

    Step 3:若是本次修改產生了髒頁,獲取log_sys->log_flush_order_mutex,隨後釋放log_sys->mutex。
    
    Step 4. 將當前Mtr修改的髒頁加入到flush list上,髒頁上記錄的lsn爲當前mtr寫入的結束點lsn。基於上述加鎖邏輯,可以保證flush list上的髒頁老是以LSN排序。
    
    Step 5. 釋放log_sys->log_flush_order_mutex鎖
    
    Step 6. 釋放當前mtr持有的鎖(主要是page latch)及分配的內存,mtr完成提交。
    • 刷盤 整個事務的提交 trx_commit. 參數innodb_flush_log_at_trx_commit

      當設置該值爲1時,每次事務提交都要作一次fsync,這是最安全的配置,即便宕機也不會丟失事務
      當設置爲2時,則在事務提交時只作write操做,只保證寫到系統的page cache,所以實例crash不會丟失事務,但宕機則可能丟失事務
      當設置爲0時,事務提交不會觸發redo寫操做,而是留給後臺線程每秒一次的刷盤操做,所以實例crash將最多丟失1秒鐘內的事務,寫入一條MLOG_FILE_NAME

    • 刷髒。刷髒後調用log checkpoint把點寫入(刷髒就是內存到磁盤和redo不要緊,redo寫多了須要清除checkpoint寫入刷髒點,以前的能夠不要了),之後崩潰恢復今後點開始
      1.刷髒會在如下情形被觸發

      啓動和關閉時會喚醒刷髒線程
        每10s後、按如下比對落後點決定是否要刷髒。
        redo log可能覆蓋寫時,調用單獨線程把未提交LSN對應的點放入log的checkpoint點,只是redolog寫checkpoint點。如下參數控制checkpoint和flush刷髒點
            log_sys->log_group_capacity = 15461874893 (90%)
            log_sys->max_modified_age_async = 12175607164 (71%)
            log_sys->max_modified_age_sync = 13045293390 (76%)
            log_sys->max_checkpoint_age_async = 13480136503 (78%)
            log_sys->max_checkpoint_age = 13914979615 (81%)
        
        LRU LIST在未能本身釋放時,先本身刷髒一頁,不行再 喚醒刷髒線程

      2.刷髒線程
      clipboard.png
      innodb_page_cleaners設置爲4,那麼就是一個協調線程(自己也是工做線程),加3個工做線程,工做方式爲生產者-消費者。工做隊列長度爲buffer pool instance的個數,使用一個全局slot數組表示。
      1)buf_flush_page_cleaner_coordinator協調線程

      主循環主線程以最多1s的間隔或者收到buf_flush_event事件就會觸發進行一輪的刷髒。
      協調線程首先會調用pc_request()函數,這個函數的做用就是爲每一個slot表明的緩衝池實例計算要刷髒多少頁,
      而後把每一個slot的state設置PAGE_CLEANER_STATE_REQUESTED, 喚醒等待的工做線程。
      因爲協調線程也會和工做線程同樣作具體的刷髒操做,因此它在喚醒工做線程以後,會調用pc_flush_slot(),和其它的工做線程並行去作刷髒頁操做。
      一但它作完本身的刷髒操做,就會調用pc_wait_finished()等待全部的工做線程完成刷髒操做。
      完成這一輪的刷髒以後,協調線程會收集一些統計信息,好比這輪刷髒所用的時間,以及對LRU和flush_list隊列刷髒的頁數等。
      而後會根據當前的負載計算應該sleep的時間、以及下次刷髒的頁數,爲下一輪的刷髒作準備。

      2)buf_flush_page_cleaner_worker工做線程

      主循環啓動後就等在page_cleaner_t的is_requested事件上,
      一旦協調線程經過is_requested喚醒全部等待的工做線程,
      工做線程就調用pc_flush_slot()函數去完成刷髒動做。
      
      pc_flush_slot:
          先找到一個空間的slot,
          page_cleaner->n_slots_requested--; // 代表這個slot開始被處理,將未被處理的slot數減1 
          page_cleaner->n_slots_flushing++; //這個slot開始刷髒,將flushing加1 
          slot->state = PAGE_CLEANER_STATE_FLUSHING;
          
          刷LRU,FLUSH LIST
          
          page_cleaner->n_slots_flushing--; // 刷髒工做線程完成次輪刷髒後,將flushing減1 p
          age_cleaner->n_slots_finished++; //刷髒工做線程完成次輪刷髒後,將完成的slot加一 
          slot->state = PAGE_CLEANER_STATE_FINISHED; // 設置此slot的狀態爲FINISHED
          如果最後一個,os_event_set(page_cleaner->is_finished)
      pc_wait_finished:
          os_event_wait(page_cleaner->is_finished);
          統計等
      
      每次刷多少srv_max_buf_pool_modified_pct決定

      3.log_checkpoint

      clipboard.png

      入口函數爲log_checkpoint,其執行流程以下:
      Step1. 持有log_sys->mutex鎖,並獲取buffer pool的flush list鏈表尾的block上的lsn,這個lsn是buffer pool中未寫入數據文件的最老lsn,在該lsn以前的數據都保證已經寫入了磁盤。checkpoint 點,        在crash recover重啓時,會讀取記錄在checkpoint中的lsn信息,而後從該lsn開始掃描redo日誌。
      Step 2. 調用函數fil_names_clear
          掃描fil_system->named_spaces上的fil_space_t對象,若是表空間fil_space_t->max_lsn小於當前準備作checkpoint的Lsn,則從鏈表上移除並將max_lsn重置爲0。同時爲每一個被修改的表空間構建MLOG_FILE_NAME類型的redo記錄。(這一步將來可能會移除,只要跟蹤第一次修改該表空間的min_lsn,而且min_lsn大於當前checkpoint的lsn,就能夠忽略調用fil_names_write)
          寫入一個MLOG_CHECKPOINT類型的CHECKPOINT REDO記錄,並記入當前的checkpoint LSN
      Step3 . fsync 被修改的redo log文件
          更新相關變量:
          log_sys->next_checkpoint_no++
          log_sys->last_checkpoint_lsn = log_sys->next_checkpoint_lsn
      Step4. 寫入checkpoint信息
          函數:log_write_checkpoint_info --> log_group_checkpoint
          checkpoint信息被寫入到了第一個iblogfile的頭部,但寫入的文件偏移位置比較有意思,當log_sys->next_checkpoint_no爲奇數時,寫入到LOG_CHECKPOINT_2(3 *512字節)位置,爲偶數時,寫入到LOG_CHECKPOINT_1(512字節)位置。
    • 崩潰恢復

      1.從第一個iblogfile的頭部定位要掃描的LSN(數據落盤點)
      2.掃描redo log
      1) 第一次redo log的掃描,主要是查找MLOG_CHECKPOINT,不進行redo log的解析,
      2) 第二次掃描是在第一次找到MLOG_CHECKPOINT(獲取表和路徑)基礎之上進行的,該次掃描會把redo log解析到哈希表中,若是掃描完整個文件,哈希表尚未被填滿,則不須要第三次掃描,直接進行recovery就結束
      3)第二次掃描把哈希表填滿後,還有redo log剩餘,則須要循環進行掃描,哈希表滿後當即進行recovery,直到全部的redo log被apply完爲止。
      3.具體redo log的恢復

      MLOG_UNDO_HDR_CREATE:解析事務ID,爲其重建undo log頭;
         MLOG_REC_INSERT 解析出索引信息(mlog_parse_index)和記錄信息(    page_cur_parse_insert_rec)等
         在完成修復page後,須要將髒頁加入到buffer pool的flush list上;查找紅黑樹找到合適的插入位置
         MLOG_FILE_NAME用於記錄在checkpoint以後,全部被修改過的信息(space, filepath);        MLOG_CHECKPOINT用於標誌MLOG_FILE_NAME的結束。
      
         在恢復過程當中,只須要打開這些ibd文件便可,固然因爲space和filepath的對應關係經過redo存了下來,恢復的時候也再也不依賴數據字典。
      
         在恢復數據頁的過程當中不產生新的redo 日誌;
    • 二次寫
      MySQL 一直使用double write buffer來解決一個page寫入的partial write問題,但在linux系統上的Fusion-io Non-Volatile Memory (NVM) file system支持原子的寫入。這樣就能夠省略掉double write buffer的使用, 5.7.4之後,若是Fusion-io devices支持atomic write,那麼MySQL自動把dirty block直接寫入到數據文件了。這樣減小了一次內存copy和IO操做。
      redo會記spaceid,pageno,偏移量內的邏輯日誌只記錄:’這是一個插入操做’和’這行數據的內容‘,這是一個更新操做,更新內容(爲了省地方)。可是這樣就有個問題。在redo log恢復執行時,若是頁邏輯時一半斷電了,redo log在恢復時沒法正確恢復更新操做。這就須要在髒頁落盤時採起二次寫。數據寫入ibd前先順序寫入ibdata.在崩潰恢復時,先檢驗checksum.不合法載入ibdata的數據。

      clipboard.png

      Redo爲了保證原子性,要求一塊一寫。不夠的話要先讀舊的而後改而後寫。以512字節(最小扇區)對其方式寫入,不須要二次寫。設置一個值innodb_log_write_ahead_size,不須要這個過程,超過該值補0到一塊直接插入
      [ps 數據須要二次寫,由於可能誇多扇區,leveldb的log增長頭直接跳過壞頁,redo log固定大小,正常日誌都是寫成功纔會被回放,寫內存與寫壞後丟只能丟失,解析錯誤跳過到下一塊吧,問題就是要有個大小找到下一個位置]

    server與innodb的事務保證

    • server和引擎層事務的界限
      1.開啓事務。server只會調用引擎層。
      server層若是不以命令,是不會顯示開啓事務的。在SQLCOM_BEGIN等命令會調用trans_begin 分佈式事務會調trans_begin(跟蹤下)
      證實是正確的,在外層trans_begin並無調用。並不研究了
      提交會在server層調用各個引擎的事務提交。
      下面說下innodb層的trx
      2.提交事務。根據是否開啓binlog和是否有多個引擎執行不一樣。好比開了Binlog且使用了事務引擎,用Mysql_bin_log的兩階段和組提交。若是沒有用事務引擎,直接記log等就能夠
      3.事務回滾:分爲真正xa回滾仍是普通回滾。普通回滾調用引擎層回滾
      4.崩潰恢復:沒有server層的崩潰恢復
    • 開啓 分配回滾段,獲取事務id,加入事務鏈表
    • 提交 入口: MYSQL_BIN_LOG::commit,若是是分佈式事務,用xa,兩階段。prepare和commit。咱們先研究普通的提交。XA不做爲重點。可是因爲server層和Innodb層兩個日誌,須要保證順序,也按照XA的兩階段設計。也叫內部xa
      1) xa兩階段
      Prepare

      undo log寫入xid,設置狀態爲PREPARED

      Commit  

      Flush Stage:由leader依次爲別的線程對flush redo log到LSN,再寫binlog文件
      Sync Stage:若是sync_binlog計數超過配置值,以組爲維度文件fsync
      Commit Stage:隊列中的事務依次進行innodb commit,修改undo頭的狀態爲完成;並釋放事務鎖,清理讀寫事務鏈表、readview等一系列操做,落盤redo。

      2) 緣由
      兩階段是爲了保證binlog和redo log一致性。server和備庫用binlog來恢復同步。innodb用undo和redo恢復。
      1落undo 2flush redo 3 flush binlog 4fsync binlog 5fsync redo [ps:sync可能只是內核緩衝放入磁盤隊列,fsync只保證放入磁盤,都是同步] 6 undo D
      保證binlog若成功了,根據Undo的p結果不會回滾出現主從不一致的狀況
      3) 組提交:兩階段提交,在併發時沒法保證順序一致,用ordered_commit控制
      clipboard.png
      一個On-line的backup程序新建一個slave來作replication,那麼事務T1在slave機器restore MySQL數據庫的時候發現未在存儲引擎內提交,T1事務被roll back,此時主備數據不一致(搭建Slave時,change master to的日誌偏移量記錄T3在事務位置以後)。
      若是關閉binlog_order_commits。事務各自提交。這時候沒有設置不寫redo log。不能保證Innodb commit順序和Binlog寫入順序一直,可是不會影響數據一致性。只是物理備份數據不一致。可是依賴於事務頁上記錄binlog恢復的,好比xtrabackup就會發生備份數據不一致的狀況。
      每一個stage階段都有各自的隊列,使每一個session的事務進行排隊。,leader控制,當一組事務在進行Commit階段時,其餘新的事務能夠進行Flush階段

    • 回滾
      兩階段:正常應該根據undo非DONE回滾,但看到undo爲prepare且binlog有,就不回滾
      當因爲各類緣由(例如死鎖,或者顯式ROLLBACK)須要將事務回滾時,ha_rollback_trans=》ha_rollback_low,進而調用InnoDB函數trx_rollback_for_mysql來回滾事務。對於innodb回滾的方式是提取undo日誌,作逆向操做。
      提交失敗會回滾。走的非xa,調用trx_rollback_for_mysql。原來一直糾結binlog會不會刪除。。。跟蹤了很久也沒看出來,實際上是undo中的在提交時從新寫一下binlog。這裏在子事務裏會介紹。

    第四章 分佈式

    主從複製

    • 三種日誌模式
      1.基於行的複製  row
      優勢:符合冪等性,高度保障數據一致。
      缺點:數據量大

      2.基於語句的複製  statement
      優勢:日誌量少
      缺點:特定功能函數致使主從數據不一致,重複執行時沒法保證冪等

      3.混合類型的複製  mixed  (默認語句,語句沒法精準複製,則基於行)

    • 主從同步過程
      clipboard.png

      其中1. Slave 上面的IO線程鏈接上 Master,並請求從指定日誌文件的指定位置(或者從最開始的日誌)以後的日誌內容;
      重放過程和master同樣,也redolog

    • GTID

      MySQL 5.6引入全局事務ID的首要目的,是保證Slave在並行複製(並行順序會亂)的時候不會重複執行相同的事務操做;用全局事務IDs代替由文件名和物理偏移量組成的複製位點(每一個日誌包含GID_Sets,xx:1-100形式)。

      GTID的組成部分:

      前面是server_uuid:後面是一個串行號
       例如:server_uuid:sequence number
       7800a22c-95ae-11e4-983d-080027de205a:10
       UUID:每一個mysql實例的惟一ID,因爲會傳遞到slave,因此也能夠理解爲源ID。
       Sequence number:在每臺MySQL服務器上都是從1開始自增加的串行,一個數值對應一個事務。

      當事務提交時,無論是STATEMENT仍是ROW格式的binlog,都會添加一個XID_EVENT事件做爲事務的結束。該事件記錄了該事務的id(這個是存儲引擎裏的事務id,崩潰恢復時決是否提交存儲引擎中狀態爲prepared的事務)。

    • 同步方案
      1.同步複製 所謂的同步複製,意思是master的變化,必須等待slave-1,slave-2,...,slave-n完成後才能返回。
      2.異步複製 master只須要完成本身的數據庫操做便可。至於slaves是否收到二進制日誌,是否完成操做,不用關心
      3.半同步複製 master只保證slaves中的一個操做成功,就返回,其餘slave無論。

      這裏有個不一致的問題。
       開始提交事務 =>write binlog => sync binlog => engine commit => send events =>返回 commit後崩潰,send_events失敗,會致使master有slave沒有,須要靠binlog同步補一下。
       開始提交事務 =>write binlog => sync binlog => send events => engine commit =>返回 send_events失敗,若sync binlog未落盤,致使XA不會重作,slave領先,若binlog落盤則沒有問題,可接受和單機redo同樣。

      master既要負責寫操做,還的維護N個線程,負擔會很重。能夠這樣,slave-1是master的從,slave-1又是slave-2,slave-3,...的主,同時slave-1再也不負責select。slave-1將master的複製線程的負擔,轉移到本身的身上。這就是所謂的多級複製的概念。

    • 並行複製

      clipboard.png
      按期checkout-point將隊列中執行結束的刪除。記錄checkpoint後每一個worker是否執行過的bitmap。崩潰恢復時執行Bitmap未執行的部分。按db分粒度大能夠換成table

    擴展性

    當主庫支撐不了。水平擴展。拆表。

    可靠性

    須要proxy保證

    一致性

    同步策略影響。
    XA分爲外部和內部。對於外部。要應用程序或proxy做爲協調者。(二階段提交協調者判斷全部prepare後commit)。對於內部,binlog控制。

    第五章 proxy功能

    • 數據庫分片的合併
    • 共享式的緩存
    • 讀寫分離路由
    • 可靠性的保證,主從切換,故障發現與定位
    • XA一致性的實現
    • 過濾加註釋

    例如:https://github.com/mariadb-co...

    相關文章
    相關標籤/搜索