原博客格式更友好:http://www.straka.cn/blog/golang-mgo-consistency-copy-clone/
業務上的注意點:
Mongodb當系統內存滿時會掛掉,所以使用時要注意其他服務佔用內存的監控
sort【排序】、pipe【聚合】、單條document比較大,或者數據條目多時的 filter字段 一定要建索引,因爲mongodb留給排序的內存空間爲32M, 雖然可以設置跟大,但是最好的解決辦法爲 addindex
document使用不能太隨意,反例就是某些服務中任務部署狀態由多個 部分互斥的狀態標誌組成,造成檢索效率低下,無法優化。
現有的mongodb實現中,mongoServer的連接池只伸不縮,因而會導致較高的服務端mongodb內存佔用
注意mongodb使用中髒數據造成的結果異常問題,調試、測試的數據要分賬號分collection放置
Coding注意:
type Session struct {
m sync.RWMutex
cluster_ *mongoCluster
...
slaveSocket *mongoSocket
masterSocket *mongoSocket
...
consistency Mode
...
poolLimit int
...
}
type mongoServer struct {
sync.RWMutex
...
unusedSockets []*mongoSocket
liveSockets []*mongoSocket
...
info *mongoServerInfo
}
type mongoCluster struct {
sync.RWMutex
...
servers mongoServers
masters mongoServers
...
setName string
...
}
type mongoServers struct {
slice mongoServerSlice
}
type mongoServerSlice []*mongoServer
mongo客戶端實現了集羣信息的管理,mongoCluster管理着master / slave的servers, servers維護了對應master / slave的sockets,並按unused / live 分開,也就是實現了連接池,session在使用過程中會向servers的連接池中拿socket。
func DialWithInfo(info *DialInfo) (*Session, error) {
cluster := newCluster(addrs, info.Direct, info.FailFast, dialer{info.Dial, info.DialServer}, info.ReplicaSetName)
session := newSession(Eventual, cluster, info.Timeout)
}
DialWithInfo的時候會創建cluster 和 session, 並設定兩者的關聯。這裏提一個遇到過的bug,看到別人代碼裏處理mongo session操作失敗後進行了session重連,即重新DialWithInfo,如果重連之前進行了資源釋放,則容易造成併發問題,如果重連之前沒有釋放資源,又會造成資源泄漏問題,實際上session在copy的時候會refresh,裏面會多次判斷連接合法性,本次操作失敗隻影響該次操作,而沒必要重連,重連導致的cluster/server/sockets重建是極大的浪費。
Consistency Mode: [Read Preferred Mode]
Strong【強一致性】: session 的讀寫操作總向 primary 服務器發起並使用一個唯一的連接,因此所有的讀寫操作完全的一致(不存在亂序或者獲取到舊數據的問題)
Monotonic【單調一致性】: session 的讀操作開始是向某個 secondary 服務器發起(且通過一個唯一的連接),只要出現了一次寫操作,session 的連接就會切換至 primary 服務器。由此可見此模式下,能夠分散一些讀操作到 secondary 服務器,但是讀操作不一定能夠獲得最新的數據
Eventual【最終一致性】: session 的讀操作會向任意的 secondary 服務器發起,多次讀操作並不一定使用相同的連接,也就是讀操作不一定有序。session 的寫操作總是向 primary 服務器發起,但是可能使用不同的連接,也就是寫操作也不一定有序。Eventual 一致性模式最快,其是一種資源友好(resource-friendly)的模式。[// Same as Nearest, but may change servers between reads.]
iot項目中使用的是enventual,此外還有 Primary / PrimaryPreferred / Secondary / SecondaryPreferred / Nearest
用SetMode設置一致性模式,同時第二個參數爲是否刷新以保證切換模式後能成功切換爲該模式,否則假如從strong切換爲monotonic,切換後會依舊保持所有讀寫向primary發起,但如果向高等級的一致性切換,比如從monotonic向strong切換,則因爲連接檢查的時候不符合strong的要求,所以會自動刷新,也就是說flash參數保證的是從高等級的一致性向低等級的一致性切換的時候能刷新併成功切換。
一般在設置一致性模式的時候,同時會設置session.SetSafe(&mgo.Safe{})
type Safe struct {
W int // Min # of servers to ack before success
WMode string // Write mode for MongoDB 2.0+ (e.g. "majority")
WTimeout int // Milliseconds to wait for W before timing out
FSync bool // Sync via the journal if present, or via data files sync otherwise
J bool // Sync via the journal if present
}
Safe的註釋其實說的比較清楚,主要設置的是數據寫入的確認等級,影響的是數據的寫入安全和延遲時間
w參數爲確認寫操作的集羣中的服務器數量,當爲0或1是,表示主服務器寫完即返回,WMode是Mongo 2.0+纔有,如果設置爲majority表明集羣中大部分服務器(過半)確認寫入纔會返回,在內部實現上,還是用的W參數,不過WMode有更多的語法去表達W。
J參數和FSync參數類似,但不可同時設置(原因後述),J參數如果爲true表明服務器會在本次操作記錄到日誌後返回,但如果服務器沒有開啓操作日誌,在2.6版本前則會忽略該選項,而2.6版本開始寫操作會直接報錯,
FSync參數就是解決當服務器沒有開啓日誌時J參數的問題的,如果服務開啓了日誌,FSync和J表現一致,如果服務器沒有開啓日誌,FSync爲true,表明服務器直到操作寫入磁盤纔會返回。
copy & clone:
// Clone works just like Copy, but also reuses the same socket as the original
// session, in case it had already reserved one due to its consistency
// guarantees. This behavior ensures that writes performed in the old session
// are necessarily observed when using the new session, as long as it was a
// strong or monotonic session. That said, it also means that long operations
// may cause other goroutines using the original session to wait.
func (s *Session) Clone() *Session {
s.m.Lock()
scopy := copySession(s, true)
s.m.Unlock()
return scopy
}
// Copy works just like New, but preserves the exact authentication
// information from the original session.
func (s *Session) Copy() *Session {
s.m.Lock()
scopy := copySession(s, true)
s.m.Unlock()
scopy.Refresh()
return scopy
}
// Refresh puts back any reserved sockets in use and restarts the consistency
// guarantees according to the current consistency setting for the session.
func (s *Session) Refresh() {
s.m.Lock()
s.slaveOk = s.consistency != Strong
s.unsetSocket()
s.m.Unlock()
}
copy比clone源碼上多了個refresh,該函數操作了slaveOK標誌slaveOK = consistencyMode != Strong,
並unsetSocket(), 也就是把session對應的masterSocket 和 slaveSocket變成了nil,
如此一來,當我們使用session的時候比如,all / one / update / insert 等都會調用 session.acquiresocket, 該函數會根據前述consistencymode判斷使用 master / slave socket,如果copy模式由於socket爲nil,所以會重新創建socket,
因而如果是copy會使用新的socket,而如果是 clone,由於使用的是舊的 socket,會引起阻塞【因爲socket共享是用鎖保護的,多次的引用同一個socket類似於sharedptr, 僅增減引用計數,當引用計數爲0,會收到session.unusedsocket中,】,
綜上,copy模式適合每次業務耗時長,重用socket會導致鎖爭用,或者連接數量固定【同一個session, copy出的不同socket的session的不同的獨立的query】
clone模式適合,業務簡單、效率敏感,用clone可以避免反覆進行的socket重建開銷,或者業務沒有狀態要求【同一session clone出的用相同socket的session的query「非eventual」】,可以任意順序進行通信
copy和clone模式都必須close,否則引用計數不減,連接數會不停增加
此外,eventual模式下,由於不緩存連接,每次session.acquireSocket 都會重新向連接池申請socket,用完也不會保存到session.masterSocket 或者 session.slaveSocket 所以也就不能用於有狀態【同一session的多次query】的通信。
通常我們的業務實現採用 eventual + copy 模式,每次從session copy一個連接,該連接完成一個業務後就釋放,其實是一種浪費,沒有發揮連接池的效用。
maxPoolSize:
源碼中默認爲4096,當連接數超過該限制,會導致請求自旋100ms:
func (cluster *mongoCluster) AcquireSocket(mode Mode, slaveOk bool, syncTimeout time.Duration,....
for{
.......
s, abended, err := server.AcquireSocket(poolLimit, socketTimeout)
if err == errPoolLimit {
if !warnedLimit {
warnedLimit = true
log("WARNING: Per-server connection limit reached.")
}
time.Sleep(100 * time.Millisecond)
continue
}
.......
}
}
而mongo server端壓力會比較大,10k個connection基本就差不多100G,所以一個session的4k連接池還是比較大, server端一個connection佔內存幾乎10M, 某些業務場景下客戶端單連接近10M,所以要注意mongodb自身的內存佔用以防mongodb 宕機。
結合前文,如果用重用模式,由於連接的維護,server端會佔用較多資源。
reference:
golang連接池: https://studygolang.com/articles/6514
Golang 的 mgo 連接池 :https://cardinfolink.github.io/2017/05/17/mgo-session/
原博客格式更友好:http://www.straka.cn/blog/golang-mgo-consistency-copy-clone/