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

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。sql

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並存儲在事務中。app

在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也是,即它們之間並不存在衝突,是能夠併發執行的。ui

針對上面的狀況,爲了進一步增長複製性能,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_committed
        sql_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_number

  if (!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_number
                        cache_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 continuity
    ptr_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_estimate
        rli->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;
else
  return 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併發度會愈來愈小。

>>>>閱讀全文

相關文章
相關標籤/搜索