架構與思維:高並發下解決主從延時的一些思路

1 回顧下MySQL主從複製

主從複製,是指建立一個和主數據庫完全一樣的數據庫環境(稱為從數據庫),並將主庫的操作行為進行複製的過程:將主數據庫的DDL和DML的操作日誌同步到從數據庫上,

然後在從數據庫上對這些日誌進行重新執行,來保證從數據庫和主數據庫的數據的一致性。

1.1 為什麼要做主從複製

1、在複雜的業務操作中,經常會有操作導致鎖行甚至鎖表的情況,如果讀寫不解耦,會很影響運行中的業務,使用主從複製,讓主庫負責寫,從庫負責讀。

即使主庫出現了鎖表的情景,通過讀從庫也可以保證業務的正常運行。

2、保證數據的熱備份,主庫宕機後能夠及時替換主庫,保障業務可用性。

3、架構的演進:業務量擴大,I/O訪問頻率增高,單機無法滿足,主從複製可以做多庫方案,降低磁盤I/O訪問的頻率,提高單機的I/O性能。

4、本質上也是分治理念,主從複製、讀寫分離即是壓力分拆的過程。

5、讀寫比也影響整個拆分方式,讀寫比越高,主從庫比例應越高,才能保證讀寫的均衡,才能保證較好的運行性能。讀寫比下的主從分配方法下:

讀寫比(大約) 主庫 從庫
50:50 1 1
66.6:33.3 1 2
80:20 1 4
— — — — — —

 

1.2 主從複製的原理

當在從庫上啟動複製時,首先創建I/O線程連接主庫,主庫隨後創建Binlog Dump線程讀取數據庫事件並發送給I/O線程,I/O線程獲取到事件數據後更新到從庫的中繼日誌Relay Log中去,之後從庫上的SQL線程讀取中繼日誌Relay Log中更新的數據庫事件並應用,

如下圖所示:

  

細化一下有如下幾個步驟:

1、MySQL主庫在事務提交時把數據變更(insert、delet、update)作為事件日誌記錄在二進制日誌表(binlog)裏面。

2、主庫上有一個工作線程 binlog dump thread,把binlog的內容發送到從庫的中繼日誌relay log中。

3、從庫根據中繼日誌relay log重做數據變更操作,通過邏輯複製來達到主庫和從庫的數據一致性。

4、MySQL通過三個線程來完成主從庫間的數據複製,其中binlog dump線程跑在主庫上,I/O線程和SQL線程跑在從庫上。擁有多個從庫的主庫會為每一個連接到主庫的從庫創建一個binlog dump線程。  

1.3 主從延遲的原因

MySQL主從複製讀寫分離是我們常用的數據庫架構,但是在並發量較大、數據變化大的場景下,主從延時會比較嚴重。 

延遲的本質原因是:系統TPS並發較高時,主庫產生的DML(也包含一部分DDL)數量超過Slave一個Sql線程所能承受的範圍,效率就降低了。

我們看到這個sql thread 是單個線程,所以他在重做RelayLog的時候,能力也是有限的。
 

2 幾種解決方案

2.1 最優的系統配置

優化系統配置(系統級、鏈接層、存儲引擎層),讓數據庫處在最優狀態:最大連接數、允許錯誤數、允許超時時間、pool_size、log_size等,保證內存、CPU、存儲空間的擴容(硬件部分)。

倒金字塔法則告訴我們,這一塊往往是被忽略的,但是又是必不可少的。
0 
如果MySQL部署在linux系統上,可以適當調整操作系統的參數來優化MySQL性能,下面是對Linux內核參數進行適當調整。 
 1 # TIME_WAIT超時時間,默認是60s
 2 net.ipv4.tcp_fin_timeout = 30 
 3 # 增加tcp支持的隊列數,加大隊列長度可容納更多的等待連接
 4 net.ipv4.tcp_max_syn_backlog = 65535
 5 # 減少斷開連接時 ,資源回收
 6 net.ipv4.tcp_max_tw_buckets = 8000
 7 net.ipv4.tcp_tw_reuse = 1
 8 net.ipv4.tcp_tw_recycle = 1
 9 net.ipv4.tcp_fin_timeout = 10
10 # 打開文件的限制
11 *soft nofile 65535
12 *hard nofile 65535 
MySQL5.5+版本之後,默認存儲引擎為InnoDB,我們這邊列出部分可能影響數據庫性能的參數。

公共參數默認值:

1 max_connections = 151
2 # 同時處理最大連接數,建議設置最大連接數是上限連接數的80%左右,一般默認值為151,可以做適當調整。
3 sort_buffer_size = 2M
4 # 查詢排序時緩衝區大小,只對order by和group by起作用,建議增大為16M
5 open_files_limit = 1024 
6 # 打開文件數限制,如果show global status like 'open_files'查看的值等於或者大於open_files_limit值時,程序會無法連接數據庫或卡死

