enhance graceful stop tidb

測試了一下滾動重啟 tidb 導(dǎo)致事務(wù)失敗的情況谅辣,發(fā)現(xiàn)基本當(dāng)前在跑的事務(wù)基本全會失敗修赞,原因是當(dāng)前 tidb 停止的時候?qū)B接的處理還是比較粗暴。

如何測試

k apply -f tc.yaml 部署一個版本 v5.4.0-pre 的集群 (3 tidb, 1 tikv, 1 pd)桑阶,spec 如下:

apiVersion: pingcap.com/v1alpha1
kind: TidbCluster
metadata:
  name: test
spec:
  version: v5.4.0-pre
  timezone: Asia/Kolkata
  pvReclaimPolicy: Delete
  enableDynamicConfiguration: true
  configUpdateStrategy: RollingUpdate
  discovery: {}
  helper:
    image: busybox:1.34.1
  pd:
    baseImage: pingcap/pd
    maxFailoverCount: 0
    enableDashboardInternalProxy: true
    service:
      type: ClusterIP
      clusterIP: "None"
    replicas: 1
    # if storageClassName is not set, the default Storage Class of the Kubernetes cluster will be used
    # storageClassName: local-storage
    requests:
      storage: "1Gi"
    config: {}
  tikv:
    baseImage: pingcap/tikv
    maxFailoverCount: 0
    evictLeaderTimeout: 1m
    replicas: 1
    requests:
      storage: "10Gi"
    config:
      storage:
        reserve-space: "0MB"
  tidb:
    # just update it to trigger rolling update
    annotations:
      kk: v9
    baseImage: pingcap/tidb
    maxFailoverCount: 0
    replicas: 3
    service:
      type: ClusterIP
    storageVolumes:
      - name: log
        # storageClassName: ${storageClass}
        storageSize: "10Gi"
        mountPath: /var/tidb/log
    config: |
      graceful-wait-before-shutdown = 10
      [log]
      level = "debug"
      [log.file]
      filename = "/var/tidb/log/tidb.log"

寫一個簡單的測試程序柏副,并發(fā) n 個連接割择,不停跑事務(wù), 每個事務(wù)簡單跑兩個 replace 語句萎河。代碼在這里蕉饼。

k apply test.yaml 跑一個 pod 運行測試程序:

apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  containers:
  - name: write
    image: july2993/tk:latest
    command:
      - "/tk"
      - "test"
      - "--host"
      - "test-tidb"
      - "--max-connection"
      - "1000"

隨便修改 tidb spec 的 annotations 觸發(fā)滾動重啟 tidb

...
 tidb:
    # just update it to trigger rolling update
    annotations:
      kk: v99
 ...

重啟完三個 tidb 后失敗事務(wù)個數(shù) 1275

...
[mysql] 2022/02/05 07:00:13 packets.go:123: closing bad idle connection: EOF
[mysql] 2022/02/05 07:00:13 packets.go:37: read tcp 10.244.0.30:56934->10.96.1.38:4000: read: connection reset by peer
[mysql] 2022/02/05 07:00:13 packets.go:123: closing bad idle connection: EOF
...
[2022/02/05 07:00:13.425 +00:00] [ERROR] [test.go:81] ["failed to run txn"] [error="failed to begin txn: invalid connection"]
[mysql] 2022/02/05 07:00:13 packets.go:37: read tcp 10.244.0.30:57754->10.96.1.38:4000: read: connection reset by peer
...
[2022/02/05 07:02:37.508 +00:00] [ERROR] [test.go:81] ["failed to run txn"] [error="failed to exec statement: invalid connection"] [stack="github.com/july2993/tk/cmd.glob..func1.2\n\t/go/src/github.com/july2993/tk/cmd/test.go:81"]
...
totalCount: 1030993, failCount: 1275, totalCountDiff: 2576, failCountDiff: 0

注意下這里有的是在 begin 的時候報錯昧港,有的是 exec statement 的時候報錯创肥。

為什么失敗

為了理解為什么會失敗值朋,我們需要了解下 tidb 是如何處理退出跟 go sql driver 。

tidb 如何處理退出

我們來看下處理 signal 退出的代碼code


