mgo的session 與連接池

轉載自:https://cardinfolink.github.io/2017/05/17/mgo-session/

簡介

       mgo是由Golang編寫的開源mongodb驅動。由於mongodb官方並沒有開發Golang驅動,因此這款驅動被廣泛使用。mongodb官網也推薦了這款開源驅動,並且作者在github也表示受到了mongodb官方的贊助。但由於作者的個人安排原因,該驅動的更新、bug修復、issue維護略微受到詬病。

       mgo在功能方面還是比較完善的,api使用也方便。由於mongodb豐富的玩法,mgo代碼龐大,其中大部分是與mongodb的協議代碼。核心的處理連接和請求的結構,邏輯上還是比較清晰的。

簡單的使用

func dial() {    session ,_ := mgo.Dial("mongodb://127.0.0.1") }

mgo面向調用者的核心數據結構是mgo.Session,dial函數演示瞭如何獲取一個session

func foo1() {    session.DB("test").C("coll").Insert(bson.M{"name":"zhangsan"}) }

foo1函數通過生成的session,向test數據庫的coll集合寫入了一條數據。

但mgo的正確使用方法並非如此,而是應該在每次使用時從源session拷貝

func foo2() {    s := session.Copy()    defer s.Close()    s.DB("test").C("coll").Insert(bson.M{"name":"zhangsan"})

}

foo2函數從源session拷貝出了一個臨時的session,使用臨時session寫入一條數據,在函數退出時關閉這個臨時的session。

session的拷貝與併發

       爲什麼要在每次使用時都Copy,而不是直接使用Dial生成的session實例呢?個人認爲,這與mgo.Session的Socket緩存機制有關。來看Session的核心數據結構。

type Session struct {    m                sync.RWMutex    ...    slaveSocket      *mongoSocket    masterSocket     *mongoSocket

   ...    consistency      Mode    ...    poolLimit        int    ...}

這裏列出了mgo.Session的五個私有成員變量,與Copy機制有關的是,m,slaveSocket,masterSocket。

mmgo.Session的併發鎖,因此所有的Session實例都是線程安全的。

slaveSocket,masterSocket代表了該Session到mongodb主節點和從節點的一個物理連接的緩存。而Session的策略總是優先使用緩存的連接。是否緩存連接,由consistency也就是該Session的模式決定。假設在併發程序中,使用同一個Session實例,不使用Copy,而該Session實例的模式又恰好會緩存連接,那麼,所有的通過該Session實例的操作,都會通過同一條連接到達mongodb。雖然mongodb本身的網絡模型是非阻塞通信,請求可以通過一條鏈路,非阻塞地處理;但經過比較簡陋的性能測試,在mongodb3.0中,10條連接併發寫比單條連接的效率高一倍(在mongodb3.4中基本沒有差別)。所以,使用Session Copy的一個重要原因是,可以將請求併發地分散到多個連接中。

以上只是效率問題,但第二個問題是致命的。mgo.Session緩存的一主一從連接,實例本身不負責維護。也就是說,當slaveSocket,masterSocket任意其一,連接斷開,Session自己不會重置緩存,該Session的使用者如果不主動重置緩存,調用者得到的將永遠是EOF。這種情況在主從切換時就會發生,在網絡抖動時也會發生。在業務代碼中主動維護數據庫Session的可用性,顯然是不招人喜歡的。

func (s *Session) Copy() *Session {    s.m.Lock()    scopy := copySession(s, true)    s.m.Unlock()    scopy.Refresh()    return scopy }

以上是Copy函數的實現,解決了使用全局Session的兩個問題。其中,copySession將源Session淺拷貝到臨時Session中,這樣源Session的配置就拷貝到了臨時Session中。關鍵的Refresh,將源Session淺拷貝到臨時Session的連接緩存指針,也就是slaveSocket,masterSocket置爲空,這樣臨時Session就不存在緩存連接,而轉爲去嘗試獲取一個空閒的連接。

Session的連接從哪裏來?連接池

明確了使用Session Copy機制的必要性,那麼問題來了,Copy出來的臨時Session是怎麼獲取一個到mongodb的物理連接的。答案就是連接池。mgo自身維護了一套到mongodb集羣的連接池。這套連接池機制以mongodb數據庫服務器爲最小單位,每個mongodb都會在mgo內部,對應一個mongoServer結構體的實例,一個實例代表着mgo持有的到該數據庫的連接。來看該連接池的定義。

type mongoServer struct {    sync.RWMutex    ...    unusedSockets []*mongoSocket

   liveSockets   []*mongoSocket    ...    info          *mongoServerInfo }

其中,info代表了該實例對應的數據庫服務器在集羣中的信息——是否master,ReplicaSetName等。而兩個Slice,就是傳說中的連接池。unusedSockets存儲當前空閒的連接,liveSockets存儲當前活躍中的連接,Session緩存的連接就同時存放在liveSockets切片中,而臨時Session獲取到的連接就位於unusedSockets切片中。

