:Orchestrator+ProxySQL切換全復(fù)盤)
上周線上把 MySQL 5.7 一主三從集群做了一次真實的主庫宕機演練。整套高可用方案使用的是 Orchestrator 負(fù)責(zé)自動化主從選舉與切換ProxySQL 負(fù)責(zé)讀寫分離和入口流量調(diào)度從 kill 掉主庫進程到應(yīng)用側(cè)寫入完全恢復(fù)整個過程大概 14 秒。這篇文章不是功能清單式的介紹而是把這套架構(gòu)從“為什么這么選型”、到一主三從在 TB 級數(shù)據(jù)下怎么初始化、再到故障切換的完整鏈路、以及上線后踩過的坑完整復(fù)盤一遍。如果你正在評估或者準(zhǔn)備落地 OrchestratorProxySQL這篇可以直接當(dāng)實施參考。1. 為什么我沒有選 MHA 或 Keepalived而是 Orchestrator ProxySQL很多朋友一提到 MySQL 高可用第一反應(yīng)就是 MHA或者 Keepalived 掛一個 VIP。這些方案不是不能用只是在 TB 級、一主三從、需要讀寫分離的線上環(huán)境里它們各自的短板會比較明顯。我先把選型過程寫清楚方便你判斷自己的場景是不是適合這套組合。1.1 MHA 方案的問題在哪MHA 的核心能力是在主庫故障時自動找到 relay log 最完整的從庫補全缺失的 binlog 后提升為新主并把其他從庫重新指向它。這套邏輯很成熟早期我也用它搭過不少環(huán)境日常故障切換確實能跑。但放到這套集群里有兩個實際痛點。第一MHA Manager 本身是單點的。雖然可以想辦法做雙機但官方對 Manager 高可用的支持一直比較弱一旦 Manager 所在機器掛了切換就沒人觸發(fā)。第二MHA 只負(fù)責(zé)“數(shù)據(jù)庫內(nèi)部的角色切換”不負(fù)責(zé)應(yīng)用側(cè)流量調(diào)度。切換完成后應(yīng)用連的還是原來的主庫地址如果主庫 IP 變了或者故障節(jié)點還在那要么等著連接超時要么還得靠額外腳本去改應(yīng)用配置。我們這套集群每天讀寫請求量很大不可能接受切換完數(shù)據(jù)庫角色后還要人工把應(yīng)用流量切到新主。1.2 Keepalived VIP 為什么也排除了Keepalived VIP 的思路是把一個虛擬 IP 掛到主庫上主庫掛了 VIP 漂移到從庫。它解決的是“連接地址漂移”的問題但本身對 MySQL 主從狀態(tài)一無所知。直接用 VIP 做主從切換很容易出現(xiàn)一種很尷尬的情況主庫只是 MySQL 進程假死網(wǎng)絡(luò)還在VIP 不漂移但業(yè)務(wù)已經(jīng)寫不進去了或者主庫徹底宕機VIP 漂到一臺 relay log 明顯落后的從庫上丟數(shù)據(jù)的風(fēng)險完全不可控。更重要的是Keepalived 是二層網(wǎng)絡(luò)里做地址漂移的跨機房、跨網(wǎng)段的時候不好用。我們后期計劃把從庫放到另一個機房做異地容災(zāi)單獨靠 VIP 根本覆蓋不了這種拓?fù)洹K鳛橐粋€“前置入口漂移”的輔助手段可以但承擔(dān)不起完整的故障決策。1.3 這套組合的核心分工最后定下來的架構(gòu)很清晰Orchestrator 負(fù)責(zé)決策層ProxySQL 負(fù)責(zé)數(shù)據(jù)面流量調(diào)度。Orchestrator 會持續(xù)監(jiān)控所有 MySQL 實例的健康狀態(tài)、主從關(guān)系、復(fù)制延遲一旦判定主庫不可用自動完成候選從庫選擇、新主提升、其余從庫重指向、舊主恢復(fù)后的收斂這些操作ProxySQL 則獨立于數(shù)據(jù)庫角色去管理應(yīng)用連接通過 hostgroup 和查詢規(guī)則把讀流量和寫流量分流數(shù)據(jù)庫切換后應(yīng)用不需要改任何代碼。為什么這兩個工具組合起來特別適合一主三從因為 Orchestrator 有很成熟的外部 hook 機制切換完成后可以執(zhí)行自定義腳本把新主的信息同步給 ProxySQL而 ProxySQL 的管理接口又允許在運行中動態(tài)修改后端節(jié)點并立即生效。一個負(fù)責(zé)“選誰當(dāng)主”一個負(fù)責(zé)“把流量發(fā)給誰”兩者通過一個腳本串起來切主對應(yīng)用來說是無感的。這套思路后來也成了我們做故障演練的基礎(chǔ)。2. 一主三從初始化TB 級數(shù)據(jù)怎么在不停業(yè)務(wù)的情況下把三臺從庫補起來標(biāo)題里寫了 TB 級這一步必須單獨拿出來講。因為數(shù)據(jù)量一旦到了 TB 級別很多東西跟小庫完全不是一個玩法。比如你不可能用 mysqldump 去導(dǎo)全量數(shù)據(jù)那得導(dǎo)到天荒地老也不可能停庫拷貝數(shù)據(jù)文件業(yè)務(wù)根本不允許。我們當(dāng)時的辦法是用 Percona XtraBackup 2.4 做物理備份從一個現(xiàn)網(wǎng)從庫拉全量再配合 GTID 自動位點追同步。2.1 集群規(guī)劃與基礎(chǔ)參數(shù)先說我用的版本和硬件規(guī)劃方便你對照。MySQL 用的是 5.7.4x一主三從四臺機器都是獨立的物理機磁盤是 SSD 陣列。主庫和三個從庫都開啟 binlogserver_id全局唯一核心參數(shù)大概是這樣[mysqld] server_id 101 gtid_mode ON enforce_gtid_consistency ON log_bin mysql-bin binlog_format ROW log_slave_updates ON read_only ON super_read_only ON主庫那臺要允許寫入所以把read_only和super_read_only注釋掉只保留 binlog 和 GTID 相關(guān)配置。這里有個小細(xì)節(jié)即使是從庫我仍然要求log_slave_updates ON否則從庫不會把從主庫同步過來的事務(wù)寫進自己的 binlogOrchestrator 在后續(xù)切換中很難判斷誰的事務(wù)最完整這個坑我后面會詳細(xì)說。2.2 物理備份同步限速、斷點續(xù)傳和備份校驗TB 級數(shù)據(jù)做初始化最怕的就是備份過程把線上 IO 打滿。我的做法是從一臺從庫拉備份而不是從主庫這樣能盡量減少對主庫的影響。備份命令類似這樣xtrabackup --backup \ --target-dir/data/backup/full_$(date %Y%m%d) \ --host127.0.0.1 \ --userbackup \ --passwordbackuppass \ --slave-info \ --safe-slave-backup \ --throttle200--throttle200是 XtraBackup 自帶的 IO 限速參數(shù)單位是每秒多少次 IO 操作。具體值要根據(jù)線上磁盤負(fù)載去試我們的機器壓到 200 的時候主庫 IO 基本沒明顯波動備份速度大概能跑到每小時 600GB一個 TB 多的全量兩到三個小時能備份完成。備份文件很大不可能直接在本地流式傳輸我一般用xbstream先打包再通過pv限速推到目標(biāo)從庫xtrabackup --backup \ --streamxbstream --target-dir/data/backup/full_xxx \ --throttle200 2/tmp/backup.log | \ pv -L 80m | ssh root10.0.0.12 cat /data/restore/full_xxx.xbstreampv -L 80m把網(wǎng)絡(luò)傳輸限到 80MB/s避免帶寬被占滿影響應(yīng)用。限速的好處是初始化整個過程可以放在業(yè)務(wù)高峰期之外慢慢跑不需要為了趕時間讓線上環(huán)境承擔(dān)風(fēng)險。文件傳過去之后先校驗備份日志確認(rèn)沒有異常再解流xbstream -x -C /data/restore/full_xxx /data/restore/full_xxx.xbstream xtrabackup --prepare --target-dir/data/restore/full_xxx這里必須說一個容易忽略的點如果是從從庫拉的備份去初始化新從庫prepare的時候不需要加--apply-log-only只要不是做增量合并直接準(zhǔn)備到位就行如果后面想在備份基礎(chǔ)之上再合并增量才需要--apply-log-only。準(zhǔn)備完成后用--copy-back把文件放回數(shù)據(jù)目錄調(diào)整屬主后啟動 MySQL。2.3 GTID 建立復(fù)制以及為什么這段必須用 GTID數(shù)據(jù)文件恢復(fù)好以后啟動 MySQL直接從備份集里的xtrabackup_info或xtrabackup_binlog_info拿位點和 GTID 集合。在 GTID 模式下建立復(fù)制的命令比傳統(tǒng) binlog 位置簡潔得多CHANGE MASTER TO MASTER_HOST10.0.0.10, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDreplpass, MASTER_AUTO_POSITION1; START SLAVE; SHOW SLAVE STATUS\G只要Seconds_Behind_Master在持續(xù)下降并且最終接近 0主從就正常了。為什么這里我堅持用 GTID因為后面 Orchestrator 做切換時候選從庫的選擇和其余從庫的重指向都依賴 GTID 自動定位。傳統(tǒng) binlog 位點模式下每次切主都要人去確認(rèn)每個從庫應(yīng)該指向哪個位點錯一步就是數(shù)據(jù)不一致GTID 模式下Orchestrator 可以直接說“誰的事務(wù)集合最完整”新主一確認(rèn)其余從庫MASTER_AUTO_POSITION1就能自動對齊。一主三從這種規(guī)模靠手工維護位點是撐不住的。另外要說一個第一次初始化容易撞上的問題復(fù)制完成后如果從庫的server_uuid和源實例重復(fù)MySQL 會因為 UUID 沖突直接啟動失敗。XtraBackup 在備份時一般不會把auto.cnf一起恢復(fù)但如果你手工拷貝過數(shù)據(jù)文件一定要檢查${datadir}/auto.cnf如果存在且和主庫 UUID 相同直接刪掉再重啟 MySQL讓它重新生成一個新的。這個問題出現(xiàn)過不止一次報錯信息還不明顯容易排查半天。3. Orchestrator 的部署和切換鏈路從發(fā)現(xiàn)故障到發(fā)起選舉Orchestrator 是這套高可用里的“大腦”。它本身不處理業(yè)務(wù)流量只干一件事持續(xù)監(jiān)控拓?fù)淅锼?MySQL 實例發(fā)現(xiàn)主庫故障后自動執(zhí)行恢復(fù)流程。部署本身不復(fù)雜但有幾個設(shè)計點直接決定了切換的可靠性。3.1 Orchestrator 自身的可用性三節(jié)點 Raft 比單機強在哪很多人搭 Orchestrator 只在單機跑一個實例這是比較危險的做法。Orchestrator 自己掛了故障切換就停了和 MHA Manager 單點的問題一樣。我這里是三個節(jié)點組成了 Raft 集群數(shù)據(jù)層用一個獨立的小 MySQL 實例存 Orchestrator 的元數(shù)據(jù)。三節(jié)點 Raft 的核心價值是避免誤判。單個 Orchestrator 節(jié)點可能因為網(wǎng)絡(luò)抖動、負(fù)載高或者其他原因誤以為主庫掛了但集群模式下至少要多數(shù)節(jié)點都判定主庫不可達才會真正觸發(fā)切換。這個設(shè)計對一主三從這種生產(chǎn)環(huán)境非常重要因為誤切換造成的危害往往比不切換還大。你想想如果只是 Orchestrator 到主庫的網(wǎng)絡(luò)閃斷了一下結(jié)果主庫被切掉了全部寫流量瞬間打到一臺從庫上這是完全不能接受的。部署時需要注意三個 Orchestrator 節(jié)點之間的網(wǎng)絡(luò)延遲要低最好在同一個機房的同一網(wǎng)段Raft 數(shù)據(jù)目錄要放到持久化磁盤元數(shù)據(jù)庫不能跟著 Orchestrator 一起掛否則集群雖然活著但無法讀取狀態(tài)。我們?yōu)榱俗屧獢?shù)據(jù)庫也可靠把它的主從狀態(tài)通過同一套監(jiān)控盯起來了屬于基礎(chǔ)設(shè)施的基礎(chǔ)設(shè)施不敢省。3.2 關(guān)鍵配置項里容易踩的部分Orchestrator 的配置文件是/etc/orchestrator.conf.json。下面這段是我實際用的把關(guān)鍵項都標(biāo)出來了{ ListenAddress: :3000, MySQLTopologyUser: orc, MySQLTopologyPassword: orcpass, BackendDB: mysql, BackendDSN: orcmeta:orcmetapasstcp(10.0.0.9:3306)/orchestrator?timeout5s, RaftEnabled: true, RaftDataDir: /var/lib/orchestrator, RaftBind: 10.0.0.7, RaftNodes: [10.0.0.7, 10.0.0.8, 10.0.0.9], DetectClusterAlias: true, RecoverMasterClusterFilters: [*], RecoveryPeriodBlockSeconds: 30, PostMasterFailoverProcesses: [ bash /usr/local/orchestrator/scripts/failover_to_proxy.sh {failureType} {destinationHost} {destinationPort} ] }Orchestrator 用來連接 MySQL 拓?fù)涞馁~號權(quán)限要謹(jǐn)慎給不要直接給 root。我的做法是單獨建orc賬號授權(quán)CREATE USER orc% IDENTIFIED BY orcpass; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT, PROCESS, SUPER ON *.* TO orc%;SUPER權(quán)限是必須的因為 Orchestrator 需要執(zhí)行START SLAVE、STOP SLAVE、CHANGE MASTER TO、修改read_only這類操作。一開始我嘗試去掉這個權(quán)限結(jié)果切換到一半就報權(quán)限不足整個恢復(fù)流程直接卡住后來老老實實加回去了。還有一個重要配置是RecoveryPeriodBlockSeconds。它表示同一集群在一次切換后的多少秒內(nèi)不會再次觸發(fā)恢復(fù)避免頻繁抖動導(dǎo)致集群狀態(tài)反復(fù)橫跳。我設(shè)成了 30 秒實際線上如果舊主恢復(fù)后一直不穩(wěn)這個值可以再調(diào)大比如 60 秒保證有足夠時間人工介入。3.3 故障切換的判定與候選從庫規(guī)則Orchestrator 對主庫的健康檢查用的是 MySQL 原生連接每秒執(zhí)行一次簡單查詢。連續(xù)探測失敗后會進入恢復(fù)流程?;謴?fù)過程大致是確認(rèn)主庫狀態(tài)收集所有從庫的復(fù)制狀態(tài)和 GTID 集合。從候選從庫里選出一個 GTID 集合最完整、復(fù)制延遲最低的實例。停止該從庫的復(fù)制線程執(zhí)行STOP SLAVE等待 relay log 應(yīng)用完畢。把該從庫提升為新主SET GLOBAL read_onlyOFF、SET GLOBAL super_read_onlyOFF。把其他從庫通過CHANGE MASTER TO ... MASTER_AUTO_POSITION1重新指向新主。如果舊主恢復(fù)了嘗試把它也變成新主的從庫并強制設(shè)成只讀防止腦裂。這個鏈條里最關(guān)鍵的是第二步“選候選人”。Orchestrator 默認(rèn)會根據(jù) GTID 集合和延遲來選但實際生產(chǎn)環(huán)境里我們更希望讓指定的一兩臺機器優(yōu)先成為新主而不是隨便選。這可以在實例上設(shè)置 promotion rule 來實現(xiàn)。比如讓10.0.0.11這臺配置最好、和主庫同機房的機器作為首選新主我是通過 Orchestrator 的 web 界面把它的 promotion rule 設(shè)為prefer把一臺性能較差的從庫設(shè)為must_not這樣即使它的 GTID 不是最新Orchestrator 也不會選它當(dāng)主。4. ProxySQL 讀寫分離實現(xiàn)與自動感知切換Orchestrator 解決了“誰是新主”的問題但應(yīng)用側(cè)的流量不會自動知道新主是誰。這就是 ProxySQL 的工作。ProxySQL 作為應(yīng)用和 MySQL 之間的接入層它接收來自應(yīng)用的所有數(shù)據(jù)庫連接再根據(jù) hostgroup 和查詢規(guī)則把語句分發(fā)到具體的后端實例。4.1 ProxySQL 部署后的核心配置ProxySQL 版本 2.x 有兩個端口6032 是管理端口6033 是業(yè)務(wù)端口。我所有配置都是通過管理端口改的這樣可以避免去手動改配置文件再重啟進程。先配置后端節(jié)點INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight, comment) VALUES (10, 10.0.0.10, 3306, 100, writer node); INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight, comment) VALUES (20, 10.0.0.11, 3306, 100, reader node 1); INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight, comment) VALUES (20, 10.0.0.12, 3306, 100, reader node 2); INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight, comment) VALUES (20, 10.0.0.13, 3306, 100, reader node 3);這里我用 hostgroup 10 表示寫組hostgroup 20 表示讀組。配置賬號和路由規(guī)則INSERT INTO mysql_users(username, password, default_hostgroup) VALUES (appuser, apppass, 10); INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES (1, 1, ^SELECT.*FOR UPDATE, 10, 1); INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES (2, 1, ^SELECT, 20, 1); INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES (3, 1, .*, 10, 0);SELECT ... FOR UPDATE必須走寫組因為這種查詢本質(zhì)上是在鎖定行如果放讀組去執(zhí)行鎖的語義就沒了。普通SELECT走讀組剩下的全部走寫組兜底。配置完記得執(zhí)行LOAD MYSQL SERVERS TO RUNTIME、LOAD MYSQL QUERY RULES TO RUNTIME、LOAD MYSQL USERS TO RUNTIME否則不會生效。4.2 讓 ProxySQL 在切換后自動跟著新主走很多教程會提到mysql_replication_hostgroups這個表讓 ProxySQL 通過檢測實例的read_only狀態(tài)自動把節(jié)點分到寫組或讀組。這個方案理論可行但我實際用下來發(fā)現(xiàn)它依賴監(jiān)控線程的掃描周期新主提升后read_onlyOFF和舊主恢復(fù)后被設(shè)成read_onlyON這兩個狀態(tài)變化不一定能立刻被捕捉到期間流量可能短暫走錯節(jié)點。我采用的方式更直接在 Orchestrator 完成主從切換后通過PostMasterFailoverProcesses調(diào)用一個腳本腳本直接改 ProxySQL 里寫主機組的節(jié)點 IP然后立刻加載到 runtime。腳本內(nèi)容大致如下#!/bin/bash # 參數(shù)順序按當(dāng)前版本實際打印參數(shù)為準(zhǔn)首次接入時先打印一次再定 NEW_MASTER$3 NEW_PORT$4 mysql -h 127.0.0.1 -P 6032 -uadmin -padmin -e UPDATE mysql_servers SET hostname${NEW_MASTER}, port${NEW_PORT} WHERE hostgroup_id10; LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK; 你看到的$3、$4的位置要特別謹(jǐn)慎。不同版本的 Orchestrator 傳給PostMasterFailoverProcesses的參數(shù)順序不完全一樣我在本地接腳本時專門打印了所有參數(shù)確認(rèn)新主 IP 和端口在第幾個位置才定下來。如果你裝的是別的版本第一次上線前一定要模擬一次切換把這個地方驗證透。為什么我不寫死舊主 IP因為如果舊主恢復(fù)后被重新納入集群它的 IP 可能還在寫組里但角色已經(jīng)變成從庫。如果不改掉ProxySQL 會把寫流量同時發(fā)給兩個節(jié)點造成腦裂。通過腳本把寫組里的 IP 直接替換成新主 IP舊主即使在mysql_servers表里也只會放在讀組。4.3 應(yīng)用層需要注意的配合ProxySQL 切換后理論上應(yīng)用不需要改任何代碼但連接池里的長連接不會立刻斷開。ProxySQL 在探測到后端不可用后會逐步淘汰這些連接應(yīng)用下一次從連接池取連接時重新經(jīng)歷認(rèn)證和路由自然就走到新主了。為了加快這個收斂過程我在應(yīng)用側(cè)的數(shù)據(jù)庫連接池里把連接存活時間設(shè)成 30 秒以內(nèi)并且開啟空閑連接檢測避免某些老連接一直占著舊主的資源。另外要提醒一點transaction_persistent這個規(guī)則要慎用。我們最初為了解決“事務(wù)內(nèi)先寫后讀被分發(fā)到從庫導(dǎo)致數(shù)據(jù)不一致”的問題給事務(wù)開啟了 persist 模式結(jié)果所有事務(wù)里的讀也被鎖定到寫組讀從庫的利用率反而降下來了。后來在應(yīng)用代碼里強制要求事務(wù)內(nèi)不要混用實時讀這才徹底解決。如果你們的業(yè)務(wù)事務(wù)邏輯比較復(fù)雜先把事務(wù)和讀寫分離的邊界理清再上 ProxySQL否則排查問題會很痛苦。5. 故障演練把主庫直接 kill 掉的完整過程配置再好看不演練等于沒配置。故障演練是所有高可用方案上線前必須做的一步而且不能只在測試環(huán)境做要在業(yè)務(wù)低峰期、有監(jiān)控陪跑的情況下對生產(chǎn)環(huán)境做真實的切換驗證。我們那次演練是在凌晨兩點業(yè)務(wù)量最小但流量入口都是真的。5.1 演練前的準(zhǔn)備和檢查清單演練前我把狀態(tài)檢查做成了一份清單逐項核對過Orchestrator web 界面能看到完整拓?fù)湟恢魅龔臓顟B(tài)正常沒有報錯。三臺從庫SHOW SLAVE STATUS里Seconds_Behind_Master都是 0。ProxySQL 監(jiān)控賬號能正常連到所有后端節(jié)點管理接口能執(zhí)行修改。failover_to_proxy.sh已經(jīng)手動執(zhí)行過一次確認(rèn)腳本本身沒有語法錯誤。應(yīng)用側(cè)連接池參數(shù)已經(jīng)調(diào)好準(zhǔn)備接受短暫的寫連接抖動。監(jiān)控告警里關(guān)于主庫宕機的通知暫時設(shè)成靜默避免大半夜嚇到值班同學(xué)。這里有一項往往被人忽略腳本要先用“假參數(shù)”在測試環(huán)境里跑一遍確認(rèn)能改庫、能加載到 runtime、能 save 到 disk。我第一次寫腳本時直接在生產(chǎn)上跑結(jié)果因為 ProxySQL admin 的密碼在腳本里寫錯了執(zhí)行報錯整個切換鏈路直接斷掉。雖然這是小問題但在真實故障時會變成致命的。5.2 主庫宕機后的時間線和系統(tǒng)行為實際演練時我在主庫上直接執(zhí)行了kill -9模擬最嚴(yán)重的進程級故障。下面是整個時間線時間點事件00:00:00主庫 MySQL 進程被殺應(yīng)用寫請求開始報錯00:00:02Orchestrator 第一次探測失敗進入確認(rèn)階段00:00:05多節(jié)點確認(rèn)主庫不可達觸發(fā)恢復(fù)流程00:00:08候選從庫被選中停止復(fù)制并回放完 relay log00:00:10新主read_only被關(guān)閉其余從庫重新指向新主00:00:12PostMasterFailoverProcesses腳本執(zhí)行ProxySQL 寫組切換到新主00:00:14應(yīng)用新連接全部走到新主寫入恢復(fù)整個切換過程大約 14 秒其中數(shù)據(jù)庫角色切換只花了 8 秒剩下幾秒是等待 relay log 追平和流量刷新。很多系統(tǒng)顯示“秒級切換”實際上說的是探測器發(fā)現(xiàn)問題到選出新主的時間真正要等到 relay log 全部應(yīng)用、ProxySQL 刷新生效業(yè)務(wù)才能恢復(fù)。這個時間受從庫的積壓和硬件性能影響很大像我們這次沒有明顯積壓所以 14 秒完成如果從庫本身延遲好幾秒切換時間也會相應(yīng)拉長。5.3 演練中暴露出的兩個問題第一次演練其實不是一次過的暴露了兩個當(dāng)時沒注意到的問題。第一個問題是其中一臺從庫在切換完成后復(fù)制線程一直顯示NO。原因是它和新主的 GTID 集合不完全匹配Orchestrator 雖然已經(jīng)發(fā)了CHANGE MASTER TO MASTER_AUTO_POSITION1但該從庫的 relay log 里還殘留著一些舊主的事務(wù)這些事務(wù)在 GTID 集合里已經(jīng)存在所以從庫認(rèn)為沒有新事務(wù)要執(zhí)行直接卡住。排查方法是在從庫上執(zhí)行STOP SLAVE; START SLAVE;如果還不行就需要RESET SLAVE強制重新初始化復(fù)制。這里有個原則在 GTID 環(huán)境下不要輕易去手工指定位點否則很容易破壞 GTID 集合的完整性正確做法是讓從庫從新主重新拉全量或者重置復(fù)制狀態(tài)。第二個問題比較隱蔽切換完成后應(yīng)用側(cè)有大量長連接沒有立即感知到新主導(dǎo)致部分寫請求在舊主連接上持續(xù)超時。監(jiān)控里看到這段時間內(nèi)連接池里的borrow失敗率和timeout明顯飆升。后來我在應(yīng)用連接池里配置了連接最大存活時間和連接有效性探測才把這個收斂時間從幾十秒縮短到幾秒。為什么這時候才注意因為測試環(huán)境連接量小根本看不出來連接池回收慢的后果。6. 上線后陸續(xù)踩過的幾個坑以及對應(yīng)的優(yōu)化方案上線幾個月后集群平穩(wěn)運行過也陸續(xù)出現(xiàn)過一些不算致命但很影響體驗的問題。這些經(jīng)驗比搭建過程更值錢單獨列一節(jié)講清楚。6.1 從庫復(fù)制延遲從 100 秒降到 3 秒的調(diào)整TB 級數(shù)據(jù)、讀寫分離后讀從庫很容易被大查詢拖慢進而導(dǎo)致Seconds_Behind_Master居高不下。我一直覺得一主三從里最需要監(jiān)控的不是主庫負(fù)載而是從庫的復(fù)制延遲。一次大促前巡檢我發(fā)現(xiàn)其中一臺從庫延遲已經(jīng)到 100 秒再下去讀寫分離的意義就沒了。排查后原因有兩層。一是 MySQL 5.7 的默認(rèn)配置里從庫復(fù)制是單線程的SQL 線程一個事務(wù)一個事務(wù)地往前推遇到大事務(wù)或 DDL 就卡住。我在從庫上加了并行復(fù)制參數(shù)[mysqld] slave_parallel_type LOGICAL_CLOCK slave_parallel_workers 8 slave_preserve_commit_order ON注意 5.7 的并行復(fù)制只有在binlog_format ROW且主庫開啟了組提交的情況下才有比較明顯的效果。另外slave_preserve_commit_order保證了并行回放時不會因為事務(wù)順序錯亂導(dǎo)致數(shù)據(jù)不一致這個參數(shù)不能省。其次是讀寫分離帶來的讀放大問題。大量的分析型查詢直接打到同一臺從庫上把它的 CPU 打滿了復(fù)制線程吃不到資源。解決方法是把查詢量最大的幾個報表接口單獨分到指定從庫其他常規(guī)讀走另外兩臺從庫之間做按業(yè)務(wù)維度的分流而不是完全隨機負(fù)載均衡。調(diào)整完以后三臺從庫的延遲長期穩(wěn)定在 3 秒以內(nèi)。6.2 半同步復(fù)制要不要上MySQL 5.7 默認(rèn)的復(fù)制是異步的主庫事務(wù)提交成功就返回binlog 還沒送到從庫。這種模式在主庫宕機時理論上可能丟失最后一個事務(wù)。半同步復(fù)制能壓縮這個窗口但也帶來額外的性能開銷。我在實際環(huán)境中是分階段開的先只在主庫和兩臺關(guān)鍵從庫之間開啟半同步觀察了大半個月確認(rèn)沒有明顯寫入性能劣化后才在剩下那臺上同步開啟。半同步配置需要注意參數(shù)[mysqld] plugin-load semisync_master.so;semisync_slave.so rpl_semi_sync_master_enabled 1 rpl_semi_sync_slave_enabled 1 rpl_semi_sync_master_timeout 1000 rpl_semi_sync_master_wait_point AFTER_SYNCrpl_semi_sync_master_timeout設(shè)置成 1000 毫秒很關(guān)鍵。如果從庫確認(rèn)延遲超過這個值主庫會自動退化為異步復(fù)制至少保證寫入不會一直卡死。AFTER_SYNC是比默認(rèn)值更安全的模式它保證在事務(wù)提交到存儲引擎之前就等待從庫確認(rèn)這樣即使主庫馬上宕機事務(wù)也已經(jīng)在從庫的 relay log 里了。網(wǎng)上很多文章只提開啟半同步不提AFTER_SYNC在這個點上容易被帶偏。不過要提醒你半同步不能保證零丟失。主庫已經(jīng)提交但還沒拿到從庫確認(rèn)的那個瞬間如果主庫掛了事務(wù)仍然可能丟失。所以它只能作為降低丟數(shù)據(jù)概率的手段不能替代定期的完整備份和 binlog 備份。6.3 日常巡檢、備份和監(jiān)控切換做得再好數(shù)據(jù)也可能在某個角落出問題。我現(xiàn)在的日常運維節(jié)奏是每周用 XtraBackup 做一次全量備份保留最近四周的備份集每天做一次增量備份保留最近七天。巡檢時用orchestrator -c topology -alias cluster看拓?fù)涫欠褚恢掠肧HOW SLAVE STATUS看延遲重點看有沒有NO狀態(tài)。數(shù)據(jù)一致性用pt-table-checksum分庫分表跑限速參數(shù)開到很低避免給線上帶來壓力。發(fā)現(xiàn)差異后用pt-table-sync修復(fù)修復(fù)前必須先備份相關(guān)表結(jié)構(gòu)。監(jiān)控指標(biāo)主要盯主從延遲、從庫復(fù)制進程狀態(tài)、主庫磁盤空間、ProxySQL 后端健康狀態(tài)、連接池使用率、慢查詢數(shù)量。這套方案跑到現(xiàn)在最讓我放心的不是“主庫掛了自動切”而是每次切換后都知道流量會往哪里走舊主恢復(fù)后不會再被寫入從庫會重新對齊到新主。這種確定性才是高可用方案真正該追求的東西。最后再分享一個小技巧所有和 Orchestrator 掛鉤的切換腳本一定要寫成冪等的。也就是說同一個腳本連續(xù)執(zhí)行兩次要么第二次不產(chǎn)生新影響要么能安全地糾正掉第一次的殘留異常。我的腳本里在UPDATE mysql_servers之前加了判斷如果目標(biāo) IP 已經(jīng)存在就不再重復(fù)執(zhí)行這樣即使 Orchestrator 因為異常重復(fù)觸發(fā)同一個恢復(fù)流程ProxySQL 的配置也不會被寫爛。這些小細(xì)節(jié)看著不起眼但關(guān)鍵時刻能少挨幾次半夜的告警電話。