InnoDB參數默認值:

 1 innodb_buffer_pool_size = 128M
 2 # 索引和數據緩衝區大小,建議設置物理內存的70%左右(這個前提是這個服務器只用做Mysql數據庫服務器)
 3 innodb_buffer_pool_instances = 1    
 4 # 緩衝池實例個數,推薦設置4個或8個
 5 innodb_flush_log_at_trx_commit = 1  
 6 # 關鍵參數,0代表大約每秒寫入到日誌並同步到磁盤,數據庫故障會丟失1秒左右事務數據。1為每執行一條SQL後寫入到日誌並同步到磁盤,I/O開銷大,執行完SQL要等待日誌讀寫,效率低。2代表只把日誌寫入到系統緩存區,再每秒同步到磁盤,效率很高,如果服務器故障,才會丟失事務數據。對數據安全性要求不是很高的推薦設置2,性能高,修改後效果明顯。
 7 sync_binlog=1
 8 
 9 innodb_file_per_table = ON  
10 # 是否共享表空間,5.7+版本默認ON,共享表空間idbdata文件不斷增大,影響一定的I/O性能。建議開啟獨立表空間模式,每個表的索引和數據都存在自己獨立的表空間中,可以實現單表在不同數據庫中移動。
11 innodb_log_buffer_size = 8M  
12 # 日誌緩衝區大小,由於日誌最長每秒鐘刷新一次,所以一般不用超過16M 

2.2 數據庫層做合理分治

數據庫分區是永恆的話題,主從延遲一定程度上是單台數據庫主服務操作過於頻繁,使得單線程的SQL thread 疲於應付。可以適當的從功能上對數據庫進行拆分,分擔壓力。

數據庫拆分可以參考我的這篇文章《分庫分表》,這邊就不贅述。

2.3  從庫同步完成後響應

假如你的業務時間允許,你可以在寫入主庫的時候,確保數據都同步到從庫了之後才返回這條數據寫入成功,當然如果有多個從庫,你也必須確保每個從庫都寫入成功。當然,這個方案對性能和時間的消耗是極大的,會直接降低你的系統吞吐量,不推薦

 

2.4 適當引入緩存

可以引入redis或者其他nosql數據庫來存儲我們經常會產生主從延遲的業務數據。當我在寫入數據庫的同時,我們再寫入一份到redis中。

讀取數據的時候,我們可以先去查看redis中是否有這個數據,如果有我們就可以直接從redis中讀取這個數據。當數據真正同步到數據庫中的時候,再從redis中把數據刪除。如下圖:

這邊還需注意兩點,很重要喲,面試必問:

1、雖然一定程度上緩解延遲的問題,但如果遇到高並發的情況,對Redis的頻繁刪除也不合理,所以需要結合場景綜合考慮,比如定期刪除緩存。 

2、高並發情況下可能存在slave還沒同步,又有新的值寫進來了,這時候Master –> Slave 還在排隊中,但是Cache已經被更新了。所以如果對Redis進行刪除,可能會誤刪除最新的緩存值,導致讀取到的數據是舊的。

如上圖情況,對一個值分別更新 1,2,3,主從同步按照順序進行,剛同步完1,Cache就更新到3了,這時候如果把Cache刪除了,讀請求就會走到從庫去讀,讀到了1,數據就會出現短暫不一致了。

所以這個地方也需要注意,可以同時將唯一鍵(比如主鍵)也做保存,刪除之前做一個判斷,避免誤刪。或者乾脆不實時刪除緩存,低峰值期再來處理。

2.5 多線程重放RelayLog

MySQL使用單線程重放RelayLog,那能不能在這上面做解法呢,比如使用多線程並行重放RelayLog,就可以縮短時間。但是這個對數據一致性是個考驗。

需要考慮如何分割RelayLog,才能夠讓多個數據庫實例多個線程並行重放RelayLog,不會出現不一致。比如RelayLog包含這三條語句給學生授予學分的記錄,你就不知道結果會變成什麼。可能是806甚至是721。

1 update t_score set score = 721 where stu_code=374532;
2 update t_score set score = 806 where stu_code=374532;
3 update t_score set score = 899 where stu_code=374532;

解法就是:

相同庫表上的寫操作,用相同的線程來重放RelayLog;不同庫表上的寫操作,可以並發用多個線程並發來重放RelayLog。

 

 

設計一個哈希算法,hash(db-name) % thread-num,表名稱hash之後再模上線程數,就能很輕易做到,同一個庫表上的寫操作,被同一個重放線程串行執行,做到提效的目的。

這其實也是一種分治的思維,類似上面直接對數據庫進行拆分。

2.6 少量讀業務直連主庫

業務量不多的情況下,不做主從分離.既然主從延遲是由於從庫同步寫庫不及時引起的,那我們也可以在有主從延遲的地方改變讀庫方式,由原來的讀從庫改為讀主庫。當然這也會增加代碼的一些邏輯複雜性。
這邊需要注意的是,直接讀主庫的業務量不宜多,而且是讀實時一致性有剛性需求的業務才這麼做。否則背離讀寫分離的目的。 

2.7 適當的限流、降級

任何的服務器都是有吞吐量的限制的,沒有任何一個方案可以無限制的承載用戶的大量流量。所以我們必須估算好我們的服務器能夠承載的流量上限是多少。
達到這個上限之後,就要採取緩存,限流,降級的這三大殺招來應對我們的流量。這也是應對主從延遲的根本處理辦法。

3 總結

上面提到了多種方案都是可以討論,每個方法都有弊端和優勢,根據實際情況進行選型。在面試的過程中,也經常遇到候選人跟我討論的。

另外,mysql5.6之後,可以按照庫並行複製。mysql5.7之後,提供了基於GTID並行複製的能力。可以參考學習下。