天天看點

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整體框圖。

MySQL事務在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中漫遊直到被宣判進天堂(送出)還是入地獄(復原)。

MySQL事務在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。下圖為消息的封裝過程及最終表現形式。

MySQL事務在MGR中的漫遊記—路線圖

這裡需要注意的是:每次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 三個流程的具體實作後續另開一篇,在此僅放一個圖,不展開說明。

MySQL事務在MGR中的漫遊記—路線圖

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隊列中。

MySQL事務在MGR中的漫遊記—路線圖

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處理。

MySQL事務在MGR中的漫遊記—路線圖

事務在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。

MySQL事務在MGR中的漫遊記—路線圖

首先,調用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進行最後處理。

MySQL事務在MGR中的漫遊記—路線圖

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()交由下一階段處理。

MySQL事務在MGR中的漫遊記—路線圖

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()進行事務認證。

MySQL事務在MGR中的漫遊記—路線圖

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被丢棄。對于非本地事務(遠端事務),若未認證通過,也意味着結束處理,否則還需要繼續流水線下一步處理。

MySQL事務在MGR中的漫遊記—路線圖

除了遠端事務認證通過的調用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檔案的事務會被複制線程回放。

MySQL事務在MGR中的漫遊記—路線圖
MySQL事務在MGR中的漫遊記—路線圖

其實本階段隻會處理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是如何初始化和如何進行成員變更的。

網易雲資料庫RDS是一種穩定可靠、可彈性伸縮的線上關系型資料庫服務,目前支援MySQL引擎,提供基礎版,高可用版,金融版針對不同業務場景的高可用解決方案,同時提供多重安全防護措施,性能監控體系,專業的資料庫備份、恢複及優化方案,使您能專注于應用開發和業務發展。

相關文章:

【推薦】 使用QUIC

【推薦】 關于評審--從思想到落地

繼續閱讀