引言
在日常项目中,我们常常会遇到线上性能问题,尤其在微服务的场景下,调用链错综复杂,如何才能快速的定位和解决问题,然后享受美好的夏日时光。枯藤老树昏鸦,空调WiFi西瓜,葛优同款沙发,夕阳西下,我就往上一趴。岂不美哉?
SkyWalking是一个观察性分析平台和应用性能管理系统(APM)。由吴晟等人开发,目前已经是Apache顶级项目。SkyWalking提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。能非常方便的定位线上性能问题。
本文将基于SkyWalking
7.0.0,开发Firefly服务端、客户端的agent插件为例,与大家分享SkyWalking的插件开发流程。
基本概念
SkyWalking整体架构主要分三层,探针、后端和界面。
探针(Probe)
在应用端收集性能度量数据并发送给后端。SkyWalking支持三种探针:
Agent --
基于ByteBuddy字节码增强技术实现,通过jvm的agent参数加载,并在程序启动时拦截指定的方法来收集数据。
SDK -- 程序中显式调用SkyWalking提供的SDK来收集数据,对应用有侵入。
Service Mesh -- 通过Service mesh的网络代理来收集数据。
后端(Backend)
接受探针发送过来的数据,进行度量分析,调用链分析和存储。后端主要分为两部分:
OAP(Observability Analysis Platform)-
进行度量分析和调用链分析的后端平台,并支持将数据存储到各种数据库中,如:ElasticSearch,MySQL,InfluxDB等。
OAL(Observability Analysis Language)-
用来进行度量分析的DSL,类似于SQL,用于查询度量分析结果和警报。
界面(UI)
RocketBot UI -- SkyWalking 7.0.0 的默认web UI
CLI -- 命令行界面
这三个模块的交互流程:
构建SkyWalking
各种编程语言开发的应用(JVM、.Net、Go、Python等)都可以使用SkyWalking探针来收集数据。平时我们用的最多的是基于JVM的技术栈(Java、Scala、Kotlin),对于JVM技术栈,使用agent
plugin的方式来收集性能数据有如下优势:
对应用代码无侵入。
对应用进程性能开销小,可以做到线上实时性能分析和调用链追踪。
支持任意粒度性能数据的收集, 比如:API级别、方法级别等。
支持同步、异步、跨线程(Thread)、协程(Coroutine)的性能追踪。
SkyWalking本身已经提供了足够多agent
plugins,支持了JVM技术栈常用的开源框架和库(Spring
Cloud、各种网络框架、Dubbo、各种数据库等),但是JVM的生态系统非常的庞大和活跃,各种开源框架和库层出不穷,成熟框架的版本更新也非常快,SkyWalking本身不一定能够及时的追踪这些新的框架或者新版本的库,所以有时候需要根据具体的项目或者工具来做定制的plugin开发。
SkyWalking提供的完整的plugin开发和自动测试框架,开发一个新的plugin需要下载SkyWalking源码进行构建。构建步骤如下:
mvn clean package -DskipTests
IDEA导入SkyWalking工程
mvn compile -DskipTests
开发插件
源码构建成功之后,就可以开始插件开发了。首先我们在源代码的apm-sdk-plugin目录下建立自己的plugin
module,目录结构如下:
[skywalking]
|-[apm-sniffer]
|-[apm-sdk-plugin]
|-[firefly-5.x-plugins]
|-[firefly-5.x-net-http-client-plugin]
|-pom.xml
|-[firefly-5.x-net-http-server-plugin]
|-pom.xml
|-pom.xml
定义拦截点
Agent
plugin是使用ByteBuddy做字节码增强,类似于AOP,相当于给目标类增加了一个代理。SkyWalking封装了相关的操作,形成了自己的开发框架。
上述代码定义了一个针对firefly http server的拦截器,用来收集firefly http
server的性能数据。
enhanceClass
表示需要拦截的类,这里拦截的是AsyncHttpServerConnectionListener,因为firefly
http
server接收到的所有请求都会进入这个listener,只需要对这个类做一个代理就可以拿到所有请求的性能数据了。这里除了可以按类名去拦截,还可以按照Annotation、前缀等方式去拦截目标类,具体可以参考ClassMatch接口的子类:
定义方法拦截点
InstanceMethodsInterceptPoint用来定义,这个类中对哪些方法进行拦截,这里直接按方法名拦截onHttpRequestComplete方法。除了按方法名拦截,SkyWalking还封装了各种方式匹配要拦截方法,这里就不在赘述,可以参考ElementMatchers类相关源码或者文档。
第57行getMethodsInterceptor定义了拦截器的实现类的类名。一会儿我们就要实现这个类来记录性能数据。
实现拦截器
在实现拦截器之前我们需要了解分布式追踪系统中的一些关键数据结构。
上图展示了一个简单的场景,server3通过远程调用server2返回结果给用户。右侧图表中的每一行称为一个跨度(Span),在拦截器中,我们就需要构造对应的Span发送给SkyWalking后端,那么SkyWalking的后端就能够根据Span的相关信息做调用链和度量分析。
跨度(Span)
Span是分布式追踪的主要构造块,表示⼀个独⽴的⼯作单元,它包含如下状态记录:
操作名称(Operation Name)。
开始和结束时间戳。
自定义标签(Tags),k-v结构,一般记录操作的一些信息如:http状态码,http方法、ip、port、db.statement等。
Logs,k-v结构,用来记录错误日志。
SpanContext,用来记录trace_id、parent_id等用作调用链分析。能够把一个API中的跨线程、跨进程调用连接起来。
用yml表示span的结构如下:
其中spanType分三种:
Entry -- 服务端入口,比如Spring的Controller、MQ的Consumer等。
Exit -- 客户端远程调用,如http client、JDBC、redis client,MQ
producer等。
Local -- 本地方法调用,用来记录本地方法的执行时间。
Span的Context记录分两种:
ContextCarrier -- 用于跨进程传递上下文数据。
ContextSnapshot -- 用于跨线程传递上下文数据。
方法拦截器实现
了解了Span的基本概念,我们就可以开始实现刚才定义的方法拦截器AsyncHttpServerConnectionListenerInterceptor了。
首先看一下我们准备去拦截的目标方法onHttpRequestComplete的代码:
Firefly 5.x是一个基于Coroutine的网络框架,所有的http
request都会进入这个callback然后找到相关的router来处理request,router的handler会运行在一个coroutine上,为了保持和java的兼容性,异步结果通过CompletableFuture返回。
AsyncHttpServerConnectionListenerInterceptor的实现:
beforeMethod在进入onHttpRequesComplete方法之前执行。
49-57行。创建ContextCarrier,并且判断http
header中是否有需要恢复的上下文数据,如果有则填充到ContextCarrier中。
62-69行。创建一个Entry类型的Span,并用Tags记录一些请求相关的信息。
71行。捕获上下文快照,并记录到Firefly的RoutingContext中,因为目标方法是异步返回的,我们需要保持当前请求的上下文快照后续可以在另外一个线程中恢复。
72行。设置Span的状态为异步执行。
afterMethod方法在onHttpRequestComplete方法执行之后执行。这里可以拿到该方法的返回值,CompletableFuture,然后在future执行完成之后调用span.asyncFinish()来结束当前span并把span的相关数据发送给SkyWalking的后端OAP平台。
handleMethodException用来处理目标方法抛异常的情况,这里直接记录一下错误日志。
至此Firefly 5.x http server的plugin就开发完成了,http
client的插件也类似,就不在赘述,区别就在于client的方法拦截器中是先创建ExitSpan,然后吧ContextCarrier中的信息存储到http
request中发送给server,流程和server是正好相反的。
组件定义配置
开发完所有代码之后,还需要在ComponentsDefine类和component-libraries.yml配置文件中增加新plugin的id、名称等信息的配置。
构建插件
由于我们在下载源码后,已经对SkyWalking做过一次全量构建,开发新的插件之后,就不需要对整个项目进行构建,这里可以只构建agent模块即可。运行命令:mvn
clean package -Pagent,dist -DskipTests=true
这里总结一下插件的开发流程如下:
apm-sdk-plugin目录下建立自己的plugin module
定义拦截点(实现ClassInstanceMethodsEnhancePluginDefine)
实现方法拦截器(InstanceMethodsAroundInterceptor)
在ComponentsDefine和component-libraries.yml中配置插件信息
构建agent模块mvn clean package -Pagent,dist -DskipTests=true
运行插件
插件代码构建完成之后,我们可以在实际的代码中加载一下新开发的插件看看运行是否正常。
启动SkyWalking
构建完成的SkyWalking目录结构如下:
运行bin/startup.sh启动SkyWalking的后端服务和UI界面。看logs目录中的日志启动成功后,打开浏览器就可以访问SkyWalking的UI界面了
启动应用代码
Server3应用代码如下:
启动的时候要在启动参数,配置agent路径、服务名称、SkyWalking后端地址等。配置如下:
-javaagent指定到刚才构建好的skywalking-agent.jar
SW_AGENT_NAME -- 指定服务名称
SW_AGENT_COLLECTOR_BACKEND_SERVICES -- 指定OAP server
的地址和端口
配置完成后用IDEA运行server3
同样的方法配置server2并启动。
浏览器访问 ,浏览器显示
Server 3 -> server2 coroutine
这个时候我们就可以通过SkyWalking看到刚才请求的调用栈了。
同时也可以在拓补图中看到服务间的调用关系。
这样就说明插件工作正常了。
插件自动测试框架
刚刚我们已经成功的构建并通过启动实际的应用运行了新开发的SkyWalking
plugin。这种手工启动应用来进行插件的测试效率较低,SkyWalking自身已经提供了一套全自动的插件测试框架,来将刚才的构建、运行应用、发起测试请求、观察测试结果等步骤自动化运行。
创建自动测试脚手架
自动测试框架在源码的test目录下,目录结构如下:
[skywalking]
|-[plugin]
|-[scenarios]
|-generator.sh
|-run.sh
|-pom.xml
运行generator.sh命令,根据提示输入scenarios名称、类型等信息。完成后,测试脚手架会放到scenarios目录下,脚手架目录结构如下。
[plugin-scenario]
|- [bin]
|- startup.sh
|- [config]
|- expectedData.yaml
|- [src]
|- [main]
|- ...
|- [resource]
|- log4j2.xml
|- pom.xml
|- configuration.yaml
|- support-version.list
配置测试场景
创建完脚手架之后需要在configuration.yaml中对测试场景进行一些配置。
type -
测试类型,jvm表示应用直接通过main函数启动,tomcat表示应用在tomcat中加载。Firefly
http server直接在main函数启动所以这里配置jvm。
entryService -
测试请求的URL,测试场景启动成功后,首先进行健康检查,检查成功后会向此URL发起测试请求。
healthCheck --
健康检查URL,场景启动后会向此URL发送一个HEAD请求。返回200表示检查成功。
startScript -- 场景启动脚本。
开发测试场景
把刚才运行的server3和server2的代码移植到测试场景中即可。代码如下:
配置断言
在expectedData.yaml中,我们可以配置一些断言来测试plugin向后端发送的Span数据是否正确来测试plugin的功能。
断言配置中,数字类型的字段可以用
nq、eq、ge、gt等表达式,字符串类型的字段可以用not
null、null、eq等表达式。
运行测试场景
SkyWalking的插件自动测试框架会把我们测试应用打包成docker镜像然后运行,所以在运行测试场景之前需要在本机启动docker,然后运行:
./test/pugin/run.sh -f \${scenario_name}
就可以运行刚才开发的测试场景了。
写在最后
在错综复杂的微服务架构环境下,SkyWalking可以对整个应用的各项性能指标以及调用链进行追踪和分析,能够帮助我们快速的定位和发现性能瓶颈。本文分享了SkyWalking插件开发的完整步骤和流程,希望对大家有所帮助。