mysql的主從復制延遲問題--看這一篇就夠了


在之前我們已經講解了一主一從,雙主雙從的mysql集群搭建,在單機應用的時候看起來沒有問題,但是在企業的生產環境中,在很多情況下都會有復制延遲的問題。

​ 主從復制的原理我們在此處就不再贅述了,之前已經講過了,這是一個老生常談的問題,原理性質的也幾乎在面試中問爛了,這些原理性質的東西並不是很難,但是你需要注意了,主從復制的延遲問題會成為一個難點,能非常全面的考驗同學們的技術實力。

1、如何查看同步延遲狀態?

​ 在從服務器上通過 show slave status 查看具體的參數,有幾個參數比較重要:

​ master_log_file: slave中的IO線程正在讀取的主服務器二進制日志文件的名稱

​ read_master_log_pos: 在當前的主服務器二進制日志中,slave中的IO線程已經讀取的位置

​ relay_log_file: sql線程當前正在讀取和執行的中繼日志文件的名稱

​ relay_log_pos: 在當前的中繼日志中,sql線程已經讀取和執行的位置

​ relay_master_log_file: 由sql線程執行的包含多數近期事件的主服務器二進制日志文件的名稱

​ slave_io_running: IO線程是否被啟動並成功的連接到主服務器上

​ slave_sql_running: sql線程是否被啟動

​ seconds_behind_master: 從屬服務器sql線程和從屬服務器IO線程之間的事件差距,單位以秒計

​ 在觀察同步延遲的時候,上述的幾個參數都是比較重要的,其中有一個最最重要的參數需要同學們引起注意,那就是seconds_behind_master,這個參數就表示當前備庫延遲了多長時間,那么這個值是如何計算的呢?

​ 在進行主從復制的時候,需要注意以下幾個關鍵的時刻:

​ 1、主庫A執行完成一個事務,寫入binlog,我們把這個時刻記為T1;

​ 2、之后傳給備庫B,我們把備庫B接受完這個binlog的時刻記為T2;

​ 3、備庫B執行完成這個事務,我們把這個時刻記為T3;

​ 所謂的主備延遲就是同一個事務,在備庫執行完成的時間和主庫執行完成的時間之間的差值,也就是T3-T1。SBM在進行計算的時候也是按照這樣的方式,每個事務的binlog中都有一個時間字段,用於記錄主庫寫入的時間,備庫取出當前正在執行的事務的時間字段的值,計算它與當前系統時間的差值,得到SBM。

​ 如果剛剛的流程聽明白了,那么下面我們就要開始分析產生這個時間差值的原因有哪些了,以方便我們更好的解決生產環境中存在的問題。

2、主從復制延遲產生的原因有哪些?

​ 1、在某些部署環境中,備庫所在的機器性能要比主庫所在的機器性能差。此時如果機器的資源不足的話就會影響備庫同步的效率;

​ 2、備庫充當了讀庫,一般情況下主要寫的壓力在於主庫,那么備庫會提供一部分讀的壓力,而如果備庫的查詢壓力過大的話,備庫的查詢消耗了大量的CPU資源,那么必不可少的就會影響同步的速度

​ 3、大事務執行,如果主庫的一個事務執行了10分鍾,而binlog的寫入必須要等待事務完成之后,才會傳入備庫,那么此時在開始執行的時候就已經延遲了10分鍾了

​ 4、主庫的寫操作是順序寫binlog,從庫單線程去主庫順序讀binlog,從庫取到binlog之后在本地執行。mysql的主從復制都是單線程的操作,但是由於主庫是順序寫,所以效率很高,而從庫也是順序讀取主庫的日志,此時的效率也是比較高的,但是當數據拉取回來之后變成了隨機的操作,而不是順序的,所以此時成本會提高。

​ 5、 從庫在同步數據的同時,可能跟其他查詢的線程發生鎖搶占的情況,此時也會發生延時。

