Java语言编程,通过阿里云mongo数据库监控实现数据库的连接池优化

ops/2024/11/26 4:17:39/

一、背景

线上程序连接mongos超时,mongo监控显示连接数已使用100%。

java程序报错信息:

org.mongodb.driver.connection: Closed connection [connectionId{localValue:1480}] to 192.168.10.16:3717 because there was a socket exception raised by this connectionorg.springframework.data.mongodb.UncategorizedMongoDbException: Prematurely reached end of stream; nested exception is com.mongodb.MongoSocketReadException: Prematurely reached end of streamat org.springframework.data.mongodb.core.MongoExceptionTranslator.translateExceptionIfPossible(MongoExceptionTranslator.java:138)at org.springframework.data.mongodb.core.MongoTemplate.potentiallyConvertRuntimeException(MongoTemplate.java:2902)at org.springframework.data.mongodb.core.MongoTemplate.executeFindMultiInternal(MongoTemplate.java:2810)at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:2532)at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:2515)at org.springframework.data.mongodb.core.MongoTemplate.find(MongoTemplate.java:876)

此时java应用程序的监控指标是,接口超时。

在这里插入图片描述

走过的弯路是,怀疑出现了慢查询,数据量剧增的同时没有索引。

所以,前期解决方向着重在优化Mongodb查询速度,增加索引。

但是,接口还是报错,超时;服务健康检测时,还是进入了不健康状态。

而进一步查看Mongodb数据库并没有很慢(超过500毫秒)的慢查询。

再查看Mongodb的内存、CPU、网络流量等指标本身也没有异常,唯独遗漏了连接数指标。

通过本文,希望读者也有同感,连接数指标很重要。

二、连接池配置

  • 最小连接数
  • 最大连接数
  • 连接的空闲时间
  • 连接的存活时间
  • 等待队列的长度
  • 等待可用的超时

参考链接:
mongo connection-string

因为不同语言的Mongo驱动实现不同,本文从java实现看一看其源码。

在这里插入图片描述
在这里插入图片描述
从上图也可以看到,mongo数据库总共创建的连接数多达1189个,活跃的只有12个。

所以需要配置连接的空闲时间,及时释放连接,才不会导致有效请求无法连接mongodb

而我们每个mongos能创建的连接数上限是2000,从监控信息可以看出,见下图:
在这里插入图片描述
当这里的连接使用率为100%时,程序后面想创建新的mongo连接,就会失败了。

既然知道这些指标重要,所以需要设置报警规则。

在这里插入图片描述

  • mongos配置及使用
    在这里插入图片描述
    购买的mongos,规格显示是最大3K,最后却只有2K。这是个大坑么?
    所以当我们的程序节点越来越多,只好购买多个mongos,截止目前,我们都已买了4个Mongos

在这里插入图片描述
在配置spring.data.mongodb.uri的值时,格式如下:

java">//指定连某个mongos
mongodb://{用户名}:{密码}@{域名信息}:3717/db_name//配置多个mongos
mongodb://{用户名}:{密码}@{域名信息1}:3717,{域名信息2}:3717,{域名信息3}:3717,{域名信息4}:3717/db_name

三、源码spring.boot.autoconfigure

java_79">1、入口类MongoAutoConfiguration.java

见jar包spring.boot.autoconfigure-2.2.4.RELEASE.jar

在这里插入图片描述
主要代码:

java">    @Bean@ConditionalOnMissingBean(type = { "com.mongodb.MongoClient", "com.mongodb.client.MongoClient" })public MongoClient mongo(MongoProperties properties, ObjectProvider<MongoClientOptions> options,Environment environment) {return new MongoClientFactory(properties, environment).createMongoClient(options.getIfAvailable());}

使用MongoClientFactory工厂模式创建并实例化类MongoClient。

下一步看一看工厂类MongoClientFactory的主要实现。

java_97">2、工厂类MongoClientFactory.java

在这里插入图片描述
读取MongoProperties配置以及MongoClientOptions配置,前者是通过application.yaml配置,后者是通过uri追加参数的方式。

下面看一看这两个配置类里都有哪些配置项,着重分析是否有针对连接池相关的。