func main() {
  //... omit some code
    exited := make(chan struct{})
    signal.SetupSignalHandler(func(graceful bool) {
        svr.Close()
        cleanup(svr, storage, dom, graceful)
        cpuprofile.StopCPUProfiler()
        close(exited)
    })
    topsql.SetupTopSQL()
    terror.MustNil(svr.Run())
    <-exited
    syncLog()
}

這里 graceful 只有 signal 是 QUIT 才會是 true(不明原因為什么這樣), 我們可以先忽略只考慮 false 情況趾代。因為我們都是發(fā) SIGTERM 來停止 tidb, 一定時間后再直接 SIGKILL撒强。

svr.Close() 主要做如下事情 (code):

  1. 設(shè)置 inShutdownMode 為 true, 并等待 s.cfg.GracefulWaitBeforeShutdown糯俗,目的是為了先讓 LB 發(fā)現(xiàn)并且摘掉這個 tidb 睦擂。
  2. Close 掉全部 Listener 拒絕新連接。

cleanup() 主要看最后調(diào)用的 GracefulDwon()

// TryGracefulDown TryGracefulDown will try to gracefully close all connection first with timeout. if timeout, will close all connection directly.
func (s *Server) TryGracefulDown() {
    ctx, cancel := context.WithTimeout(context.Background(), gracefulCloseConnectionsTimeout)
    defer cancel()
    done := make(chan struct{})
    go func() {
        s.GracefulDown(ctx, done)
    }()
    select {
    case <-ctx.Done():
        s.KillAllConnections()
    case <-done:
        return
    }
}

func (s *Server) GracefulDown(ctx context.Context, done chan struct{}) {
    logutil.Logger(ctx).Info("[server] graceful shutdown.")
    metrics.ServerEventCounter.WithLabelValues(metrics.EventGracefulDown).Inc()

    count := s.ConnectionCount()
    for i := 0; count > 0; i++ {
        s.kickIdleConnection()

        count = s.ConnectionCount()
        if count == 0 {
            break
        }
        // Print information for every 30s.
        if i%30 == 0 {
            logutil.Logger(ctx).Info("graceful shutdown...", zap.Int("conn count", count))
        }
        ticker := time.After(time.Second)
        select {
        case <-ctx.Done():
            return
        case <-ticker:
        }
    }
    close(done)
}

GracefulDown()s.kickIdleConnection() 主要是掃一遍 s.clients(維護的全部連接),如果當(dāng)前這個連接不處在一個事務(wù)中它就會 close 掉這個連接臼闻。但是注意它是每一秒檢查一遍,如果一個連接是不停的跑事務(wù)它很可能一直不會 close, 到最后超過 gracefulCloseConnectionsTimeout(15s) 后不管連接當(dāng)前狀態(tài)直接 close 掉連接述呐。前文說的 exec statement 的時候報錯的就都是等到這里直接 close 的。

go sql driver

這里我們使用 driver 是 https://github.com/go-sql-driver/mysql, driver 實現(xiàn)并不自己管理連接池而是 go 的database/sql package 來管理的思犁,driver 實現(xiàn) database/sql/driver 下的一些接口激蹲。實現(xiàn)通過返回 driver.ErrBadConn 來告訴 sql package 這個連接狀態(tài) invalid(比如 server 端 close 掉了連接)江掩, 你需要使用新的連接重試乘瓤。

go-sql-driver/mysql 對連接的檢查邏輯主要在 conncheck.go, 參考 pr924衙傀。做的主要是當(dāng)一個連接在連接池被拿來用的時候第一次執(zhí)行語句的時候先非阻塞讀下這個連接萨咕,如果讀不到任何數(shù)據(jù)并且 err 是 syscall.EAGAIN 或者 syscall.EWOULDBLOCK 這個連接就是正常的,否則返回 ErrBadConn蓄喇。我們重啟的時候部分跑 begin 失敗的事務(wù)就是因為 client 側(cè)還沒感知到我們 server 要 close 或者已經(jīng) close 掉了這個連接交掏,然后拿著連接跑 "START TRANSACTION" 語句的時候失敗了。

原因總結(jié)

