最近公司给了一个任务,关于监控的。
在一个风和日丽的中午,老大神秘的对我说,性能监控,链路监控已经都有了,现在还缺点业务方面的监控。我当时直接黑人问号脸。心想:你要干嘛,你不要过来啊。
老大说,我们决定让你去做一下关于服务埋点采集数据,然后上报Prometheus(普罗米修斯)进行数据收集然后展示,其实我当时是拒绝的,因为这都是顶级轮子,你就是接入就完了。没啥挑战,我出来工作不就是为了学点高级技术吗,这种扔两个馒头狗都能干的,你埋汰谁呢?狗头保命。
但是转念一想,监控业务,那就是全部的业务以后都会接入这个东西,那我正好不就能整体梳理一下业务了吗。而且大伙都会配合你。毕竟拿着鸡毛令箭呢。
所以就总结一下吧,本文简单的涉及一些关于我本次调研学习梳理的一些心得和笔记。
一、技术栈
技术栈比较简单,就是普罗米修斯和granfa大屏监控。
具体怎么搭环境不说了,贼简单。
二、关于监控
在一些架构体系中,有一块非常重要的技术服务支撑就是监控系统。这是因为在系统的运行过程中需要时刻监测系统的运行健康度,QPS、TPS、可用率、响应时常、调用量、负载、CPU等等信息。采集此类信息可以让我们及时发现系统中存在出现的一些问题,从而及时处理修正。
以上是这种属于一些对于性能的监控,我们这里要做的是一个对于业务层面的监控。诸如对于指定时间范围内的下单数,每天的支付金额,页面的pv数量等对于业务方面的一些指标的统计等。此类监控可以让我们对于业务的数据进行定量的分析,对于以后的产品设计,营销手段提供一定的数据支持。
以上是对于监控方面的简单介绍,下面我们从实现方面和目前成熟的一些框架知识来分析一下。
三、数据埋点采集
1、业务侧
以上我们已经知道了监控的概念,而且知道了监控实质上是对项目数据的获取,整理,聚合。基于此,我们在业务上要确定的是监控的指标,以及埋点的位置。
实现难点
1、埋点位置
我们目前的系统代码比较庞大,经过大量的版本迭代。业务代码很多交错的地方,比如A业务和B业务共用一块代码。这使得埋点A业务的时候会受到B业务的影响。
2、校验数据
目前无法准确验证收集数据的准确度和误差范围。需要对MYSQL的业务表做查询做比对。
2、实现侧
以谷歌的 Dapper 为参考,开发大规模分布式系统的跟踪系统,这样的系统在实现上是非入侵式的,可以在工程加载时通过字节码插装给方法增强,采集必要信息汇总到服务端平台,展示出各个系统中方法的监控状况。
目前针对监控这些情况来看,一般是两种实现方案为代表:
1、使用类似 ASM 字节码插桩技术,在类加载的时候插桩埋点,进行数据的捕获。这种方式的好处是对于业务代码几乎无侵入,但是不太直观。增加维护和学习成本。
2、直接在业务代码中做埋点,这样的好处是比较直观,缺点就是对于业务代码的侵入性比较大,一旦遇到升级迭代这样的操作会比较麻烦。目前基于这两种方案,我们都有做一个相应的实现。
2.1、方案1:切入埋点采集
基于我们的项目都是在spring框架的基础上开发的,所以无需使用底层的字节码技术去实现。直接使用
上层的spring的切面特性来实现。
我们基于切面在要调用的方法上做切入,然后在切面类中对方法的入参和返回值做获取,然后统计数据
做统计。
2.2、方案2:在业务代码中埋点采集
在项目启动的时候初始化好所有的指标统计,然后在对应的业务代码中加入埋点采集的API调用即可。
四、普罗米修斯
Prometheus是一个开源监控解决方案,用于收集和聚合指标作为时间序列数据。更简单地说,
Prometheus 商店中的每个项目都是一个指标事件,并带有它发生的时间戳。简而言之,使用普罗米修
斯需要设计一系列你监控的指标,这些指标将会以counter guage histogram summary的形式来统计收
集埋点数据,然后提供查询聚合语句来以不同形式展示对应的监控数据。四种形式的介绍如下。
1、Counter(计数器)
Counter类型的指标其工作方式和计数器一样,只增不减(除非系统发生重置)。常见的监控指标,
如http_requests_total,node_cpu都是Counter类型的监控 指标。 一般在定义Counter类型指标的名
称时推荐使用_total作为后缀。
2、Guage(仪表盘)
可以随时向任何方向变化的值。Gauge类型的指标侧重于反应系统的当前状态。因此这类指标的样本数
据可增可减。常见指标如:node_memory_MemFree(主机当前空闲的内容大小)、
node_memory_MemAvailable(可用内存大小)都是Gauge类型的监控指标。
3、Histogram(直方图)
多个值的采样,提供所有存储值的总和,以及记录事件的计数。
4、Summary(摘要)
摘要的功能类似于直方图,但支持可配置的分位数,用于在滑动时间段内进行聚合监控。我们可以根据需要来定义对应的形式,然后再以对应的函数展示。
1、具体实现
1.1、切面埋点方式
目前以一个业务比如微信下单支付来设计对应的指标。
1、指标设计如下:
每一类业务设计Counter和Guage两种类型的指标作为展示统计:
/*** 自定义监控指标类
* Counter: 计数器可以用于记录只会增加不会减少的指标类型,比如记录应用请求的总量(http_requests_total),cpu使用时间(process_cpu_seconds_total)等。 一般而言,Counter类型的metrics指标在命 名中我们使用_total结束。
* Gauge:使用Gauge可以反映应用的当前状态,例如在监控主机时,主机当前空闲的内容大小 (node_memory_MemFree),可用内存大小(node_memory_MemAvailable)。或者容器当前的CPU使用率,内存使用率。这里我们 使用Gauge记录当前应用正在处理的Http请求数量。
public class MyMetrics {
public static Map<String, Object> map = new HashMap<>();
/*** 构造函数,初始化构造器,存入map中作为统一提取 */
public MyMetrics(){
// 私人支付
map.put("person_pay_counter_count",bOrderPayCounterCount);
map.put("person_pay_gauge_count",bOrderPayGaugeCount);
}
// 私人支付
public static Counter personPayCounterCount = Counter.build()
.name("person_pay_counter_count")
.labelNames("path", "method", "code")
.help("Total requests.")
.register();
public static Gauge personPayGaugeCount = Gauge.build()
.name("person_pay_gauge_count")
.labelNames("path", "method")
.help("Inprogress requests.")
.register();
/**
* 容器初始化的时候创建对应的组件,放在postconstruct实现
*/
@Component
public class InitPrometheusListener{
@Resource
PrometheusMeterRegistry meterRegistry;
//创建注册组件,把所有的指标注册到普罗米修斯的容器中
@PostConstruct
public void createCollectorRegistry(){
new MyMetrics();
CollectorRegistry prometheusRegistry = meterRegistry.getPrometheusRegistry();
//****************************************私人支付注册指标加入容器*********************************
prometheusRegistry.register(MyMetrics.personPayCounterCount );
prometheusRegistry.register(MyMetrics.personPayGaugeCount );
}
}
2、自定义注解
// 我们使用切面的方式来切入代码,需要实现一个自定义注解的功能来完成。
/**
* 普罗米修斯监控自定义注解
*/
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface PrometheusAnno {
// 设计的指标名称集合,字符串形式,以逗号隔开即可
String names();
}
3、切面实现对于埋点位置数据的收集
/**
* 监控切面
*/
@Slf4j
@Aspect
@Component
public class PrometheusAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(PrometheusAspect.class);
@Around("@annotation(com.test.annotation.PrometheusAnno)")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
// 获取请求的路径以及当前请求方法
String requestURI = request.getRequestURI();
String method = request.getMethod();
try {
int status = response.getStatus();
// 获取当前注解
PrometheusAnno annotation = ((MethodSignature) pjp.getSignature()).getMethod().getAnnotation(PrometheusAnno.class);
// 拿到注解的属性,也就是监控的指标名称的集合,字符串以逗号隔开的
String names = annotation.names();
// 遍历指标集合
if (StringUtils.isNotEmpty(names)) {
String[] nameArr = names.split(",");
for (String name : nameArr) {
// Prometheus的Counter实现
if (MyMetrics.map.get(name) instanceof Counter) {
((Counter) MyMetrics.map.get(name)).labels(requestURI, method, String.valueOf(status)).inc();
continue;
}
// Prometheus的Gauge实现
if (MyMetrics.map.get(name) instanceof Gauge) {
((Gauge) MyMetrics.map.get(name)).labels(requestURI, method).inc();
continue;
}
}
}
} catch (Exception e) {
LOGGER.error("请求埋点监控异常,请求路径为:{},请求方法为:{}", requestURI, method);
}
return pjp.proceed();
}
}
总结以上实现就是在指定放法上加上注解,配置好你的监控指标名,放法调用时在切面中统一处理埋点数据。利用spring的切面可以在放法调用的前中后拿到相应的数据做处理即可。如果需要还可以把这一个东西拆分出去做成starter启动器,外部配置即可。
4、在调用方法上加上注解配置即可
@Override
@Transactional(rollbackFor = {FeignException.class, Exception.class})
@PrometheusAnno(names = "person_pay_counter_count,person_pay_guage_count")// 此处添加监控注解,配置好指标名称即可
@OperateLog
public Response<JSONObject> sellerOrderPay(SellerOrderPayReqData reqData) {
log.info("个人支付发起付款,请求参数:{}", JSONObject.toJSONString(reqData));
// 业务代码省略...
log.info("个人支付发起付款,返回参数:{}", jsonObject);
return Response.buildSuccess(jsonObject);
}
1.2、代码埋点方式
代码埋点方式就是无需切面或者字节码的插桩接入,按照常规接口API调用即可。
1、指标设计以及初始化
/**
* 小B端-发起付款
*
* @param reqData
* @return
*/
@Override
@Transactional(rollbackFor = {FeignException.class, Exception.class})
@OperateLog
public Response<JSONObject> sellerOrderPay(SellerOrderPayReqData reqData) {
log.info("个人支付发起付款,请求参数:{}", JSONObject.toJSONString(reqData));
// 业务代码省略...
// 下单数加1统计收集
PrometheusInit.personPayCounterCount .increment();
// 支付金额求和统计收集
PrometheusInit.personPayCounterCount .record(reqData.getPayMoney());
log.info("个人支付发起付款,返回参数:{}", jsonObject);
return Response.buildSuccess(jsonObject);
}
2、展示结果
我们以代码埋点的方式做一下测试,然后做结果的展示分析。接口模拟:
/**
* 买家订单列表-2.0,
* @return
*/
@PostMapping("/consume/orderShops/bpay")
public void bpay(@RequestBody Req req) {
// 下单加1:对应上述指标为:create_order_total
PrometheusInit.personPayCounterCount .increment();
// 支付金额求和:对应上述指标为:order_amount
PrometheusInit.personPayCounterCount .record(req.getMoney());
}
在调用发起三次之后,分别统计指标的计数。
# 其余指标计算表达式汇总(以个人支付为例,不同的业务只是更换指标名称即可)
increase(create_order_total[1d]) -- 一天内的下单的增量,可以在后面做除法运算
sum(increase(create_order_total[1d])) -- 所有实例的累计一天下单的增量
rate(create_order_total[30m]) -- 30分钟的支付次数增长率
create_order_total{type = "smallb",create_order_total="success"} -- 小b支付成功的总数
increase(create_order_total{type = "smallb",create_order_total="success"}[1d]) --一天内小b支付成功的总数
同理金额也可以如上述配置。
此外
五、总结
业务监控就是需要我们以某种方式去在对应方法的入口处做埋点数据收集,然后我们可以在普罗米修斯这里使用他提供的函数去做数据的对应展示就行了。