pinpoint agent改造之全局采样部分上报

背景

公司在18年开始,使用pinpoint(1.7.2)做应用监控,考虑到数据上报所带来的网络、存储、应用负载等成本,只配置了部分采样(10%)

profiler.sampling.rate=10

因此,只能拿到10%的全局traceId,导致部分异常请求无法定位到上游的应用.经过相关调研以及成本估算(使用其他APM方案,例如skywalking、cat),还是决定通过改造pinpoint的方式来实现全局采样、部分上报.

数据采样原理熟悉

pinpoint进行全链路监控时,支持对请求的采样,某条请求是否被采样,取决于整个链路开始的机器.该机器使用特定的采样算法,采样的标志会一直在链路中透传.

比如在http里面,会在header里面增加Pinpoint-Sampled字段,使用不同的值表示是否采样.

  • s0:此条请求不采样
  • s1:此条请求采样

通过搜索profiler.sampling.rate,最终能在代码中定位到SamplingRateSampler.java.

下面分析一下相关代码。
image
Sampler有三个实现类,我们主要关注SamplingRateSampler(比例采样).结合代码,很容易得出,比例采样使用原子累加计数,当累加之后得到的数为samplingRate的倍数则表示该请求被采样.

package com.navercorp.pinpoint.profiler.sampler;

import com.navercorp.pinpoint.bootstrap.sampler.Sampler;
import com.navercorp.pinpoint.common.util.MathUtils;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author emeroad
 */
public class SamplingRateSampler implements Sampler {

    private final AtomicInteger counter = new AtomicInteger(0);
    private final int samplingRate;

    public SamplingRateSampler(int samplingRate) {
        if (samplingRate <= 0) {
            throw new IllegalArgumentException("Invalid samplingRate " + samplingRate);
        }
        this.samplingRate = samplingRate;
    }



    @Override
    public boolean isSampling() {
        int samplingCount = MathUtils.fastAbs(counter.getAndIncrement());
        int isSampling = samplingCount % samplingRate;
        return isSampling == 0;
    }

    @Override
    public String toString() {
        return "SamplingRateSampler{" +
                    "counter=" + counter +
                    "samplingRate=" + samplingRate +
                '}';
    }
}

再看一下这个类的初始化链路
在这里插入图片描述

SamplerProvider继承了com.google.inject.Provider,使用Guice来做依赖注入

Guice是Google开发的一个轻量级,基于Java5(主要运用泛型与注释特性)的依赖注入框架(IOC)。Guice非常小而且快。Guice是类型安全的,它能够对构造函数,属性,方法(包含任意个参数的任意方法,而不仅仅是setter方法)进行注入。Guice采用Java加注解的方式进行托管对象的配置,充分利用IDE编译器的类型安全检查功能和自动重构功能,使得配置的更改也是类型安全的。Guice提供模块对应的抽象module,使得架构和设计的模块概念产物与代码中的module类一一对应,更加便利的组织和梳理模块依赖关系,利于整体应用内部的依赖关系维护,而其他IOC框架是没有对应物的。此外,借助privateModule的功能,可以实现模块接口的明确导出和实现封装,使得支持多数据源这类需求实现起来异常简单。

了解Guice相关知识后,直接看ApplicationContextModule类,其中对Sampler.class和SamplerProvider.class进行了绑定,并且为单例实例.

ApplicationContextModule.java

bind(Sampler.class).toProvider(SamplerProvider.class).in(Scopes.SINGLETON);

数据上报原理熟悉

通过源码及相关文档了解到,调用链数据使用TcpDataSender进行上报.

TcpDataSender:调用链数据上报通道,单线程,数据队列(LinkedBlockingQueue)大小5120,重试队列(LinkedBlockingQueue)大小1024,底层通迅使用的Netty

调用链数据上报分为两种:

  • api、sql、string常量;上报失败会先放到重试队列,后续进行重试,默认重试3次
  • 调用链详情数据;上报失败则不会重试

图片源自网络

再看一下添加SpanEvent到缓存区部分的实现