java_104">3、MongoProperties.java

在这里插入图片描述

这里就不一一贴出来,发现并没有连接池相关的配置。

那么进一步查看com.mongodb.MongoClientOptions.java类有哪些属性。

java_111">4、MongoClientOptions.java

在这里插入图片描述
可以看到,连接池配置相关参数,是在这个类中。

那么,他们是在什么哪里赋值的呢?

它们跟Mongodb驱动有关,让我们跳到jar包momgo-java-driver-3.11.2.jar

javadriver_119">四、源码momgo-java-driver

数据库驱动使用jdni技术,避免了程序与数据库之间的紧耦合,使应用更加易于配置、易于部署。

在这里插入图片描述
找到类com.mongodb.client.jndi.MongoClientFactory.java
在这里插入图片描述

java_125">1、工厂类MongoClientFactory.java

java">package com.mongodb.client.jndi;import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.MongoException;
import com.mongodb.diagnostics.logging.Logger;
import com.mongodb.diagnostics.logging.Loggers;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;public class MongoClientFactory implements ObjectFactory {private static final Logger LOGGER = Loggers.getLogger("client.jndi");private static final String CONNECTION_STRING = "connectionString";public MongoClientFactory() {}public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {String connectionString = null;if (environment.get("connectionString") instanceof String) {connectionString = (String)environment.get("connectionString");}if (connectionString == null || connectionString.isEmpty()) {LOGGER.debug(String.format("No '%s' property in environment.  Casting 'obj' to java.naming.Reference to look for a javax.naming.RefAddr with type equal to '%s'", "connectionString", "connectionString"));if (obj instanceof Reference) {Enumeration props = ((Reference)obj).getAll();while(props.hasMoreElements()) {RefAddr addr = (RefAddr)props.nextElement();if (addr != null && "connectionString".equals(addr.getType()) && addr.getContent() instanceof String) {connectionString = (String)addr.getContent();break;}}}}if (connectionString != null && !connectionString.isEmpty()) {MongoClientURI uri = new MongoClientURI(connectionString);return new MongoClient(uri);} else {throw new MongoException(String.format("Could not locate '%s' in either environment or obj", "connectionString"));}}
}

这里引入了一个关键类MongoClientURI.java

java_184">2、MongoClientURI.java

它有一个属性:ConnectionString对象,也就是说,MongoClientURI是用来解析数据库连接参数。

在这里插入图片描述
见关键代码: new ConnectionString(uri)

mongodbConnectionStringjava_190">3、连接参数类com.mongodb.ConnectionString.java

该类的代码行数比较多,首要看的是其构造函数。(写出了从mongo.uri中解析数据库连接池参数的全过程)

spring:data:mongodb:uri: mongodb://192.168.10.16:3717/db_name?maxPoolSize=50
  • 构造函数

主要围绕着解析数据库连接相关参数来说明,其他的可以自行看源码。

在这里插入图片描述

  • 解析配置项 private Map<String, List> parseOptions(String optionsPart)

在这里插入图片描述

  • 赋值给当前类ConnectionString的属性
