Mongodb当系统内存满时会挂掉,所以使用时要注意其他服务占用内存的监控
sort【排序】、pipe【聚合】、单条document比较大,或者数据条目多时的 filter字段 一定要建索引,因为mongodb留给排序的内存空间为32M, 虽然可以设置跟大,但是最好的解决办法为 addindex
document使用不能太随意,反例就是某些服务中任务部署状态由多个 部分互斥的状态标志组成,造成检索效率低下,无法优化。
现有的mongodb实现中,mongoServer的连接池只伸不缩,因而会导致较高的服务端mongodb内存占用
注意mongodb使用中脏数据造成的结果异常问题,调试、测试的数据要分账号分collection放置
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重建是极大的浪费。
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,表明服务器直到操作写入磁盘才会返回。
// 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一个连接,该连接完成一个业务后就释放,其实是一种浪费,没有发挥连接池的效用。
源码中默认为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/