使用MongoDB cursor从中大批量拉取数据做ETL。
处理程序连续运行25到35分钟肯定会遇到过早关闭连接,有时还会遇到游标找不到问题。
com.mongodb.MongoSocketReadException: Prematurely reached end of stream
at com.mongodb.internal.connection.SocketStream.read(SocketStream.java:112)
at com.mongodb.internal.connection.SocketStream.read(SocketStream.java:135)
at com.mongodb.internal.connection.InternalStreamConnection.receiveResponseBuffers(InternalStreamConnection.java:713)
at com.mongodb.internal.connection.InternalStreamConnection.receiveMessageWithAdditionalTimeout(InternalStreamConnection.java:571)
at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:410)
at com.mongodb.internal.connection.InternalStreamConnection.receive(InternalStreamConnection.java:369)
at com.mongodb.internal.connection.DefaultServerMonitor$ServerMonitorRunnable.lookupServerDescription(DefaultServerMonitor.java:221)
at com.mongodb.internal.connection.DefaultServerMonitor$ServerMonitorRunnable.run(DefaultServerMonitor.java:157)
at java.lang.Thread.run(Thread.java:748)
1. 前面链接关闭比较懵圈,但是游标这个很直接了。阅读MongoDB文档了解到每个Session默认的timeout是30min。timeout之后session会被标记删除,等待删除线程完成销毁。特别地,当session被删除时,无论其中的cursor是否正在被使用也会被一并删除。看起来就是session超时了。
2. session为什么会超时。因为我使用的是Spring的MongoTemplate来操作,其中的session是共享的。因此当session超时时,所有进行中的操作都得完蛋。这么看来,我需要调整这么几个点。
a. 基于cursor的操作调整为,专用session处理;
b. 开辟一个session刷新线程对session做刷新;
c. 当操作结束或者异常退出时,刷新线程能够感知session已不再使用,将其删除;
1. 在service中增加获取session绑定的MongoTemplate的方法;
2. 调整方法不再基于@Autowired MongoTemplate来操作,而是基于传入的SessionTuple来操作;
3. 对游标进行操作前将其加入刷新线程的session队列中,并基于try-with-resource包裹保证无论异常/还是正常结束调用到close方法;
4. 刷新线程对队列中的对象做判断,如果已close则从队列中删除;
一顿操作猛如虎,欢天喜地地上测试,结果这个异常依然坚挺地存在。之所以说存在,是因为不是超过30分钟必现,而是连续运行几个小时还会出现,而且没啥时间规律。仅仅比之前出现需要的时间长而已。
1. 按道理来说我刷新了session,可以保证session不会timeout。那么游标应该也不会timeout。再看文档,cursortimeout的时间为10分钟而且是idle timeout。MongoDB Server Parameters — MongoDB Manualhttps://www.mongodb.com/docs/v4.4/reference/parameters/#logical-session
当然也可以通过命令再确认,在mongoshell上切换到admin查看相关参数。
use admin
db.adminCommand( { getParameter : "*" } )看返回值里的
cursorTimeoutMillis: Long("600000"),
我的一直在获取数据并且每次都小于1s,肯定不是这里的问题。
2. 没其他思路,再看看日志吧,觉得日志太少。于是修改到TRACE级别,看看有没有更多信息。
3. 结果发现最终产生原因是ServerMonitor线程发现的。然后上代码分析,发现直接颠覆了对heartbeat的认知。
Caused by: com.mongodb.MongoSocketReadException: Prematurely reached end of stream
at com.mongodb.internal.connection.SocketStream.read(SocketStream.java:112)
at com.mongodb.internal.connection.SocketStream.read(SocketStream.java:135)
at com.mongodb.internal.connection.InternalStreamConnection.receiveResponseBuffers(InternalStreamConnection.java:713)
at com.mongodb.internal.connection.InternalStreamConnection.receiveMessageWithAdditionalTimeout(InternalStreamConnection.java:571)
at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:410)
at com.mongodb.internal.connection.InternalStreamConnection.receive(InternalStreamConnection.java:369)
at com.mongodb.internal.connection.DefaultServerMonitor$ServerMonitorRunnable.lookupServerDescription(DefaultServerMonitor.java:221)
at com.mongodb.internal.connection.DefaultServerMonitor$ServerMonitorRunnable.run(DefaultServerMonitor.java:157)
... 1 common frames omitted
4. 在DefaultServerMonitor线程中有一个专用链接对集群进行状态探测在方法(lookupServerDescription)而这个探测的间隔就是心跳频次,也就是hearbeatFrequenceMs参数。如果成功,啥都好说。如果失败,那不好意思关闭整个ConnectionPool.
public class ServerMonitor{
@Override
public void run() {
ServerDescription currentServerDescription = unknownConnectingServerDescription(serverId, null);
try {
while (!isClosed) {
ServerDescription previousServerDescription = currentServerDescription;
// 具体的就在这个lookupServerDescription方法里
currentServerDescription =
lookupServerDescription(currentServerDescription);
if (isClosed) {
continue;
}
if (currentCheckCancelled) {
waitForNext();
currentCheckCancelled = false;
continue;
}
logStateChange(previousServerDescription, currentServerDescription);
sdamProvider.get().update(currentServerDescription);
if (((connection == null || shouldStreamResponses(currentServerDescription))
&& currentServerDescription.getTopologyVersion() != null)
|| (connection != null && connection.hasMoreToCome())
|| (currentServerDescription.getException() instanceof MongoSocketException
&& previousServerDescription.getType() != UNKNOWN)) {
continue;
}
waitForNext();
}
} finally {
if (connection != null) {
connection.close();
}
}
}
}
5. 所以,这次的失败应该是探测超时引起的。但是好端端地为啥超时了呢?总之就是实际心跳间隔与设置的心跳间隔不匹配。那实际到底需要多久呢?直接debug下发现不执行数据拉取3秒左右,数据任务一开始大概8秒左右,再联想到我还会开并发。这样不仅mongodb server压力大,提供query service的mongodb client机器的压力也会变大。再看我设置的值是10s。只好斗胆猜测是这里的问题,果断调整为20s。
稳如老狗地运行了2个小时,然后放到测试环境来个拉力测试。结果,稳如老狗地跑了1天,问题总算解决。
当第一次看到链接过早关闭,首先想到的是心跳,觉得自己设置的心跳挺合理。但Mongodb Driver sync 提供的ConnectionPool仅基于Connection的使用空闲时间做管理,也就是多久不使用则关闭链接。同时没有对connection做心跳探活。实际心跳是用在对集群状态的探活上,如果该专用连接上探活失败,则将connection pool销毁而后重建,相当于此时是一个全新的cluster以及connection pool。同时在connection层面也确实有关于generation的设计,连接池管理中也会删除上一个generation的connection。过往的经验是connection有心跳设置,则肯定是没问题的。这种经验主义影响了判断。