DefaultTrace.java

 public void traceBlockEnd(int stackId) {
        if (closed) {
            if (isWarn) {
                stackDump("already closed trace");
            }
            return;
        }

        final SpanEvent spanEvent = callStack.pop();
        if (spanEvent == null) {
            if (isWarn) {
                stackDump("call stack is empty.");
            }
            return;
        }

        if (spanEvent.getStackId() != stackId) {
            // stack dump will make debugging easy.
            if (isWarn) {
                stackDump("not matched stack id. expected=" + stackId + ", current=" + spanEvent.getStackId());
            }
        }

        if (spanEvent.isTimeRecording()) {
            spanEvent.markAfterTime();
        }
        logSpan(spanEvent);
    }
    
    private void logSpan(SpanEvent spanEvent) {
        this.storage.store(spanEvent);
    }

可以看到,这部分代码对SpanEvent做一些处理后,直接添加到缓存区.

由此产生想法,那如果我们不想上报某个trace,是不是只要将某个trace增加一个标记,此处拿到这个标记之后拒绝添加SpanEvent到缓存区就可以了?

所以接下来的问题就是怎么全局控制上报比例以及如何给Trace增加不上报的标记

改造方案

1⃣先解决第一个问题,怎么全局控制上报比例.

通过上文中采样比例部分的分析,上报可以采用同样的方式,即通过一个原子计数器,如果上报的数目是上报比例的倍数,则该条记录需要上报,反之则不上报.

具体实现:

1)在配置文件中增加上报比例的配置(每多少条上报1条,默认为1表示全部上报,0表示都不上报)

2)参考Sampler进行代码实现,然后使用Guice进行绑定,同样需要为单例模式

2⃣再来解决第二个问题,如何增加不上报的标记.

结合代码,会发现每个Span里都会有一个TraceRoot,并且TraceRoot在整个链路的Span中都是透传的,所以可以考虑在TraceRoot节点进行标记.

具体实现:

1)修改DefaultBaseTraceFactory,提供带Reporter的构造方法

2)在创建TraceRoot的时候,调用计数器来判断是否需要上报

3)添加SpanEvent到缓存区的时候,取出Span的TraceRoot校验标记

整体变动点如下图,代码已上传github 链接
image

说明

针对此次改造做几点说明:

  • 整体基于pinpoint 1.7.2源码修改
  • 在代码构建以及环境搭建过程中可能会碰到问题,对于过程中碰到的部分已在下文列出,仅供参考
  • 改动对整体性能的影响未做测试
  • 方案纯属个人想法,未做深入研究,可能会违反pinpoint设计原则;整个改造过程耗时四天左右
附录

针对整体改造过程中碰到的问题做一下记录,有需要可以参考

1、本地构建可能无法拿到相关jar包或者时间特别长,可以在pom文件增加仓库地址,同时部分仓库链接需从http改成https

 <repositories>
        <repository>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>cloudera</id>
            <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
        </repository>
        <repository>
            <id>spring-maven-repository</id>
            <name>Spring Framework Maven Release Repository</name>
            <url>https://maven.springframework.org/release/</url>
        </repository>
        <repository>
            <id>spring-maven-release-remote</id>
            <name>Spring Framework Maven Release Remote Repository</name>
            <url>https://repo.spring.io/libs-release-remote/</url>
        </repository>
        <repository>
            <id>bintray</id>
            <name>bintray</name>
            <url>http://jcenter.bintray.com</url>
        </repository>
        <repository>
            <id>jboss-3rd-party-releases</id>
            <name>Jboss Third Party Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/thirdparty-releases/</url>
        </repository>
    </repositories>

2、pinpoint对环境要求比较多,首先需要按照官网配置java6、7、8,然后是maven、hbase版本.本次使用的是maven 3.2.1+hbase 1.4.13

3、如果Springboot应用有agent信息,但没有请求数据,可以先排查采样比例是否为1,比例没有问题则可以看启动日志中打印的
ApplicationServerType是否为SPRING_BOOT,不是的话可以手动指定

profiler.applicationservertype=SPRING_BOOT
profiler.tomcat.conditional.transform=false

4、如果需要在日志中打印PtxId,需要先修改配置

profiler.logback.logging.transactioninfo=true

同时需要注意,如果使用Dubbo Filter方式打印日志时,需要确定相关日志框架拦截器的顺序.例如LoggingEventOfLogbackInterceptor,这些拦截器会往MDC中put相关值,如果在拦截器之前打印日志,put操作未执行,则无法显示

针对Dubbo,可以直接从RpcContext中取

private static final String TRACE_ID_KEY = "_DUBBO_TRASACTION_ID";

RpcContext.getContext().getAttachment(TRACE_ID_KEY)

至此,整个改造方案基本完成,结果后续发布验证.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值