​ 6、 當主庫的TPS並發非常高的時候,產生的DDL數量超過了一個線程所能承受的范圍的時候,那么也可能帶來延遲

​ 7、 在進行binlog日志傳輸的時候,如果網絡帶寬也不是很好,那么網絡延遲也可能造成數據同步延遲

​ 這些就是可能會造成備庫延遲的原因

3、如何解決復制延遲的問題

​ 先說一些虛的東西,什么叫虛的東西呢?就是一聽上去感覺很有道理,但是在實施或者實際的業務場景中可能難度很大或者很難實現,下面我們從幾個方面來進行描述:

1、架構方面

​ 1、業務的持久化層的實現采用分庫架構,讓不同的業務請求分散到不同的數據庫服務上,分散單台機器的壓力

​ 2、服務的基礎架構在業務和mysql之間加入緩存層,減少mysql的讀的壓力,但是需要注意的是,如果數據經常要發生修改,那么這種設計是不合理的,因為需要頻繁的去更新緩存中的數據,保持數據的一致性,導致緩存的命中率很低,所以此時就要慎用緩存了

​ 3、使用更好的硬件設備,比如cpu,ssd等,但是這種方案一般對於公司而言不太能接受,原因也很簡單,會增加公司的成本,而一般公司其實都很摳門,所以意義也不大,但是你要知道這也是解決問題的一個方法,只不過你需要評估的是投入產出比而已。

2、從庫配置方面

​ 1、修改sync_binlog的參數的值

​ 想要合理設置此參數的值必須要清楚的知道binlog的寫盤的流程:

​ 

​ 可以看到,每個線程有自己的binlog cache,但是共用同一份binlog。

​ 圖中的write,指的就是把日志寫入到文件系統的page cache,並沒有把數據持久化到磁盤,所以速度快

​ 圖中的fsync,才是將數據持久化到磁盤的操作。一般情況下,我們認為fsync才占用磁盤的IOPS

​ 而write和fsync的時機就是由參數sync_binlog來進行控制的。

​ 1、當sync_binlog=0的時候,表示每次提交事務都只write,不fsync

​ 2、當sync_binlog=1的時候,表示每次提交事務都執行fsync

​ 3、當sync_binlog=N的時候,表示每次提交事務都write,但積累N個事務后才fsync。

​ 一般在公司的大部分應用場景中,我們建議將此參數的值設置為1,因為這樣的話能夠保證數據的安全性,但是如果出現主從復制的延遲問題,可以考慮將此值設置為100~1000中的某個數值,非常不建議設置為0,因為設置為0的時候沒有辦法控制丟失日志的數據量,但是如果是對安全性要求比較高的業務系統,這個參數產生的意義就不是那么大了。

​ 2、直接禁用salve上的binlog,當從庫的數據在做同步的時候,有可能從庫的binlog也會進行記錄,此時的話肯定也會消耗io的資源,因此可以考慮將其關閉,但是需要注意,如果你搭建的集群是級聯的模式的話,那么此時的binlog也會發送到另外一台從庫里方便進行數據同步,此時的話,這個配置項也不會起到太大的作用。

​ 3、設置innodb_flush_log_at_trx_commit 屬性,這個屬性在我講日志的時候講過,用來表示每一次的事務提交是否需要把日志都寫入磁盤,這是很浪費時間的,一共有三個屬性值,分別是0(每次寫到服務緩存,一秒鍾刷寫一次),1(每次事務提交都刷寫一次磁盤),2(每次寫到os緩存,一秒鍾刷寫一次),一般情況下我們推薦設置成2,這樣就算mysql的服務宕機了,卸載os緩存中的數據也會進行持久化。

4、從根本上解決主從復制的延遲問題

​ 很多同學在自己線上的業務系統中都使用了mysql的主從復制,但是大家需要注意的是,並不是所有的場景都適合主從復制,一般情況下是讀要遠遠多於寫的應用,同時讀的時效性要求不那么高的場景。如果真實場景中真的要求立馬讀取到更新之后的數據,那么就只能強制讀取主庫的數據,所以在進行實現的時候要考慮實際的應用場景,不要為了技術而技術,這是很嚴重的事情。

