MySQL 5.7 LOGICAL_CLOCK 并行复制原理及实现分析
MySQL 5.7 LOGICAL_CLOCK 并行復制原理及實現分析
在MySQL5.7 引入基于Logical clock的并行復制方案前,MySQL使用基于Schema的并行復制,使不同db下的DML操作可以在備庫并發回放(在優化后,可以做到不同table下并發)。但是如果業務在Master端高并發寫入一個庫(或者表),那么slave端就會出現較大的延遲。基于schema的并行復制,Slave作為只讀實例提供讀取功能時候可以保證同schema下事務的因果序(Causal Consistency,本文討論Consistency的時候均假設Slave端為只讀),而無法保證不同schema間的。例如當業務關注事務執行先后順序時候,在Master端db1寫入T1,收到T1返回后,才在db2執行T2。但在Slave端可能先讀取到T2的數據,才讀取到T1的數據。
MySQL 5.7的LOGICAL CLOCK并行復制,解除了schema的限制,使得在主庫對一個db或一張表并發執行的事務到slave端也可以并行執行。Logical Clock并行復制的實現,最初是Commit-Parent-Based方式,同一個commit parent的事務可以并發執行。但這種方式會存在保證沒有沖突的事務不可以并發,事務一定要等到前一個commit parent group的事務全部回放完才能執行。后面優化為Lock-Based方式,做到只要事務和當前執行事務的Lock Interval都存在重疊,即保證了Master端沒有鎖沖突,就可以在Slave端并發執行。LOGICAL CLOCK可以保證非并發執行事務,即當一個事務T1執行完后另一個事務T2再開始執行場景下的Causal Consistency。
LOGICAL_CLOCK Commit-Parent-Based 模式
由于在MySQL中寫入是基于鎖的并發控制,所以所有在Master端同時處于prepare階段且未提交的事務就不會存在鎖沖突,在Slave端執行時都可以并行執行。因此可以在所有的事務進入prepare階段的時候標記上一個logical timestamp(實現中使用上一個提交事務的sequence_number),在Slave端同樣timestamp的事務就可以并發執行。
Master端
在SQL層實現一個全局的logical clock: commit_clock。
當事務進入prepare階段的時候,從commit_clock獲取timestamp并存儲在事務中。
在transaction在引擎層提交之前,推高commit_clock。這里如果在引擎層提交之后,即釋放鎖后操作commit_clock,就可能出現沖突的事務擁有相同的commit-parent,所以一定要在引擎層提交前操作。
Slave端
事務擁有相同的commit-parent就可以并行執行,不同commit-parent的事務,需要等前面的事務執行完畢才可以執行。
LOGICAL_CLOCK Lock-Based模式原理及實現分析
Commit-Parent-Based 模式,用事務commit的點將clock分隔成了多個intervals。在同一個time interval中進入prepare狀態的事務可以被并發。例如下面這個例子(引自WL#7165):
Trx1 ------------P----------C-------------------------------->| Trx2 ----------------P------+---C---------------------------->| | Trx3 -------------------P---+---+-----C---------------------->| | | Trx4 -----------------------+-P-+-----+----C----------------->| | | | Trx5 -----------------------+---+-P---+----+---C------------->| | | | | Trx6 -----------------------+---+---P-+----+---+---C---------->| | | | | | Trx7 -----------------------+---+-----+----+---+-P-+--C------->| | | | | | |每一個水平線代表一個事務。時間從左到右。P表示prepare階段讀取commit-parent的時間點。C表示事務提交前增加全局counter的時間點。垂直線表示每個提交劃分出的time interval。
從上圖可以看到因為Trx5和Trx6的commit-parent都是Trx2提交點,所以可以并行執行。但是Commit-Parent-Based模式下Trx4和Trx5不可以并行執行,因為Trx4的commit-parent是Trx1的提交點。Trx6和Trx7也不可以并行執行,Trx7的commit-parent是Trx5的提交點。但Trx4和Trx5有一段時間同時持有各自的所有鎖,Trx6和Trx7也是,即它們之間并不存在沖突,是可以并發執行的。
針對上面的情況,為了進一步增加復制性能,MySQL將LOGICAL_CLOCK優化為Lock-Based模式,使同時hold住各自所有鎖的事務可以在slave端并發執行。
Master端
-
添加全局的事務計數clock產生事務timestamp和記錄當前最大事務timestamp的clock。
class MYSQL_BIN_LOG: public TC_LOG {...public:/* Committed transactions timestamp */Logical_clock max_committed_transaction;/* "Prepared" transactions timestamp */Logical_clock transaction_counter;... } - 對每個事務存儲其lock interval,并記錄到binlog中。
在每個transaction中添加下面兩個member。
class Transaction_ctx {...int64 last_committed;int64 sequence_number;... }其中last_committed表示事務lock interval的起始點,是事務所有的鎖都獲得時候的max-commited-timestamp。由于在一個事務執行過程中,數據庫無法知道當前的鎖是否為最后一個,在實際實現的時候,會對每次DML操作都更新一次last_committed。
static int binlog_prepare(handlerton *hton, THD *thd, bool all) {...if (!all)//DML操作{Logical_clock& clock= mysql_bin_log.max_committed_transaction;thd->get_transaction()->store_commit_parent(clock.get_timestamp());//更新transaction中的last_committedsql_print_information("stmt prepare");}... }class Transaction_ctx {...void store_commit_parent(int64 last_arg){last_committed= last_arg;}... }sequence_number為lock interval的結束點,理論上是commit釋放鎖的時間點。在實現中選擇在最后更新last_committed之后,引擎層commit前的一個時刻即可,滿足這一條件的情況下時間點越靠后越能獲得更大lock interval,Slave執行也就能獲得更大并發度。由于我們需要把該信息記錄到binlog中,所以實現中在flush binlog cache到binlog文件中的時候記錄。而且當前的MySQL5.7已經disable掉了設置GTID_MODE為OFF的功能,會強制記錄GTID_EVENT。這樣事務的last_committed和sequence_number就記錄在事務開頭的Gtid_log_event中。
int binlog_cache_data::flush(THD *thd, my_off_t *bytes_written, bool *wrote_xid) {...if (flags.finalized){trn_ctx->sequence_number= mysql_bin_log.transaction_counter.step();//獲取sequence_numberif (!error)if ((error= mysql_bin_log.write_gtid(thd, this, &writer)))//記錄Gtid_log_event... }bool MYSQL_BIN_LOG::write_gtid(THD *thd, binlog_cache_data *cache_data,Binlog_event_writer *writer) {...Transaction_ctx *trn_ctx= thd->get_transaction();Logical_clock& clock= mysql_bin_log.max_committed_transaction;DBUG_ASSERT(trn_ctx->sequence_number > clock.get_offset());int64 relative_sequence_number= trn_ctx->sequence_number - clock.get_offset(); int64 relative_last_committed=trn_ctx->last_committed <= clock.get_offset() ?SEQ_UNINIT : trn_ctx->last_committed - clock.get_offset();...Gtid_log_event gtid_event(thd, cache_data->is_trx_cache(),relative_last_committed, relative_sequence_number,//Gtid_log_event中記錄relative_last_committed和relative_sequence_numbercache_data->may_have_sbr_stmts());... }同時可以看到記錄在Gtid_log_event中的sequence_number和last_committed使用的是相對當前binlog文件clock的值。即每個binlog file中事務的last_commited起始值為0,sequence_number為1。由于binlog切換后,需要等待上一個文件的事務執行完,所以這里記錄相對值并不會導致沖突事務并發執行。由于server在每次啟動的時候都會生成新的binlog文件,這樣做帶來的一個明顯好處是max_committed_transaction和transaction_counter不需要持久化。
- 更新max_committed_transaction。
max_committed_transaction的更新一定要在引擎層commit(即鎖釋放)之前,如果之后更新,釋放的鎖被其他事務獲取到并且獲取到last_committed小于該事務的sequence_number,就會導致有鎖沖突的事務lock interval卻發生重疊。
void MYSQL_BIN_LOG::process_commit_stage_queue(THD *thd, THD *first) {...if (head->get_transaction()->sequence_number != SEQ_UNINIT)update_max_committed(head);...if (head->get_transaction()->m_flags.commit_low){if (ha_commit_low(head, all, false))head->commit_error= THD::CE_COMMIT_ERROR;...}Slave端
當事務的lock interval存在重疊,即代表他們的鎖沒有沖突,可以并發執行。下圖中L代表lock interval的開始,C代表lock interval的結束。
- 可并發執行:Trx1 -----L---------C------------>Trx2 ----------L---------C------->- 不可并發執行:Trx1 -----L----C----------------->Trx2 ---------------L----C------->slave端在并行回放時候,worker的分發邏輯在函數Slave_worker Log_event::get_slave_worker(Relay_log_info rli)中,MySQL5.7中添加了schedule_next_event函數來決定是否分配下一個event到worker線程。對于DATABASE并行回放該函數實現為空。
bool schedule_next_event(Log_event* ev, Relay_log_info* rli) {...error= rli->current_mts_submode->schedule_next_event(rli, ev);... }int Mts_submode_database::schedule_next_event(Relay_log_info *rli, Log_event *ev) {/*nothing to do here*/return 0; }Mts_submode_logical_clock的相關實現如下。
在Mts_submode_logical_clock中存儲了回放事務中已經提交事務sequence_number的low-water-mark lwm。low-water-mark表示該事務已經提交,同時該事務之前的事務都已經提交。
class Mts_submode_logical_clock: public Mts_submode {.../* "instant" value of committed transactions low-water-mark */longlong last_lwm_timestamp;...longlong last_committed;longlong sequence_number;在Mts_submode_logical_clock的schedule_next_event函數實現中會檢查當前事務是否和正在執行的事務沖突,如果當前事務的last_committed比last_lwm_timestamp大,同時該事務前面還有其他事務執行,coordinator就會等待,直到確認沒有沖突事務才返回。這里last_committed等于last_lwm_timestamp的時候,實際這兩個值各自事務的lock interval是沒有重疊的,也可能有沖突。在前面lock-interval介紹中,這種情況是前面一個事務執行結束,后面一個事務獲取到last_committed為前面一個的sequence_number的情況,他們的lock interval沒有重疊。但由于last_lwm_timestamp更新表示事務已經提交,所以等于的時候,該事務也可以執行。
int Mts_submode_logical_clock::schedule_next_event(Relay_log_info* rli,Log_event *ev) {...switch (ev->get_type_code()){case binary_log::GTID_LOG_EVENT:case binary_log::ANONYMOUS_GTID_LOG_EVENT:// TODO: control continuityptr_group->sequence_number= sequence_number=static_cast<Gtid_log_event*>(ev)->sequence_number;ptr_group->last_committed= last_committed=static_cast<Gtid_log_event*>(ev)->last_committed;break;default:sequence_number= last_committed= SEQ_UNINIT;break;}...if (!is_new_group){longlong lwm_estimate= estimate_lwm_timestamp();if (!clock_leq(last_committed, lwm_estimate) && //如果last_committed > lwm_estimaterli->gaq->assigned_group_index != rli->gaq->entry) //當前事務前面還有執行的事務{...if (wait_for_last_committed_trx(rli, last_committed, lwm_estimate))...}...} }@return true when a "<=" b,false otherwise */ static bool clock_leq(longlong a, longlong b) { if (a == SEQ_UNINIT)return true; else if (b == SEQ_UNINIT)return false; elsereturn a <= b; }bool Mts_submode_logical_clock:: wait_for_last_committed_trx(Relay_log_info* rli,longlong last_committed_arg,longlong lwm_estimate_arg) {...my_atomic_store64(&min_waited_timestamp, last_committed_arg);//設置min_waited_timestamp...if ((!rli->info_thd->killed && !is_error) &&!clock_leq(last_committed_arg, get_lwm_timestamp(rli, true)))//真實獲取lwm并檢查當前是否有沖突事務{//循環等待直到沒有沖突事務do{mysql_cond_wait(&rli->logical_clock_cond, &rli->mts_gaq_LOCK);}while ((!rli->info_thd->killed && !is_error) &&!clock_leq(last_committed_arg, estimate_lwm_timestamp())); ... } }上面循環等待的時候,會等待logical_clock_cond條件然后做檢查。該條件的喚醒邏輯是:當回放事務結束,如果存在等待的事務,即檢查min_waited_timestamp和當前curr_lwm(lwm同時會被更新),如果min_waited_timestamp小于等于curr_lwm,則喚醒等待的coordinator線程。
void Slave_worker::slave_worker_ends_group(Log_event* ev, int error) {...if (mts_submode->min_waited_timestamp != SEQ_UNINIT){longlong curr_lwm= mts_submode->get_lwm_timestamp(c_rli, true);//獲取并更新當前lwm。if (mts_submode->clock_leq(mts_submode->min_waited_timestamp, curr_lwm)){/*There's a transaction that depends on the current.*/mysql_cond_signal(&c_rli->logical_clock_cond);}}... }LOGICAL_CLOCK Consistency的分析
無論是Commit-Parent-Based還是Lock-Based,Master端一個事務T1和其commit后才開始的事務T2在Slave端都不會被并發回放,T2一定會等T1執行結束才開始回放。因此LOGICAL_CLOCK并發方式在Slave端只讀時候的上述場景中能夠保證Causal Consistency。但如果事務T2只是等待事務T1執行commit成功后再執行commit操作,那么事務T1和T2在Slave端的執行順序就無法得到保證,用戶在Slave端讀取可能先讀到T2再讀到T1的提交。這種場景就無法滿足Causal Consistency。
slave_preserve_commit_order的簡要介紹
我們在前面的介紹中了解到,當slave_parallel_type為DATABASE和LOGICAL_CLOCK的時候,在Slave端的讀取操作都存在場景無法滿足Causal Consistency,都可能存在Slave端并行回放時候事務順序發生變化。復制進行中時業務方可能會在某一時刻觀察到Slave的GTID_EXECUTED有空洞。那如果業務需要完整的保證Causal Consistency呢,除了使用單線程復制,是否可以在并發回放的情況下滿足這一需求?
MySQL提供了slave_preserve_commit_order,使LOGICAL_CLOCK的并發執行時候獲得Sequential Consistency。這里Sequential Consistency除了滿足之前分析的Causal Consistency的各個場景外,還滿足即使T1T2均并發執行的時候,第三個客戶端在主庫觀察到T1先于T2發生,在備庫也會觀察到T1先于T2發生,即在備庫獲得和主庫完全一致的執行順序。
slave_preserve_commit_order實現的關鍵是添加了Commit_order_manager類,開啟該參數會在獲取worker時候向Commit_order_manager注冊事務。
Slave_worker * Mts_submode_logical_clock::get_least_occupied_worker(Relay_log_info *rli,Slave_worker_array *ws,Log_event * ev) {...if (rli->get_commit_order_manager() != NULL && worker != NULL)rli->get_commit_order_manager()->register_trx(worker);... }void Commit_order_manager::register_trx(Slave_worker *worker) {...queue_push(worker->id);... }在事務進入FLUSH_STAGE前, 會等待前面的事務都進入FLUSH_STAGE。
int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit) {...if (has_commit_order_manager(thd)){Slave_worker *worker= dynamic_cast<Slave_worker *>(thd->rli_slave);Commit_order_manager *mngr= worker->get_commit_order_manager();if (mngr->wait_for_its_turn(worker, all)) //等待前面的事務都進入FLUSH\_STAGE{thd->commit_error= THD::CE_COMMIT_ERROR;DBUG_RETURN(thd->commit_error);}if (change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log))DBUG_RETURN(finish_commit(thd));}... }bool Commit_order_manager::wait_for_its_turn(Slave_worker *worker,bool all) {...mysql_cond_t *cond= &m_workers[worker->id].cond;...while (queue_front() != worker->id){...mysql_cond_wait(cond, &m_mutex);//等待condition} ... }當該事務進入FLUSH_STAGE后,會通知下一個事務的worker可以進入FLUSH_STAGE。
bool Stage_manager::enroll_for(StageID stage, THD *thd, mysql_mutex_t *stage_mutex) {bool leader= m_queue[stage].append(thd);if (stage == FLUSH_STAGE && has_commit_order_manager(thd)){Slave_worker *worker= dynamic_cast<Slave_worker *>(thd->rli_slave);Commit_order_manager *mngr= worker->get_commit_order_manager();mngr->unregister_trx(worker);}... }void Commit_order_manager::unregister_trx(Slave_worker *worker) {...queue_pop();//退出隊列if (!queue_empty())mysql_cond_signal(&m_workers[queue_front()].cond);//喚醒下一個... }在保證binlog flush的順序后,通過binlog_order_commit即可獲取同樣的提交順序。
淺談LOGICAL_CLOCK依然存在的不足
LOGICAL_CLOCK為了準確性和實現的需要,其lock interval實際實現獲得的區間比理論值窄,會導致原本一些可以并發執行的事務在Slave中沒有并發執行。當使用級聯復制的時候,這會后面層級的Slave并發度會越來越小。
實際很多業務中,雖然事務沒有Lock Interval重疊,但這些事務操作的往往是不同的數據行,也不會有鎖沖突,是可以并發執行,但LOGICAL_CLOCK的實現無法使這部分事務得到并發回放。
雖然有上述不足,LOGICAL_CLOCK的復制方式在有多客戶端寫入同樣database的場景中相比DATABASE并行方式能夠獲得很大的復制性能提升,實際場景中很多業務的寫入也都是在一個database下。
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的MySQL 5.7 LOGICAL_CLOCK 并行复制原理及实现分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 迟滞比较器
- 下一篇: 绕月飞行维生系统进展如何?美国人准备好了