每個mongoServer都會隸屬於一個mongoCluster結構,相當於mgo在內部,模擬出了mongo數據庫集羣的模型。

type mongoCluster struct {    sync.RWMutex    ...    servers      mongoServers    masters      mongoServers

   ...    setName      string    ...

}

如定義所示,mongoCluster持有一系列mongoServer的實例,以主從結構分散到兩個數組中。

每個Session都會存儲自己對應的,要操作的mongoCluster的引用。

長途跋涉的Session

       以下描述一個Copy出來的臨時Session是如何獲取到一個mongodb物理連接的。

當臨時Session被Copy出來,並且通過調用一系列api,將一次數據庫操作設置到了Session內部後,此時萬事俱備,只差連接。新生的Session首先會檢查自己的緩存裏是否有連接可用,初來乍到的他當然不知道自己是一個一無所有的光桿司令。由於mgo的實現,可憐的他還要去檢查兩次,一次使用讀鎖,一次使用寫鎖。作者的意圖應該是期望在對同一個session併發操作時,能在第二次排他鎖檢查之前,恰巧緩存到一條連接,那麼就可以減少一次對連接池的操作。但這次,這種好事沒有發生在這個Session身上,“摸”了兩次“口袋”反覆確認以後,他終於還是發現自己身無分文。沒有連接的他向組織求救,也就是這個session所要操作的mongodb集羣,也就是所提到的mongoCluster結構。

“組織”問了這個Session一系列問題,其中最主要的是兩個問題,一是”你要主庫連接還是從庫連接”,二是“你期望的連接池最大大小是多少”。第一個問題,Session很好回答,他首先看了看自己的模式,是必須到主庫還是必須到從庫,還是兩者皆可看情況而定。再看了看自己手裏的操作是讀還是寫,寫操作當然不可能到從庫去完成。第二個問題就有點強人所難,但是他不用自己思考,因爲這是從源Session那裏拿過來的配置,也算是一點祖產吧。

這個Cluster此時表現得像一個掌櫃,他先根據主從,從自己手下的mongoServer裏挑出了一個,然後問他,你現在手裏有沒有空閒的連接。如果有,那幸運的Session就可以順利地獲取到這個空閒的連接,高高興興的揣到兜裏回家幹活。但如果不巧,正好unusedSockets爲空,那麼掌櫃會問另一個問題,你有沒有超過這個傢伙的期望的最大連接數。如果沒有超過,那還好,作爲夥計的mongoServer就幹活了,他會跑到他負責的數據庫服務器那裏去申請一條全新的連接,親手交到Session的手裏。但如果這個夥計算了算,還去申請新連接的話,恐怕就超限了,那就Session同學對不起了您,您等吧。每100ms,夥計自旋一次,等着unusedSockets裏出現可用的連接。

當然有人會問,那這麼自旋下去,如果連接一直被其他Session佔用,會不會就死循環了呢,答案是不會。這個夥計作爲一個數據庫服務器的管理員吧可以說,他自己也要常常去確認他負責的這個服務器是不是還活着。因此,夥計同學每15s會給服務器發一個ping命令。作爲管理員,夥計可就不管什麼連接池大小超不超的問題了,那是他們那些普通session要考慮的瑣事。夥計同學要ping的時候,也去unusedSockets裏看,如果有最好,就拿一個來用;沒有的話,直接去問服務器要新的。ping完之後,新的連接就會被放入unusedSockets中。這樣的話,自旋中的獲取連接請求,就可以拿到連接了。

經過摸口袋,找組織,問夥計,夥計再幹點小活,臨時Session終於拿到了夢寐以求的數據庫物理連接,把他放到了自己的口袋裏(當然有些模式的Session不會這麼幹)。心滿意足地將自己手裏的操作通過這條連接寫了出去,等到數據庫給了他想要的應答,他的生命也就結束了。通過Close方法,我們剝奪了他口袋裏的得之不易的連接,放回到了對應mongoServerunusedSockets中。不久之後,GC又殺死了這個Session。

爲什麼我司的代碼沒有使用Copy也沒有出問題?

看過我司Go項目代碼的同學可能知道,我司的服務端代碼中並沒有使用Copy,而是類似如下的使用

func Dial(){    localSession ,_ := mgo.Dial(mongoUrl)    localSession.SetMode(mgo.Eventual)    globalDatabase = localSession.DB("db") }


