db

MongoDB 시계열 데이터 조회 성능 개선

기존 문제점

설비 시계열 데이터를 조회하는 기능이 있었다. 조회 기간이 길어지면 결과를 보기까지 약 5~10분이 걸렸다.

원인은 MongoDB에서 조회한 전체 데이터를 Java 애플리케이션에서 다시 순회하며 샘플링하던 구조였다. 데이터가 많을수록 전송량과 반복 처리 시간이 함께 증가했다.

기존 방식

기존에는 전체 데이터를 조회한 뒤 데이터 수에 따라 애플리케이션에서 샘플링했다.

cursor = MongoHelper.query(vo);

if (count < MAX_SAMPLING) {
    samplingAll(cursor, vo, seriesMap, trendChart);
} else {
    samplingBlock(cursor, vo, seriesMap, trendChart);
}

샘플링 과정에서도 커서를 끝까지 순회했다.

while (cursor.hasNext()) {
    DBObject object = cursor.next();
    while (!calculator.calculate(object, vo));
}

화면에 필요한 데이터는 일부였지만, 전체 데이터를 애플리케이션으로 가져와 처리한다는 점이 병목이었다.

개선 방식

샘플링을 MongoDB의 Aggregation Pipeline에서 처리하도록 변경했다. DB에서 최대 1,000건만 선별한 뒤 애플리케이션으로 반환하는 방식이었다.

먼저 전체 건수를 기준으로 샘플링 간격을 계산했다.

int queryMaxSampling = 1000;

long total = collection.count(
    new BasicDBObject(
        "ts",
        new BasicDBObject("$gte", from).append("$lte", to)
    )
);

int step = (int) Math.ceil((double) total / queryMaxSampling);

예를 들어 조회 결과가 100,000건이면 100건마다 1건을 선택했다.

Pipeline은 시간 범위 필터링, 정렬, 순번 생성, 샘플 선택 순으로 구성했다.

List<DBObject> pipeline = new ArrayList<>();

pipeline.add(new BasicDBObject(
    "$match",
    new BasicDBObject(
        "ts",
        new BasicDBObject("$gte", from).append("$lte", to)
    )
));

pipeline.add(new BasicDBObject(
    "$setWindowFields",
    new BasicDBObject("sortBy", new BasicDBObject("ts", 1))
        .append(
            "output",
            new BasicDBObject(
                "rowNumber",
                new BasicDBObject("$documentNumber", new BasicDBObject())
            )
        )
));

pipeline.add(new BasicDBObject(
    "$match",
    new BasicDBObject(
        "$expr",
        new BasicDBObject(
            "$eq",
            List.of(
                new BasicDBObject("$mod", List.of("$rowNumber", step)),
                0
            )
        )
    )
));

pipeline.add(new BasicDBObject(
    "$project",
    new BasicDBObject("rowNumber", 0)
));

pipeline.add(new BasicDBObject("$limit", queryMaxSampling));

애플리케이션에서는 선별된 결과만 차트 데이터로 변환했다.

AggregationOutput output = collection.aggregate(pipeline);

for (DBObject trend : output.results()) {
    timestamps.add((Long) trend.get("ts"));
    samplingBySeries(seriesMap, trend, trendChart);
}

결과

전체 데이터를 애플리케이션으로 전송하고 순회하던 작업이 사라졌다. 반환 데이터도 최대 1,000건으로 제한돼 네트워크 전송량과 Java의 반복 처리 비용이 줄었다. 그 결과 긴 기간의 시계열 조회 성능을 크게 개선할 수 있었다.

다만 이 방식은 일정 간격으로 데이터를 선택하는 균등 샘플링이다. 순간적인 최솟값이나 최댓값이 중요한 차트라면 해당 값이 누락되지 않는지 별도 검증이 필요했다.

또한 $setWindowFields$documentNumber는 MongoDB 5.0 이상에서 사용할 수 있다. 시간 범위 검색과 정렬에 사용하는 ts 필드의 인덱스도 함께 확인해야 했다.

db.collection.createIndex({ ts: 1 })

Leave a comment