​ 在mysql5.6版本之后引入了一個概念,就是我們通常說的並行復制,如下圖:

​ 通過上圖我們可以發現其實所謂的並行復制,就是在中間添加了一個分發的環節,也就是說原來的sql_thread變成了現在的coordinator組件,當日志來了之后,coordinator負責讀取日志信息以及分發事務,真正的日志執行的過程是放在了worker線程上,由多個線程並行的去執行。

-- 查看並行的slave的線程的個數,默認是0.表示單線程 show global variables like 'slave_parallel_workers'; -- 根據實際情況保證開啟多少線程 set global slave_parallel_workers = 4; -- 設置並發復制的方式,默認是一個線程處理一個庫,值為database show global variables like '%slave_parallel_type%'; -- 停止slave stop slave; -- 設置屬性值 set global slave_parallel_type='logical_check'; -- 開啟slave start slave -- 查看線程數 show full processlist; 

​ 通過上述的配置可以完成我們說的並行復制,但是此時你需要思考幾個問題

​ 1、在並行操作的時候,可能會有並發的事務問題,我們的備庫在執行的時候可以按照輪訓的方式發送給各個worker嗎?

​ 答案是不行的,因為事務被分發給worker以后,不同的worker就開始獨立執行了,但是,由於CPU的不同調度策略,很可能第二個事務最終比第一個事務先執行,而如果剛剛好他們修改的是同一行數據,那么因為執行順序的問題,可能導致主備的數據不一致。

​ 2、同一個事務的多個更新語句,能不能分給不同的worker來執行呢?

​ 答案是也不行,舉個例子,一個事務更新了表t1和表t2中的各一行,如果這兩條更新語句被分到不同worker的話,雖然最終的結果是主備一致的,但如果表t1執行完成的瞬間,備庫上有一個查詢,就會看到這個事務更新了一半的結果,破壞了事務邏輯的隔離性。

​ 我們通過講解上述兩個問題的最主要目的是為了說明一件事,就是coordinator在進行分發的時候,需要遵循的策略是什么?

​ 1、不能造成更新覆蓋。這就要求更新同一行的兩個事務,必須被分發到同一個worker中。

​ 2、同一個事務不能被拆開,必須放到同一個worker中。

​ 聽完上面的描述,我們來說一下具體實現的原理和過程。

​ 如果讓我們自己來設計的話,我們應該如何操作呢?這是一個值得思考的問題。其實如果按照實際的操作的話,我們可以按照粒度進行分類,分為按庫分發,按表分發,按行分發。

​ 其實不管按照什么方式進行分發,大家需要注意的就是在分發的時候必須要滿足我們上面說的兩條規則,所以當我們進行分發的時候要在每一個worker上定義一個hash表,用來保存當前這個work正在執行的事務所涉及到的表。hash表的key值按照不同的粒度需要存儲不同的值:

​ 按庫分發:key值是數據庫的名字,這個比較簡單

​ 按表分發:key值是庫名+表名

​ 按行分發:key值是庫名+表名+唯一鍵

1、MySQL5.6版本的並行復制策略

​ 其實從mysql的5.6版本開始就已經支持了並行復制,只是支持的粒度是按庫並行,這也是為什么現在的版本中可以選擇類型為database,其實說的就是支持按照庫進行並行復制。

​ 但是其實用過的同學應該都知道,這個策略的並行效果,取決於壓力模型。如果在主庫上有多個DB,並且各個DB的壓力均衡,使用這個策略的效果會很好,但是如果主庫的所有表都放在同一DB上,那么所有的操作都會分發給一個worker,變成單線程操作了,那么這個策略的效果就不好了,因此在實際的生產環境中,用的並不是特別多。

2、mariaDB的並行復制策略