java">   private void translateOptions(Map<String, List<String>> optionsMap) {boolean tlsInsecureSet = false;boolean tlsAllowInvalidHostnamesSet = false;Iterator var4 = GENERAL_OPTIONS_KEYS.iterator();while(var4.hasNext()) {String key = (String)var4.next();String value = this.getLastValue(optionsMap, key);if (value != null) {if (key.equals("maxpoolsize")) {this.maxConnectionPoolSize = this.parseInteger(value, "maxpoolsize");} else if (key.equals("minpoolsize")) {this.minConnectionPoolSize = this.parseInteger(value, "minpoolsize");} else if (key.equals("maxidletimems")) {this.maxConnectionIdleTime = this.parseInteger(value, "maxidletimems");} else if (key.equals("maxlifetimems")) {this.maxConnectionLifeTime = this.parseInteger(value, "maxlifetimems");} else if (key.equals("waitqueuemultiple")) {this.threadsAllowedToBlockForConnectionMultiplier = this.parseInteger(value, "waitqueuemultiple");} else if (key.equals("waitqueuetimeoutms")) {this.maxWaitTime = this.parseInteger(value, "waitqueuetimeoutms");} else if (key.equals("connecttimeoutms")) {this.connectTimeout = this.parseInteger(value, "connecttimeoutms");} else if (key.equals("sockettimeoutms")) {this.socketTimeout = this.parseInteger(value, "sockettimeoutms");} else if (key.equals("tlsallowinvalidhostnames")) {this.sslInvalidHostnameAllowed = this.parseBoolean(value, "tlsAllowInvalidHostnames");tlsAllowInvalidHostnamesSet = true;} else if (key.equals("sslinvalidhostnameallowed")) {this.sslInvalidHostnameAllowed = this.parseBoolean(value, "sslinvalidhostnameallowed");tlsAllowInvalidHostnamesSet = true;} else if (key.equals("tlsinsecure")) {this.sslInvalidHostnameAllowed = this.parseBoolean(value, "tlsinsecure");tlsInsecureSet = true;} else if (key.equals("ssl")) {this.initializeSslEnabled("ssl", value);} else if (key.equals("tls")) {this.initializeSslEnabled("tls", value);} else if (key.equals("streamtype")) {this.streamType = value;LOGGER.warn("The streamType query parameter is deprecated and support for it will be removed in the next major release.");} else if (key.equals("replicaset")) {this.requiredReplicaSetName = value;} else if (key.equals("readconcernlevel")) {this.readConcern = new ReadConcern(ReadConcernLevel.fromString(value));} else if (key.equals("serverselectiontimeoutms")) {this.serverSelectionTimeout = this.parseInteger(value, "serverselectiontimeoutms");} else if (key.equals("localthresholdms")) {this.localThreshold = this.parseInteger(value, "localthresholdms");} else if (key.equals("heartbeatfrequencyms")) {this.heartbeatFrequency = this.parseInteger(value, "heartbeatfrequencyms");} else if (key.equals("appname")) {this.applicationName = value;} else if (key.equals("retrywrites")) {this.retryWrites = this.parseBoolean(value, "retrywrites");} else if (key.equals("retryreads")) {this.retryReads = this.parseBoolean(value, "retryreads");}}}if (tlsInsecureSet && tlsAllowInvalidHostnamesSet) {throw new IllegalArgumentException("tlsAllowInvalidHostnames or sslInvalidHostnameAllowed set along with tlsInsecure is not allowed");} else {this.writeConcern = this.createWriteConcern(optionsMap);this.readPreference = this.createReadPreference(optionsMap);this.compressorList = this.createCompressors(optionsMap);}}

这个方法揭示了mongodb驱动所支持的全部参数,而且它读取的key字符都是小写字母。

而我们在实际配置mongodb.uri连接参数的时候,一般都会采用驼峰格式。

这是因为在方法parseOptions()解析的时候,强制把所有的key都转换为小写了。
在这里插入图片描述

五、参数的默认值

至此,我们已知道了mongodb连接支持哪些参数,但是,当缺省未配置时,它们的默认值分别是多少呢?

这就得看另一个jar包mongodb-driver-core-3.11.2.jar, package为com.mongodb.connection下,有一个类ConnectionPoolSettings采用builder构造模式,可以看到,在构建对象的时候有进行默认赋值。

在这里插入图片描述
所以,如果你没有对属性maxConnectionIdleTimeMS进行设置,默认是0,不会释放空闲连接。

前面4个属性都可以不管,属性maxConnectionIdleTimeMS是一定要设置的。

否则不活跃的连接都一直占据着mongo的连接,随着服务节点增多,就会影响到所有依赖Mongo集群的服务。

体现出来的报错就是连接超时,你还以为是服务的qps过高导致服务挂了呢。

mongodb的慢查询又没有,服务的qps很低的时候,仍旧报连接mongo超时错误。(真的是要怀疑人生)

使出重启大法,服务也无法健康。

如果你想对节点扩容,那就离曙光越来越远了。

文末,我这里给出Mongo连接池相关的参数:

