mysql半同步複製和非同步複製的差別如上述架構圖所示:在mysql非同步複製的情況下,Mysql Master Server將自己的Binary Log通過複製線程傳輸出去以後,Mysql Master Sever就自動返回資料給用戶端,而不管slave上是否接受到了這個二進位日誌。在半同步複製的架構下,當master在將自己binlog發給slave上的時候,要確保slave已經接受到了這個二進位日誌以後,才會返回資料給用戶端。對比兩種架構:非同步複製對於使用者來說,可以確保得到快速的響應結構,但是不能確保二進位日誌確實到達了slave上;半同步複製對於客戶的請求響應稍微慢點,但是他可以保證二進位日誌的完整性。
1.問題背景
預設情況下,線上的mysql複製都是非同步複製,因此在極端情況下,主備切換時,會有一定的機率備庫比主庫資料少,因此切換後,我們會通過工具進行復原回補,確保資料不丟失。半同步複製則要求主庫執行每一個事務,都要求至少一個備庫成功接收後,才真正執行完成,因此可以保持主備庫的強一致性。為了確保主備庫資料強一致,減少資料丟失,嘗試在生產環境中開啟mysql的複製的半同步(semi-sync)特性。實際操作過程中,發現大部分執行個體半同步都可以正常運行,但有少部分執行個體始終開不起來(只能以普通複製方式運行),更奇葩的是同一個主機的兩個執行個體,一個能開啟,一個不能。最終定位的問題也很簡單,但排查出來還是花了一番功夫,下文將描述整個問題的排查過程。
2.半同步複製原理
mysql的主備庫通過binlog日誌保持一致,主庫本地執行完事務,binlog日誌落盤後即返回給使用者;備庫通過拉取主庫binlog日誌來同步主庫的操作。預設情況下,主庫與備庫並沒有嚴格的同步,因此存在一定的機率備庫與主庫的資料是不對等的。半同步特性的出現,就是為了保證在任何時刻主備資料一致的問題。相對於非同步複製,半同步複製要求執行的每一個事務,都要求至少有一個備庫成功接收後,才返回給使用者。實現原理也很簡單,主庫本地執行完畢後,等待備庫的響應訊息(包含最新備庫接收到的binlog(file,pos)),接收到備庫響應訊息後,再返回給使用者,這樣一個事務才算真正完成。在主庫執行個體上,有一個專門的線程(ack_receiver)接收備庫的響應訊息,並以通知機制告知主庫備庫已經接收的日誌,可以繼續執行。有關半同步的具體實現,可以參考另外一篇文章,mysql半同步(semi-sync)源碼實現。
3.問題分析
前面簡單介紹了半同步複製的原理,現在來看看具體問題。在主備庫開啟半同步開關後,問題執行個體的狀態變數"Rpl_semi_sync_master_status"始終是OFF,表示複製一直運行在普通複製的狀態。
(1).修改rpl_semi_sync_master_timeout參數。
半同步複製參數中有一個rpl_semi_sync_master_timeout參數,用以控制主庫等待備庫響應訊息的時間,如果超過該值,則認為備庫一直沒有收到(備庫可能掛了,也可能備庫執行很慢,較主庫相差很遠),這個時候覆制會切換為普通複製,避免主庫的執行事務長時間等待。線上這個值預設是50ms,簡單想是不是這個值太小了,遂將其改到10s,但問題依然不解。
(2).列印日誌
排查問題最簡單最笨的方法就是打日誌,看看到底是哪個環節出了問題。主庫和備庫分別有rpl_semi_sync_master_trace_level和rpl_semi_sync_slave_trace_level參數來控制半同步複製列印日誌。將兩個參數值設定為80(64+16),記錄詳細日誌資訊,以及進出的函數調用。
master:
2016-01-04 18:00:30 13212 [Note] ReplSemiSyncMaster::updateSyncHeader: server(-1721062019), (mysql-bin.000006, 500717950) sync(1), repl(1)
2016-01-04 18:00:40 13212 [Warning] Timeout waiting for reply of binlog (file: mysql-bin.000006, pos: 500717950), semi-sync up to file , position 0.
2016-01-04 18:00:40 13212 [Note] Semi-sync replication switched OFF.
slave:
2016-01-04 18:00:30 38932 [Note] ---> ReplSemiSyncSlave::slaveReply enter
2016-01-04 18:00:30 38932 [Note] ReplSemiSyncSlave::slaveReply: reply (mysql-bin.000006, 500717950)
2016-01-04 18:00:30 38932 [Note] <--- ReplSemiSyncSlave::slaveReply exit (0)
從master日誌可以看到在2016-01-04 18:00:30時,主庫設定了半同步標記,並開始等待備庫的響應,等待10s後,仍然沒有收到響應,則認為逾時,遂將半同步模式關閉,切換為普通模式。但從slave日誌來看,在2016-01-04 18:00:30已經將(mysql-bin.000006, 500717950)發送給主庫,表示已經收到該日誌。這就說明,master日誌已經打了semi-sync標,slave收到了日誌,並且也回了包,master也確實等了10s,就是沒有收到包,所以就切換為普通複製。現在問題就變成了,為什麼master沒有收到?
(3)select函數
前面提到了,主庫執行個體上有一個專門接收響應包的線程(ack_receiver),它通過select函數監聽socket,發現有slave的響應訊息後,讀取訊息,通知背景工作執行緒可以繼續執行。那麼問題是不是出現在select函數上面?因為select是一個系統調用,一直沒有懷疑,但已經跟到這裡來了,那就得看看。與select函數相關的有幾個重要的宏定義和說明。主要實現在/usr/include/bits/typesizes.h,/usr/include/bits/select.h和/usr/include/sys/select.h這三個檔案中。
FD_ZERO(fd_set *fdset):清空fdset與所有檔案控制代碼的聯絡。FD_SET(int fd, fd_set *fdset):建立檔案控制代碼fd與fdset的聯絡。FD_CLR(int fd, fd_set *fdset):清除檔案控制代碼fd與fdset的聯絡。FD_ISSET(int fd, fd_set *fdset):檢查fdset聯絡的檔案控制代碼fd是否可讀寫,當>0表示可讀寫。
array{__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; 1024/64=16 (long int)}fd_set#define __FD_SET_SIZE 1024typedef long int __fd_mask; //8個位元組#define __NFDBITS (8 * (int) sizeof (__fd_mask)) // 64位#define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) //fd%64=N,則在第N位設定為1#define __FDELT(d) ((d) / __NFDBITS) //表示在第幾個long int#define __FDS_BITS(set) ((set)->__fds_bits) #define __FD_SET(d, set) (__FDS_BITS (set)[__FDELT (d)] |= __FDMASK (d))#define __FD_CLR(d, set) (__FDS_BITS (set)[__FDELT (d)] &= ~__FDMASK (d))#define __FD_ISSET(d, set) \((__FDS_BITS (set)[__FDELT (d)] & __FDMASK (d)) != 0)
通過FD_SET可以設定我們想要監聽的控制代碼,控制代碼資訊儲存在fd_set位元組中,數組元素的個數由__FD_SETSIZE/64決定,對於__FD_SETSIZE=1024而言,整個數組只有16個long int。每個控制代碼佔有一個位,就是1024個位,可以儲存1024個控制代碼。假設控制代碼值為138,那麼138/64=2,138%64=10,那麼這個控制代碼在數組的標示在第2個long int的第10位置1。那麼如果控制代碼值超出1024呢,這裡不就溢出了?我仔細擼了擼代碼,發現根本就沒有容錯判斷,如果控制代碼值超過1024就一定會溢出。由於select函數是遍曆數組中的每個位,然後去判斷該控制代碼是否可讀可寫,因此對於超過1024的控制代碼,永遠也不會去判斷,因此主庫永遠不知道備庫是否發送了響應包。
(4)驗證
上面只是理論分析,如果實際啟動並執行執行個體控制代碼確實是超過了1024,那麼問題就定位到了。
1.得到mysql進程mysql-pid
ps –aux | grep mysqld | grep port
2.gdb attach到該進程
gdb –p mysql-pid
3.找到ack_receive線程,並切換
info thread
thread thread_id
4.列印socket的值,這裡fd值為2344。
(5)如何解
我們看到了由於__FD_SETSIZE的定義,一般是1024,導致select函數最多隻能監聽1024個控制代碼,並且最大控制代碼值不超過1024。第一個方法是調大該參數,但這種方法需要重新編譯linux核心。而且由於select機制,每次都需要遍曆 的每一位來判斷控制代碼上是否有訊息到來,因此如果設定很大,將導致效率非常低。select是一種比較老的IO複用機制,比較先進的poll,epoll都有類似的功能,並且更強大,也沒有控制代碼總數和最大控制代碼的限制。有關select,poll,epoll等機制,大家可以去網上查資料,這裡不展開討論。
(6)官方版本
看了最新oracle官方版本git上5.7的原始碼,這塊也是用select來實現的,所以也存在類似的問題。當然,由於控制代碼號有複用機制,當執行個體上串連數很少,或者長串連不多時,不容易出現fd>1024的情況,所以這個bug不是很容易出現,但問題是普遍存在的。
(7)問題延生
問題定位後,另外一個問題還困擾我了半天。因為mysql核心中有監聽的部分有3塊,1是監聽連接埠的select,2是線程池的監聽epoll,3是半同步的select監聽。slave binlog dump的線程就是普通的背景工作執行緒,而背景工作執行緒的socket會受epoll的監聽,這樣一來,binlog dump的socket會同時受半同步的select監聽和線程池的epoll監聽,這不亂了嗎?後來仔細看了看代碼,才發現線程池的epoll監聽採用的是EPOLLONESHOT模式,每次接收訊息後會解除綁定,需要重新註冊,因此不會出現同一個控制代碼被兩種監聽機制同時監聽的情況。
到此,排查問題過程就結束了,結論是比較簡單的,但定位這個問題確實花費了一些功夫。由於select一種比較通用的多路IO複用機制,因此有用到select函數的童鞋,可能要注意下它的限制。