上一篇我们聊了MongoDB在物联网平台中的基础应用,不过光会用还不够。当系统真正运行起来,设备从几百台增长到几万台,数据量从GB级别增长到TB级别,这时候就会遇到各种性能问题。
我记得当时我们项目刚上线,MongoDB用着还挺顺手的。但是随着业务增长,设备接入量快速增加,系统就开始出现问题了——查询变慢,连接池不够用,各种问题接踵而来。那段时间确实比较辛苦,经常收到系统告警。
后来经过一番性能调优和监控改进,系统总算稳定下来了。今天就把这些实践经验分享出来,希望大家能少走一些弯路。
连接池这块确实需要重点关注。一开始我们图方便,直接用默认配置,结果高峰期就出现了连接超时的问题。后来发现是连接池设置太小,很多请求在排队等连接。
物联网场景的特点是设备数据写入频繁,查询请求也比较多,对连接池要求比较高。下面这几个参数建议重点调整:
mongodb:
host: localhost
port: 27017
database: iot_platform
connection-timeout: 10000
socket-timeout: 10000
max-connections-per-host: 100
min-connections-per-host: 10
几个关键参数说明:
索引策略确实是性能优化的重点。我们一开始没有重视这个问题,结果查询性能很差,用户体验不好。后来仔细分析了查询模式,发现物联网场景的查询其实很有规律,主要是按设备编码、时间、告警状态这几个维度进行查询。
基于这个分析,我们重新设计了索引策略:
// 单字段索引:支持按设备编码快速查询
db.device_data.createIndex({"deviceCode": 1})
// 时间索引:支持按时间倒序查询最新数据
db.device_data.createIndex({"timestamp": -1})
// 复合索引:设备+时间,支持单设备历史数据查询
db.device_data.createIndex({"deviceCode": 1, "timestamp": -1})
// 告警状态索引:快速筛选告警数据
db.device_data.createIndex({"alarmStatus": 1})
// 三字段复合索引:支持复杂的组合查询条件
// 索引字段顺序:查询频率高的字段在前
db.device_data.createIndex({
"deviceCode": 1, // 最常用的查询条件
"timestamp": -1, // 时间范围查询
"alarmStatus": 1 // 告警状态过滤
})
光建索引还不够,查询语句也要写得合理才行。我们之前就遇到过这个问题,索引建得不错,但查询还是很慢,后来发现是查询条件写得有问题,没有有效利用索引。
下面这个时间范围查询的写法,我们用了很久了,效果还不错:
public List<Map<String, Object>> findDeviceDataByTimeRange(String deviceCode,
long startTime, long endTime) {
// 构建查询条件
Map<String, Object> query = new HashMap<>();
query.put("deviceCode", deviceCode); // 精确匹配设备编码
// 构建时间范围查询条件
Map<String, Object> timeRange = new HashMap<>();
timeRange.put("$gte", startTime); // 大于等于开始时间
timeRange.put("$lte", endTime); // 小于等于结束时间
query.put("timestamp", timeRange);
// 设置排序规则:按时间倒序,获取最新数据
Map<String, Object> sort = new HashMap<>();
sort.put("timestamp", -1);
// 执行查询:限制返回1000条记录,从第0条开始
return mongoDBService.find("device_data", query, 1000, 0, sort);
}
物联网环境确实比较复杂,网络可能不稳定,设备可能断线,数据量可能突然增大,各种异常情况都可能出现。如果异常处理做得不好,很容易导致系统崩溃。
我们采用分层异常处理的策略,针对不同类型的异常采用不同的处理方式,下面是我们一直在使用的代码:
@Service
public class MongoDBServiceImpl implements IMongoDBService {
@Override
public boolean insert(String collectionName, Map<String, Object> document) {
try {
// 第一层:参数合法性验证,快速失败
validateParameters(collectionName, document);
// 第二层:执行核心业务逻辑
boolean result = MongoDBUtils.insertOne(collectionName, document);
// 记录操作结果,便于问题排查
if (result) {
log.info("MongoDB向集合{}插入文档成功", collectionName);
} else {
log.error("MongoDB向集合{}插入文档失败", collectionName);
}
return result;
} catch (IllegalArgumentException e) {
// 参数异常:直接抛出,由上层处理
log.error("参数验证失败: {}", e.getMessage());
throw e;
} catch (MongoException e) {
// MongoDB特定异常:记录日志但不中断流程
log.error("MongoDB操作异常: {}", e.getMessage());
return false;
} catch (Exception e) {
// 未知异常:记录完整堆栈信息
log.error("未知异常: {}", e.getMessage(), e);
return false;
}
}
/**
* 参数验证方法:确保输入参数的有效性
*/
private void validateParameters(String collectionName, Map<String, Object> document) {
if (StringUtils.isEmpty(collectionName)) {
throw new IllegalArgumentException("集合名称不能为空");
}
if (document == null || document.isEmpty()) {
throw new IllegalArgumentException("文档数据不能为空");
}
}
}
监控方面,健康检查是必不可少的。我们以前就遇到过问题,数据库连接断了都不知道,等用户反馈才发现,这样很被动。
现在我们使用Spring Boot Actuator监控MongoDB,连接状态有任何变化都能及时发现:
@Component
public class MongoDBHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// 执行ping操作检查连接状态
// ping是轻量级操作,不会对数据库造成负担
MongoDBUtils.ping();
// 返回健康状态,包含数据库基本信息
return Health.up()
.withDetail("database", MongoDBUtils.getDatabaseName())
.withDetail("status", "连接正常")
.withDetail("timestamp", System.currentTimeMillis())
.build();
} catch (Exception e) {
// 连接异常时返回DOWN状态,包含错误信息
return Health.down()
.withDetail("error", e.getMessage())
.withDetail("status", "连接异常")
.withDetail("timestamp", System.currentTimeMillis())
.build();
}
}
}
光知道系统慢还不够,需要知道具体哪里慢。我们使用AOP切面监控所有MongoDB操作,找出性能瓶颈,这样既不需要修改业务代码,又能全面收集性能数据。
这个切面比较实用,推荐使用:
@Aspect
@Component
@Slf4j
public class MongoDBPerformanceAspect {
// 定义切点:拦截MongoDB服务实现类的所有方法
@Around("execution(* com.xinye.iot.data.mongodb.service.impl.MongoDBServiceImpl.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录方法开始执行时间
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
// 执行目标方法
Object result = joinPoint.proceed();
// 计算方法执行耗时
long duration = System.currentTimeMillis() - startTime;
// 根据执行时间判断是否需要告警
if (duration > 1000) {
// 超过1秒的操作记录警告,可能存在性能问题
log.warn("MongoDB操作{}执行时间过长: {}ms", methodName, duration);
} else {
// 正常执行时间,记录调试日志
log.debug("MongoDB操作{}执行时间: {}ms", methodName, duration);
}
return result;
} catch (Exception e) {
// 方法执行异常时,记录耗时和错误信息
long duration = System.currentTimeMillis() - startTime;
log.error("MongoDB操作{}执行失败,耗时: {}ms, 错误: {}", methodName, duration, e.getMessage());
throw e; // 重新抛出异常,不影响原有异常处理逻辑
}
}
}
关于测试,这确实是很重要的经验。我们之前就因为测试不充分,导致一个小问题在生产环境引发了较大的影响。现在我们对MongoDB的每个操作都会编写完整的单元测试。
这里分享一下我们的测试用例,用@Order注解来控制执行顺序,这样可以模拟真实的业务流程:
@SpringBootTest
@TestMethodOrder(OrderAnnotation.class) // 确保测试按顺序执行
class MongoDBServiceTest {
@Autowired
private IMongoDBService mongoDBService;
private static final String TEST_COLLECTION = "test_collection";
private static String testDocumentId; // 用于在测试方法间传递文档ID
@Test
@Order(1) // 第一步:测试插入操作
void testInsert() {
// 构造测试文档数据
Map<String, Object> document = new HashMap<>();
document.put("name", "测试设备");
document.put("type", "sensor");
document.put("value", 25.5);
document.put("timestamp", System.currentTimeMillis());
// 执行插入操作并验证结果
boolean result = mongoDBService.insert(TEST_COLLECTION, document);
assertTrue(result, "文档插入应该成功");
}
@Test
@Order(2) // 第二步:测试查询操作
void testFind() {
// 构造查询条件
Map<String, Object> query = new HashMap<>();
query.put("type", "sensor");
// 执行查询操作
List<Map<String, Object>> results = mongoDBService.find(TEST_COLLECTION, query);
assertFalse(results.isEmpty(), "应该找到匹配的文档");
// 验证查询结果的数据正确性
Map<String, Object> document = results.get(0);
assertEquals("测试设备", document.get("name"));
assertEquals("sensor", document.get("type"));
// 保存文档ID,供后续测试使用
testDocumentId = document.get("_id").toString();
}
@Test
@Order(3) // 第三步:测试更新操作
void testUpdate() {
// 构造更新数据
Map<String, Object> update = new HashMap<>();
update.put("value", 30.0);
update.put("lastModified", System.currentTimeMillis());
// 执行更新操作
boolean result = mongoDBService.updateById(TEST_COLLECTION, testDocumentId, update);
assertTrue(result, "文档更新应该成功");
// 验证更新结果:重新查询文档确认数据已更新
Map<String, Object> updated = mongoDBService.findById(TEST_COLLECTION, testDocumentId);
assertEquals(30.0, updated.get("value"));
}
@Test
@Order(4) // 第四步:测试计数操作
void testCount() {
// 测试集合总文档数
long count = mongoDBService.count(TEST_COLLECTION);
assertTrue(count > 0, "集合中应该有文档");
// 测试条件查询的文档数
Map<String, Object> query = new HashMap<>();
query.put("type", "sensor");
long sensorCount = mongoDBService.count(TEST_COLLECTION, query);
assertTrue(sensorCount > 0, "应该有sensor类型的文档");
}
@Test
@Order(5) // 第五步:测试删除操作(清理测试数据)
void testDelete() {
// 执行删除操作
boolean result = mongoDBService.deleteById(TEST_COLLECTION, testDocumentId);
assertTrue(result, "文档删除应该成功");
// 验证删除结果:确认文档已不存在
Map<String, Object> deleted = mongoDBService.findById(TEST_COLLECTION, testDocumentId);
assertNull(deleted, "文档应该已被删除");
}
}
单元测试还不够,需要集成测试来验证整套流程。以前为了配置测试环境的数据库比较麻烦,现在用TestContainers就方便多了,直接启动一个真实的MongoDB容器来测试。
这样做的好处是,每次都是全新的环境,不用担心数据污染:
@SpringBootTest
@Testcontainers // 启用TestContainers支持
class MongoDBIntegrationTest {
// 定义MongoDB容器,使用4.4版本
@Container
static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.4")
.withExposedPorts(27017); // 暴露MongoDB默认端口
// 动态配置Spring Boot属性,使用容器的连接信息
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
// 获取容器分配的主机地址
registry.add("mongodb.host", mongoDBContainer::getHost);
// 获取容器映射的端口号
registry.add("mongodb.port", mongoDBContainer::getFirstMappedPort);
// 设置测试数据库名称
registry.add("mongodb.database", () -> "test_db");
}
@Autowired
private IMongoDBService mongoDBService;
@Test
void testCompleteWorkflow() {
// 测试完整业务流程
// 主要验证这几个方面:
// 1. 大批量数据插入的性能表现
// 2. 复杂查询和分页的处理能力
// 3. 批量更新的系统影响
// 4. 删除操作的执行效率
// 5. 高并发场景下的系统稳定性
// 简单的性能测试示例
long startTime = System.currentTimeMillis();
// 这里编写具体的测试逻辑
long duration = System.currentTimeMillis() - startTime;
// 验证执行时间是否在可接受范围内
}
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。