max_execution_time的作用
據(jù)說max_execution_time是mysql5.7.8(沒有實際的驗證過)提供的一個feature棠耕,能夠有效地控制慢查詢响驴,尤其對于數(shù)據(jù)庫性能要求比較高的業(yè)務(wù)場景非常有用。我在mysql和TiDB的生產(chǎn)環(huán)境下,之前都遇到過因為慢查詢消耗過多的機器資源,從而影響生產(chǎn)環(huán)境可用性的情況:
- mysql遇到問題的場景是:我們豐巢的一個基礎(chǔ)服務(wù)采用的mycat分庫分表,有個字段是字符串類型徒恋,但是開發(fā)小哥哥在查詢數(shù)據(jù)的時候傳的值是數(shù)值,造成索引失效欢伏,從而全表掃描入挣。當(dāng)時對于io等資源消耗非常嚴(yán)重,直接影響了生產(chǎn)環(huán)境的使用硝拧;
- TiDB的問題也是一樣的径筏,如果column是字符串,但傳的值是數(shù)值類型障陶,它的執(zhí)行計劃會不斷的讀取TiKV的返回值并在TiDB做運算cast滋恬,當(dāng)時我們用的是千兆網(wǎng)卡,網(wǎng)絡(luò)直接打滿抱究;
雖然我們豐巢針對生產(chǎn)環(huán)境的數(shù)據(jù)庫做了很多的限制恢氯,但總是難免會出現(xiàn)類似于上面提到的問題。在我們之前使用的TiDB版本(2.1.4)是沒有max_execution_time這個特性的鼓寺,當(dāng)時我們?yōu)榱朔乐股厦娴膯栴}再次出現(xiàn)勋拟,還專門的寫了一個TiDB的監(jiān)控模塊,當(dāng)出現(xiàn)高消耗的慢sql時妈候,監(jiān)控程序可以直接kill tidb session來完成「颐遥現(xiàn)在有了max_execution_time,一切都變得簡單了苦银,我們可以在系統(tǒng)變量的級別來設(shè)置max_execution_time的值啸胧,從而限定了query sql的最大執(zhí)行毫秒數(shù)赶站,避免災(zāi)難的發(fā)生。
TiDB max_execution_time初見
我記得TiDB在2.1的某一個小版本里面便引入了max_execution_time的hint語法纺念,但那時沒有實際的作用,看release的介紹烙博,max_execution_time可以在實際環(huán)境中使用焙格,是在2.1.14版本才開始的。如圖:
global 變量方式測試
我只進行了global級別的系統(tǒng)變量的測試,沒有測試hint的方式,因為hint的方式在我們豐巢使用的概率比較低氯窍,兩方面的原因:一是hint的對于sql的改動較大;二是很難完成所有的sql添加此hint,一旦出現(xiàn)漏網(wǎng)之魚并且出了問題布隔,那前面做的工作都沒有啥意義。測試的準(zhǔn)備環(huán)境如下:
- TiDB版本 2.1.15
- TiDB節(jié)點數(shù)量:3
- TiDB開啟binlog服務(wù)pump
- TiKV節(jié)點數(shù)量:12(3臺物理機计济,每臺4個實例)
- PD情況:與TiDB淘衙、pump服務(wù)部署在相同的3臺物理機上毯侦,3個節(jié)點
- 硬件條件:全部是SSD的磁盤
- 負載均衡:nginx1.17.1+stream
測試用例
我的測試用例都比較簡單筝蚕,首先使用數(shù)據(jù)同步工具和豐巢自研的流量錄制和實時回放工具起宽,將生產(chǎn)環(huán)境的實際數(shù)據(jù)實時同步到測試環(huán)境绿映,單表最大行數(shù)在幾十億這個級別叉弦。主要是想測試3種實際的情況:
- 變量max_execution_time的測試:多次設(shè)置global max_execution_time 的值,測試它在不同session的情況下是否實時生效;
- 測試在TiDB端和TiKV端都有大量計算的查詢語句情況,這個測試用例比較容易就可以實現(xiàn),就用上面說的那個把網(wǎng)絡(luò)打滿的生產(chǎn)問題用例即可袱饭,使用傳遞的值為數(shù)字,但是列為字符串的sql語句晾虑,像這樣:select user_id,user_name from test where user_name = 18612345678;
- 測試計算主要是在TiKV端執(zhí)行的語句疹味,其中列user_content沒有索引:select user_id,user_name,user_content from test where user_content = '123456';
測試結(jié)果
變量實時生效結(jié)果測試
我在一開始測試的時候,變量在1秒和10秒之間來回設(shè)置帜篇,如下:
set GLOBAL MAX_EXECUTION_TIME = 10000 //10秒
set GLOBAL MAX_EXECUTION_TIME = 1000 //1秒
發(fā)現(xiàn)我在把最大執(zhí)行時間從1秒切到10秒時糙捺,query被中斷的時間卻還是1秒。經(jīng)過深入的分析笙隙,發(fā)現(xiàn)是global和session級別的原因洪灯,當(dāng)我們設(shè)置了global的變量后,已經(jīng)啟動的session實際用到的變量值逃沿,還是之前的session變量值婴渡,也就是舊值幻锁。這個時候凯亮,如果我們重新打開一個session,MAX_EXECUTION_TIME是生效的哄尔,也就是說新的session會讀取最新的global變量的值假消。那么,這里就需要我們在實際的生產(chǎn)環(huán)境使用的時候要注意岭接,因為大部分的生產(chǎn)環(huán)境都是使用的長連接富拗,session很長時間都不會被關(guān)閉的,因為和我們的預(yù)期值不一致鸣戴,很有可能會帶來生產(chǎn)問題啃沪。關(guān)于TiDB和mysql的session和global的詳細細節(jié),我也沒有深入了解過窄锅,后面有時間會對這塊做個分析创千,我覺得類似于MAX_EXECUTION_TIME這種變量,最好是只有g(shù)lobal和hint兩種級別入偷,否則很容易帶來理解上的混淆以及潛在的生產(chǎn)環(huán)境問題追驴。我猜測TiDB這樣做,是為了要兼容mysql的原因疏之。
TiDB端和TiKV端都有大量計算測試結(jié)果
首先設(shè)置MAX_EXECUTION_TIME為10秒
set GLOBAL MAX_EXECUTION_TIME = 10000
再啟動一個新的連接殿雪,執(zhí)行類似于下面的語句,user_name為字符串類型變量锋爪,test表行數(shù)有一億以上的數(shù)據(jù)
select user_id,user_name from test where user_name = 18612345678;
測試結(jié)果為
> 1317 - Query execution was interrupted
> 時間: 12.31s
從結(jié)果上看丙曙,此種慢查詢的語句爸业,在超過最大執(zhí)行時間后,是可以被TiDB正常的結(jié)束掉的亏镰。
計算主要是在TiKV端執(zhí)行的語句測試結(jié)果
這個測試用例的目的是想看看沃呢,計算已經(jīng)下推導(dǎo)TiKV上的query語句,能不能在超過最大執(zhí)行時間后正常的結(jié)束掉拆挥,還是在超時時間為10秒的情況下薄霜,執(zhí)行下面的語句,如前面說的纸兔,user_content是沒有索引的
select user_id,user_name,user_content from test where user_content = '123456';
測試結(jié)果如下:
> OK
> 時間: 75.91s
測試結(jié)果說明惰瓜,TiDB是無法正常結(jié)束這種計算都是在TiKV上做的語句的,在TiDB判斷了超時時間過后汉矿,是無法通知到TiKV去結(jié)束掉這次計算的崎坊,只能等待TiKV返回結(jié)果后,再做決定洲拇。
源碼分析
大家有興趣奈揍,可以跟隨這個PR,Add support for MAX_EXECUTION_TIME赋续,去詳細的分析源碼男翰,在這里我們來簡單的看一下相關(guān)的源碼。TiDB里面有一個processinfo的存儲空間纽乱,主要是存儲所有session的當(dāng)前執(zhí)行sql的情況蛾绎,我之前還寫過一篇源碼分析show processlist的源碼里面有講到過processinfo的情況。
- 首先我們來看看max_execution_time是如何存儲到processinfo中的:
maxExecutionTime := getMaxExecutionTime(sctx, a.StmtNode)
// Update processinfo, ShowProcess() will use it.
pi.SetProcessInfo(sql, time.Now(), cmd, maxExecutionTime)
a.Ctx.GetSessionVars().StmtCtx.StmtType = GetStmtLabel(a.StmtNode)
代碼在adapter.go的Exec方法中鸦列,主要就是在sql執(zhí)行前租冠,先獲取max_execution_time的實際值,然后存到當(dāng)前session的processinfo存儲空間里面薯嗤。
- getMaxExecutionTime
那么maxExecutionTime的具體值的到底是怎么來的呢顽爹?當(dāng)hint和session同時存在時,優(yōu)先級是如何計算的呢骆姐?
func getMaxExecutionTime(sctx sessionctx.Context, stmtNode ast.StmtNode) uint64 {
ret := sctx.GetSessionVars().MaxExecutionTime
if sel, ok := stmtNode.(*ast.SelectStmt); ok {
for _, hint := range sel.TableHints {
if hint.HintName.L == variable.MaxExecutionTime {
ret = hint.MaxExecutionTime
break
}
}
}
return ret
}
由上面的代碼可知镜粤,hint的優(yōu)先級會高于session的優(yōu)先級,這也符合我們正常的思維方式诲锹。
- 如何kill掉超時的query
最后我們來看看繁仁,TiDB是如何判斷query超時了,并kill掉它的归园,在expensivequery.go中有一個goroutine會不斷的check黄虱,主要邏輯如下:
for {
select {
case <-ticker.C:
processInfo := eqh.sm.ShowProcessList()
for _, info := range processInfo {
if info.Info == nil || info.ExceedExpensiveTimeThresh {
continue
}
costTime := time.Since(info.Time)
if costTime >= time.Second*time.Duration(threshold) && log.GetLevel() <= zapcore.WarnLevel {
logExpensiveQuery(costTime, info)
info.ExceedExpensiveTimeThresh = true
} else if info.MaxExecutionTime > 0 && costTime > time.Duration(info.MaxExecutionTime)*time.Millisecond {
eqh.sm.Kill(info.ID, true)
}
}
threshold = atomic.LoadUint64(&variable.ExpensiveQueryTimeThreshold)
case <-eqh.exitCh:
return
}
}
這個goroutine會通過ShowProcessList不斷的讀取當(dāng)前正在執(zhí)行的sql語句,并判斷costTime是否已經(jīng)超過了之前設(shè)置到processinfo中的MaxExecutionTime庸诱,如果超過了捻浦,則kill掉這條query晤揣。其中的time.Millisecond 也表明了MaxExecutionTime的單位是毫秒。
最后
我個人覺得這個feature對于高并發(fā)的交易型業(yè)務(wù)是非常有必要的朱灿,它是可以作為一個最后的兜底策略昧识。希望pingcap公司后面能在TiKV層面也能支持這個feature,真正的將風(fēng)險降到最低盗扒,我本人對于TiDB是充滿了無限期待的跪楞,希望它能越來越NB。