使用了一個全局的mgo.Database實例,所有的對該db的操作,都通過這個實例完成。 原因就在於,我們使用的是模式是mgo.Eventual,該模式最大的特點就是不會緩存連接,拒絕持有mongodb的一針一線。通過該mgo.Database實例的操作,每次都會發現自己的口袋裏一無所有,都會經過一次上一節所述的長途跋涉獲取連接,因此也規避了不使用Copy帶來的兩個副作用。一併發效率問題,Eventual的Session每次操作都從連接池取連接,相當於分散在連接池中完成了操作,二連接可用性問題,連接池機制確保了,從mongoServer取得的連接,都是活的連接。

Copy機制或Eventual模式的併發模型的問題

併發鎖效率

Copy機制或Eventual模式的共同點是,每次的數據庫操作都要經過一次代碼路徑略深的獲取連接的過程。而這個路徑中,會操作多個線程安全的結構體,包括mongoServermongoCluster等,線程安全的代價就是併發鎖衝突帶來的性能下降。舉個例子,假設有十個寫操作併發,無論使用Copy還是Eventual,最終都會走到cluster的masterServer,請求一個主庫連接;而完成這個請求,需要寫兩個slice,將連接從unusedSockets刪除,並加入liveSockets,對切片的更新勢必要加排他鎖,因此這十個請求很有可能會產生鎖衝突。

連接池上限與衝擊數據庫

mgo對Session有一個poolLimit配置,也就是上文中所說的cluster問session的第二個問題——代表了對連接池連接數的上限限制。默認配置的連接數上限是4096,顯然對生產環境來說太過大了。但這個配置我以爲非常的雞肋,屬於設置也不好,不設置也不好;個人認爲這是被mgo的併發模型所拖累了。

假設高併發場景,若設置的連接池上限爲4096,併發爲10000,那麼理論上,一瞬間,mgo可能會產生4096個到mongodb的物理連接,而剩下的六千的請求會自旋等待。4096個連接對mongodb來說,首先意味着4096 * 10M的內存消耗,如此高的連接會導致各種各樣的問題。那麼加入設置連接池上限爲100,併發爲10000,9900個等待的請求每次100個排隊完成,對應用的效率又是不小的消耗。況且實際測試中,poolLimit的設置也無法嚴格地限制住連接數。

連接池只伸不縮

mgo另一個問題是連接池連接不釋放,一旦由於併發原因,連接池的數量被撐大,之後再也不會變小,除非客戶機或服務器重啓。

M:1? 1:1 ? M:N!

排除連接可用性問題,全局緩存連接的Session的問題是M個數據庫操作通過1個連接完成。通過Copy、Eventual完成數據庫操作的問題是,取到一個連接後,只做一件事情就歸還了連接。這兩種併發模型都存在問題。因此,最好的模型是M:N,有M個數據庫操作需要完成,一次性取N個連接,分散到N個連接中完成,此後無論有多少批請求,都可以在N個連接中分散完成。第一可以規避連接池鎖衝突,第二不會大規模產生真實連接,充分利用已建立的連接。

SessionPool

M:N的模型無法通過mgo原生支持完成,api也無法支持用戶獲取到物理連接。

可以利用Session會緩存連接的特性,通過一些小技巧實現一個SessionPool。例如,有M個寫操作,則可以一次性生成N個StrongSession,每個StrongSession自己會緩存一條masterSocket;於是,之後的寫操作,可以以某種方式負載均衡到這N個由Strong模式的Session緩存的連接中。

mgop簡單實現了上述的StrongSessionPool,以輪詢的方式負載。對從庫連接的緩存以及動態負載還有待實現。

實現SessionPool要特別注意的問題是刷新問題,緩存Session中的連接隨時可能會失效,mgop的方式是遍歷發送isMaster命令,第一確認連接存活,第二確認連接確實是到主庫的。若發現問題,則馬上重置緩存。

空閒連接釋放

mgo的連接池釋放問題,在我的mgo fork中做了一個簡單的實現解決這個問題。github.com/JodeZer/mgo

在mongodb url標準中,有兩個option:minPoolSize,maxIdleTimeMS。mgo沒有支持這兩個選項,通過實現這兩個選項可以達到釋放連接的目的。官網描述:

minPoolSize
The minimum number of connections in the connection pool. The default value is 0.

maxIdleTimeMS
The maximum number of milliseconds that a connection can remain idle in the pool before being removed and closed.

實現方式是,將mongoServer中的unusedSockets類型改造爲timedMongoSocket(fork中自定義的類型)

type timedMongoSocket struct {    

    soc *mongoSocket    lastTimeUsed *time.Time

}

每次有連接被重置到空閒池時,打一個時間戳。在輪詢goroutine中每隔一段時間review空閒連接的空閒時長,當時長大於maxIdleTimeMS時,就釋放連接,將空閒池的大小控制在minPoolSize

實現中,沒有特地寫設置函數,可以通過在mongo url中寫入選項設置,如:mongodb://127.0.0.1?minPoolSize=0&maxIdleTimeMS=3000,若maxIdleTimeMS不設置或爲0,則默認爲不進行釋放


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章