【全链路】指标中心设计

525 阅读6分钟

1.背景

全链路监控相比场控的一些区别:

场控:

基于MQ + Redis 的事件驱动型监控中台。特点是实时性极高(s级延迟),支持任意指标监控逻辑。

优点:

  1. 实时性较高
  2. 理论上支持任意指标逻辑

缺点:

  1. 上游需要send mq,场控平台RD需要强感知上游一些业务逻辑。总的来说对双方都有一定侵入性
  2. 所有指标都代表着当前时间的状态,没有历史指标值,也就是说不能做同环比的逻辑。

全链路监控

基于XxlJob + ES 的监控中台。特点是对业务0侵入,任意场景可以快速接入。

优点:

  1. 对业务0侵入,任意场景可以快速接入。
  2. 支持同环比逻辑

缺点:

  1. 实时性不如场控,依赖XXLJOB调度,达不到s级延迟
  2. 对于一些复杂的指标逻辑不能支持。

所以全链路监控和场控是两套相辅相成的产品。

2.设计

举个例子

假设现在有一张表,他的结构如下

idnamescore
1qyh110
2zwd120
3qyh230
4zwd240
5qyh350
6zwd360
7qyh470
8zwd4100
id : 主键,全局唯一
name:学生姓名
score : 学生分数

现在抽象一些指标:

ps:规定 >= 60分是及格, ==100分是满分

  1. 及格人数;显而易见及格人数是 3
  2. 满分人数: 显而易见满分人数是 1
  3. 平均分:显而易见平均分是 380 / 8 = 47.5
  4. 总分:380
  5. 最低分:10
  6. 最高分:100

各个指标是怎么算出来的呢?

及格人数

select count(*) from t where score >= 60;

满分人数

select count(*) from t where score == 100;

平均分

select sum(score) / count(*)  from t; 

总分

select sum(score)  from t; 

最高分

select max(score)  from t; 

最低分

select min(score)  from t; 

这个过程抽象为两个动作:

  1. 将符合条件的数据先过滤出来(例如 及格人数,就是先通过 score = 60 把这些符合条件的数据过滤出来)
  2. 对过滤出来的数据做聚合操作,(例如 及格人数,就是先通过 score = 60 把这些符合条件的数据过滤出来,然后通过 count(*) 就能得到及格人数)

过滤类型

过滤的类型可以分为

== , != , >, <, >=, >=, in, notIn, exists(xx字段存在), notExists(xx字段不存在)...等等

聚合类型

聚合类型可以分为

sum(), count(), min(), max(), avg()... 等等

QueryFilter

QueryFilter 你可以理解为一种SQL语言,只不过他是面向ES的SQL语言。看一下类设计.

public class QueryFilter {

    private String field;
    private String exp;

    private FilterType type;

    private List<QueryFilter> must;
    private List<QueryFilter> mustNot;
    private List<QueryFilter> should;

    private Collection<?> in;
    private Collection<?> notIn;
    private Object eq;
    private Object notEq;
    private Object lt;
    private Object lte;
    private Object gt;
    private Object gte;
    private Object like;
    private Object likeLeft;
    private Object likeRight;
    private Object notLike;
    private Object notLikeLeft;
    private Object notLikeRight;
}

举例

image.png

image.png

最底层的queryFilter 表示的是最小的逻辑单元(==, != , in, notIn.....)

上层的queryFilter 可以是 must , mustNot, should, shouldNot。它里面组合了很多最底层的queryFilter,这样就可以实现复杂的异或逻辑。

指标元信息

基于上面的讲解,就可以写一下最终我们的指标元信息应该设计成什么样子?

及格数:

{
	"aggField": "id",
	"aggType": "COUNT",
	"queryFilter": {
		"field": "score",
		"lte": 60,
		"type": "RANGE"
	}
}
// 先过滤出 score >= 60的记录, 然后对这些记录 count(*) 操作

满分人数

{
	"aggField": "id",
	"aggType": "COUNT",
	"queryFilter": {
		"field": "score",
		"lte": 100,
		"type": "EQ"
	}
}
// 先过滤出 score == 100 的记录, 然后对这些记录 count(*) 操作

