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語法,實際上是到哪邊去了

#### 1-1. 呼叫底層的Exec()
user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go

::: info
既然可以使用Exec(),表示*DB 是有指向某個實體,否則nil pointer是無法使用任何method。
:::
#### 1-2. package sql 找到DB的定義
user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go

#### 1-3. 找找看實體化DB 的部分
user/local/Cellar/go/1.12.7/libexec/database/sql/sql.go

::: 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

#### 1-5. 回到我們的程式,在建立DB連線時,就使用了底層的database/sql的Open()


:::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

::: 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

::: 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

::: info
* 果不其然,在init()做了註冊的動作,換句話說當第一個import go-sql-driver/mysql 進入程式的地方,就將整個套件最主要的實體,MySQLDriver{}整個灌入到database/sql裡面使用。
:::

::: 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

::: info
傳入Register的第二個參數需要滿足driver.Driver的型態,而仔細一看它是一個interface,定義這個形態,需要擁有Open()這樣的method
:::
#### 2-4. go-sql-driver/mysql套件內,MySQLDriver就實踐了Open(),所以作為driver沒問題
go-sql-driver/mysql/driver.go

#### 2-5. 細看這個Open()
go-sql-driver/mysql/driver.go

::: 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 三者關係

::: 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實作業務?


::: 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
}
}
```