3.在这里插入图片描述
3.1 Skywalking架构
- SkyWalking 逻辑上分为四部分: 探针, 平台后端, 存储和用户界面
- 探针:用来采集app的请求,及服务请求第三方的服务的信息。
- 平台后端:可观测性分析OAP, 支持数据聚合, 数据分析以及驱动数据流从探针到用户界面的流程。分析包括 Skywalking 原生追踪和性能指标以及第三方来源, 你甚至可以使用 Observability Analysis
Language 对原生度量指标 和 用于扩展度量的计量系统 自定义聚合分析。 - 存储:通过开放的插件化的接口存放 SkyWalking 数据. 你可以选择一个既有的存储系统, 如 ElasticSearch, H2 或 MySQL 集群(Sharding-Sphere 管理),及时序数据influxdb
- UI:一个基于接口高度定制化的Web系统,用户可以可视化查看和管理 SkyWalking 数据。
3.2核心功能介绍
- Traces(链路追踪)
- Metrics(报表统计)
- 异常日志分析
- agent探针采集
4. java agent技术
4.1 agent介绍
java agent是java命令的一个参数。参数[-javaagent:]可以用于指定一个 jar 包,并且对该java包有2个要求:
- 这个 jar 包的MANIFEST.MF 文件必须指定 Premain-Class 项。
- Premain-Class 指定的那个类必须实现 premain()方法。
- 重点就在 premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行 -javaagent 所指定 jar 包内 Premain-Class 这个类的
premain 方法,其中,该方法可以签名如下:
1.public static void premain(String agentArgs, Instrumentation inst)
2.public static void premain(String agentArgs)
4.2 agent用途
- java agent能做什么?不修改目标应用达到代码增强的目的,就好像spring的aop一样,但是java agent是直接修改字节码,而不是通过创建代理类。例如skywalking就是使用java
agent技术,为目标应用代码植入监控代码,监控代码进行数据统计上报的。这种方式实现了解耦,通用的功能。
其实对我们实现一些需要通过字节码的形式隐式注入到业务代码中的中间件非常有用,APM系统中经常可见,比如国产的优秀APM组件Skywalking(http://skywalking.apache.org/),现在是Apache的顶级项目之一
;韩国Naver开源的应用性能管理工具Pinpoint(https://github.com/naver/pinpoint),当然,Java
Agent还能实现动态对运行的Java应用进行字节码注入,做到“窥探”运行时的信息,典型的代表有Java追踪工具BTrace(https://github.com/btraceio/btrace)、阿里开源的JVM-SANDBOX(https://github.com/alibaba/jvm-sandbox)、Java在线问题诊断工具Greys(https://github.com/oldmanpushcart/greys-anatomy)等。
3. arthas
4.3 实践动手
- agent 启动的例子
- DemoMainTest 演示
- 字节码增强的例子
- 通过java agent技术进行类的字节码修改最主要使用的就是Java Instrumentation API。下面将介绍如何使用Java Instrumentation API进行字节码修改。
- instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现。在JDK
1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK
1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()
方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。 - DemoMainTestTwo 演示
public class TestTransformer implements ClassFileTransformer {
//目标类名称, .分隔
private String targetClassName;
//目标类名称, /分隔
private String targetVMClassName;
private String targetMethodName;
public TestTransformer(String className,String methodName){
this.targetVMClassName = new String(className).replaceAll("\\.","\\/");
this.targetMethodName = methodName;
this.targetClassName=className;
}
//类加载时会执行该函数,其中参数 classfileBuffer为类原始字节码,返回值为目标字节码,className为/分隔
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//判断类名是否为目标类名
if(!className.equals(targetVMClassName)){
return classfileBuffer;
}
try {
ClassPool classPool = ClassPool.getDefault();
CtClass cls = classPool.get(this.targetClassName);
CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);
ctMethod.insertBefore("{ System.out.println(\"start\"); }");
ctMethod.insertAfter("{ System.out.println(\"end\"); }");
return cls.toBytecode();
} catch (Exception e) {
}
return classfileBuffer;
}
}
- 启动时修改,原理简述
- 启动时修改主要是在jvm启动时,执行native函数的Agent_OnLoad方法,在方法执行时,执行如下步骤: 创建InstrumentationImpl对象 监听ClassFileLoadHook事件
- 调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法
5. skywalking-java 搭建
- 演示功能,及请求url
- skywalk,及app演示启动过程
skywalking-java 实现介绍
- byte Buddy是一个字节码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。agent就是采用byte
Buddy是实现各个中间件jar的采集。比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。
2.skywalking-java提供proto协议作为通讯组件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRSCkkXd-1668244391371)(images/skywalking-1.png)] - 通过为不同的组件及对应版本plugin插件,采集对应的信息,
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J2j4lKH6-1668244391372)(images/agent-two-plugin.png)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKFGP5OE-1668244391374)(images/plugin-3.png)]
- 通过httpclient-4的plugin 植入的是InternalHttpClient类doExecute方法。通过插件我们获取Method方式,url,响应代码,调用次数,响应时间。
- httpclient-4 的InternalHttpClient类
protected CloseableHttpResponse doExecute(HttpHost target, HttpRequest request, HttpContext context) throws IOException, ClientProtocolException {
Args.notNull(request, "HTTP request");
HttpExecutionAware execAware = null;
if (request instanceof HttpExecutionAware) {
execAware = (HttpExecutionAware)request;
}
- 定义的切入点
/**
* {@link AbstractHttpClientInstrumentation} presents that skywalking intercepts InternalHttpClient#doExecute by using
* {@link HttpClientInstrumentation#INTERCEPT_CLASS}.
*/
public class InternalHttpClientInstrumentation extends HttpClientInstrumentation {
private static final String ENHANCE_CLASS = "org.apache.http.impl.client.InternalHttpClient";
@Override
public ClassMatch enhanceClass() {
return NameMatch.byName(ENHANCE_CLASS);
}
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[] {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return named("doExecute");
}
@Override
public String getMethodsInterceptor() {
return getInstanceMethodsInterceptor();
}
@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
}
- 采集拦截器
public class HttpClientExecuteInterceptor implements InstanceMethodsAroundInterceptor {
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
if (allArguments[0] == null || allArguments[1] == null) {
return;
}
final HttpHost httpHost = (HttpHost) allArguments[0];
HttpRequest httpRequest = (HttpRequest) allArguments[1];
final ContextCarrier contextCarrier = new ContextCarrier();
String remotePeer = httpHost.getHostName() + ":" + port(httpHost);
String uri = httpRequest.getRequestLine().getUri();
String requestURI = getRequestURI(uri);
String operationName = requestURI;
AbstractSpan span = ContextManager.createExitSpan(operationName, contextCarrier, remotePeer);
span.setComponent(ComponentsDefine.HTTPCLIENT);
Tags.URL.set(span, buildSpanValue(httpHost, uri));
Tags.HTTP.METHOD.set(span, httpRequest.getRequestLine().getMethod());
SpanLayer.asHttp(span);
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
httpRequest.setHeader(next.getHeadKey(), next.getHeadValue());
}
if (HttpClientPluginConfig.Plugin.HttpClient.COLLECT_HTTP_PARAMS) {
collectHttpParam(httpRequest, span);
}
}
@Override
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) throws Throwable {
if (allArguments[0] == null || allArguments[1] == null) {
return ret;
}
if (ret != null) {
HttpResponse response = (HttpResponse) ret;
StatusLine responseStatusLine = response.getStatusLine();
if (responseStatusLine != null) {
int statusCode = responseStatusLine.getStatusCode();
AbstractSpan span = ContextManager.activeSpan();
Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode);
if (statusCode >= 400) {
span.errorOccurred();
}
HttpRequest httpRequest = (HttpRequest) allArguments[1];
// Active HTTP parameter collection automatically in the profiling context.
if (!HttpClientPluginConfig.Plugin.HttpClient.COLLECT_HTTP_PARAMS && span.isProfiling()) {
collectHttpParam(httpRequest, span);
}
}
}
ContextManager.stopSpan();
return ret;
}
@Override
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
Class<?>[] argumentsTypes, Throwable t) {
AbstractSpan activeSpan = ContextManager.activeSpan();
activeSpan.log(t);
}
6. 分布式链路追踪
6.1 为什么需要分布式链路追踪
- 随着分布式系统和微服务架构的出现,一次用户的请求会经过多个系统,不同服务之间的调用关系十分复杂,任何一个系统出错都可能影响整个请求的处理结果。以往的监控系统往往只能知道单个系统的健康状况、一次请求的成功失败,无法快速定位失败的根本原因。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4eYOGhM-1668244391375)(images/trace-req.png)] - 除此之外,复杂的分布式系统也面临下面这些问题:
- 【1】性能分析:
一个服务依赖很多服务,被依赖的服务也依赖了其他服务。如果某个接口耗时突然变长了,那未必是直接调用的下游服务慢了,也可能是下游的下游慢了造成,如何快速定位耗时变长的根本原因呢? - 【2】链路梳理:
需求迭代很快,系统之间调用关系变化频繁,靠人工很难梳理清楚系统链路拓扑(系统之间的调用关系)。 为了解决这些问题。Google推出了一个分布式链路追踪系统 Dapper,之后各个互联网公司都参照
Dapper的思想推出了自己的分布式链路追踪系统,而这些系统就是分布式系统下的 APM系统。
6.2 什么是OpenTracing
- 分布式链路跟踪最先由 Google在 Dapper论文中提出,而 OpenTracing 通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。
- 下图是一个分布式调用的例子,客户端发起请求,请求首先到达负载均衡器,接着经过认证服务,订单服务,然后请求资源,最后返回结果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-18OVA4iQ-1668244391377)(trace/openTrace-1.png)]
- 虽然这种图对于看清各组件的组合关系是很有用的,但是存在如下问题:
- 【1】不能很好的显示组件的调用时间,是串行调用还是并行调用,如果展现更复杂的调用关系,会更加复杂。
- 【2】这种图也无法显示调用间的时间间隔以及是否通过定时调用来启动调用。
- 一种更有效的展现一个调用过程的图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mrLGyDJw-1668244391379)(trace/OpenTranc-2.png)]
- OpenTracing 类似一套标准监控接口,集成在各个应用组件。后端可以由不同中间件来采集实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ET2CYnUW-1668244391381)(trace/trace-two.png)]
- 数据模型 这部分在 OpenTracing 的规范中写的非常清楚,细节可参考原始文档 《The OpenTracing Semantic Specification》。
Causal relationships between Spans in a single Trace
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G `FollowsFrom` Span F)
- Trace 是调用链,每个调用链由多个 Span 组成。Span 的单词含义是范围,可以理解为某个处理阶段。Span 和 Span 的关系称为 Reference。上图中,总共有标号为 A-H 的 8 个阶段。
Temporal relationships between Spans in a single Trace
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
- 核心接口语义
OpenTracing 希望各个实现平台能够根据上述的核心概念来建模实现,不仅如此,OpenTracing 还提供了核心接口的描述,帮助开发人员更好的实现 OpenTracing 规范。
Span 接口
Span接口必须实现以下的功能:
获取关联的 SpanContext:通过 Span 获取关联的 SpanContext 对象。
关闭(Finish)Span:完成已经开始的 Span。
添加 Span Tag:为 Span 添加 Tag 键值对。
添加 Log:为 Span 增加一个 Log 事件。
添加 Baggage Item:向 Baggage 中添加一组键值对。
获取 Baggage Item:根据 Key 获取 Baggage 中的元素。
SpanContext 接口
SpanContext 接口必须实现以下功能,用户可以通过 Span 实例或者 Tracer 的 Extract 能力获取 SpanContext 接口实例。
遍历 Baggage 中全部的 KV。
Tracer 接口
Tracer 接口必须实现以下功能:
创建 Span:创建新的 Span。
注入 SpanContext:主要是将跨进程调用携带的 Baggage 数据记录到当前 SpanContext 中。
提取 SpanContext ,主要是将当前 SpanContext 中的全局信息提取出来,封装成 Baggage 用于后续的跨进程调用。
总结
- 演示MockTracerTest例子
- OpenTracing的api介绍
- opentracing-api:主要的API,无其他依赖。
- opentracing-noop:为主要API提供无意义实现(NoopTracer),依赖于opentracing-api。
- opentracing-util:工具类,例如GlobalTracer和默认的基于ThreadLocal存储的ScopeManager实现,依赖于上面所有的构件。
- opentracing-mock:用于测试的mock层。包含MockTracer,简单的将Span存储在内存中,依赖于opentracing-api和opentracing-noop。
- OpenTracing 社区贡献 除了官方的API,也有一些苦在opentracing-contribe,保管通用的辅助类像TracerResolver和框架工具库,例如 Java Web Servlet Filter and Spring
Cloud,可以用于在使用这些框架工具的项目中方便的集成OpenTracing。 - 可以通过RestTemplate.setInterceptors注册拦截器。解析
public class TracingRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private static final Log log = LogFactory.getLog(TracingRestTemplateInterceptor.class);
private Tracer tracer;
private List<RestTemplateSpanDecorator> spanDecorators;
public TracingRestTemplateInterceptor() {
this(GlobalTracer.get(), Collections.<RestTemplateSpanDecorator>singletonList(
new RestTemplateSpanDecorator.StandardTags()));
}
/**
* @param tracer tracer
*/
public TracingRestTemplateInterceptor(Tracer tracer) {
this(tracer, Collections.<RestTemplateSpanDecorator>singletonList(
new RestTemplateSpanDecorator.StandardTags()));
}
/**
* @param tracer tracer
* @param spanDecorators list of decorators
*/
public TracingRestTemplateInterceptor(Tracer tracer, List<RestTemplateSpanDecorator> spanDecorators) {
this.tracer = tracer;
this.spanDecorators = new ArrayList<>(spanDecorators);
}
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
ClientHttpResponse httpResponse;
//构建span,并开始
Span span = tracer.buildSpan(httpRequest.getMethod().toString())
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
.start();
tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS,
new HttpHeadersCarrier(httpRequest.getHeaders()));
for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
try {
spanDecorator.onRequest(httpRequest, span);
} catch (RuntimeException exDecorator) {
log.error("Exception during decorating span", exDecorator);
}
}
try (Scope scope = tracer.activateSpan(span)) {
httpResponse = execution.execute(httpRequest, body);
for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
try {
spanDecorator.onResponse(httpRequest, httpResponse, span);
} catch (RuntimeException exDecorator) {
log.error("Exception during decorating span", exDecorator);
}
}
} catch (Exception ex) {
for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
try {
spanDecorator.onError(httpRequest, ex, span);
} catch (RuntimeException exDecorator) {
log.error("Exception during decorating span", exDecorator);
}
}
throw ex;
} finally {
span.finish();
}
return httpResponse;
}
}
- 通过框架的拦截器能力实现HTTP请求追踪 通过上文中的代码,我们知道了如何使用Tracer对象构建Span,如何在线程中激活Span,以及如何在异步环境的不同线程间传递Span。
- 在实际的业务开发中,我们很难使用这种侵入的方式来实现追踪,更多的是利用各种框架提供的拦截器机制,来对各种业务调用进行自动追踪,比如Spring AOP,Servlet Filter,等等。下面一段代码展示了如何通过Servlet
Filter来进行服务端的HTTP请求追踪。
public class TracingWebFilter implements WebFilter, Ordered {
private static final Log LOG = LogFactory.getLog(TracingWebFilter.class);
/**
* Used as a key of {@link ServerWebExchange#getAttributes()}} to inject server span context
*/
static final String SERVER_SPAN_CONTEXT = TracingWebFilter.class.getName() + ".activeSpanContext";
private final Tracer tracer;
private final int order;
@Nullable
private final Pattern skipPattern;
private final Set<PathPattern> urlPatterns;
private final List<WebFluxSpanDecorator> spanDecorators;
public TracingWebFilter(
final Tracer tracer,
final int order,
final Pattern skipPattern,
final List<String> urlPatterns,
final List<WebFluxSpanDecorator> spanDecorators
) {
this.tracer = tracer;
this.order = order;
this.skipPattern = (skipPattern != null && StringUtils.hasText(skipPattern.pattern())) ? skipPattern : null;
final PathPatternParser pathPatternParser = new PathPatternParser();
this.urlPatterns = urlPatterns.stream().map(pathPatternParser::parse).collect(Collectors.toSet());
this.spanDecorators = spanDecorators;
}
@Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
final ServerHttpRequest request = exchange.getRequest();
if (!shouldBeTraced(request)) {
return chain.filter(exchange);
}
if (exchange.getAttribute(SERVER_SPAN_CONTEXT) != null) {
if (LOG.isTraceEnabled()) {
LOG.trace("Not tracing request " + request + " because it is already being traced");
}
return chain.filter(exchange);
}
return new TracingOperator(chain.filter(exchange), exchange, tracer, spanDecorators);
}
/**
* It checks whether a request should be traced or not.
*
* @return whether request should be traced or not
*/
protected boolean shouldBeTraced(final ServerHttpRequest request) {
final PathContainer pathWithinApplication = request.getPath().pathWithinApplication();
// skip URLs matching skip pattern
// e.g. pattern is defined as '/health|/status' then URL 'http://localhost:5000/context/health' won't be traced
if (skipPattern != null) {
final String url = pathWithinApplication.value();
if (skipPattern.matcher(url).matches()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Not tracing request " + request + " because it matches skip pattern: " + skipPattern);
}
return false;
}
}
if (!urlPatterns.isEmpty() && urlPatterns.stream().noneMatch(urlPattern -> urlPattern.matches(pathWithinApplication))) {
if (LOG.isTraceEnabled()) {
LOG.trace("Not tracing request " + request + " because it does not match any URL pattern: " + urlPatterns);
}
return false;
}
return true;
}
@Override
public int getOrder() {
return order;
}
}
- Jaeger 是Uber开发的一套分布式追踪系统,受启发于 dapper 和 OpenZipkin,兼容 OpenTracing 标准,CNCF的开源项目
结合OpenTracing看skywalking代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MbKI56PE-1668244391383)(skywalking/sky2.png)]
- trace:表示一整条链路(跨线程,跨进程的所有segment的集合),代表全局的一个唯一id,主要看 GlobalIdGenerator类的生成
- TraceSegment: 表示一个jvm的进程的所有操作的集合
- span 代表具体的某一个操作
- DataCarrier 数据缓存
- AbstractTracerContext 在一个进程中的上下文操作对象,创建TraceSegment,span
- ContextManager上下文对象管理器,主要把AbstractTracerContext存放到ThreadLocal
数据发送结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7dkTQWS2-1668244391384)(skywalking/send-xx.png)]