NYC's Blog - APM http://niyanchun.com/tag/apm/ Skywalking流程简析及源码调试 http://niyanchun.com/skywalking-code-debug.html 2020-07-19T22:01:00+08:00 如之前的文章所介绍,Skywalking主要由Agent、OAP、Storage、UI四大模块组成(如下图):Agent和业务程序运行在一起,采集链路及其它数据,通过gRPC发送给OAP(部分Agent采用http+json的方式);OAP还原链路(图中的Tracing),并分析产生一些指标(图中的Metric),最终存储到Storage中。本文从源码角度来串联一下这整个流程(基于目前最新的Skywalking 8.0.1)。源码编译Skywalking本地调试必须先从源码编译Skywalking,有两种方式,一种是从GitHub拉取代码,一种是从Apache Skywalking的release页面下载代码。区别在于GitHub上面的代码是使用git module管理的,拉取下来需要执行一系列操作,最主要的是没有科学上网的话,速度比较慢。Release页面下载的是已经把依赖关系全部整理好的代码,整个源码包不到3MB,还有很多国内镜像地址,所以下载非常快。两种我都使用过,我的建议是:如果你想看历史提交记录或者想持续跟上游版本的话,就选用从GitHub拉取代码的方式;如果你想方便或者从GitHub clone超级慢的话,建议直接从Release处下载。不管哪种,编译以及导入IDEA或Eclipse官方文档写的都比较详细,我就不做翻译了,基本都是命令操作,英文不好也看得懂(just copy-and-paste~~):How to build.源码编译成功以后(务必保证编译成功),就可以准备进行调试了。源码流程简析及调试这里通过一个简单的Spring MVC程序来演示如何调试Agent和OAP。创建一个Spring MVC程序在Skywalking项目下增加一个简单的Spring MVC模块(注意这里一定要以Skywalking项目module的方式添加),这里我创建了一个名叫simple-springmvc的module,增加了一个简单的Controller:/hello/{name}。如下图:然后在这个这个MVC程序的VM option中增加如下配置:-javaagent:{源码根目录}/skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=simple-springmvc注意,-javaagent后面那个skywalking-agent.jar路径换成你自己的路径。源码如果编译成功的话,源码根目录下面会出现这个skywalking-agent目录,并且里面会有这个skywalking-agent.jar。如下图:这样调试用的示例程序以及Skywalking的agent注入也配置好了。先别启动,接下来还需要启动OAP。启动OAP如果想先只调试agent的话,可以单独下载一个Skywalking的二进制(编译完以后,根目录下的dist目录也有二进制安装包),本地启动(参考我之前的文章)即可。第一次调试的话,我建议agent和OAP单独调试,因为两者有一些公用代码,在一个工程里面启动的话,容易造成混淆。分开调试的话就本地单独起一个Skywalking就行,这里讲直接在项目里面启动一个OAP的方式。启动OAP非常简单,OAP的代码是源码根目录下的oap-server,入口函数是org.apache.skywalking.oap.server.starter包下面的OAPServerStartUp类。直接启动即可。需要注意的是这样只启动了OAP,为了方便查看还原的链路(不启动也不影响调试,不看Web的直接跳过),我们再手动启动一个Web UI。直接在Skywalking安装目录下面(注意是二进制安装目录,不是源码目录)的webapp目录下执行:java -jar skywalking-webapp.jar即可。默认访问地址为http://127.0.0.1:8080/。OAP和UI(optional)启动好以后,就可以开始调试了。流程简析启动调试之前,我先简单介绍一下数据流向以及一些关键的函数,方便提前打断点。整个数据流如下图:这里我们先创建了一个Spring MVC程序simple-springmvc,并且配置了javaagent,这样Skywalking agent就会以字节码注入的方式运行在simple-springmvc里面。当我们使用curl命令发送请求时,就会产生链路数据。需要注意的是,Skywalking默认已经实现了Spring MVC的插件{源码根目录}/skywalking-agent/plugins/apm-springmvc-annotation-commons-8.0.1.jar,对应的源码是{源码根目录}/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons。它的增强函数就是在这个模块下的AbstractMethodInterceptor类中实现的,给这个类的beforeMethod方法打个断点(为了节省篇幅,省略了一些不重要的代码),就可以观察数据agent增强流程:package org.apache.skywalking.apm.plugin.spring.mvc.commons.interceptor; public abstract class AbstractMethodInterceptor implements InstanceMethodsAroundInterceptor { @Override public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) throws Throwable { // 给下面这行打个断点 Boolean forwardRequestFlag = (Boolean) ContextManager.getRuntimeContext().get(FORWARD_REQUEST_FLAG); /** * Spring MVC plugin do nothing if current request is forward request. * Ref: https://github.com/apache/skywalking/pull/1325 */ if (forwardRequestFlag != null && forwardRequestFlag) { return; } // 以下省略 } // 以下省略 }然后启动了OAP,后端存储使用了默认内建的内存数据库H2。为了方便查看链路,可以选择性启动一个UI。Agent和OAP之间是通过gRPC来发送链路信息的。Agent端维护了一个队列(默认5个channel,每个channel大小为300)和一个线程池(默认1个线程,后面称为发送线程),链路数据采集后主线程(即业务线程)会写入这个队列,如果队列满了,主线程会直接把把数据丢掉(丢的时候会以debug级别打印日志)。发送线程会从队列取数据通过gRPC发送给后端OAP,OAP经过处理后写入存储。为了看得清楚,我把涉及的框架类画到了下面的图里面(格式是:{类名}#{方法名}({方法中调用的重要函数}):这里只列举了核心函数,每个函数内部的方法就不赘述了。需要说明的就是Skywalking代码的模块化还是做得很不错,大家跟踪代码的时候可以关注一下功能所属的模块,更有利于学习整个项目或者进行二次开发。调试给这些核心方法,打上断点,以Debug模式启动oap-server和simple-springmvc,然后用curl发一个请求,就可以愉快的调试了。总结Just read the source code, good luck ! OpenTracing概念术语介绍 http://niyanchun.com/opentracing-introduction.html 2020-07-12T22:04:00+08:00 本来计划接着上文介绍Skywalking的架构,但是我想了一下,觉得还是有必要先把链路跟踪里面涉及的一些基础概念术语介绍一下,介绍这些Skywalking并不是一个比较好的选择。原因一方面是Skywalking只是众多APM实现之一,里面有些设计并不适合其它APM,另一方面Skywalking提供的比较好的探针多时Java的,而且是字节码注入的,不利于观察学习。当然最重要的是有一个更合适的选择:OpenTracing。OpenTracing介绍分布式请求/链路跟踪(Distributed Request Tracing)最早是Google内部在用,后来相对成熟以后,2010年对外发布了一篇论文:Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,但没有将系统开源。接着就有一些公司和社区开始基于Dapper实现自己的链路跟踪系统,比较有名的有(这里只列举开源的):Twitter的Zipkin、韩国的PinPoint、大众点评的CAT,以及一些后起之秀:Uber的Jaeger,Apache Skywalking等。在百花齐放的时候,出现了OpenTracing,关于它的介绍官方是这样说的:What is OpenTracing?It is probably easier to start with what OpenTracing is NOT.OpenTracing is not a download or a program. Distributed tracing requires that software developers add instrumentation to the code of an application, or to the frameworks used in the application.OpenTracing is not a standard. The Cloud Native Computing Foundation (CNCF) is not an official standards body. The OpenTracing API project is working towards creating more standardized APIs and instrumentation for distributed tracing.OpenTracing is comprised of an API specification, frameworks and libraries that have implemented the specification, and documentation for the project. OpenTracing allows developers to add instrumentation to their application code using APIs that do not lock them into any one particular product or vendor.我简单概括一下,OpenTracing制定了一些链路跟踪的API规范,并且提供了一些框架和库,这些框架和库实现了它制定的那些API规范。而且它是一个独立开放的项目,现在已经是云原生基金会(Cloud Native Computing Foundation, CNCF)的项目了。任何组织和个人都可以贡献符合API规范的库/框架。虽然OpenTracing不是一个标准规范,但现在大多数链路跟踪系统都在尽量兼容OpenTracing。需要重点说明的是OpenTracing提供的框架和库只是采集最原始的链路数据,并不做分析。如果放到Skywalking的架构中,它只实现了探针部分。也就是OpenTracing并不是一个完备的链路系统,所以我们无法单独使用,必须配合兼容OpenTracing规范的系统使用,比如Jaeger、LightStep、Apache Skywalking、Elastic APM等。最新的进展是OpenTracing已经和CNCF的另外一个项目OpenTelemetry合并了。题外话:虽然APM各个系统号称兼容OpenTracing,但兼容性到底如何,其实还是参差不齐的,比如Jaeger就比Skywalking兼容性好。OpenTracing 术语介绍说明:基本上所有的链路跟踪系统以及OpenTracing里面术语大多都是来自于Dapper论文里面的。下面介绍的规范都是OpenTracing定义的一些规范为了方便理解和交流,概念术语等使用英文,不做翻译。分布式链路跟踪系统的数据模型:Traces(一般翻译为链路):一起请求从发出,然后经过多个模块(这个模块可能是函数或者系统,或者都有),最终得到请求回复,整个请求按照调用时间和关系串起来就是一个trace。Span则是组成trace的最基本单元,它一般代表分布式系统中一个独立的工作单元。有点抽象,没关系,后面看一些例子就懂了。一个Span包含如下几部分:操作名称:一般用于展示、过滤、聚合开始和结束时间戳:用于计算耗时由key-value组成的Tags:用于添加一些时间无关的信息(可选)由key-value组成并包含时间戳的Logs:用于添加一些时间相关的信息(可选)span上下文,即SpanContext。一般包含两部分数据:(1)span的状态数据,比如traceID和spanID(2)Baggage Items。Baggage是链路跟踪提供的一个通用的跨进程/服务传递数据的方式,格式也是key-value形式的。Trace就是由若干个span组成的有向无环图,图中的每个节点就是Span,连接节点的边称之为References。每个trace有一个唯一标识符traceID,每个span也有一个唯一标识符spanID。一个链路中的所有span的traceID是相同的,但spanID各不相同。一个链路中span典型的调用关系图如下: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)对应的时间维度为: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··]其实很简单,就是个调用关系。需要说明的是一个trace的span间有两种可能的关系:ChildOf:即父子关系,也是最常见的关系。比如上图中的Span A和Span B、Span C就是父子关系,表示只有Span B和Span C(包括Span A和Span B的所有子Span)都完成了,Span A才能完成,类似于同步调用(仅仅是类似于,并不完全一样)。FollowsFrom:其实也是父子关系,子Span是由父Span调用产生的,但父Span是否完成不依赖于子Span。比如图中的Span F和Span G就属于FollowsFrom关系。Span G由Span F调用创建,但Span F是否完成不依赖于Span G,有点类似于异步调用。最后需要介绍的一个概念就是“active span”。一个线程里面可以包含多个span,但同一时刻只能有一个span处于工作状态,这个span称之为ActiveSpan。Span可以有这么几个状态:StartedNot FinishedNot "active"ActiveSpan的状态由ScopeManager管理,但是否实现由开发者决定。另外OpenTracing定义了Inject和Extract接口来简化SpanContext跨进程传递。如果你是第一次了解分布式链路跟踪,看了上面这些,我相信你还是一头雾水,心里有很多疑问。没事,理论结合实践是掌握知识最佳的手段,先对这些概念有个大概理解和印象,然后看下面的几篇实战文章:OpenTracing Java Library教程(1)——trace和span入门OpenTracing Java Library教程(2)——进程间传递SpanContextOpenTracing Java Library教程(3)——跨服务传递SpanContextOpenTracing Java Library教程(4)——Baggage介绍说明:这4篇文章内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明以及在Jaeger UI上面的展示,方便理解,习惯看英文的也可以看原文,代码自行从GitHub拉取。通过这几篇文章,对于分布式链路跟踪基本概念和原理应该可以理解的比较好了。后面会介绍一些SDK如何写,以及一些具体的APM。 OpenTracing Java Library教程(4)——Baggage介绍 http://niyanchun.com/opentracing-java-library-tutorial-4.html 2020-07-12T21:59:00+08:00 本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第4篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档。第1篇:单span的trace创建。第2篇:多span的trace创建(进程内SpanContext传递)。第3篇:跨服务(进程)传递SpanContext。第4篇:Baggage介绍(本文)。目标学习:理解分布式上下文的传递使用baggage来传递数据开发流程在上一节中看到,SpanContext是如何在不同的服务之间传递的。我们可以将这种机制通用化,相当于实现了一个在分布式环境中进行信息传递的通道,这样就可以在服务间传递一些自定义数据了。这就是OpenTracing定义的Baggage:baggage是SpanContext的一部分,也是以key-value形式存在的。和tags、logs的差别在于,baggage是全局传递的。父Span里面的baggage信息会自动被所有子span继承,这就是所谓的全局性。也就是你能在调用链上任意地方读取到该信息。下面我们看个具体的例子。在Client中增加Baggage代码还是在上一篇文章的代码之上修改。我们再增加一个命令行参数,然后将这个参数放到baggage里面。修改Hello的main方法:public static void main(String[] args) { if (args.length != 2) { throw new IllegalArgumentException("Expecting two arguments, helloTo and greeting"); } String helloTo = args[0]; String greeting = args[1]; Tracer tracer = Tracing.init("hello-world"); new Hello(tracer).sayHello(helloTo, greeting); }然后将第二个参数放到baggage里面:private void sayHello(String helloTo, String greeting) { Span span = tracer.buildSpan("say-hello").start(); try (Scope scope = tracer.scopeManager().activate(span)) { span.setTag("hello-to", helloTo); // 把第二个参数信息放到baggage里面 span.setBaggageItem("greeting", greeting); String helloStr = formatString(helloTo); printHello(helloStr); } finally { span.finish(); } }其它地方都不用修改。在Formatter服务中读取Baggage@GET public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) { Span span = Tracing.startServerSpan(tracer, httpHeaders, "format"); try (Scope scope = tracer.scopeManager().activate(span)) { // 读取Baggage String greeting = span.getBaggageItem("greeting"); if (greeting == null) { greeting = "Hello"; } String helloStr = String.format("%s, %s!", greeting, helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } finally { span.finish(); } }Baggage注意点首先,上面实现的功能也可以通过在接口中增加参数来实现。但服务比较多的时候比较麻烦,而且接口改动是一个比较大的改动,一般定了以后不能随意更改。而Baggage是不需要改动接口的,基本对于服务自身是透明的。这也是Baggage的意义所在。这里列举一些Baggage的使用场景:多租户系统的租户信息底层调用者的标识信息混沌工程中传递一些错误注入指令passing request-scoped dimensions for other monitoring data, like separating metrics for prod vs. test traffic但需要主要的是Baggage是全局传递的,所以数据量不能太大,否则可能会产生性能问题。一些库/框架会在实现层限制这个大小。总结Baggage是链路跟踪给出的一个通用的分布式数据传输机制,可以根据场景合理利用。 OpenTracing Java Library教程(3)——跨服务传递SpanContext http://niyanchun.com/opentracing-java-library-tutorial-3.html 2020-07-12T21:58:00+08:00 本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第3篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档。第1篇:单span的trace创建。第2篇:多span的trace创建(进程内SpanContext传递)。第3篇:跨服务(进程)传递SpanContext(本文)。第4篇:Baggage介绍。目标学习如何:跨服务做链路跟踪使用Inject和Extract方法在服务间传递span上下文(SpanContext)信息使用一些OpenTracing推荐的tags开发流程构建Hello-World微服务为了演示跨服务做链路跟踪,我们先来构建几个服务:Hello.java :基于上一节的代码,修改了部分代码,增加了HTTP请求代码。Formatter.java:基于Dropwizard-based的HTTP服务器,提供这样的一个接口:发送GET 'http://localhost:8081/format?helloTo=Bryan',返回"Hello, Bryan!" 字符串。Publisher.java:类似Formatter.java,提供这样一个接口:发送GET 'http://localhost:8082/publish?helloStr=hi%20there'请求,就往标准输出打印一个"hi there" 字符串。先把后面两个HTTP Server运行起来:// terminal tab 1 $ ./run.sh lesson03.exercise.Formatter server // terminal tab 2 $ ./run.sh lesson03.exercise.Publisher server然后发送一个HTTP请求:$ curl 'http://localhost:8081/format?helloTo=Bryan' Hello, Bryan!如果出现上面打印,说明我们的服务已经OK了。最后我们像前一篇文章一样,继续运行Hello服务:./run.sh lesson03.solution.Hello Bryan 进程/服务间链路信息传递虽然我们的Hello中做了两个RPC请求(HTTP也是RPC的一种),但运行之后会发现链路图和之前的一样:产生了一个包含三个span的链路,都是hello-world这个服务产生的。我们当然希望链路可以展示出这个调用中的涉及的所有服务,这个时候就需要实现在服务间(即跨进程)传递链路信息。链路信息一般包装在上下文中,这个上下文称之为SpanContext:一般至少包含链路的状态信息(比如traceID、spanID等)和Baggage信息。Baggage信息下篇文章介绍。所以链路信息的传递就是传递这个SpanContext。OpenTracing提供了一个抽象,在Tracer接口中定义了两个接口:inject(spanContext, format, carrier) :用于将SpanContext注入到RPC请求中;extract(format, carrier):用于从RPC请求中获取SpanContext。按照OpenTracing API定义,format参数表示SpanContext的编码格式(或者说传递方式吧),需要为以下三个编码之一:TEXT_MAP:将SpanContext编码为key-value形式BINARY:编码为字节流;HTTP_HEADERS:和TEXT_MAP一样,区别在于key的信息必须能够安全的放在HTTP headers里面(即不能包含一些http头不支持的特殊字符)。经常把HTTP_HEADERS归到TEXT_MAP类。carrier是基于底层RPC框架做的一层抽象,用于传递SpanContext。比如TEXT_MAP格式对应的carrier接口允许tracer实例通过put(key, value)方法将key-value格式的数据写入到请求中。同理,BINARY格式的就是ByteBuffer。下面我们看如何通过inject和extract来实现进程间的链路上下文信息传递。客户端增强首先需要在客户端发送HTTP请求前将SpanContext注入进去,发送给服务端。现在的HTTP请求是封装在 Hello#getHttp()中的,所以在这里加:import io.opentracing.propagation.Format; import io.opentracing.tag.Tags; Tags.SPAN_KIND.set(tracer.activeSpan(), Tags.SPAN_KIND_CLIENT); Tags.HTTP_METHOD.set(tracer.activeSpan(), "GET"); Tags.HTTP_URL.set(tracer.activeSpan(), url.toString()); tracer.inject(tracer.activeSpan().context(), Format.Builtin.HTTP_HEADERS, new RequestBuilderCarrier(requestBuilder));这里是以TEXT_MAP(HTTP_HEADERS)编码SpanContext的,所以需要实现TextMap类:import java.util.Iterator; import java.util.Map; import okhttp3.Request; public class RequestBuilderCarrier implements io.opentracing.propagation.TextMap { private final Request.Builder builder; RequestBuilderCarrier(Request.Builder builder) { this.builder = builder; } @Override public Iterator<Map.Entry<String, String>> iterator() { throw new UnsupportedOperationException("carrier is write-only"); } @Override public void put(String key, String value) { builder.addHeader(key, value); } }tracer会调用put方法将SpanContext中的信息以key-value的形式加到HTTP头中。这里的信息主要是我们写的一些跟请求想关的Tags信息。这样,客户端已经通过inject将SpanContext加入到请求中了。接下来看服务端收到请求后,如何使用extract取出这些信息。服务端增强服务端增强和客户端类似,先参照客户端创建一个Tracer实例。这部分一样,就略过了,重点看如何取出SpanContext信息。这里封装一个startServerSpan函数,这个函数实现的功能如下:从收到的请求中读取头信息(SpanContext信息包含在里面)使用extract方法解析出SpanContext,并基于此创建了一个新的Span,同时增加了一些服务端的信息public static Span startServerSpan(Tracer tracer, javax.ws.rs.core.HttpHeaders httpHeaders, String operationName) { // format the headers for extraction MultivaluedMap<String, String> rawHeaders = httpHeaders.getRequestHeaders(); final HashMap<String, String> headers = new HashMap<String, String>(); for (String key : rawHeaders.keySet()) { headers.put(key, rawHeaders.get(key).get(0)); } Tracer.SpanBuilder spanBuilder; try { SpanContext parentSpanCtx = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(headers)); if (parentSpanCtx == null) { spanBuilder = tracer.buildSpan(operationName); } else { spanBuilder = tracer.buildSpan(operationName).asChildOf(parentSpanCtx); } } catch (IllegalArgumentException e) { spanBuilder = tracer.buildSpan(operationName); } return spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER).start(); }Formatter和Publisher两个服务都需要做这个事情。有了这个span,就可以使用了,这里展示一下Formatter代码的代码:@GET public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) { // 调用封装的startServerSpan函数,基于客户端传递过来SpanContext的创建一个新的span,并在tags中加入服务端的一些信息 Span span = startServerSpan(tracer, httpHeaders, "format"); try (Scope scope = tracer.scopeManager.activate(span)) { String helloStr = String.format("Hello, %s!", helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } finally { span.finish(); } }至此,服务端的增强也实现好了,是时候见证奇迹了。见证奇迹重新运行Formatter、 Publisher和Hello服务(我没有改时区,所以日志中的时间差了8小时,实际现在是周日早晨8点):// terminal tab 1:启动Formatter服务 $ ./run.sh lesson03.exercise.Formatter server // 省略了部分日志 INFO [2020-07-12 00:57:48,181] io.jaegertracing.internal.reporters.LoggingReporter: Span reported: 2b20ca6e8ddc6547:2eb6a1fbef6e9789:8a92a88a65fb4776:1 - format 127.0.0.1 - - [12/Jul/2020:00:57:48 +0000] "GET /format?helloTo=Bryan HTTP/1.1" 200 13 "-" "okhttp/3.9.0" 4 // terminal tab 2:启动Publisher服务 $ ./run.sh lesson03.exercise.Publisher server // 省略了部分日志 Hello, Bryan! INFO [2020-07-12 00:57:48,440] io.jaegertracing.internal.reporters.LoggingReporter: Span reported: 2b20ca6e8ddc6547:93916ee579078535:75065f170bf15bff:1 - publish 127.0.0.1 - - [12/Jul/2020:00:57:48 +0000] "GET /publish?helloStr=Hello,%20Bryan! HTTP/1.1" 200 9 "-" "okhttp/3.9.0" 137 // terminal tab 3:启动Hello,启动后会分别调用Formatter服务和Publisher服务 -> % ./run.sh lesson03.solution.Hello Bryan // 省略了部分日志 08:57:48.206 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 2b20ca6e8ddc6547:8a92a88a65fb4776:2b20ca6e8ddc6547:1 - formatString 08:57:48.468 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 2b20ca6e8ddc6547:75065f170bf15bff:2b20ca6e8ddc6547:1 - printHello 08:57:48.468 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 2b20ca6e8ddc6547:2b20ca6e8ddc6547:0:1 - say-hello然后看下生成的链路图:可以看到新生成的链路包含了3个服务,共5个span。点击查看链路详情:从右侧可以清楚的看出调用关系,左侧可以看出耗时。然后再看下每个服务的一些详细信息:Tags包含了各个span的一些关键信息。总结本文主要展示了如何跨进程/服务传递SpanContext,下一篇介绍另外一种传递信息的方式,也是SpanContext中非常重要的一部分:Baggage。 OpenTracing Java Library教程(2)——进程间传递SpanContext http://niyanchun.com/opentracing-java-library-tutorial-2.html 2020-07-12T21:57:00+08:00 本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第2篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档。第1篇:单span的trace创建。第2篇:多span的trace创建(进程内SpanContext传递)(本文)。第3篇:跨服务(进程)传递SpanContext。第4篇:Baggage介绍。目标学习如何:跟踪不同的函数创建一个包含多个span的链路在进程(其实是线程)内部上下文之间传递链路信息开发步骤说明:源代码的exercise包下面的类是空的,是留给我们按教程一步步补充完善的;solution包是已经编写好的代码。本文基于教程1,所以先把把教程1里面solution下面的Hello.java拷贝到lesson02/exercise/Hello.java下面。跟踪不同的函数上篇文章的sayHello(String helloTo)方法做了两件事情:格式化输出字符串和打印输出。因为在一个方法里面,所以只产生了一个span,为了演示如何产生多个san,这里我们将这个方法拆分成两个单独的函数:String helloStr = formatString(span, helloTo); printHello(span, helloStr);函数体如下:// 格式化字符串 private String formatString(Span span, String helloTo) { String helloStr = String.format("Hello, %s!", helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } // 打印输出 private void printHello(Span span, String helloStr) { System.out.println(helloStr); span.log(ImmutableMap.of("event", "println")); }然后我们再给每个函数里面加上span信息:private String formatString(Span rootSpan, String helloTo) { Span span = tracer.buildSpan("formatString").start(); try { String helloStr = String.format("Hello, %s!", helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } finally { span.finish(); } } private void printHello(Span rootSpan, String helloStr) { Span span = tracer.buildSpan("printHello").start(); try { System.out.println(helloStr); span.log(ImmutableMap.of("event", "println")); } finally { span.finish(); } }运行(这截取了span相关的日志信息):-> % ./run.sh lesson02.solution.HelloManual Bryan 08:07:28.078 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 7e2c775eb9a9ac55:7e2c775eb9a9ac55:0:1 - formatString Hello, Bryan! 08:07:28.087 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: ab294a35b8deb4c3:ab294a35b8deb4c3:0:1 - printHello 08:07:28.087 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 45dcde709e09015d:45dcde709e09015d:0:1 - say-hello 这里简单介绍一下Span reported:后面的输出,这里有几个使用冒号分隔的ID:第一个ID代表traceID,唯一标识一个链路;第二个是spanID,唯一标识一个span;第三个是当前span的父spanID,如果没有(即自己就是根节点),则为0。这里的确产生了三个span,但这三个span的TraceID各不一样,也就是说这三个span分别代表一个链路,即我们创建了三个trace,每个trace包含1个span,这显然不是我们想要的,因为这里三个span是有调用关系的,它们最终应该形成一个trace,这个trace包含3个span。产生这个结果的原因是tracer.buildSpan("操作名").start();会创建一个新的span,而且默认这个span就是根节点,如果需要将其作为子节点的话,需要使用ChildOf明确指出其父节点:// formatString Span span = tracer.buildSpan("formatString").asChildOf(rootSpan).start(); // printHello Span span = tracer.buildSpan("printHello").asChildOf(rootSpan).start();修改代码后,重新运行函数:./run.sh lesson02.solution.HelloManual Bryan 08:20:56.204 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 836db6b9e1cb6e87:159e8c7d440d171a:836db6b9e1cb6e87:1 - formatString Hello, Bryan! 08:20:56.209 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 836db6b9e1cb6e87:149c8f7aeadb5319:836db6b9e1cb6e87:1 - printHello 08:20:56.209 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: 836db6b9e1cb6e87:836db6b9e1cb6e87:0:1 - say-hello 这次可以看到,三个span的traceID是一样的,说明它们三个都属于一个trace,且前两个spanID的父spanID都是最后一个,这个结果是比较符合预期的。我们再看下链路图:可以看到只新产生了一个trace,且里面包含了3个span。点开这个trace:从这个链路图就可以看出来函数间的调用关系,以及各个调用的耗时。如果点击每个span,还可以查看附加的一些基本信息。 至此,功能基本就完成了。美中不足的是每个函数调用都要传递父span,非常不方便,幸运的是java有thread local变量,可以使用这个机制省略掉同一个线程里面函数间的这个参数传递。代码如下:import io.opentracing.Scope; private void sayHello(String helloTo) { Span span = tracer.buildSpan("say-hello").start(); try (Scope scope = tracer.scopeManager().activate(span)) { span.setTag("hello-to", helloTo); String helloStr = formatString(helloTo); printHello(helloStr); } finally { span.finish(); } } private String formatString(String helloTo) { Span span = tracer.buildSpan("formatString").start(); try (Scope scope = tracer.scopeManager().activate(span)) { String helloStr = String.format("Hello, %s!", helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } finally { span.finish(); } } private void printHello(String helloStr) { Span span = tracer.buildSpan("printHello").start(); try (Scope scope = tracer.scopeManager().activate(span)) { System.out.println(helloStr); span.log(ImmutableMap.of("event", "println")); } finally { span.finish(); } }代码说明:trace.scopeManager().active(span) 使当前span变为active span(每个线程内只能有一个active span)。当这个span关闭后,上一个span会自动又变为active span。Scope 实现了AutoCloseable接口, 所以我们可以使用try-with-resource语法.如果当前(当前线程内)已经有一个active span的话,使用buildSpan()创建新span的时候,会自动将当前active span设置为新span的父span。修改完后运行程序,效果和前面我们自己在函数间传递span信息是一样的。总结本文展示了如何在线程内部传递链路信息,下篇文章介绍如何跟踪RPC请求,也就是如何在网络间(跨进程/线程)传递链路信息。 OpenTracing Java Library教程(1)——trace和span入门 http://niyanchun.com/opentracing-java-library-tutorial-1.html 2020-07-12T21:51:00+08:00 本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第1篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档。第1篇:单span的trace创建(本文)。第2篇:多span的trace创建(进程内SpanContext传递)。第3篇:跨服务(进程)传递SpanContext。第4篇:Baggage介绍。目标学习如何:实例化一个Tracer创建一个简单的链路给链路增加一些注解(annotation):即增加Tag和Log。开发步骤说明:源代码的exercise包下面的类是空的,是留给我们按教程一步步补充完善的;solution包是已经编写好的代码。我翻译的时候,都直接运行的是solution里面的代码,但教程里面是逐步完善代码的,也就是会有一个中间状态。所以我会根据内容作了一些必要的注释和修改。但如果你是第一次看的话,建议按照教程自己手动在exercise里面完善。跟着教程一步步学习。创建一个简单的Hello-World程序创建一个简单的打印程序:接受一个参数,输出"Hello, {arg}!"。代码如下:package lesson01.exercise; public class Hello { private void sayHello(String helloTo) { String helloStr = String.format("Hello, %s!", helloTo); System.out.println(helloStr); } public static void main(String[] args) { if (args.length != 1) { throw new IllegalArgumentException("Expecting one argument"); } String helloTo = args[0]; new Hello().sayHello(helloTo); } }运行:$ ./run.sh lesson01.exercise.Hello Bryan Hello, Bryan!创建一个trace一个trace是由若干span组成的有向无环图。一个span代表应用中的一个逻辑操作,每个span至少包含三个属性:操作名(an operation time)、开始时间(start time)、结束时间(finish time)。下面我们使用一个io.opentracing.Tracer实例创建由一个span组成的trace,可以使用io.opentracing.util.GlobalTracer.get()创建一个全局的Tracer实例。代码如下:import io.opentracing.Span; import io.opentracing.Tracer; import io.opentracing.util.GlobalTracer; public class Hello { private final Tracer tracer; private Hello(Tracer tracer) { this.tracer = tracer; } private void sayHello(String helloTo) { Span span = tracer.buildSpan("say-hello").start(); String helloStr = String.format("Hello, %s!", helloTo); System.out.println(helloStr); span.finish(); } public static void main(String[] args) { if (args.length != 1) { throw new IllegalArgumentException("Expecting one argument"); } String helloTo = args[0]; new Hello(GlobalTracer.get()).sayHello(helloTo); } }这里我们使用了OpenTracing API的一些基本特性:调用tracer实例的buildSpan()方法创建一个spanbuildSpan()方法的参数就是span的操作名调用start()方法真正创建出一个span通过调用finish()方法结束一个spanspan的开始时间和结束时间由具体的tracer实现自动生成(获取创建和结束span时的系统时间戳)此时,我们运行程序并不会和原来的程序有什么区别,也不会产生链路数据。因为OpenTracing只提供了SDK,并没有提供具体的链路实现,所以要产生真正的链路数据,需要借助具体的链路实现。部署Jaeger(补充段落,原文没有)这里我们选择Uber开源的Jaeger(发音为\ˈyā-gər\ ),因为它对OpenTracing支持的比较好,而且部署使用也非常简单。另外Jaeger的作者就是Yurishkuro。这里就不介绍Jaeger的细节了,有兴趣的可以去官网了解:Jaeger官网。Jaeger部署非常简单,从这里下载安装包或者下载docker镜像。这里我下载的macOS的安装包,解压后可以看到如下文件:example-hotrod jaeger-agent jaeger-all-in-one jaeger-collector jaeger-ingester jaeger-query直接运行./jaeger-all-in-one便可以启动一个完整的Jaeger。此时访问http://localhost:16686/即可查看Jaeger的UI:这样,一个OpenTracing的实现(Jaeger)就有了。接下来我们看如何在代码中集成。集成Jaeger在pom.xml中引入Jaeger的依赖:<dependency> <groupId>io.jaegertracing</groupId> <artifactId>jaeger-client</artifactId> <version>0.32.0</version> </dependency>然后写一个创建tracer的函数:import io.jaegertracing.Configuration; import io.jaegertracing.Configuration.ReporterConfiguration; import io.jaegertracing.Configuration.SamplerConfiguration; import io.jaegertracing.internal.JaegerTracer; public static JaegerTracer initTracer(String service) { SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv().withType("const").withParam(1); ReporterConfiguration reporterConfig = ReporterConfiguration.fromEnv().withLogSpans(true); Configuration config = new Configuration(service).withSampler(samplerConfig).withReporter(reporterConfig); return config.getTracer(); }最后,修改原来代码中的main函数:Tracer tracer = initTracer("hello-world"); new Hello(tracer).sayHello(helloTo);注意我们给initTracer()方法传入了一个参数hello-world,这个是服务名。该服务里面产生的所有span公用这个服务名,一般服务名会用来做过滤和聚合。现在运行代码,可以看到日志中有输出产生的span信息,而且也能看到Tracer实例的一些信息:19:07:10.645 [main] DEBUG io.jaegertracing.thrift.internal.senders.ThriftSenderFactory - Using the UDP Sender to send spans to the agent. 19:07:10.729 [main] DEBUG io.jaegertracing.internal.senders.SenderResolver - Using sender UdpSender() # tracer实例信息 19:07:10.776 [main] INFO io.jaegertracing.Configuration - Initialized tracer=JaegerTracer(version=Java-1.1.0, serviceName=hello-world, reporter=CompositeReporter(reporters=[RemoteReporter(sender=UdpSender(), closeEnqueueTimeout=1000), LoggingReporter(logger=Logger[io.jaegertracing.internal.reporters.LoggingReporter])]), sampler=ConstSampler(decision=true, tags={sampler.type=const, sampler.param=true}), tags={hostname=NYC-MacBook, jaeger.version=Java-1.1.0, ip=192.168.0.109}, zipkinSharedRpcSpan=false, expandExceptionLogs=false, useTraceId128Bit=false) Hello, Bryan! # span信息 19:07:10.805 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: a86d76defe28d413:a86d76defe28d413:0:1 - say-hello 当然也可以以调试模式启动,观察更多细节。这个时候,我们打开Jaeger的UI,左侧的Service选择“hello-world”,然后点击最下面的“Find Traces”,就可以查到刚才这次程序运行产生的Trace信息了:点击链路详情进去后,再次点击操作名,可以查看一些基本信息,Jaeger默认已经加了一些基本的信息。下面我们来看如何加一些自定义的信息。增加Tags和LogsOpenTracing规定了可以给Span增加三种类型的注解信息:Tags:key-value格式的数据,key和value完全由用户自定义。需要注意的是Tags增加的信息应该是属于描述整个span的,也就是它是span的一个静态属性,记录的信息适用于span从创建到完成的任何时刻。再说直白点就是记录和时间点无关的信息,这个主要是和下面的Logs作区分。Logs:和Tags类似,也是key-value格式的数据,区别在于Logs的信息都会带一个时间戳属性,记录这条属性产生的时间戳,所以比较适合记录日志、异常栈等一些和时间相关的信息。Baggage Items:这个主要是用于跨进程全局传输数据,后面的lesson04专门演示这个特性,这里先不展开介绍了。Tags和Logs的记录非常的简单和方便:private void sayHello(String helloTo) { Span span = tracer.buildSpan("say-hello").start(); // 增加Tags信息 span.setTag("hello-to", helloTo); String helloStr = String.format("Hello, %s!", helloTo); // 增加Logs信息 span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); System.out.println(helloStr); // 增加Logs信息 span.log(ImmutableMap.of("event", "println")); span.finish(); }注意这里使用了Guava's ImmutableMap.of()来构造一个Map。再次运行程序,同样会产生一个span,但这次span会多了一个Tag和Log信息(Jaeger默认已经加了一些内部的tag数据):从图中可以看到代码中加的Tags信息和Logs信息,而且Logs信息是带了时间了(这里展示的是从span开始时间经过的毫秒数)。关于Tags和Logs的规范,OpenTracing做了一些引导规范,可以参考:semantic_conventions.总结本文主要展示了如何创建一个span,下篇文章演示如何如果创建一个包含多个span的trace,以及如何在进程内部(不同方法间)传递span信息。 APM SkyWalking基本使用介绍 http://niyanchun.com/skywalking-introduction.html 2020-07-09T22:29:00+08:00 APM介绍APM是什么?APM全称Application Performance Monitor,即应用性能监控(也有翻译成Application Performance Management,应用性能管理的),所有和应用性能相关的指标、管理相关的事情都属于它的范畴。Skywalking就是一个具体的APM软件,也是Apache下面的一个顶级项目,而且是由中国人发起。随着分布式和微服务架构的普及,一个系统所包含的服务、节点越来越多,APM的需求越来越明显。这里就拿一个具体的例子来看APM有什么用处吧。现在我们有如下一个基于Spring Cloud的微服务系统:这个系统是一个非常简化的微服务架构了,它包含了5个服务:网关服务:使用zuul实现;注册中心:使用Eureka实现;用户服务:提供RESTful API的用户服务,使用Spring MVC实现;该服务会调用产品服务;产品服务:提供RESTful API的产品服务,使用Spring MVC实现,使用了一个H2数据库;Web服务:外部用户通过该服务来访问系统各项服务,这里使用curl命令模拟。这个系统的代码可以从这里下载:github: skywalking-demo。现在呢,这个系统跑起来以后客户反馈了两个问题:系统偶尔出现请求调用失败。请求有时候响应很慢。现在该怎么定位这2个问题呢?如果没有APM,最直观的方式从调用发起端(Web)一个个往后排查。如果服务少,机器少这个方式还行,但如果有很多服务,每个服务还有多个实例,那可能就有几十个甚至成百上千个服务实例,一个一个排查估计客户没发飙之前运维同学已经离职了。这个时候,是该让APM上场了。本文介绍Skywalking。Skywalking介绍本文目标是先把整个系统运行起来,具体Skywalking的细节,后面文章再讲述。Skywalking架构图如下(图片来自官方):可以看到主要由四部分组成:Agent(也叫Probe):代理或者探针,集成在被监测的应用中(SDK形式或者动态注入),采集应用的数据发送给后端(OAP)。UI:自带的Web页面。OAP:后端,接收Agent的数据并进行分析。Agent和OAP通信走的是gRPC,端口号默认为11800;UI和OAP通信走的是HTTP接口,端口号默认为12800。Storage:后端存储,OAP处理后的数据存储在这里。生产比较常用的是MySQL和ES。先从官网下载安装包:下载页面,目前最新的版本是8.0.1。下载后解压即可完成安装。然后在解压后的目录执行bin/startup.sh即可启动(Windows下执行startup.bat),这样默认启动了两个模块:Skywalking UI,默认端口为8080。Skywalking OAP,即后端。默认存储使用的是H2数据库,对于测试和体验来说,够用了。浏览器访问http://127.0.0.1:8080/,如果页面正常,说明Skywalking已经部署好了。下面看如何借助Skywalking来定位上面客户反馈的两个问题。服务集成Skywalking首先得在服务里面集成Skywalking的Agent。Java的Agent是通过字节码注入实现的。所以启动时在虚拟机参数中配置javaagent即可。下载前面说的代码,然后编辑每个服务启动时的虚拟机参数,如图:-javaagent:/Users/allan/portable-software/apache-skywalking-apm-bin-8.0.1/agent/skywalking-agent.jar:将这个里面的路径改为你自己Skywalking的安装部署路径,所有服务都一样。-Dskywalking.agent.service_name=eureka-server:这个是指定服务的名字。注意不同的服务修改成不同的名字。这些配置好以后,按如下顺序依次启动4个服务(不按照这个顺序启动也可以,只是可能会报些错误以及服务发现可能慢一些,但不影响功能):eureka-server:监听端口为8761。启动后,如果浏览器能访问http://127.0.0.1:8761/,则表示启动成功。product-service:监听端口为8082。提供一个RESTful API接口:/products/{id},这个接口内部会有一个数据库访问操作,数据库默认使用的是H2.user-service:监听端口为8081。提供一个RESTful API接口:/users/{id},这个接口内部会使用RestTemplate调用product-service的/products/{id}接口。zuul-server:监听端口为8000。默认配置了两个路由:zuul: routes: user-service: path: /user-service/** serviceId: USER-SERVICE product-service: path: /product-service/** serviceId: product-service配置的含义是将/user-service/开头的请求转发给USER-SERVICE(不区分大小写)服务,转发的时候去掉前面的/user-service。比如 /user-service/users/1转发到USER-SERVICE时会变为/users/1。product-service部分配置一样。都启动后,在浏览器能访问http://127.0.0.1:8761/,看下3个服务是否已经在Eureka中注册成功(Eureka自身不注册)。如果成功了,我们使用curl模拟web发送如下请求(这里我用的是HTTPie这个工具,比curl使用简单,且输出自动高亮且格式化了。如果你没有装,直接将下面的http命令替换成curl即可,或者使用postman之类的工具都OK):# curl http://localhost:8000/user-service/users/1 http http://localhost:8000/user-service/users/1 # 上面的命令多执行几次,保证出现过以下两种结果 # 结果1:即客户反映的有调用失败的情况 { "error": "Internal Server Error", "message": "500 : [<Map><timestamp>2020-07-09T09:17:36.024+0000</timestamp><status>500</status><error>Internal Server Error</error><message>something error in product service</message><path>/products/1</path></Map>]", "path": "/users/1", "status": 500, "timestamp": "2020-07-09T09:17:36.065+0000" } # 结果2:结果正常,但明显比较慢。 { "products": [ "audi", "benz", "volvo", "honda" ], "user_id": "1" }我们发现多次执行请求(curl命令),的确有时会失败(结果1);不失败的时候,一次请求挺慢的。和客户反馈的是一致的。这个时候,打开Skywalking的Web页面(http://127.0.0.1:8080/)来看看为什么吧(Web上面的详细功能本文就不介绍了,后面文章介绍):先看trace菜单,也就是调用的链路数据(如果你看不到数据,先点击一下那个Clear,然后选一下时间范围,再点击Search),就能出现链路图了。先在左侧找一个没有标红的链路,右侧选择“Table”视图,如下:可以看到,整个请求耗时9057ms,product-service的一个/products/{id}接口就占了9029ms,这样我们就知道调用慢到底是哪个服务导致的了。而且可以看到的是,里面的数据库操作并没有占用太多时间。在去检查/products/{id}到底做了什么事情,耗费了这么多时间之前,我们再找一个标红的链路看看。标红的链路表示请求出错了,对应刚才发请求时返回的结果1。我们随便找一个出错的,点击右侧链路中最下面的那个请求(即图中的/products/{id},根据颜色可以看出来这个接口属于product-service),右侧会弹出一个详细内容框,这里有一些基本的元数据信息,最主要的是对于出错的,展示出了调用栈信息:从调用栈可以看出,错误发生在ProductServiceApplication.java文件的第30行。至此,我们已经知道了偶尔出错和请求慢的罪魁祸首都是product-service提供的/products/{id}接口导致的。然后赶紧看下那里的代码到底在搞什么:@SpringBootApplication @RestController public class ProductServiceApplication { public static void main(String[] args) { SpringApplication.run(ProductServiceApplication.class, args); } @Autowired ProductRepository productRepo; @GetMapping("/products/{id}") public Object getProductsById(@PathVariable String id) throws Exception { // 产品经理让加的,说方便后面让客户给钱升级版本 int trap = (int) (Math.random() * 10); if (trap % 2 == 0) { throw new Exception("something error in product service"); } Thread.sleep(trap * 1000); productRepo.findByDescriptionLike("%world%"); List<String> productList = new ArrayList<>(8); productRepo.findAll().forEach(product -> productList.add(product.getName())); return productList; } }第30行代码是throw new Exception("something error in product service");。看了代码上下文我们才发现:原来罪魁祸首都是产品经理:让在代码里面加了一个随机的出错,即使不出错,也要sleep。怪不得请求偶尔会出错或者请求比较慢呢。原来都是产品经理的锅,是时候杀个产品经理来祭天了。至此,可以看到,APM提供的链路分析对于定位请求出错以及耗时方面是非常方便的。当然APM除了链路跟踪这个功能之外,一般还会提供很多其它跟性能相关的数据。比如Skywalking还提供了指标分析(Dashboard)、拓扑图(Topology)、告警(Alarm)、性能剖析(Profile)功能。下面截几张图看看,后面文章专门介绍。Dashboard的APM页面:Dashboard的数据库页面:如果你的图那里没有看到数据的话,去Skywalking安装目录下,修改config/application.yml文件,找到下面的行,将default后面的值改为1(只有超过这个值的SQL语句才会被认为是慢SQL),如下图:slowDBAccessThreshold: ${SW_SLOW_DB_THRESHOLD:default:1,mongodb:100}然后重启Skywalking,重新发几次请求,再查看这个页面。拓扑图页面:可以根据服务调用自动还原应用拓扑图,那个连线会根据请求关系动态流动,截图体现不出来。可以看到,这个拓扑图和前面画的那个架构图是一致的。告警页面:如果你之前没有接触过APM,我想看到这些,应该是有被惊艳到的。的确,APM是应用性能监控或管理的大杀器。除了Skywalking,还有一些开源和商用的APM。网上的文章很多,有兴趣的可以自行Google。下篇文章介绍一下Skywalking的整体架构。