1.背景
全链路监控相比场控的一些区别:
场控:
基于MQ + Redis 的事件驱动型监控中台。特点是实时性极高(s级延迟),支持任意指标监控逻辑。
优点:
- 实时性较高
- 理论上支持任意指标逻辑
缺点:
- 上游需要send mq,场控平台RD需要强感知上游一些业务逻辑。总的来说对双方都有一定侵入性
- 所有指标都代表着当前时间的状态,没有历史指标值,也就是说不能做同环比的逻辑。
全链路监控
基于XxlJob + ES 的监控中台。特点是对业务0侵入,任意场景可以快速接入。
优点:
- 对业务0侵入,任意场景可以快速接入。
- 支持同环比逻辑
缺点:
- 实时性不如场控,依赖XXLJOB调度,达不到s级延迟
- 对于一些复杂的指标逻辑不能支持。
所以全链路监控和场控是两套相辅相成的产品。
2.设计
举个例子
假设现在有一张表,他的结构如下
| id | name | score |
|---|---|---|
| 1 | qyh1 | 10 |
| 2 | zwd1 | 20 |
| 3 | qyh2 | 30 |
| 4 | zwd2 | 40 |
| 5 | qyh3 | 50 |
| 6 | zwd3 | 60 |
| 7 | qyh4 | 70 |
| 8 | zwd4 | 100 |
id : 主键,全局唯一
name:学生姓名
score : 学生分数
现在抽象一些指标:
ps:规定 >= 60分是及格, ==100分是满分
- 及格人数;显而易见及格人数是 3
- 满分人数: 显而易见满分人数是 1
- 平均分:显而易见平均分是 380 / 8 = 47.5
- 总分:380
- 最低分:10
- 最高分: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;
这个过程抽象为两个动作:
- 将符合条件的数据先过滤出来(例如 及格人数,就是先通过 score = 60 把这些符合条件的数据过滤出来)
- 对过滤出来的数据做聚合操作,(例如 及格人数,就是先通过 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;
}
举例
最底层的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
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)