首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> MySQL 資料庫 >> MySQL 綜合教學

MySQL 半同步複制原理深度解析與 FD_SETSIZE 導致監聽失效的排查實戰

     編輯:MySQL 綜合教學

MySQL 半同步複制原理深度解析與 FD_SETSIZE 導致監聽失效的排查實戰

異步 vs 半同步複制架構對比

圖示對比了異步與半同步複制的核心差異:異步模式下,主庫寫入 Binlog 後立即回應客戶端,不保證從庫已接收;半同步模式要求至少一個從庫確認收到 Binlog 事件後,主庫才提交事務並回應。犧牲少量延遲,換取主從強一致性。

一、問題背景

線上預設採用異步複制,極端情況(主庫故障切換)下從庫可能缺少最後幾筆事務,需人工回滾補齊。為實現「零資料遺失」,嘗試在生產環境啟用半同步(Semi-sync)。大多數實例正常,但少數實例始終無法啟用(Rpl_semi_sync_master_status=OFF),甚至同一主機上兩個實例一正一異。經深度排查,根因竟是系統級 select() 的 FD_SETSIZE=1024 限制。

二、半同步複制核心原理

  • 主庫執行事務 → Binlog 落盤 → 等待從庫 ACK → 收到 ACK → 提交事務 → 回應客戶端。
  • 主庫啟動專用 ack_receiver 執行緒,透過 select() 監聽從庫 Socket,接收 ACK 封包並通知工作執行緒繼續。
  • 從庫 IO 執行緒收到 Binlog 事件 → 寫入 Relay Log → 回傳 ACK(含最新 Binlog file/pos)。
  • 超時參數 rpl_semi_sync_master_timeout(預設 10 秒)控制等待上限,超時自動降級為異步複制。

三、問題排查全過程

1. 調大超時時間無效

將 rpl_semi_sync_master_timeout 從 50ms 改為 10s,問題依舊,排除網路延遲導致的超時可能。

2. 開啟追蹤日誌定位斷點

設定 rpl_semi_sync_master_trace_level=80、rpl_semi_sync_slave_trace_level=80(64+16,記錄詳細流程與函數調用)。

# Master 日誌
2016-01-04 18:00:30 [Note] ReplSemiSyncMaster::updateSyncHeader: server(-1721062019), (mysql-bin.000006, 500717950) sync(1), repl(1)
2016-01-04 18:00:40 [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 [Note] Semi-sync replication switched OFF.

# Slave 日誌
2016-01-04 18:00:30 [Note] ---> ReplSemiSyncSlave::slaveReply enter
2016-01-04 18:00:30 [Note] ReplSemiSyncSlave::slaveReply: reply (mysql-bin.000006, 500717950)
2016-01-04 18:00:30 [Note] <--- ReplSemiSyncSlave::slaveReply exit (0)

關鍵發現:從庫已在 18:00:30 發送 ACK,主庫等待 10 秒未收到,判定超時降級。問題轉化為:為何主庫 ack_receiver 執行緒收不到封包?

3. 深入 select() 系統調用與 FD_SETSIZE 限制

ack_receiver 使用 select() 監聽多個從庫 Socket。select() 依賴 fd_set 位陣列,大小由 __FD_SETSIZE 決定(通常為 1024)。

# 核心巨集定義(簡化)
#define __FD_SETSIZE 1024
typedef long int __fd_mask;           // 8 bytes = 64 bits
#define __NFDBITS (8 * sizeof(__fd_mask))  // 64
// fd_set 內部陣列長度 = 1024/64 = 16 個 long int
// 每個 fd 佔 1 bit,支援 fd 0~1023
// 若 fd > 1023,FD_SET 會陣列越界,select() 永不檢查該 bit

若 MySQL 進程打開的檔案描述符(含長連線、Binlog Dump 連線等)超過 1023,ack_receiver 監聽的從庫 Socket fd 可能落在 1024 之後,導致 select() 根本不會輪詢該 fd,ACK 封包雖到達核心卻永不被讀取。

4. GDB 實測驗證 fd 值

  1. ps -aux | grep mysqld | grep port 取得 PID
  2. gdb -p 掛載
  3. info threads 找到 ack_receiver 執行緒 ID,thread 切換
  4. p m_slaves 印出 Socket fd → 實測值為 2344,遠超 1023

GDB 查看 Socket fd=2344

鐵證如山:fd=2344 > 1023,select() 監聽陣列覆蓋範圍外,ACK 必失。

四、解決方案與規避策略

根治方案(需重編或改碼)

  • 擴大 FD_SETSIZE 並重編內核/glibc:但 select() 需掃描全陣列,fd 越大效能越差,非最佳解。
  • 改用 poll()/epoll():無 fd 數量與最大值限制,為現代高併發網路模型標準。需修改 MySQL 原始碼重編。

臨時規避(無需重啟主庫)

在從庫執行 STOP SLAVE; START SLAVE; 重建連線。新 Socket fd 由核心分配,若 0~1023 區間有空閒 fd,新連線就會落在可監聽範圍內,半同步即可生效。若 0~1023 全被長連線佔用,則無效。

官方版本現狀

檢視 Oracle MySQL 5.7/8.0 源碼,ack_receiver 仍用 select(),問題普遍存在。但典型部署連線數較少、長連線不多時,fd 不易超 1023,故觸發機率低。

五、延伸思考:同一 Socket 被 select 與 epoll 雙重監聽?

MySQL 內核三處 I/O 多路複用:

  1. 主監聽埠 select()
  2. 執行緒池 epoll()(採 EPOLLONESHOT 模式)
  3. 半同步 ack_receiver select()

Binlog Dump 執行緒屬於工作執行緒,其 Socket 理應同時被 epoll(執行緒池)與 select(半同步)監聽。實測發現 epoll 採 EPOLLONESHOT:每次讀取後自動解綁,需重新 epoll_ctl(EPOLL_CTL_MOD) 註冊。故同一時刻 Socket 僅被單一機制監聽,不會衝突。

六、總結

半同步複制失效根因是 select() 的 FD_SETSIZE=1024 硬性限制,導致高 fd 值的從庫連線 ACK 無法被感知。生產環境若遭遇「從庫明明回 ACK 卻超時降級」,優先檢查 ack_receiver 監聽的 Socket fd 是否超 1023。長期建議社區推動將 ack_receiver 遷移至 epoll/kqueue,徹底解決擴展性瓶頸。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved