目前的Trace Profiling是服务-端点方法维度的,换句话说,在指定了要监控的服务和对应的端点方法后,生成的Trace Profiling Task会被对应服务的所有实例拉取并使用,即相应服务的所有机器都进行Trace Profiling。
但问题发生的场景是多种多样,我们有的时候,并不需要某个服务的所有实例的Trace Profiling,比如说我们进行了ABTest或者灰度部署,在这种情况下,我们真实的需求是针对某个实例进行Trace Profiling。因此,支持实例维度的Trace Profiling功能在某些场景下能够有效地降低我们排查问题带来的性能损耗。
Profiling 实现原理
用户提交一个Trace Profling Task,具体的执行流程如下:
用户的任务创建请求会直接入库保存,之后的所有操作其实都是定时线程池进行异步执行的。要完成这项功能,我们需要对以下流程进行考量和改造:
OAP与前端的交互,包括创建以及展示Trace Profiling Task的两个接口。
Trace Profiling Task的存储,我们使用的是ES,按天建索引,需要修改创建索引的mapping。
OAP与Agent的交互,Agent获取到对应的任务配置并执行。
单机Trace Profiling方案实现
1. 新建任务
我们的页面设计如下:
在原本页面的最后加入了实例一栏,用户能够选择这个服务上对应的实例进行实例维度的Trace Profiling任务创建,为了简化处理流程,传入的字段为instanceName,而非instanceId,对应创建任务的接口方法为org.apache.skywalking.oap.query.graphql.resolver.ProfileMutation#createProfileTask,具体的参数类为org.apache.skywalking.oap.server.core.query.input.ProfileTaskCreationRequest:
同时修改对应的graphqls文件:
2. 查询任务
和上面的内容类似,我们修改了查询任务部分的返回结果。
查询任务的接口实现方法为org.apache.skywalking.oap.query.graphql.resolver.ProfileQuery#getProfileTaskList,具体的结果类为org.apache.skywalking.oap.server.core.query.type.ProfileTask:
同样地,修改graphqls文件:
3. ES 交互
我们在修改了ProfileTask后,需要考虑ES交互方面的修改,具体内容如下:
ES 索引创建
ES 数据插入
ES 查询结果解析
在数据插入和查询结果解析方面和原本的内容大同小异,在此不做赘述。接下来需要考虑的是ES对应索引的mapping更新,ProfileTask存放到ES的模型类是ProfileTaskRecord,是Record类的子类。在ES存储的场景下,Record类的子类对应的索引名默认为records-all,
org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.IndexController:
records-all索引是一个按天维度创建的索引,这个索引对应了多个model类,第一个model类会创建这个索引,后面的model类会用自己的mapping对这个索引的mapping进行更新,model类中的mapping信息是根据类中的注解结算得到的, 具体的代码如下
org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.StorageEsInstaller#createTimeSeriesTable:
为此我们需要将新加的字段补充在对应model的mapping文件上,在对应的模型类org.apache.skywalking.oap.server.core.profiling.trace.ProfileTaskRecord添加字段及对应注解:
并在对应的Builder实体与存储之间相互转换代码中添加该字段的内容即可,我们在注解中设置了storageOnly为true,这个注解的使用场景在org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.StorageEsInstaller#createMapping:
如果某个字段不会被设为查询条件,可以进行这样的设置,ES便不会对这个字段进行索引,进而节省了对应的内存空间。
4. init模式部署
在更改ES mapping字段后必须重新运行SkyWalking的init步骤,在部署的时候OAP会检查model对应的索引是否在ES中存在,如果不存在且是no-init模式会进行while循环等待,这个等待会一直持续下去,最终导致部署超时失败,具体代码位置如图所示:org.apache.skywalking.oap.server.core.storage.model.ModelInstaller#whenCreating
在内部的model判断内会将ES template中的mapping和代码中model生成的mapping做对比,如果前者不能包含后者的所有字段,则会返回false,具体代码位置如下:org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.StorageEsInstaller#isExists:
在init模式下,代码会对已经存在的索引的mapping以及ES template进行更新,进而解决循环等待问题,具体代码位置如下:
org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base.StorageEsInstaller#createTimeSeriesTable
对比两者mapping上的不同,更新索引mapping:
5. Agent端Trace Profiling逻辑改造
Agent端的Trace Profiling是一个极其复杂的功能。
我们能够在这部分代码中看到大量的流程抽象、SPI扩展点设计以及异步执行策略等内容。通过这些内容,功能的可靠性和可维护性得到了有效的保证。
以下是Agent端的Trace Profiling整体流程:
整体功能比较复杂,为了方便理解可以将整个流程图划分成四个子流程,以流程图中四个星号标记作为起始点,四个子流程分别是:
ProfileTaskChannelService与OAP的交互主要是通过gRPC定时从OAP中拉取任务配置,以及定时将切片数据发送给OAP。
接收到具体的Profiling Task后Agent开始创建ProfileTask,然后异步执行任务,在任务中异步执行ProfileThread。
Trace创建TracingContext的过程中,尝试将对应的线程封装为Profiler并添加到profilingSegmentSlots上,在进行子线程的传递时也会再次插入profilingSegmentSlots。
ProfileThread循环遍历profilingSegmentSlots,针对每个非null的profiler进行状态的转换,将PROFILING状态的profiler执行Thread Dump,并将得到的数据封装成snapshot,存储到ProfileTaskChannelService的数据queue中。
在我们本次的功能实现中,只会涉及到前两个流程部分。
首先,拉取任务配置,代码如下
org.apache.skywalking.apm.agent.core.profile.ProfileTaskChannelService#run:
使用serviceName、instanceName、 lastCommandCreateTime三个参数拉取到任务配置,对应的配置被封装为commands,需要进一步解析使用。对应在OAP方面,则是在接收到调用后,调用任务配置的缓存,并将结果返回,具体代码位置如下:
org.apache.skywalking.oap.server.receiver.profile.provider.handler.ProfileTaskServiceHandler#getProfileTaskCommands
这个缓存是在用户提交配置给ES后,OAP端有定时任务从ES拉取下来放入缓存,在这里其实也存在两种做法:
OAP将非对应instance的任务,在返回结果时过滤掉。
OAP按照原逻辑返回,在Agent端过滤。
第一个方案的优点是不需要考虑Agent端的反馈,缺点是Agent的lastCommandCreateTime没有得到更新,后续同一时刻每个Agent请求的lastCommandCreateTime都可能不一致。
第二个方案的优点是所有的Agent都获取到了一致的任务配置,缺点是会增加交互时字段解析的工作量,而实际上这部分内容是以key-value格式放在Commands的args中的,并不需要修改协议部分。
考虑到Agent端的管理,我们选择了第二种方案,让OAP的处理逻辑简单化。OAP只需要向对应服务全部实例的Agent返回相同的任务配置,由Agent端判断是否应该真正地执行任务。
因为snapshot数据格式和内容均未发生改变,所以将snapshot数据传回OAP相关的逻辑不需要任何变动。
6. Agent端Profiling任务的创建
Agent端在拉取到任务配置后,如果任务配置不为空,这些具体的配置会被解析成对应的ProfileTaskCommand,并封装为ProfileTask进行任务提交,代码如下:
org.apache.skywalking.apm.agent.core.commands.executor.ProfileTaskCommandExecutor#execute
但如果写在这里,会有一个问题,Agent的lastCommandCreateTime依旧没有被更新,所以将任务是否执行的判断逻辑移动到下一层级,代码如下:
org.apache.skywalking.apm.agent.core.profile.ProfileTaskExecutionService#addProfileTask
至此我们完成了Agent端的改动。
7. 延伸讨论:Agent端 Trace Profiling做了什么?
在这一环节,我们主要讨论Agent端的第三、四子流程部分。
将这部分功能从总流程图里单独提取出来,如图所示:
从流程图中,我们也能够看出这几个类在流程中起到的作用:
TracingContext 跟随线程,监控线程的具体情况、生命周期
ProfileTaskExecutionService 操纵任务上下文,是任务执行的具体封装
ProfileTaskExecutionContext 通过profilingSegmentsSlots来确定哪些线程会被trace profile
ProfileThread 不断轮循profilingSegmentsSlots,对合适的线程进行Thread Dump
TreadProfiler 持有具体的业务线程,拥有profile status状态标识,是业务线程Thread Dump操作的具体执行者
我们从一个正常业务线程的entrySpan开始,假设Agent已经开始了trace profiling,具体的执行流程如下:
一个业务线程的开始,TracingContext被创建,尝试添加profiler,设置profile status
添加profiler的本质是将线程封装成profiler,然后通过CAS尝试在profilingSegmentSlots中找到一个null的位置并占据
ProfileThread 在不断循环遍历profiler时,遍历到了这个profiler:
如果它还没到设置的开始监控时长,继续等待
如果它到达了设置的开始监控时长,状态流转,从PENDING改为PROFILING
如果它是PROFILING,则进行Thread Dump,将当前线程的栈信息封装成snapshot并保存到snapshotQueue
线程结束时,会将profiler在profilingSegmentSlots中占据的位置释放
ProfileTaskChannelService会定时将snapshotQueue的数据发送给OAP
而SkyWalking则是综合这些Thread Dump数据和Trace数据,进而得到了具体代码的耗时。
对于一个被Trace Profiling的线程,示例如下:
如上图所示,通过Trace中进入各个Endpoint的时间,再结合Thread Dump时的栈信息,OAP能够较为精准地估计出程序在具体的代码行上耗费的时间。
总结
在本文中我们讨论了 SkyWalking单实例Profiling的具体实现,并对Agent端Trace Profiling的实现流程与原理进行了阐述。