最高分

{
	"aggField": "score",
	"aggType": "MAX",
	"queryFilter": {
	   // 这里的意思就是不添加任何筛选条件,取全量的数据
	}
}
// 拿到所有数据,对 score 字段求最大值

最低分

{
	"aggField": "score",
	"aggType": "MIN",
	"queryFilter": {
	   // 这里的意思就是不添加任何筛选条件,取全量的数据
	}
}
// 拿到所有数据,对 score 字段求最小值

总分

{
	"aggField": "score",
	"aggType": "SUM",
	"queryFilter": {
	   // 这里的意思就是不添加任何筛选条件,取全量的数据
	}
}
// 拿到所有数据,对 score 字段求和

平均分

{
	"aggField": "score",
	"aggType": "AVG",
	"queryFilter": {
	   // 这里的意思就是不添加任何筛选条件,取全量的数据
	}
}
// 拿到所有数据,对 score 字段求平均

如你所见,这套监控系统相比于场控来说,他没有指标更新的逻辑,他只关注怎么取指标。也就是说如果其他场景需要接入,那么只需要向全链路监控系统提供可用的es数据源即可,所以说对代码是0侵入的。

3. queryFilter 是怎么和es的查询建立上联系的?

其实queryFilter你可以想象成 es java highlevelClient 里面的 BoolQueryBuilder 类,只是queryFilter是全链路系统里面业务层的概念,它更加简洁,更加简单。但是他最终还是会转换成 BoolQueryBuilder,然后根据指标提供的过滤信息,过滤出信息,然后聚合就得到了指标值。

queryBuilder的设计思想正是来自于 es java sdk , org.elasticsearch.index.query.BoolQueryBuilder image.png

queryFilter怎么转化成BoolQueryBuilder的?

package com.hdu.metrics_center.core;

import lombok.val;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.WildcardQueryBuilder;
import org.elasticsearch.script.Script;

import java.util.Objects;

import static com.hdu.metrics_center.core.FilterType.*;
import static java.lang.String.format;
import static java.util.Optional.ofNullable;
import static org.elasticsearch.index.query.QueryBuilders.*;


/**
 * 提供 boolQueryFilter -> QueryBuilder 功能
 */

public class EsQueryBuilderBuildHelper {

    private static final String LIKE_QUERY_TEMPLATE = "*%s*";
    private static final String LIKE_LEFT_QUERY_TEMPLATE = "*%s";
    private static final String LIKE_RIGHT_QUERY_TEMPLATE = "%s*";

    public static QueryBuilder convertToQueryBuilder(QueryFilter queryFilter) {

        if (queryFilter == null) {
            return null;
        }

        val boolQuery = QueryBuilders.boolQuery();
        val type = queryFilter.getType();
        switch (type) {
            case NESTED:
                buildNestedQuery(queryFilter, boolQuery);
                return boolQuery;
            case EQ:
                return termQuery(queryFilter.getField(), queryFilter.getEq());
            case NOT_EQ:
                return boolQuery.mustNot(termQuery(queryFilter.getField(), queryFilter.getNotEq()));
            case RANGE:
                return buildRangeQuery(queryFilter);
            case NOT_RANGE:
                return boolQuery.mustNot(buildRangeQuery(queryFilter));
            case IN:
                return termsQuery(queryFilter.getField(), queryFilter.getIn());
            case NOT_IN:
                return boolQuery.mustNot(termsQuery(queryFilter.getField(), queryFilter.getNotIn()));
            case EXISTS:
                return existsQuery(queryFilter.getField());
            case NOT_EXISTS:
                return boolQuery.mustNot(existsQuery(queryFilter.getField()));
            case EXP:
                return scriptQuery(new Script(queryFilter.getExp()));
            case LIKE:
                return buildWildcardQuery(
                    queryFilter.getField(),
                    format(LIKE_QUERY_TEMPLATE, queryFilter.getLike()),
                    LIKE
                );
            case LIKE_LEFT:
                return buildWildcardQuery(
                    queryFilter.getField(),
                    format(LIKE_LEFT_QUERY_TEMPLATE, queryFilter.getLikeLeft()),
                    LIKE_LEFT
                );
            case LIKE_RIGHT:
                return buildWildcardQuery(
                    queryFilter.getField(),
                    format(LIKE_RIGHT_QUERY_TEMPLATE, queryFilter.getLikeRight()),
                    LIKE_RIGHT
                );
            case NOT_LIKE:
                return buildNotWildcardQuery(
                    queryFilter.getField(),
                    format(LIKE_QUERY_TEMPLATE, queryFilter.getNotLike()),
                    LIKE
                );
            case NOT_LIKE_LEFT:
                return buildNotWildcardQuery(
                    queryFilter.getField(),
                    format(LIKE_LEFT_QUERY_TEMPLATE, queryFilter.getNotLikeLeft()),
                    LIKE_LEFT
                );
            case NOT_LIKE_RIGHT:
                return buildNotWildcardQuery(
                    queryFilter.getField(),
                    format(LIKE_RIGHT_QUERY_TEMPLATE, queryFilter.getNotLikeRight()),
                    LIKE_RIGHT
                );
            default:
                throw new UnsupportedOperationException("Unsupported FilterType: " + type);
        }
    }

