GOlang DB連線底層原理解析 === ###### tags: `技術分享` `Golang` # go-sql-driver/mysql 與database/sql 的恩怨情了 <br/><br/> ## Part 00 : 事前準備 #### 建議事項 接下來的說明基本上是追著golang 原始碼在分析,建議一邊看本文一邊看code效果更好。 有使用的package主要有三者: 1. [go-sql-driver/mysql ](https://github.com/go-sql-driver/mysql) 2. [database/sql/sql (底層) ](https://github.com/golang/go/blob/dev.boringcrypto.go1.12/src/database/sql/sql.go) 3. [database/sql/driver (底層) ](https://github.com/golang/go/blob/dev.boringcrypto.go1.12/src/database/sql/driver/driver.go) go-sql-driver/mysql 需自行用go get下載後,底層的package請找自己安裝go的目錄資料夾。 <br/><br/> ## Part 1 :專案內的sql 語法之旅 #### 1-0. 追蹤專案內程式碼執行query語法,實際上是到哪邊去了 ![](https://i.imgur.com/WAibLsq.png) #### 1-1. 呼叫底層的Exec() user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go ![](https://i.imgur.com/qHK7ZqI.png) ::: info 既然可以使用Exec(),表示*DB 是有指向某個實體,否則nil pointer是無法使用任何method。 ::: #### 1-2. package sql 找到DB的定義 user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go ![](https://i.imgur.com/Q8THzw8.png) #### 1-3. 找找看實體化DB 的部分 user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go ![](https://i.imgur.com/OUkuAAm.png) ::: info OpenDB() 裡面初始化了DB,所以從這裡拿到了實體。OpenDB 函式命名為大寫,所以這個method是該package sql 對外開放的method。 由此看來,要使用這包處理DB連線的package,必須先透過呼叫OpenDB(),後續才能進行。 ::: #### 1-4. 誰使用了OpenDB(),是上層的Open() user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go ![](https://i.imgur.com/andtf7N.png) #### 1-5. 回到我們的程式,在建立DB連線時,就使用了底層的database/sql的Open() ![](https://i.imgur.com/ffmJ4jS.png) ![](https://i.imgur.com/qGFKJrh.png) :::danger 那麼這段歷程,明顯只看見底層的database/sql參與,go-sql-driver/mysql 究竟扮演什麼角色?怎麼都沒有它的縱跡? 讓我們繼續看下去。 ::: <br/><br/> ## Part 2 :go-sql-driver/mysql 的參與 #### 2-0. 回到1-4.觀察,我們可以發現driver就是在Open()的時候首次登場 user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go ![](https://i.imgur.com/iMCJat6.png) ::: info 該處有兩個重點 1. 如紅框部分,判斷driver map內是否有符合的driver name,如1-5. 的部分,是傳入"mysql",既然可在這個map找東西,表示在這之前,drivers這個map已經做過初始化,並取儲存了某些資料。 2. 傳入OpenDB()的是connector,已經不是簡單的字串了,而DB{}這個大結構,將connector收納起來,成為DB這個struct實實在在的內部成員之一。 ::: #### 2-1. 接下來探討是什麼時候,drivers 這個map被塞入了資料。 user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go ![](https://i.imgur.com/DrVaGkW.png) ::: info 1. 原來package sql 有提供一個外面呼叫的Register(),而drivers 作為package sql的全域變數,在package 首次import的時候便初始化這個map (drivers = make(map[string]driver.Driver))。 2. 透過Register(),可以將外部的資料塞入這個被準備好的map,所以這個『註冊』的method也如字面上的用意,提供外部註冊一個driver進來。 ::: #### 2-2. 找找看,是不是go-sql-driver/mysql 呼叫了這個Register() go-sql-driver/mysql ![](https://i.imgur.com/O5RFjeo.png) ::: info * 果不其然,在init()做了註冊的動作,換句話說當第一個import go-sql-driver/mysql 進入程式的地方,就將整個套件最主要的實體,MySQLDriver{}整個灌入到database/sql裡面使用。 ::: ![](https://i.imgur.com/aQmdQcV.png) ::: info * 在import的前面加上 _ ,亦即使用套件的init()函式,若非如此沒使用go-sql-driver/mysql 的任何method在編譯時go是不允許的import這套件。 * import 卻不使用,是不被允許,是golang的準則之一。 * 然而go-sql-driver/mysql 的method是注入到database/sql底層做使用。 * 所以造就了一個隱性的需求:你寫的程式不直接使用go-sql-driver/mysql的任何method,卻要讓使用go-sql-driver/mysql的init()有執行。 * 上圖會有如此的import方式,便是如此而來。 ::: #### 2-3. 不是阿貓阿貓都可以當作driver滿足database/sql開出的條件 user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go ![](https://i.imgur.com/dD5TWE0.png) ::: info 傳入Register的第二個參數需要滿足driver.Driver的型態,而仔細一看它是一個interface,定義這個形態,需要擁有Open()這樣的method ::: #### 2-4. go-sql-driver/mysql套件內,MySQLDriver就實踐了Open(),所以作為driver沒問題 go-sql-driver/mysql/driver.go ![](https://i.imgur.com/BKChBT5.png) #### 2-5. 細看這個Open() go-sql-driver/mysql/driver.go ![](https://i.imgur.com/RIPWqE9.png) ::: warning * driver.Conn 是什麼? * driver.Conn 是go底層user/local/Cellar/go/1.12.7/libexec/database/sql/driver.go 定義的interface * 2-3所提到的interface也是來自package driver的定義 * 所以DB連線的業務,go底層需要兩個package 互相合作,sql 和driver * driver 定義各種interface,sql 引用了driver,專注使用driver當中有制定的method。 * 能夠進來參一腳的咖,需要了解driver制定的driver package的規格,才有辦法注入到底層sql做使用。 ::: #### 2-6. 觀察一下得知sql、driver、go-sql-driver/mysql 三者關係 ![](https://i.imgur.com/At7phxn.png) ::: info * driver 被sql 和 go-sql-driver/mysql 做import * sql 被 go-sql-driver/mysql 做import * 所以driver和sql根本不曉得是誰import它們,這樣的設計是合情合理,並且非常必須。 * 試想,如果換成sql server 或 oracle DB 的連線,go底層該如何自處呢? * 所以sql 只管拿著driver定義好的method做執行和管理,不需知道注入進來的mysql內容實際是如何。 ::: #### 2-7. DB連線,究竟是底層database/sql,還是go-sql-driver/mysql實作業務? ![](https://i.imgur.com/fZOjQJn.png) ![](https://i.imgur.com/hb07gfH.png) ::: info * 這就必須好好觀察一下struct的組成結構,如上兩張圖,sql 本身也處理龐大的邏輯業務,如進入的Request控制,通道開關,大體上看來,比較像是總體的連線控管。 * 不過一路找下來,我們知道如果是driver定義出的型態,如圖示紅色框框部分,那麼實際上去執行的部分是外部的package處理。 * 在package sql拿到注入來的實體,雖然不知道長得圓的還扁的,是mysql還是oracle,但知道有哪些method可以用(package driver的interface定義好了),所以就大力用狠狠用。 * 總結起來是各有各自的工作,簡單這樣看來,go-sql-driver/mysql 比較像是處理單一一次下query,Input和Output的部分。而底層sql像是管理全部request和連線的工作。 ::: <br/><br/> # 困擾的bad connection 問題 ## 問題1 ::: success * db.SetConnMaxLifetime(180) 參數錯誤使用 * db.SetConnMaxLifetime(180) 會是等待180ns * db.SetConnMaxLifetime(180 * time.Second) 才是等待180秒 ::: ### 實驗1 db.SetConnMaxLifetime(180)變成設定180 ns ,並非180秒 當所有可用的連線都塞滿,在等待的query request就會耐心依序等待,直到有人做完放出連線 等待時,一但收到有人前面做完的消息,先檢查自己等待是否超過180ns, 若超過這個時間,不管空出的連線可不可用,直接放棄,回傳bad connection 可想而知,悲劇發生了,假設只有100個窗口處理業務,有1萬個人分別散佈在這100個窗口排隊 基本上後面9900個人都在排隊等待,這些排隊的人,希望前面的人180ns可以做完..... 可是,不是說失敗會重試2次嗎? 沒錯,悲劇不夠慘,就不叫悲劇了 前面某個窗口辦完事情,放出連線,不是後面9900的人一起檢查自己是否等待超過180ns 這9900個等待的人,可以看做他們是有抽號碼牌的,第一順位的人才有資格檢查。 因為180ns實在太短,這第一順位的人基本上,檢查會發現超過等待時間,所以放棄,回頭重做 那你知道他在哪裡排隊嗎?隊伍的最後面,重抽號碼牌,號碼牌順位9900號 所以等到他檢查等待時間的時候,早早過了180ns 所以後面9900個人重做2次都會失敗是無可厚非 更正一下,上述是開gorutine去連線DB的狀況,以上述舉例9900個等待空位的人,應該是隨機,而不是抽號碼牌後還有順位關係 抽的是門票,進場後沒有順位, 空出來的連線就猶如台上拋繡球,台下9900個人都有資格接,誰接到誰檢查自己連線等待時間, 這個機制會導致有連線壅塞等待的時候,可能有雖鬼一輩子都接不到繡球,而導致連線卡很久的問題 ``` go // Out of free connections or we were asked not to use one. If we're not // allowed to open any more connections, make a request and wait. if db.maxOpen > 0 && db.numOpen >= db.maxOpen { // Make the connRequest channel. It's buffered so that the // connectionOpener doesn't block while waiting for the req to be read. req := make(chan connRequest, 1) reqKey := db.nextRequestKeyLocked() db.connRequests[reqKey] = req db.waitCount++ db.mu.Unlock() waitStart := time.Now() // Timeout the connection request with the context. select { case <-ctx.Done(): // Remove the connection request and ensure no value has been sent // on it after removing. db.mu.Lock() delete(db.connRequests, reqKey) db.mu.Unlock() atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart))) select { default: case ret, ok := <-req: if ok && ret.conn != nil { db.putConn(ret.conn, ret.err, false) } } return nil, ctx.Err() case ret, ok := <-req: atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart))) if !ok { return nil, errDBClosed } if ret.err == nil && ret.conn.expired(lifetime) { ret.conn.Close() return nil, driver.ErrBadConn } if ret.conn == nil { return nil, ret.err } // Lock around reading lastErr to ensure the session resetter finished. ret.conn.Lock() err := ret.conn.lastErr ret.conn.Unlock() if err == driver.ErrBadConn { ret.conn.Close() return nil, driver.ErrBadConn } return ret.conn, ret.err } } ```