java">spring:data:mongodb:uri: mongodb://192.168.10.16:3717/db_name?maxPoolSize=50&minPoolSize=10&maxIdleTimeMS=60000

六、总结

本文的内容比较长,既描述了阿里云mongodb数据库的监控(着重是连接数指标),以及Mongos的使用及购买的坑,也从Java语言的 Mongo驱动程序作为切入点,分析并总结了支持哪些数据库连接池的配置项。

本案例是基于生产实际中遇到的一个棘手问题,希望可以帮助到你。

通过本文,让我们对连接数这个指标有更深的体会,它是一个很冷的指标,却非常致命。

说它致命,是说我们在遇到程序报错的时候,极容易陷入平常思维,以为是有慢查询,或者程序QPS过高导致程序挂了。

当你想去扩容程序的节点数,或者创建数据库索引的时候,服务不健康的问题并不能得到丝毫解决。

当没有找到问题的根本时,就像一个病人感冒去看医生,结果CT和心电图等一大推检查,只会起到拖延的作用。


http://www.ppmy.cn/ops/136741.html

相关文章

Java基础面试题02:简述什么是值传递和引用传递?

面试题&#xff1a;简述什么是值传递和引用传递&#xff1f; 什么是值传递&#xff1f; 值传递&#xff08;pass by value&#xff09;是指在调用函数时&#xff0c;把实际参数的值复制一份传递给函数。换句话说&#xff0c;函数内部对参数的任何修改&#xff0c;都不会影响到…

在Excel中处理不规范的日期格式数据并判断格式是否正确

有一个Excel表&#xff0c;录入的日期格式很混乱&#xff0c;有些看着差不多&#xff0c;但实际多一个空格少一个字符很难发现&#xff0c;希望的理想格式是 1980-01-01&#xff0c;10位&#xff0c;即&#xff1a;“YYYY-mm-dd”&#xff0c;实际上数据表中这样的格式都有 19…

Spring Boot教程之五:在 IntelliJ IDEA 中运行第一个 Spring Boot 应用程序

在 IntelliJ IDEA 中运行第一个 Spring Boot 应用程序 IntelliJ IDEA 是一个用 Java 编写的集成开发环境 (IDE)。它用于开发计算机软件。此 IDE 由 Jetbrains 开发&#xff0c;提供 Apache 2 许可社区版和商业版。它是一种智能的上下文感知 IDE&#xff0c;可用于在各种应用程序…

微服务电商平台番外篇一:常用的docker命令

Docker入门手册 Docker 镜像常用命令 搜索镜像 docker search java 下载镜像 docker pull java:8docker pull macro/eureka-server:0.0.1列出镜像 docker images 删除镜像 docker rmi javadocker rmi -f javadocker rmi -f $(docker images)查看镜像 Docker 容器常用命令…

Java基础-内部类与异常处理

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 一、Java 内部类 什么是内部类&#xff1f; 使用内部类的优点 访问局部变量的限制 内部类和继承 内部…

【JavaEE初阶 — 多线程】定时器的应用及模拟实现

目录 1. 标准库中的定时器 1.1 Timer 的定义 1.2 Timer 的原理 1.3 Timer 的使用 1.4 Timer 的弊端 1.5 ScheduledExecutorService 2. 模拟实现定时器 2.1 实现定时器的步骤 2.1.1 定义类描述任务 定义类描述任务 第一种定义方法 …

Linux ntp时间服务部署

本文使用云服务器&#xff0c;系统是Ubuntu 20.04 LTS&#xff0c;以下操作都是在Ubuntu上面执行的&#xff01;&#xff01; 一、Linux系统时间命令 timedatectl命令 timedatectl 是现代 Linux 系统中最强大的时间管理命令&#xff0c;特别是对于使用 systemd 的系统。它不仅…

显示类控件

文章目录 1 QLabel1.1 常用属性1.2 例子1&#xff0c;设置文本 (textFormat)1.3 例子2&#xff0c;设置widget背景图片 (pixmap和scaledContents)1.4 例子3&#xff0c;设置对齐方式 (alignment)1.5 例子4&#xff0c;设置自动换行&#xff0c;缩进和边距1.5.1 设置换行 (wordW…