MySQL事務在MGR中的漫遊記 - 路線圖
來自專欄資料庫內核
MGR即MySQL Group Replication,是MySQL官方推出的基於Paxos一致性協議的數據高可靠、服務高可用方案。MGR在2016年12月12號發布的MySQL 5.7.17版本達到GA狀態,在這之後一年半時間裡,MySQL又相繼發布了5.7.18到5.7.22版本,每個版本都對MGR做了功能增強、性能優化和Bug修復,毫無疑問目前MGR達到了線上部署狀態。
MySQL Plugin簡介
MGR是一個MySQL Plugin(插件),簡單來說,Plugin是MySQL官方提供的一套擴展機制,在MySQL實現事務處理、Binlog傳輸和持久化等操作時,在代碼邏輯中預埋了一些(Hook)鉤子,Plugin可以在鉤子上註冊處理函數,增加Plugin專有的功能實現。
Plugin提供了包括事務處理(Trans_observer)、伺服器狀態變化(Server_state_observer)、Binlog存儲(Binlog_storage_observer)、Binlog發送(Binlog_transmit_observer)和Binlog回放(Binlog_relay_IO_observer)等不同功能模塊的鉤子集合。
舉個栗子,比如事務處理鉤子集包括了before_dml,before_commit,before_rollback,after_commit,after_rollback等五個鉤子,分別用於在事務執行DML操作前,在事務提交前,在事務回滾前,在事務提交後,在事務回滾後進行特定的操作。
插件通過INSTALL PLUGIN啟用,UNSTALL PLUGIN卸載。在INSTALL時會調用初始化函數向MySQL實例註冊上述介紹的不同模塊鉤子集。
MGR作為一個官方插件,同樣實現了這些鉤子,其中事務處理集合的before_dml和before_commit是MGR中2個與事務處理相關的最主要鉤子,註冊函數分別為group_replication_trans_before_dml和group_replication_trans_before_commit,前者用於在執行DML前進行事務操作的合法性檢查,包括所操作的表是否顯式定義了主鍵,是否使用了InnoDB存儲引擎等;後者是本文要重點介紹的MGR事務處理入口,它將MySQL中已經進入提交階段的事務攔截下來,進入MGR處理流程,由MGR決定該事務應該提交還是回滾後,在返回MySQL通用代碼進行後續處理。下圖為作為Plugin的MGR整體框圖。
MGR before_commit對事務的處理
MySQL事務通過before_commit鉤子進入MGR,before_commit位於MYSQL_BIN_LOG::commit()函數中,具體是在進入事務組提交MYSQL_BIN_LOG::ordered_commit()之前,這就意味著執行到before_commit這個鉤子時,事務還未提交,產生的Binlog還未寫入Binlog文件中,事務GTID還未產生。接下來我們從group_replication_trans_before_commit開始詳細分析事務如何一步步在MGR中漫遊直到被宣判進天堂(提交)還是入地獄(回滾)。
事務處理合法性判斷
首先,需要判斷該事務是否需要交由MGR處理以及MGR當前是否可以處理事務。如果事務屬於group_replication_applier或group_replication_recovery複製通道(channel),說明該事務已經被本節點或其他節點的MGR模塊處理過,無需再進入;如果MGR節點當前狀態非在線(ONLINE),或雖然在線,但是正在退出(stop group_replication),或已經退出,這些情況都不適合再處理事務。
如果MGR能夠正常處理,那麼先初始化事務gtid信息,分為2種情況,一種是事務還未產生gtid,這是通常的情況,表明該事務是在本節點第一次執行的;另一種是已經有gtid,這說明事務是同個主從複製通道進入MGR的,比如該MGR節點同時是一個Master-Slave非同步複製的Slave節點,事務在SQL Thread上完成回放後在提交階段進入group_replication_trans_before_commit函數鉤子;對於第一種情況,會在完成事務認證(衝突檢測)後由MGR Applier模塊產生gtid。
事務信息收集和順序寫入
接下來,收集事務認證所需的相關信息。包括事務更新的記錄主鍵信息、產生的Binlog信息和gtid信息。記錄主鍵信息write_sets被封裝在Transaction_context_log_event(tcle)中,tcle除了write_sets外,還包括了事務執行節點的server_uuid,事務是否為dml,事務的線程id和gtid是否已指定等;由於Binlog(log_event group)和gtid信息(Gtid_log_event)本來就是log_event,不需要再次封裝。將這些信息會放入Transaction_Message對象中。
首先是衝突檢測所需的信息Transaction_context_log_event被寫入Transaction_Message對象,其次是Gtid_log_event,最後才是事務改變的數據(log_event group),這些信息寫入的順序非常有講究,這在後續的章節會體現。
完成了事務信息收集後,下一步是消息的發送和返回。MGR會在發送消息前以事務線程id為key調用registerTicket註冊一個Wait_ticket對象,對象計數設置為1,註冊成功後調用Gcs_operations::send_message()將Transaction_Message對象(類型為Plugin_gcs_message::CT_TRANSACTION_MESSAGE)發送給Paxos,隨後調用waitTicket()阻塞等待直到其他流程通過releaseTicket()將對應的Wait_ticket對象計數變為0。
最後,需要簡單說明的是,在發送消息前會調用Flow_control_module::do_wait()就行流控處理,該函數會判斷本周期(默認為1s)內本節點的事務提交配額是否已經用完,如果用完,在本周期無法再提交事務,需要等待本周期結束再發送消息。
事務在Paxos中的處理
MGR中的事務以Paxos請求的方式發送給Paxos,Paxos通過兩階段協議(propose、accept)方式使各節點達成一致後返回給MGR在進行後續處理。下面我們就具體介紹下事務請求如何進入Paxos,又是如何返回的。
Gcs_operations::send_message()函數會獲取在MGR初始化時向GCS註冊的Gcs_control_interface和Gcs_communication_interface,GCS可以理解為MGR分層實現中的消息通信層(Group Communication System),目前僅支持官方提供的基於Paxos實現的Xcom。通過Gcs_control_interface獲取本節點的identifier(Gcs_member_identifier),通過Gcs_communication_interface提供的消息發送介面Gcs_xcom_communication::send_message()發送事務請求。
進入Paxos前層層封裝
在Gcs_xcom_communication::send_message()介面中會將消息類型設置為Gcs_internal_message_header::CT_USER_DATA,交由 Gcs_xcom_proxy_impl::xcom_client_send_data()發送。
MGR初始化時會默認為Paxos創建6個socket管道來發送客戶端消息,使用m_xcom_handlers[]數組維護,在xcom_client_send_data()中,會對消息再次進行封裝,將其設置為app_type類型,該類型的消息在完成Paxos協議流程後需要返回給客戶端處理,最後基於round-robin方式選擇一個socket後調用xcom_send_client_app_data()對消息做最後封裝處理,該函數會創建一個pax_msg ,放入事務數據,並指定發送的目標節點為所有節點,類型op為client_msg,消息類型msg_type為normal,最終調用socket的send介面將消息發送給Paxos。下圖為消息的封裝過程及最終表現形式。
這裡需要注意的是:每次send_message的時候,僅是將其寫入6個socket中的一個就返回了,發送消息的鎖(shared_plugin_stop_lock)也釋放了。比如T1寫入socket1,T2寫入socket2。每個socket上有個acceptor_learner_task負責將socket上的消息寫入到prop_input_queue,proposer_task按需讀取該queue消息進行propose。是不是有可能T2先被socket2上的acceptor_learner_task寫入prop_input_queue,先被propose了?
這是有可能的,不過因為這些事務是並發執行,那麼T1、T2先後順序並不重要,只需要確保有依賴關係的事務不會先被propose即可(propose的先後表示在遠端節點回放的先後),這顯然是可以保證的,雖然事務數據消息寫入socket就返回,但是事務還不會進入order_commit()而是等待處理結果,那麼與等待的事務有依賴的事務是無法執行到事務提交階段的,更不用說寫socket這步了。
事務Propose和Execute
Paxos實例在MGR創建socket時為每個分配一個acceptor_learner_task協程,該協程調用buffered_read_msg()讀取socket消息。對於msg_type為normal的消息,會調用dispatch_op進行處理,對於op為client_msg的消息,dispatch_op會進一步調用handle_client_msg()插入到prop_input_queue請求通道末尾。每個MGR節點的Xcom有一個proposer_task,他會獲取prop_input_queue頭部的請求,並發送給MGR的其他節點進行propose操作,proposer_task會在發送前做事務請求的batch操作,所以一個Paxos propose請求可能包括多個事務的數據。Paxos的propose、accept和learn 三個流程的具體實現後續另開一篇,在此僅放一個圖,不展開說明。
Paxos實例的executor_task會按序獲取完成Paxos處理流程的事務請求,調用execute_msg()執行該請求,對於app_type類型的請求,會調用deliver_to_app(),而該函數最終調用了在MGR初始化時註冊的xcom_data_receiver處理函數cb_xcom_receive_data()。
事務請求分發和解封
cb_xcom_receive_data()只是簡單地初始化了一個Data_notification對象,賦予do_cb_xcom_receive_data()回調處理函數之後將其push到Gcs_xcom_engine對象的m_notification_queue隊列中。
process_notification_thread線程調用Gcs_xcom_engine::process()函數從m_notification_queue隊列pop請求,並交由指定的回調函數處理,對於Data_notification對象,即有do_cb_xcom_receive_data()進行處理。
do_cb_xcom_receive_data()首先會獲取Gcs_xcom_interface對象,並基於該對象的get_xcom_group_information()和請求中的group_id找到對應的Gcs_group_identifier和Gcs_xcom_control對象,再通過Gcs_group_identifier獲取Gcs_communication_interface對象(進一步轉化為Gcs_xcom_communication_interface對象),接著對事務請求進行Gcs_internal_message_header::decode()和Gcs_message_data::decode(),重新創建Gcs_message(Gcs_member_identifier,Gcs_group_identifier, message_data),對於CT_USER_DATA類型,Gcs_message會交由Gcs_xcom_communication_interface對象的Gcs_xcom_communication::xcom_receive_data(Gcs_message *message)進行下一步處理。
xcom_receive_data()判斷節點當前是否處於視圖切換狀態,如果是則需要臨時緩存該請求,完成視圖切換後再處理,如果不是則調用Gcs_xcom_communication::notify_received_message(Gcs_message *message),該函數內部獲取MGR初始化時註冊的Gcs_communication_event_listener,交由其中的Plugin_gcs_events_handler::on_message_received()根據消息的不同進行分發,對於Plugin_gcs_message::CT_TRANSACTION_MESSAGE消息類型,調用Plugin_gcs_events_handler::handle_transactional_message(),在該函數中,事務進入MGR的applier_module一系列pipeline處理。
事務在Applier_module中的處理
handle_transactional_message()調用applier_module的handle()介面創建類型為DATA_PACKET_TYPE的Data_packet對象,並將其push到Applier_module的incoming隊列中。Applier_module在(MGR)初始化時註冊了incoming隊列處理線程Applier_module::applier_thread_handle(),對於DATA_PACKET_TYPE類型的Packet對象,調用Applier_module::apply_data_packet()進行處理,完成處理後將該對象從incoming隊列中刪除。下面我們會詳細分析apply_data_packet()的具體處理流程,開始之前,先介紹Applier_module的pipeline機制。
配置事務處理pipeline
MGR在初始化Applier_module時(configure_and_start_applier_module())會調用Applier_module::setup_applier_module(),該介面除創建前述的incoming隊列外,最重要的功能就是調用get_pipeline()來配置pipeline。
首先,調用get_pipeline_configuration()根據傳入的Handler_pipeline_type來確定事務中的Event都需要經過哪幾類處理,MGR將此設計為可定製模式,默認為STANDARD_GROUP_REPLICATION_PIPELINE,即標準的組複製管道,按順序分別為CATALOGING_HANDLER、CERTIFICATION_HANDLER和SQL_THREAD_APPLICATION_HANDLER。
接著,調用configure_pipeline()來配置上一步定製的各個管道處理函數Event_handler(簡稱handler)。對於CATALOGING_HANDLER,註冊Event_cataloger作為管道處理對象;CERTIFICATION_HANDLER對應Certification_handler;SQL_THREAD_APPLICATION_HANDLER對應Applier_handler。
handler具有unique屬性和role屬性,對於unique屬性,表示在一條管道中,該類型的handler最多只能處理一次,目前標準的組複製管道中3個handler都是unique的。對於role屬性,表示在一條管道中不允許存在2個及以上相同role類型的handler,目前3個handler的role分別為EVENT_CATALOGER、CERTIFIER和APPLIER,函數get_handler_by_role()用於獲取指定role的handler。
在configure_pipeline()函數中,還會調用handler->initialize()來進行handler初始化,在該介面中Certification_handler創建了Certifier對象。最後configure_pipeline()會調用append_handler()來將每個handler按照順序加入pipeline中。
Continuation對象
apply_data_packet()會將事務數據進行拆分為多個Event,並將其依次插入管道中,只有當所有(3個)handler完成該Event處理後,才會插入下一個Event。所以其實各個Event是個串列處理過程,pipeline相當於是一條流水線。
那麼如何判斷流水線完成了Event處理呢?這裡需要引入Continuation對象,其實現了wait()和signal() 2個方法,包括ready、error_code、transaction_discarded和cond變數。ready用來表示一個Event是否已經處理結束(可能因為出錯被中斷),error_code表示處理過程是否出錯,transaction_discarded表示是否丟棄該事務。在Event被handler處理時,如果處理出現錯誤,則調用signal()方法設置ready為true,error_code為true,transaction_discarded為true;如果處理結束,但衝突檢測未通過,雖然error_code為false,但是transaction_discarded為true,表示扔需要丟棄事務數據;如果處理結束,且衝突檢測通過,但如果是本地事務,也需要丟棄事務數據,因為本地事務不需要寫relay log,而是交還給原始流程繼續處理。wait()方法會判斷ready和error_code是否有一個為true,若不滿足,則會一直在信號量cond上的等待。由於在signal()中ready被置為true,且會喚醒在cond上等待的線程,所有調用wait()即可獲知一個Event是否結束處理。
事務在pipeline中處理流程
本小節詳細分析apply_data_packet()的具體處理流程。在上個小節提到事務數據會被拆分為多個Event,其實所拆出來的Event就是在before_commit鉤子中被先後放入事務消息中的Transaction_context_log_event,Gtid_log_event和事務產生的log_event group。拆出來的Event被轉化為Pipeline_event對象後交由Applier_module::inject_event_into_pipeline()開始流水線的各個handler處理,handler會調用Event_handler::handle_event()處理,事務流水線處理是在其他線程中執行的,對於inject_event_into_pipeline()調用前述的Continuation對象wait()方法等待處理結束,並根據error_code和transaction_discarded進行最後處理。
Event_cataloger::handle_event()處理
Transaction_context_log_event:本階段僅是將Pipeline_event對象標記為TRANSACTION_BEGIN(表示事務開始)。其作用是若前個事務因設置transaction_discarded為true而被丟棄後,收到該Event可重置transaction_discarded欄位為false,避免該事務的Event被丟棄。
事務的其他Event(肯定在晚於Transaction_context_log_event被本階段處理):本階段會判斷Continuation對象的transaction_discarded是否為true,如果是,其實就說明事務衝突檢測未通過,無需再繼續處理,會調用signal(0, transaction_discarded)返回處理結果(顯然是丟棄/回滾)。
完成上述處理或是其他情況,調用next()交由下一階段處理。
Certification_handler::handle_event()處理
Transaction_context_log_event:交由Certification_handler::handle_transaction_context()處理,僅調用set_transaction_context()將Event內容緩存到transaction_context_packet變數上。
Gtid_log_event:交由Certification_handler::handle_transaction_id()處理。這是事務在pipeline處理流程中最最關鍵的一步,簡單來說,其所做的就是通過Certification_handler::get_transaction_context()獲取緩存在transaction_context_packet上的事務認證所需信息,調用Certifier::certify()進行事務認證。
certify()函數不是簡單的事務衝突檢測處理函數,而是會根據是否為本地事務,是否啟動了衝突檢測(多主模式?正在主從切換?),事務是否已經有gtid等多種場景分別進行不同的處理。如果啟用了衝突檢測,那麼需要將事務的write_sets跟衝突檢測資料庫進行一一比對,決定事務能否正常提交。認證通過的事務,其write_sets會被添加到衝突檢測資料庫中,對於認證通過或無需認證的場景,如果事務還未有gtid,則為其分配gtid,並更新系統已分配的gtid集合,若為非本地事務,還需確定其組提交次序。函數中還會更新事務認證的統計信息。最終返回生成的sequence_number(同時也表示是否認證通過)。
返回到handle_transaction_id()後,繼續根據是否為本地事務,是否認證通過來進行後續處理。對於本地事務,標誌著該事務在MGR中處理已結束,初始化Transaction_termination_ctx對象,用於在before_commit鉤子返回後MYSQL_BIN_LOG::commit()能夠判斷事務應該提交還是回滾,事務請求信息中的第三部分log_event group被丟棄。對於非本地事務(遠端事務),若未認證通過,也意味著結束處理,否則還需要繼續流水線下一步處理。
除了遠端事務認證通過的調用next()進行下一階段處理外,其他情況在均調用Continuation對象signal()結束。
上面寥寥幾段還無法道出Certification_handler::handle_transaction_id()全部內容,需要專門安排一篇來對內容和背景進行詳細分析。
Applier_handler::handle_event()處理
對於非Transaction_context_log_event類型Binlog,在本階段會最終調用queue_event()寫入到relay-log文件中。在MGR初始化是已經註冊了group_replication_applier channel這個複製通道,所以,被relay-log文件的事務會被複制線程回放。
其實本階段只會處理2種Event場景,第一種是Transaction_context_log_event,此時事務還未進行認證,所以該Event會走完pipeline全部3個階段才返回;第二種場景就是通過了認證的遠端事務,需要寫入relay-log中被回放,此時事務組提交的次序已經確定。
以Event角度重講
上面是以handler維度介紹事務處理,這裡以Binlog為主角來介紹。
首先是Transaction_context_log_event,在Event_cataloger::handle_event()中會將Pipeline_event對象標記為TRANSACTION_BEGIN,表示事務開始,並將transaction_discarded設置為false。在Certification_handler::handle_event()中,其承載的事務認證信息(write_sets)被緩存到transaction_context_packet上。至此,該Event處理結束。
接著是Gtid_log_event,其僅在Certification_handler::handle_event()中被處理,基於緩存在transaction_context_packet上的認證信息決定事務提交還是回滾,若提交且為遠端事務,則確定Gtid_log_event中的gtid後繼續交由Applier_handler::handle_event()處理,最終寫入relay-log文件。若回滾或為本地事務,則該Event處理結束。
最後是事務修改的數據log_event group,其只有在遠端事務被判定可提交時才會被Applier_handler::handle_event()寫入relay-log文件。前2個pipeline階段不處理。
所以,很顯然,Transaction_context_log_event只用來表示事務開始並將write_set傳給Certification_handler::handle_event(),不會被寫入relay-log;Gtid_log_event是事務認證的處理主場,在經過認證後會進行初始化,最終寫入relay-log;遠端事務的log_event group在認證通過後寫入relay-log。最終經過MGR的事務寫入relay-log/Binlog文件的東西跟普通事務是一樣的。
總結
本篇將一個MySQL如何進入MGR並如何一步步執行直到返回進行了詳細梳理,藉此能夠建立MGR事務處理流程的基本認識。但由於篇幅所限,事務在Paxos中達成協議的過程,事務認證過程還未充分描述,需要另外開篇。此外,事務在MGR中的各種處理,都離不開MGR初始化的時候確定的各種框架,要想深入通透的理解處理流程,還需要結合MGR是如何初始化和如何進行成員變更的。
推薦閱讀:
※為何Redis用樂觀鎖,而MySQL資料庫卻沒有?
※Mysql鎖機制簡單了解一下
※為什麼 INSERT INTO MYSQL 資料庫失敗?
※MySQL基礎練習3