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