戰(zhàn):啟動(dòng)鏈路、三層配置與分片表避坑)
簡介本資源為 Mycat2 分布式數(shù)據(jù)庫中間件的基礎(chǔ)安裝包面向需要在本地或服務(wù)器上搭建數(shù)據(jù)庫分片與讀寫分離架構(gòu)的 Java 開發(fā)者和運(yùn)維人員用于解決單機(jī)數(shù)據(jù)庫在高并發(fā)、大數(shù)據(jù)量場景下的擴(kuò)展性瓶頸。壓縮包共 51 個(gè)文件約 1.2MB以 sql 初始化腳本、so/jnilib/a/dll 等多平臺(tái)本地庫、json 與 xml 配置、bat/mycat 啟動(dòng)腳本及 jar 依賴為主覆蓋 Windows、Linux、macOS、AIX、Solaris 等系統(tǒng)的 wrapper 適配文件解壓后按需修改 schemas、datasources、server 等配置即可啟動(dòng)服務(wù)。已有 1072 人學(xué)習(xí)下載可作為理解數(shù)據(jù)分片規(guī)則、SQL 路由、讀寫分離與動(dòng)態(tài)擴(kuò)縮容的實(shí)踐起點(diǎn)包內(nèi)附帶的示例 SQL 與多平臺(tái)啟動(dòng)腳本也便于對(duì)照排查環(huán)境問題、快速驗(yàn)證部署流程。1. 拿到 mycat2 基礎(chǔ)安裝包先別急著 start單體 MySQL 撐到某個(gè)量級(jí)最先出問題的往往不是 SQL 寫得爛而是單表幾千萬行之后加個(gè)索引要鎖半小時(shí)、備份窗口越來越長、主從延遲一抖業(yè)務(wù)就報(bào)警。這時(shí)候團(tuán)隊(duì)通常會(huì)想到分庫分表中間件mycat2 基礎(chǔ)安裝包就是干這個(gè)的它是一個(gè)說 MySQL 協(xié)議的代理層應(yīng)用連它和連 MySQL 幾乎沒有區(qū)別分片路由、讀寫分離、全局表這些邏輯都在代理這一層做掉業(yè)務(wù)側(cè)的 JDBC 連接串改個(gè)端口就能接上去。它適合誰手上已經(jīng)有一套能跑的 MySQL、又不愿意為了分庫分表去改業(yè)務(wù)代碼里那堆 SQL 和連接的團(tuán)隊(duì)。不適合誰指望著裝完就自動(dòng)分片的人——mycat2 的元數(shù)據(jù)是要你自己一條條配出來的數(shù)據(jù)源、集群、邏輯庫、分片表四層?xùn)|西少一層就跑不通。下面按我實(shí)際拆包的順序?qū)懴瓤窗Y(jié)構(gòu)和啟動(dòng)鏈路再講三層配置怎么串然后是分片表落地和那五個(gè)必踩的坑。2. 解壓到第一次連上 8066包結(jié)構(gòu)與啟動(dòng)鏈路拿到包之后第一件事不是敲 start而是把目錄看一遍。mycat2 的運(yùn)行態(tài)很簡單一個(gè) Java 進(jìn)程讀 conf 下的元數(shù)據(jù)對(duì)外開一個(gè) MySQL 協(xié)議端口把 SQL 轉(zhuǎn)發(fā)到后面真實(shí)的 MySQL 實(shí)例。所以它出問題的地方也就那么幾處——JDK 版本、conf 里的 JSON 格式、端口占用、以及后面物理庫連不連得上。把這幾處的檢查順序固定下來后面 debug 能省掉一大半時(shí)間。2.1 四個(gè)目錄各管什么解壓后基本就是 bin、conf、lib、logs 四個(gè)目錄其中 conf 是你以后動(dòng)得最多的地方lib 基本不動(dòng)logs 是每次啟動(dòng)失敗的第一站。目錄/文件作用什么時(shí)候動(dòng)它bin/啟停腳本Linux 下是 mycatWindows 下是 mycat.bat調(diào) JVM 堆大小、換啟動(dòng)用戶conf/server.yaml服務(wù)端口、線程數(shù)這類進(jìn)程級(jí)參數(shù)改端口、壓測(cè)時(shí)調(diào)線程池conf/datasources/每個(gè)物理 MySQL 一個(gè) json加實(shí)例、改連接池大小conf/clusters/主從組寫節(jié)點(diǎn)和讀節(jié)點(diǎn)列表加從庫、調(diào)讀策略conf/schemas/邏輯庫和邏輯表元數(shù)據(jù)加庫、加分片表conf/users/連接代理用的賬號(hào)上線前必須改默認(rèn)密碼lib/依賴 jar 和自帶的算法類換 MySQL 驅(qū)動(dòng)、擴(kuò)分片算法logs/mycat.log 及滾動(dòng)歷史啟動(dòng)失敗、路由異常一個(gè)容易忽略的點(diǎn)conf 下的這些 json 不是參考配置是運(yùn)行時(shí)的唯一真相。你在客戶端里用命令動(dòng)態(tài)建出來的數(shù)據(jù)源、集群、邏輯庫最終也是落到這幾個(gè)目錄里。所以千萬別一邊手改文件、一邊用命令建同名對(duì)象兩邊打架的時(shí)候 reload 會(huì)給你一個(gè)很難看懂的錯(cuò)誤。2.2 JDK、啟動(dòng)腳本與端口mycat2 是 Java 進(jìn)程JDK 版本不匹配的典型表現(xiàn)是進(jìn)程起來幾秒就沒了日志里只有一行看不懂的異常。所以第一步永遠(yuǎn)是確認(rèn) java 版本再解壓。# 1. 確認(rèn) JDK8 或 11 是主流驗(yàn)證過的組合別在第一次跑通前折騰太新的版本 java -version # 2. 解壓到固定目錄路徑里不要帶空格和中文 mkdir -p /opt/mycat2 unzip mycat2-basic.zip -d /opt/mycat2 cd /opt/mycat2 # 3. 先看啟動(dòng)腳本里最終拼出來的 java 命令堆大小和 classpath 都在這 sed -n 1,80p bin/mycat # 4. 啟動(dòng)并查狀態(tài) bin/mycat start bin/mycat status # 5. 確認(rèn)端口真的監(jiān)聽了status 說 RUNNING 不代表端口起來了 ss -lntp | grep 8066這段里第 3 步是關(guān)鍵。啟動(dòng)腳本本質(zhì)上就是拼一條java -cp ... 主類的命令行堆大小、GC 參數(shù)、配置文件路徑都寫在里面。第一次跑不通的時(shí)候把這條命令手工敲一遍去掉后臺(tái)重定向異常會(huì)直接打在終端上比翻日志快得多。堆大小我一般從 2g 起步代理層本身不存數(shù)據(jù)內(nèi)存主要花在連接緩沖和 SQL 解析上給太大反而拖長 GC 停頓。參數(shù)上要盯住兩個(gè)一是-Xmx二是 server.yaml 里的監(jiān)聽地址和端口。默認(rèn)端口是 8066如果這臺(tái)機(jī)器上已經(jīng)有別的實(shí)例改 server.yaml 比改腳本靠譜。啟動(dòng)腳本報(bào)端口已被占用的時(shí)候用ss -lntp | grep 8066找到占用進(jìn)程別急著 kill先確認(rèn)是不是上一次沒停干凈的自己。啟動(dòng)失敗時(shí)不要從頭讀日志幾萬行讀不完# 只看最后 80 行異常棧的根因通常在最后 tail -n 80 logs/mycat.log # 直接把錯(cuò)誤行撈出來Caused by 才是真正的根因 grep -nE ERROR|Exception|Caused by logs/mycat.log | tail -n 30如果 logs 下的 mycat.log 是空的去看同目錄下啟動(dòng)腳本重定向產(chǎn)生的那個(gè)控制臺(tái)日志。這種情況一般是 JVM 在加載類階段就掛了業(yè)務(wù)日志框架還沒來得及初始化所以業(yè)務(wù)日志里什么都看不到——這也是為什么第 3 步手工執(zhí)行那么重要。2.3 用 mysql 客戶端驗(yàn)證代理真的活了進(jìn)程起來、端口監(jiān)聽只能說明它活著不能說明元數(shù)據(jù)是好的。真正的驗(yàn)收標(biāo)準(zhǔn)是能用 MySQL 客戶端連上并且能看到數(shù)據(jù)源列表。# 客戶端版本別太老太老的可能在握手階段就斷連 mysql -h127.0.0.1 -P8066 -uroot -p123456-- 先看庫列表確認(rèn)連的是代理而不是背后某個(gè)物理庫 show databases; -- 再看數(shù)據(jù)源這是管理命令寫在 /* */ 注釋里不是普通 SQL /* mycat:showDataSources{} */; /* mycat:showClusters{} */; /* mycat:showSchemas{} */;這三條命令返回的是代理內(nèi)存里的元數(shù)據(jù)快照能正常返回就說明配置文件解析沒問題、內(nèi)置的 prototype 數(shù)據(jù)源至少加載成功了。如果show databases只有一兩個(gè)庫、和你預(yù)期的對(duì)不上問題一定在 conf/schemas 下不在網(wǎng)絡(luò)。參數(shù)上注意兩點(diǎn)默認(rèn)賬號(hào)密碼在 conf/users 下的 user json 里明文寫著上線前必須改掉并且要把這個(gè)賬號(hào)能訪問的邏輯庫列表一起收窄端口 8066 是服務(wù)端口管理命令走的是同一個(gè)連接不需要另開管理端口這一點(diǎn)和很多老版本中間件不一樣別去找 9066。3. 三層配置模型數(shù)據(jù)源、集群、邏輯庫怎么串起來mycat2 的配置之所以讓新手懵是因?yàn)樗岩慌_(tái)物理庫和一個(gè)邏輯庫徹底拆開了。你要先在數(shù)據(jù)源層描述清楚每臺(tái)真實(shí)的 MySQL 在哪、賬號(hào)是什么、連接池多大再在集群層把若干數(shù)據(jù)源組成一個(gè)主從組說明誰是寫、誰是讀最后在邏輯庫層給應(yīng)用一個(gè)它認(rèn)識(shí)的庫名并掛上這個(gè)庫的表元數(shù)據(jù)。三層缺一層SQL 就沒有完整的路由路徑。3.1 一條 SQL 到物理庫要過幾道關(guān)應(yīng)用連上來的時(shí)候帶的是邏輯庫名和用戶名代理先按 users 里的配置校驗(yàn)賬號(hào)、確認(rèn)這個(gè)賬號(hào)能訪問哪些邏輯庫然后按邏輯庫找到它綁定的集群再按集群找到寫節(jié)點(diǎn)和讀節(jié)點(diǎn)列表最后根據(jù) SQL 是讀還是寫、表是不是分片表決定發(fā)到哪臺(tái)物理庫的哪個(gè)庫名上。這條鏈路里任何一環(huán)名字對(duì)不上報(bào)錯(cuò)都長得很像——基本就是Unknown database或者連接被拒。所以配的時(shí)候有個(gè)笨辦法特別有效三個(gè)名字?jǐn)?shù)據(jù)源名、集群名、邏輯庫名提前寫在紙上全程只用這一套命名別一會(huì)兒 dw0 一會(huì)兒 ds0。我見過不止一次因?yàn)槭侄栋?replicas 里寫了個(gè)不存在的數(shù)據(jù)源名折騰半天以為是網(wǎng)絡(luò)問題。3.2 數(shù)據(jù)源一個(gè) json 就是一臺(tái)物理 MySQL數(shù)據(jù)源配置放在 conf/datasources 下一個(gè)文件一個(gè)實(shí)例。下面是骨架字段名以你包里自帶的樣例文件為準(zhǔn)不同小版本可能有增刪。{ name: dw0, type: JDBC, dbType: mysql, url: jdbc:mysql://127.0.0.1:3306/db1?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai, user: root, password: 123456, minCon: 2, maxCon: 50, maxConnectTimeout: 3000, maxRetryCount: 3, instanceType: READ_WRITE, weight: 0 }這個(gè)文件里真正需要你判斷的只有幾個(gè)字段。name是全局唯一的引用名后面集群里寫的就是它url里一定要帶serverTimezone否則時(shí)間字段跨時(shí)區(qū)會(huì)偏 8 小時(shí)這個(gè)坑排查起來極其費(fèi)勁minCon和maxCon決定這個(gè)實(shí)例上常駐和峰值連接數(shù)把它乘以實(shí)例數(shù)、再乘以集群數(shù)就是你的 MySQL 端要扛的總連接數(shù)max_connections不夠就會(huì)在壓測(cè)時(shí)突然大面積超時(shí)maxConnectTimeout是建連超時(shí)內(nèi)網(wǎng) 3000 毫秒夠用跨機(jī)房要放大maxRetryCount是建連失敗重試次數(shù)從庫不穩(wěn)的時(shí)候它會(huì)救你一命但也會(huì)讓錯(cuò)誤暴露得慢一點(diǎn)。instanceType用來標(biāo)記這個(gè)實(shí)例的角色weight是讀流量權(quán)重多從庫分?jǐn)傋x的時(shí)候用得上。其余字段保持樣例默認(rèn)值就行別一上來就全改一遍——出問題的時(shí)候你不知道是哪個(gè)字段改壞的。3.3 集群masters 和 replicas 決定讀寫去哪集群層是很多人第一次用會(huì)寫錯(cuò)的地方。masters里放的是寫節(jié)點(diǎn)replicas里放的是讀節(jié)點(diǎn)兩個(gè)列表里填的都是數(shù)據(jù)源的name不是主機(jī)名。{ name: c0, clusterType: MASTER_SLAVE, masters: [dw0], replicas: [dw1, dw2], readBalanceType: BALANCE_ALL, switchType: SWITCH, heartbeat: { replicas: SELECT 1, timeout: 1000 } }參數(shù)上有幾個(gè)邊界值得說清楚。masters只有一個(gè)元素時(shí)就是標(biāo)準(zhǔn)主從如果放多個(gè)它表達(dá)的是多個(gè)寫分片路由會(huì)再按分片規(guī)則分配別把它當(dāng)成雙主同時(shí)寫。readBalanceType控制讀請(qǐng)求怎么分?jǐn)偟?replicas全都可用的策略吞吐最好但對(duì)主從延遲最敏感如果業(yè)務(wù)里有寫完立刻查的場景要么把讀策略調(diào)保守要么讓這類查詢走事務(wù)別硬扛。switchType是主庫掛了要不要自動(dòng)切測(cè)試環(huán)境建議關(guān)掉免得排查問題時(shí)主從角色自己變來變?nèi)ト罩径伎床欢?。heartbeat是探活語句和超時(shí)探活語句別寫太重的 SQLSELECT 1足夠。3.4 邏輯庫應(yīng)用看到的那個(gè)庫邏輯庫是應(yīng)用側(cè)唯一感知到的一層名字最好和原來的物理庫名保持一致這樣老代碼里的庫名不用動(dòng)。{ schemaName: shop, targetName: c0, normalTables: {}, globalTables: {}, shardingTables: {}, customTables: {} }targetName指向集群名。下面四個(gè)空對(duì)象是四類表的容器用哪類表就往哪類里填normalTables是不分片的普通表globalTables是每個(gè)節(jié)點(diǎn)都存一份全量數(shù)據(jù)的字典表省份、類目、配置項(xiàng)這種shardingTables是分片表customTables是你要自己接管路由的表。新手最常見的誤操作是把所有表都塞進(jìn)shardingTables結(jié)果字典表被切得七零八落join 的時(shí)候瘋狂跨節(jié)點(diǎn)。3.5 用命令在線建三條 SQL 頂三個(gè)文件除了改文件也可以在客戶端里直接建適合臨時(shí)加一個(gè)從庫驗(yàn)證效果。命令寫在/* */注釋里末尾的分號(hào)不能少。-- 建數(shù)據(jù)源字段和 json 文件里的一一對(duì)應(yīng) /* mycat:createDataSource{ name:dw3, url:jdbc:mysql://127.0.0.1:3307/db1?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai, user:root, password:123456, minCon:2, maxCon:50 } */; -- 建集群只有一個(gè)寫節(jié)點(diǎn)時(shí)replicas 可以留空 /* mycat:createCluster{name:c1,masters:[dw3]} */; -- 建邏輯庫掛到剛建的集群上 /* mycat:createSchema{schemaName:shop2,targetName:c1} */; -- 讓內(nèi)存里的元數(shù)據(jù)重新加載一遍 /* mycat:reload{} */;邏輯上要理解一件事這幾條命令改的是運(yùn)行態(tài)元數(shù)據(jù)同時(shí)會(huì)往 conf 目錄寫對(duì)應(yīng)文件所以它和手改文件是同一個(gè)數(shù)據(jù)源別兩邊同時(shí)改同一個(gè)對(duì)象。參數(shù)上和配置文件完全一致唯一要注意的是 JSON 必須寫在一行內(nèi)或者保證注釋完整透傳——有的客戶端會(huì)幫你把注釋刪掉一刪命令就退化成普通 SQL報(bào)一個(gè)莫名其妙的語法錯(cuò)誤。4. 分片表落地shardingTables 骨架、分片鍵選型與落位驗(yàn)證單庫單表跑通之后真正的工作量在分片表上。分片表的本質(zhì)是一條邏輯表名 一套路由函數(shù) 一組物理節(jié)點(diǎn)三者必須嚴(yán)格對(duì)齊節(jié)點(diǎn)數(shù)要和路由函數(shù)的參數(shù)匹配建表語句要和物理庫里的實(shí)際結(jié)構(gòu)一致。這兩條對(duì)不上表現(xiàn)是數(shù)據(jù)能插進(jìn)去但查不出來或者干脆建表就報(bào)錯(cuò)。4.1 分片表在配置里的骨架分片表掛在邏輯庫 json 的shardingTables下面鍵是邏輯表名。shardingTables: { order_detail: { createTableSQL: CREATE TABLE order_detail (id bigint NOT NULL, order_id bigint NOT NULL, user_id bigint NOT NULL, amount decimal(12,2) DEFAULT NULL, PRIMARY KEY (id), KEY idx_user (user_id)) ENGINEInnoDB DEFAULT CHARSETutf8mb4, function: { clazz: io.mycat.router.mycat1xfunction.PartitionByLong, name: partitionByLong, properties: { partitionCount: 2, partitionLength: 2 } }, shardingNodes: [c0_0.order_detail, c0_1.order_detail] } }四個(gè)字段各有分工。createTableSQL是建表模板代理在初始化時(shí)按它去物理庫建表所以它必須和你線上真實(shí)表結(jié)構(gòu)逐字一致包括字符集和索引——線上表加了個(gè)字段忘了同步這里重啟之后新節(jié)點(diǎn)建出來的表就少一列。function.clazz是路由算法類從 lib 目錄里包里自帶的那批io.mycat.router.開頭的類里選除非你非常清楚自己在做什么別自己寫一個(gè)。function.properties的鍵名跟著算法走取模類算法一般要分片鍵列名和分片數(shù)范圍類算法一般要列名和區(qū)間邊界具體以包里樣例為準(zhǔn)。shardingNodes的寫法要特別注意它是節(jié)點(diǎn)名.表名而節(jié)點(diǎn)名的形式取決于你的集群怎么定義單主從集群里直接寫數(shù)據(jù)源名dw0.order_detail多寫節(jié)點(diǎn)的分片集群里是集群名加序號(hào)c0_0.order_detail、c0_1.order_detail。這兩種寫法在不同小版本、不同集群形態(tài)下不一樣最穩(wěn)的做法是打開 conf/schemas 下自帶的樣例文件照著它的形式寫別憑記憶敲。4.2 分片鍵和算法怎么選選算法之前先看查詢長什么樣別看數(shù)據(jù)長什么樣。分片鍵必須是絕大多數(shù)查詢都會(huì)帶上的那個(gè)條件否則每次查詢都要廣播到所有分片代理層的優(yōu)勢(shì)直接歸零。算法類型適合的分片鍵擴(kuò)容代價(jià)典型翻車點(diǎn)取模/哈?;鶖?shù)高、寫入分散如 user_id、order_id高改分片數(shù)要整體重分布分片鍵為 NULL 或字符串大小寫不一致落到默認(rèn)節(jié)點(diǎn)范圍/區(qū)間有時(shí)間語義如月份、號(hào)段低加區(qū)間即可冷熱不均新數(shù)據(jù)全壓最后一個(gè)分片自定義節(jié)點(diǎn)數(shù)會(huì)頻繁變動(dòng)的場景中算法類改完 reload存量數(shù)據(jù)路由對(duì)不上我自己總結(jié)的三條硬標(biāo)準(zhǔn)第一分片鍵基數(shù)要足夠大性別、狀態(tài)這種字段分片等于沒分第二核心查詢的 where 里必須帶分片鍵報(bào)表類全表掃描的查詢要單獨(dú)規(guī)劃走不走代理第三分片鍵一旦寫入就不要更新跨分片改分片鍵等于一條 update 變兩條跨庫事務(wù)代理層幫不了你。三條都滿足再動(dòng)手配比配完再改省事得多。4.3 三步驗(yàn)證數(shù)據(jù)真的落對(duì)了配置寫完reload 之后必須驗(yàn)證別信沒報(bào)錯(cuò)就是對(duì)的。驗(yàn)證分三步從代理側(cè)寫數(shù)據(jù)、從代理側(cè)讀回來、再到每個(gè)物理庫對(duì)數(shù)。# 第一步和第二步從代理寫入并讀回走的是應(yīng)用的視角 mysql -h127.0.0.1 -P8066 -uroot -p123456 shop -e insert into order_detail(id, order_id, user_id, amount) values (1,1001,1,9.90),(2,1002,2,19.90),(3,1003,3,29.90); select count(*) as proxy_total from order_detail;# 第三步到每個(gè)物理庫分別數(shù)一遍兩邊的和要等于代理上看到的總數(shù) for p in 3306 3307; do mysql -h127.0.0.1 -P$p -uroot -p123456 -N -e \ select $p as port, count(*) from db1.order_detail; done第一段的邏輯是確認(rèn)代理層的讀寫鏈路是通的proxy_total是代理聚合后的結(jié)果。第二段的關(guān)鍵在于分布而不是總數(shù)如果兩個(gè)物理庫各 3 行、但某個(gè)庫里出現(xiàn)了本該屬于另一個(gè)分片的分片鍵說明路由函數(shù)參數(shù)寫錯(cuò)了如果總數(shù)對(duì)、但其中一個(gè)是 0說明shardingNodes只寫了一個(gè)節(jié)點(diǎn)或者分片數(shù)配成了 1。這種錯(cuò)誤不會(huì)報(bào)異常只會(huì)在數(shù)據(jù)量漲起來之后以某個(gè)庫磁盤爆了的形式暴露所以每次改分片配置都要跑一遍。4.4 哪些表不該分片字典表、配置表、類目表這類數(shù)據(jù)量小、讀多寫少、又經(jīng)常被 join 的表放globalTables里每個(gè)節(jié)點(diǎn)存一份全量join 就在本地完成。業(yè)務(wù)主表和它的明細(xì)表如果總是按同一個(gè)維度查訂單和訂單明細(xì)都按 user_id 查把兩張表的shardingNodes一一對(duì)齊保證同一份數(shù)據(jù)落在同一個(gè)節(jié)點(diǎn)join 就不會(huì)跨庫。反過來任何需要跨分片聚合的查詢都要提前想清楚count(*)、sum、深分頁、多表 join在代理層的代價(jià)遠(yuǎn)高于單庫。這些查詢要么在業(yè)務(wù)側(cè)改成按分片鍵查、要么單獨(dú)走一條不經(jīng)過代理的報(bào)表鏈路別指望中間件替你解決。5. 避坑與排查五個(gè)高頻翻車現(xiàn)場下面這五條是我自己在不同環(huán)境里反復(fù)遇到的按現(xiàn)象 → 原因 → 解決寫遇到問題時(shí)可以按順序?qū)φ铡?.1 啟動(dòng)本文還有配套的精品資源點(diǎn)擊獲取