我給Apache頂級(jí)項(xiàng)目提了個(gè)Bug
這篇文章記錄了給 Apache 頂級(jí)項(xiàng)目 - 分庫(kù)分表中間件 ShardingSphere 提交 Bug 的歷程。
說(shuō)實(shí)話(huà),這是一次比較曲折的 Bug 跟蹤之旅。10月28日,我們?cè)?GitHub 上提交 issue,中途因?yàn)楣俜介_(kāi)發(fā)者的主觀(guān)臆斷被 Close 了兩次,直到 11 月 20 日才被認(rèn)定成 Bug 并發(fā)出修復(fù)版本,歷時(shí) 20 多天。
1、疑難問(wèn)題的排查思路
2、數(shù)據(jù)庫(kù)中間件 Sharding Proxy 的原理
3、MySQL 預(yù)編譯的流程和交互協(xié)議
4、Wireshark 抓包分析 MySQL 的奇淫技巧
?01 問(wèn)題描述?
這個(gè) Bug 來(lái)源于我的公號(hào)讀者,他替公司預(yù)研 ShardingProxy(屬于 ShardingSphere 的子產(chǎn)品,可用作分庫(kù)分表,后文會(huì)詳細(xì)介紹)。他按照官方文檔寫(xiě)了一個(gè)很簡(jiǎn)單的 demo,但是運(yùn)行后無(wú)法查詢(xún)出數(shù)據(jù)。
下面是他遇到問(wèn)題后發(fā)給我的信息,希望我能幫忙一起定位下原因。
截圖中的 doc 詳細(xì)記錄了 ShardingProxy 的配置、調(diào)試分析日志、以及問(wèn)題的具體現(xiàn)象。
為了方便大家理解,我重新描述下這個(gè) Demo 的業(yè)務(wù)邏輯以及問(wèn)題表象。
1.?Demo 的業(yè)務(wù)邏輯說(shuō)明
這個(gè) Demo 很簡(jiǎn)單,主要為了跑通 ShardingProxy ?的分庫(kù)分表功能。程序用 SpringBoot + MyBatis 實(shí)現(xiàn)了一個(gè)單表的查詢(xún)邏輯,然后用這張表的一個(gè) long 類(lèi)型字段作為分區(qū)鍵,并通過(guò) ShardingProxy 進(jìn)行了分表。
前兩個(gè)字段的作用如下:
-
BIZ_DT:業(yè)務(wù)字段,date類(lèi)型,和Bug有關(guān) -
ECIF_CUST_NO:bigint 類(lèi)型,用做分區(qū)鍵
jdbc:mysql://127.0.0.1:3307/sharding_db?useServerPrepStmts=true&cachePrepStmts=true&serverTimezone=UTC
shardingColumn: ecif_cust_no
algorithmExpression: pscst_prdt_cvr${ecif_cust_no % 2}
2.?問(wèn)題描述
然后啟動(dòng) demo 程序,使用 curl 發(fā)起 post 請(qǐng)求,查詢(xún) ecifCustNo 等于 10000 的那條記錄,居然查詢(xún)不出數(shù)據(jù):
至此,背景基本交代清楚了,為什么數(shù)據(jù)庫(kù)中明明有數(shù)據(jù),但是程序卻查詢(xún)不出來(lái)呢?問(wèn)題到底出現(xiàn)在 ShardingProxy,還是應(yīng)用程序本身?
?02 ShardingProxy 原理簡(jiǎn)介?
ShardingSphere 的目標(biāo)是一個(gè)生態(tài)圈,它由非常著名的 ShardingJDBC、ShardingProxy、ShardingSidecar 3 款獨(dú)立的產(chǎn)品組成。本文重點(diǎn)普及下 ShardingProxy,另外兩個(gè)就不展開(kāi)了。
ShardingProxy 屬于和 MyCat 對(duì)標(biāo)的產(chǎn)品,定位為透明化的數(shù)據(jù)庫(kù)代理端,可以理解成:一個(gè)實(shí)現(xiàn)了 MySQL 協(xié)議的 Server(獨(dú)立進(jìn)程),可用于讀寫(xiě)分離、分庫(kù)分表、柔性事務(wù)等場(chǎng)景。
對(duì)于應(yīng)用程序或者 DBA 來(lái)說(shuō),可以把 ShardingProxy 當(dāng)做數(shù)據(jù)庫(kù)代理,能用 MySQL 客戶(hù)端工具(Navicat)或者命令行和它直接交互,而 ShardingProxy 內(nèi)部則通過(guò) MySQL 原生協(xié)議與真實(shí)的 MySQL 服務(wù)器通信。
圖1:ShardingProxy 的應(yīng)用架構(gòu)圖
從架構(gòu)圖來(lái)看,ShardingProxy 就相當(dāng)于 MySQL,它本身不存儲(chǔ)數(shù)據(jù),但是對(duì)外屏蔽了 Database 的存儲(chǔ)細(xì)節(jié),你可以用連接 MySQL 的方式去連接 ShardingProxy(除了端口不同),用你熟悉的 ORMapping 框架使用它。
再來(lái)看下 ShardingProxy 的內(nèi)部架構(gòu),后續(xù)源碼分析時(shí)會(huì)涉及到此部分。
圖2:ShardingProxy 的內(nèi)部架構(gòu)圖
整個(gè)架構(gòu)分為前端、核心組件和后端:
前端(Frontend)負(fù)責(zé)與客戶(hù)端進(jìn)行網(wǎng)絡(luò)通信,采用的是 NIO 框架,在通信的過(guò)程中完成對(duì)MySQL協(xié)議的編解碼。
核心組件(Core-module)得到解碼的 MySQL 命令后,開(kāi)始調(diào)用 Sharding-Core 對(duì) SQL 進(jìn)行解析、改寫(xiě)、路由、歸并等核心功能。
后端(Backend)與真實(shí)數(shù)據(jù)庫(kù)交互,采用 Hikari 連接池,同樣涉及到 MySQL 協(xié)議的編解碼。
3. ShardingProxy 的預(yù)編譯 SQL 功能
本文的 Bug 跟 ShardingProxy 的預(yù)編譯 SQL 有關(guān),這里單獨(dú)介紹下此功能以及與之相關(guān)的 MySQL 協(xié)議,這個(gè)是本文的關(guān)鍵,請(qǐng)耐心看完。
熟悉數(shù)據(jù)庫(kù)開(kāi)發(fā)的同學(xué)一定了解:預(yù)編譯 SQL(PreparedStatement),在數(shù)據(jù)庫(kù)收到一條 SQL 到執(zhí)行完畢,一般分為以下 3 步:
1、詞法和語(yǔ)義解析
2、優(yōu)化 SQL,制定執(zhí)行計(jì)劃
3、執(zhí)行并返回結(jié)果
SELECT?*?FROM?t_user?WHERE?user_id?=?10;
?03?問(wèn)題分析?
因?yàn)檎麄€(gè)代碼很簡(jiǎn)單,代碼層面唯一有可能存在問(wèn)題的是 Mybatis 這一層。為了確認(rèn)這一點(diǎn),我修改了 SpringBoot 的配置,將 MyBatis 的 debug 日志也打印了出來(lái)。再次發(fā)起請(qǐng)求后,能從控制臺(tái)中看到以下詳細(xì)日志:
jdbc:mysql://127.0.0.1:3306/db1?useServerPrepStmts=true&cachePrepStmts=true&serverTimezone=UTC
通過(guò)這一步,我將懷疑對(duì)象再次轉(zhuǎn)移到 ShardingProxy 上了,并將 dataSource 配置改回成原樣,繼續(xù)排查。
首先,查看 ShardingProxy 的運(yùn)行日志,沒(méi)發(fā)現(xiàn)任何異常;其次,能看到日志中的 Actual SQL 是正確的,它已經(jīng)根據(jù)分區(qū)鍵正確路由到了??pcsct_prdt_cvr0??這張表:
[INFO?]?17:25:48.804?[ShardingSphere-Command-15]?
ShardingSphere-SQL?-?Actual?SQL:?ds_0?:::?SELECT
BIZ_DT,ECIF_CUST_NO,DEP_FLG?...
FROM?pscst_prdt_cvr0
WHERE?ECIF_CUST_NO?=???:::?[10000]
我開(kāi)始懷疑:是否跟 ShardingProxy 所使用的數(shù)據(jù)庫(kù)驅(qū)動(dòng)有關(guān)?因?yàn)檫@個(gè) Jar 包是應(yīng)用方選擇版本,手動(dòng)放到 ShardingProxy 安裝目錄中的。因此,我將驅(qū)動(dòng)版本從 5.1.47 版本改成了 8.0.13 (和 Demo 使用了相同的版本),但是問(wèn)題仍然存在。
另外,還能想到的是:是否是 ShardingProxy 的這個(gè)最新版本引入了 Bug?然后,我又另外安裝了它的上一個(gè)版本 4.1.0,重新測(cè)試了一遍,還是有問(wèn)題。
這個(gè)時(shí)候,真感覺(jué)沒(méi)有其他可疑點(diǎn)了,所有能想到的點(diǎn)都排查了一遍。我再次回到了 Demo 程序本身,它和 ShardingProxy 唯一的結(jié)合點(diǎn)就在 DataSource 的 url 上。
jdbc:mysql://127.0.0.1:3307/sharding_db?useServerPrepStmts=true&cachePrepStmts=true&serverTimezone=UTC
找到這個(gè)問(wèn)題的解決方案后,我同步給了讀者。與此同時(shí),他也在 ShardingProxy ?的 GitHub 上提交了 issue,反饋了這個(gè)最新進(jìn)展。
由于工作原因,這個(gè)問(wèn)題我就暫時(shí)放一邊了,準(zhǔn)備抽空再接著排查。
大概過(guò)了一周我想起了這個(gè)問(wèn)題,然后打開(kāi) issue 想了解下調(diào)查進(jìn)度,讓我非常驚訝的是:官方開(kāi)發(fā)者居然在復(fù)現(xiàn)此問(wèn)題后,主觀(guān)臆斷地認(rèn)為是應(yīng)用程序的問(wèn)題,然后莫名奇妙的把這個(gè) issue 關(guān)閉了,他們的答復(fù)是這樣的:
mysql || tcp.port==3307
?04?根本原因定位?
當(dāng)天晚上,官方開(kāi)發(fā)者就定位到了根本原因,發(fā)出了 Pull Request。我看了下代碼改動(dòng),僅僅修改了一行代碼。
1、為什么代碼拋異常了,但是 ShardingProxy 的控制臺(tái)沒(méi)打印呢?
2、為什么 ShardingProxy 需要做 date 到 Timestamp 的類(lèi)型轉(zhuǎn)換呢?
簡(jiǎn)單理解就是:ShardingProxy 在代碼實(shí)現(xiàn)時(shí),用了一個(gè)范圍最大的 timestamp 存了三種可能的值 date, datetime 和 timestamp,然后再按照上面這個(gè)協(xié)議規(guī)范進(jìn)行二進(jìn)制的寫(xiě)入。
3、這個(gè) Bug 是只有在使用 SQL 預(yù)編譯功能時(shí)才會(huì)被觸發(fā)嗎?
?05?寫(xiě)在最后?
本文詳細(xì)復(fù)盤(pán)了這個(gè) Bug 的分析過(guò)程,并對(duì)其中的原理知識(shí)和排查經(jīng)驗(yàn)進(jìn)行了總結(jié)。
對(duì)于 ShardingSphere 這種頂級(jí)開(kāi)源項(xiàng)目來(lái)說(shuō),我個(gè)人覺(jué)得同樣值得做一次深度復(fù)盤(pán)。我不認(rèn)同他們對(duì)于 issue 的處理方式,另外在核心功能的自動(dòng)化測(cè)試上,也一定是存在 case 不完善的,不然不可能連續(xù)多個(gè)版本都沒(méi)發(fā)現(xiàn)這個(gè)嚴(yán)重 Bug。
哈嘍,我是小林,就愛(ài)圖解計(jì)算機(jī)基礎(chǔ),如果覺(jué)得文章對(duì)你有幫助,歡迎分享給你的朋友,也給小林點(diǎn)個(gè)「在看」,這對(duì)小林非常重要,謝謝你們,給各位小姐姐小哥哥們抱拳了,我們下次見(jiàn)!
推薦閱讀
小小的 float,藏著大大的學(xué)問(wèn)
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀(guān)點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!