​ 在mysql5.7的時候采用的是基於組提交的並行復制,換句話說,slave服務器的回放與主機是一致的,即主庫是如何並行執行的那么slave就如何怎樣進行並行回放,這點其實是參考了mariaDB的並行復制,下面我們來看下其實現原理。

​ mariaDB的並行復制策略利用的就是這個特性:

​ 1、能夠在同一組里提交的事務,一定不會修改同一行;

​ 2、主庫上可以並行執行的事務,備庫上也一定是可以並行執行的。

​ 在實現上,mariaDB是這么做的:

​ 1、在一組里面一起提交的事務,有一個相同的commit_id,下一組就是commit_id+1;

​ 2、commit_id直接寫到binlog里面;

​ 3、傳到備庫應用的時候,相同commit_id的事務會分發到多個worker執行;

​ 4、這一組全部執行完成后,coordinator再去取下一批。

​ 這是mariaDB的並行復制策略,大體上看起來是沒有問題的,但是你仔細觀察的話會發現他並沒有實現“真正的模擬主庫並發度”這個目標,在主庫上,一組事務在commit的時候,下一組事務是同時處於“執行中”狀態的。

​ 我們真正想要達到的並行復制應該是如下的狀態,也就是說當第一組事務提交的是,下一組事務是運行的狀態,當第一組事務提交完成之后,下一組事務會立刻變成commit狀態。

​ 但是按照mariaDB的並行復制策略,那么備庫上的執行狀態會變成如下所示:

​ 可以看到,這張圖跟上面這張圖的最大區別在於,備庫上執行的時候必須要等第一組事務執行完成之后,第二組事務才能開始執行,這樣系統的吞吐量就不夠了。而且這個方案很容易被大事務拖后腿,如果trx2是一個大事務,那么在備庫應用的時候,trx1和trx3執行完成之后,就只能等trx2完全執行完成,下一組才能開始執行,這段時間,只有一個worker線程在工作,是對資源的浪費。

3、mysql5.7的並行復制策略

​ mysql5.7版本的時候,根據mariaDB的並行復制策略,做了相應的優化調整后,提供了自己的並行復制策略,並且可以通過參數slave-parallel-type來控制並行復制的策略:

​ 1、當配置的值為DATABASE的時候,表示使用5.6版本的按庫並行策略;

​ 2、當配置的值為LOGICAL_CLOCK的時候,表示跟mariaDB相同的策略。

​ 此時,大家需要思考一個問題:同時處於執行狀態的所有事務,是否可以並行?

​ 答案是不行的,因為多個執行中的事務是有可能出現鎖沖突的,鎖沖突之后就會產生鎖等待問題。

​ 在mariaDB中,所有處於commit狀態的事務是可以並行,因為如果能commit的話就說明已經沒有鎖的問題,但是大家回想下,我們mysql的日志提交是兩階段提交,如下圖,其實只要處於prepare狀態就已經表示沒有鎖的問題了。

​ 因此,mysql5.7的並行復制策略的思想是:

​ 1、同時處於prepare狀態的事務,在備庫執行是可以並行的。

​ 2、處於prepare狀態的事務,與處於commit狀態的事務之間,在備庫上執行也是可以並行的。

​ 基於這樣的處理機制,我們可以將大部分的日志處於prepare狀態,因此可以設置

​ 1、binlog_group_commit_sync_delay 參數,表示延遲多少微秒后才調用 fsync;

​ 2、binlog_group_commit_sync_no_delay_count 參數,表示累積多少次以后才調用 fsync。

5、基於GTID的主從復制問題

​ 在我們之前講解的主從復制實操中,每次想要復制,必須要在備機上執行對應的命令,如下所示:

change master to master_host='192.168.85.11',master_user='root',master_password='123456',master_port=3306,master_log_file='master-bin.000001',master_log_pos=154; 

