物联网平台中的MongoDB(二)性能优化与生产监控

发布于:2025-09-11 ⋅ 阅读:(18) ⋅ 点赞:(0)

上一篇我们聊了MongoDB在物联网平台中的基础应用,不过光会用还不够。当系统真正运行起来,设备从几百台增长到几万台,数据量从GB级别增长到TB级别,这时候就会遇到各种性能问题。

我记得当时我们项目刚上线,MongoDB用着还挺顺手的。但是随着业务增长,设备接入量快速增加,系统就开始出现问题了——查询变慢,连接池不够用,各种问题接踵而来。那段时间确实比较辛苦,经常收到系统告警。

后来经过一番性能调优和监控改进,系统总算稳定下来了。今天就把这些实践经验分享出来,希望大家能少走一些弯路。

1. 性能优化策略

1.1 连接池配置:别让连接成为瓶颈

连接池这块确实需要重点关注。一开始我们图方便,直接用默认配置,结果高峰期就出现了连接超时的问题。后来发现是连接池设置太小,很多请求在排队等连接。

物联网场景的特点是设备数据写入频繁,查询请求也比较多,对连接池要求比较高。下面这几个参数建议重点调整:

mongodb:
  host: localhost
  port: 27017
  database: iot_platform
  connection-timeout: 10000
  socket-timeout: 10000
  max-connections-per-host: 100
  min-connections-per-host: 10

几个关键参数说明:

  • max-connections-per-host:最大连接数,需要根据并发量调整,我们设置的是100,可以作为参考
  • min-connections-per-host:最小连接数,保持一定数量的连接池,避免频繁创建连接的开销
  • connection-timeout:连接超时时间,避免长时间等待,我们设置的是10秒
  • socket-timeout:读取超时时间,防止慢查询影响整个系统,同样是10秒

1.2 索引策略:让查询飞起来

索引策略确实是性能优化的重点。我们一开始没有重视这个问题,结果查询性能很差,用户体验不好。后来仔细分析了查询模式,发现物联网场景的查询其实很有规律,主要是按设备编码、时间、告警状态这几个维度进行查询。

基于这个分析,我们重新设计了索引策略:

// 单字段索引:支持按设备编码快速查询
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    // 告警状态过滤
})

1.3 查询优化:让索引发挥作用

光建索引还不够,查询语句也要写得合理才行。我们之前就遇到过这个问题,索引建得不错,但查询还是很慢,后来发现是查询条件写得有问题,没有有效利用索引。

下面这个时间范围查询的写法,我们用了很久了,效果还不错:

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);
}

2. 错误处理和监控:保证系统稳定运行

2.1 异常处理:避免单点故障影响整个系统

物联网环境确实比较复杂,网络可能不稳定,设备可能断线,数据量可能突然增大,各种异常情况都可能出现。如果异常处理做得不好,很容易导致系统崩溃。

我们采用分层异常处理的策略,针对不同类型的异常采用不同的处理方式,下面是我们一直在使用的代码:

@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("文档数据不能为空");
        }
    }
}

2.2 健康检查:主动发现系统问题

监控方面,健康检查是必不可少的。我们以前就遇到过问题,数据库连接断了都不知道,等用户反馈才发现,这样很被动。

现在我们使用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();
        }
    }
}

2.3 性能监控:定位性能瓶颈

光知道系统慢还不够,需要知道具体哪里慢。我们使用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; // 重新抛出异常,不影响原有异常处理逻辑
        }
    }
}

3. 测试验证:确保代码质量

3.1 单元测试:避免bug进入生产环境

关于测试,这确实是很重要的经验。我们之前就因为测试不充分,导致一个小问题在生产环境引发了较大的影响。现在我们对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, "文档应该已被删除");
    }
}

3.2 集成测试:验证完整流程

单元测试还不够,需要集成测试来验证整套流程。以前为了配置测试环境的数据库比较麻烦,现在用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;
        // 验证执行时间是否在可接受范围内
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到