可以看到 tidb 嘗試在事務(wù)間 close 掉連接(或者說在連接空閑的時候 close 掉連接)钱骂。一類失敗是 server 端 close 連接跟 client 檢查 連接狀態(tài)之間的 race 導(dǎo)致的挪鹏。一類失敗是 tidb 嘗試 close 掉連接超時后直接 close 全部連接導(dǎo)致的(exec statement 失敗的部分都是)讨盒。

優(yōu)化

每個連接都會有個 gorouting 跑 func (cc *clientConn) Run(ctx context.Context)。 Run 函數(shù)做的注意是不停讀一個 packet(這里指 mysql 協(xié)議的 packet)禀苦,然后處理這個 packet遂鹊。這里我們可以改成讓這個 clientConn 的 Run 自己發(fā)現(xiàn)當(dāng)前要 shutdown 然后自己選擇在合適的時候(就是跑完當(dāng)前事務(wù))close 掉連接,而不是靠外部定時檢查慧邮。這樣我們可以做到每個連接跑完當(dāng)前事務(wù)后馬上 close, 不會出現(xiàn)超時后強制 close 的情況舟陆。但是 begin 失敗的情況,因為這里有 race脓匿,我們 close 連接的時候 client 甚至可能已經(jīng)用了這個連接發(fā)送了開啟事務(wù)的語句并且發(fā)成功了宦赠,這部分我們是沒辦法的米母。

修改后重新測試(代碼在這里) failCount: 642铁瞒,失敗只有之前的不到一半桅滋。為了測試當(dāng)不是很 active 的跑事務(wù)的時候錯誤情況,我們改下測試程序讓每個 goroutin 在 DB 里拿連接跑完一個事務(wù)后 sleep 一秒芍碧,后重新測試:

version failCount
v5.4.0-pre 57
優(yōu)化后版本 0

可以看到優(yōu)化后的版本沒有任何失敽爬(不保證一定不會有失敗)踪危。

一個進一步的可能優(yōu)化是 server 端處理 commit 語句之前就先 close tcp read 側(cè)(ref shutdown TCPConn.CloseREAD), 如果 driver 的 connCheck 實現(xiàn)再檢查下這個連接是否 close read 了那么理論上對于我們的測試程序是可以做到在滾動重啟 tidb 的時候不會有任何失敗的贞远。

總結(jié)

優(yōu)化后的 tidb 可以減少重啟 tidb 導(dǎo)致的 client 端失敗笨忌,在事務(wù)間 close 掉連接,不會出現(xiàn)超過 gracefulCloseConnectionsTimeout 后直接 close 連接的情況(在事務(wù)本身跑的時間不超過 gracefulCloseConnectionsTimeout 的前提下)杂曲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咱揍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌掩完,老刑警劉巖硼砰,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恶阴,居然都是意外死亡,警方通過查閱死者的電腦和手機冯事,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門昵仅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摔笤,你說我怎么就攤上這事“媸觯” “怎么了寞冯?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵吮龄,是天一觀的道長。 經(jīng)常有香客問我母债,道長尝抖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任衙熔,我火速辦了婚禮红氯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘痢甘。我一直安慰自己茉贡,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布放椰。 她就那樣靜靜地躺著,像睡著了一般俗壹。 火紅的嫁衣襯著肌膚如雪藻烤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天涎显,我揣著相機與錄音兴猩,去河邊找鬼。 笑死讨勤,一個胖子當(dāng)著我的面吹牛晨另,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刨晴,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼狈癞,長吁一口氣:“原來是場噩夢啊……” “哼茂契!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起账嚎,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤郭蕉,失蹤者是張志新(化名)和其女友劉穎召锈,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涨岁,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年蹬铺,在試婚紗的時候發(fā)現(xiàn)自己被綠了秉撇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡规阀,死狀恐怖谁撼,靈堂內(nèi)的尸體忽然破棺而出滋饲,到底是詐尸還是另有隱情,我是刑警寧澤屠缭,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布勿她,位于F島的核電站,受9級特大地震影響逢并,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜背稼,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一蟹肘、第九天 我趴在偏房一處隱蔽的房頂上張望俯树。 院中可真熱鬧,春花似錦许饿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赴蝇。三九已至巢掺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間熄阻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工秃殉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钾军,地道東北人绢要。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像樱哼,于是被迫代替她去往敵國和親剿配。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容