​ 在此配置中我們必須要知道具體的binlog是哪個文件,同時在文件的哪個位置開始復制,正常情況下也沒有問題,但是如果是一個主備主從集群,那么如果主機宕機,當從機開始工作的時候,那么備機就要同步從機的位置,此時位置可能跟主機的位置是不同的,因此在這種情況下,再去找位置就會比較麻煩,所以在5.6版本之后出來一個基於GTID的主從復制。

​ GTID(global transaction id)是對於一個已提交事務的編號,並且是一個全局唯一的編號。GTID實際上是由UUID+TID組成的,其中UUID是mysql實例的唯一標識,TID表示該實例上已經提交的事務數量,並且隨着事務提交單調遞增。這種方式保證事務在集群中有唯一的ID,強化了主備一致及故障恢復能力。

1、基於GTID的搭建

​ 1、修改mysql配置文件,添加如下配置

gtid_mode=on
enforce-gtid-consistency=true

​ 2、重啟主從的服務

​ 3、從庫執行如下命令

change master to master_host='192.168.85.111',master_user='root',master_password='123456' ,master_auto_position=1; 

​ 4、主庫從庫插入數據測試。

2、基於GTID的並行復制

​ 無論是什么方式的主從復制其實原理相差都不是很大,關鍵點在於將組提交的信息存放在GTId中。

show binlog events in 'lian-bin.000001'; 

previous_gtids:用於表示上一個binlog最后一個gtid的位置,每個binlog只有一個。

gtid:當開啟gtid的時候,每一個操作語句執行前會添加一個gtid事件,記錄當前全局事務id,組提交信息被保存在gtid事件中,有兩個關鍵字段,last_committed,sequence_number用來標識組提交信息。

上述日志看起來可能比較麻煩,可以使用如下命令執行:

其中last_committed表示事務提交的時候,上次事務提交的編號,如果事務具有相同的last_committed值表示事務就在一個組內,在備庫執行的時候可以並行執行。同時大家還要注意,每個last_committed的值都是上一個組事務的sequence_number值。

看到此處,大家可能會有疑問,如果我們不開啟gtid,分組信息該如何保存呢?

其實是一樣的,當沒有開啟的時候,數據庫會有一個Anonymous_Gtid,用來保存組相關的信息。

如果大家想看並行的效果的話,可以執行如下代碼:

package com.mashibing; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Date; public class ConCurrentInsert extends Thread{ public void run() { String url = "jdbc:mysql://192.168.85.111/lian2"; String name = "com.mysql.jdbc.Driver"; String user = "root"; String password = "123456"; Connection conn = null; try { Class.forName(name); conn = DriverManager.getConnection(url, user, password);//獲取連接 conn.setAutoCommit(false);//關閉自動提交,不然conn.commit()運行到這句會報錯 } catch (Exception e1) { e1.printStackTrace(); } // 開始時間 Long begin = new Date().getTime(); // sql前綴 String prefix = "INSERT INTO t1 (id,age) VALUES "; try { // 保存sql后綴 StringBuffer suffix = new StringBuffer(); // 設置事務為非自動提交 conn.setAutoCommit(false); // 比起st,pst會更好些 PreparedStatement pst = (PreparedStatement) conn.prepareStatement("");//准備執行語句 // 外層循環,總提交事務次數 for (int i = 1; i <= 10; i++) { suffix = new StringBuffer(); // 第j次提交步長 for (int j = 1; j <= 10; j++) { // 構建SQL后綴 suffix.append("(" +i*j+","+i*j+"),"); } // 構建完整SQL String sql = prefix + suffix.substring(0, suffix.length() - 1); // 添加執行SQL pst.addBatch(sql); // 執行操作 pst.executeBatch(); // 提交事務 conn.commit(); // 清空上一次添加的數據 suffix = new StringBuffer(); } // 頭等連接 pst.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } // 結束時間 Long end = new Date().getTime(); // 耗時 System.out.println("100萬條數據插入花費時間 : " + (end - begin) / 1000 + " s"+" 插入完成"); } public static void main(String[] args) { for (int i = 1; i <=10; i++) { new ConCurrentInsert().start(); } } }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM