
圖示對比了異步與半同步複制的核心差異:異步模式下,主庫寫入 Binlog 後立即回應客戶端,不保證從庫已接收;半同步模式要求至少一個從庫確認收到 Binlog 事件後,主庫才提交事務並回應。犧牲少量延遲,換取主從強一致性。
線上預設採用異步複制,極端情況(主庫故障切換)下從庫可能缺少最後幾筆事務,需人工回滾補齊。為實現「零資料遺失」,嘗試在生產環境啟用半同步(Semi-sync)。大多數實例正常,但少數實例始終無法啟用(Rpl_semi_sync_master_status=OFF),甚至同一主機上兩個實例一正一異。經深度排查,根因竟是系統級 select() 的 FD_SETSIZE=1024 限制。
ack_receiver 執行緒,透過 select() 監聽從庫 Socket,接收 ACK 封包並通知工作執行緒繼續。rpl_semi_sync_master_timeout(預設 10 秒)控制等待上限,超時自動降級為異步複制。將 rpl_semi_sync_master_timeout 從 50ms 改為 10s,問題依舊,排除網路延遲導致的超時可能。
設定 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 執行緒收不到封包?
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 封包雖到達核心卻永不被讀取。
ps -aux | grep mysqld | grep port 取得 PIDgdb -p 掛載info threads 找到 ack_receiver 執行緒 ID,thread 切換p m_slaves 印出 Socket fd → 實測值為 2344,遠超 1023
鐵證如山:fd=2344 > 1023,select() 監聽陣列覆蓋範圍外,ACK 必失。
select() 需掃描全陣列,fd 越大效能越差,非最佳解。在從庫執行 STOP SLAVE; START SLAVE; 重建連線。新 Socket fd 由核心分配,若 0~1023 區間有空閒 fd,新連線就會落在可監聽範圍內,半同步即可生效。若 0~1023 全被長連線佔用,則無效。
檢視 Oracle MySQL 5.7/8.0 源碼,ack_receiver 仍用 select(),問題普遍存在。但典型部署連線數較少、長連線不多時,fd 不易超 1023,故觸發機率低。
MySQL 內核三處 I/O 多路複用:
select()epoll()(採 EPOLLONESHOT 模式)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,徹底解決擴展性瓶頸。