    private static void buildNestedQuery(QueryFilter queryFilter, BoolQueryBuilder boolQuery) {
        val mustList = queryFilter.getMust();
        if (mustList != null && !mustList.isEmpty()) {
            mustList.stream()
                .map(EsQueryBuilderBuildHelper::convertToQueryBuilder)
                .filter(Objects::nonNull)
                .forEach(boolQuery::must);
        }

        val mustNotList = queryFilter.getMustNot();
        if (mustNotList != null && !mustNotList.isEmpty()) {
            mustNotList.stream()
                .map(EsQueryBuilderBuildHelper::convertToQueryBuilder)
                .filter(Objects::nonNull)
                .forEach(boolQuery::mustNot);
        }

        val shouldList = queryFilter.getShould();
        if (shouldList != null && !shouldList.isEmpty()) {
            shouldList.stream()
                .map(EsQueryBuilderBuildHelper::convertToQueryBuilder)
                .filter(Objects::nonNull)
                .forEach(boolQuery::should);
            boolQuery.minimumShouldMatch(1);
        }
    }

    private static QueryBuilder buildRangeQuery(QueryFilter queryFilter) {
        val rangeQueryBuilder = rangeQuery(queryFilter.getField());
        ofNullable(queryFilter.getGt()).ifPresent(rangeQueryBuilder::gt);
        ofNullable(queryFilter.getGte()).ifPresent(rangeQueryBuilder::gte);
        ofNullable(queryFilter.getLt()).ifPresent(rangeQueryBuilder::lt);
        ofNullable(queryFilter.getLte()).ifPresent(rangeQueryBuilder::lte);
        return rangeQueryBuilder;
    }

    private static QueryBuilder buildWildcardQuery(String filed, Object v, FilterType type) {
        String template;
        switch (type) {
            case LIKE:
                template = LIKE_QUERY_TEMPLATE;
                break;
            case LIKE_LEFT:
                template = LIKE_LEFT_QUERY_TEMPLATE;
                break;
            case LIKE_RIGHT:
                template = LIKE_RIGHT_QUERY_TEMPLATE;
                break;
            default:
                throw new UnsupportedOperationException("Unsupported FilterType: " + type);
        }
        return new WildcardQueryBuilder(
            filed,
            format(template, v)
        );
    }

    private static BoolQueryBuilder buildNotWildcardQuery(String filed, Object v, FilterType type) {
        return boolQuery().mustNot(
            buildWildcardQuery(filed, v, type)
        );
    }
}

4. 利用queryFilter 设计一套 es-orm

之前实习的时候,也是利用了这一套queryFilter模型结合mybatis-plus的一些思想,写了一个es-orm,来简化es查询开发, 运用在项目数据中心全链路。具体实现逻辑参见 自研ES-ORM - 掘金 (juejin.cn)

5. 源码

aggregate_metrics_center: aggregate_metrics_center (gitee.com)