使用SkyWalking-go2sky收集Golang运行时指标技术
需求理解
本次项目,是将go2sky作为agent,在用户的代码中导入,并借助go2sky收集golang runtime metrics,并将metrics上报到skywalking-OAP,skywalking-OAP提供对应的UI进行展示。最终呈现给用户的应该类似下面的界面:
设计方案
总体流程
收集golang runtime metrcis的设计分为go2Sky和skywalking OAP两个模块:
-
go2Sky完成对golang runtime metrcis的收集并通过gRPC上报到skywalking OAP
-
skywalking OAP接收来自go2sky的数据,并对数据进行处理并持久化

客户端方案
客户端的目的是收集golang runtime metrcis并且格式化,再通过gRPC发送到服务端
设计客户端方案时,主要考虑以下几点:
-
确定收集各类型golang runtime metrcis的收集工具
-
确定数据收集和数据上报的协作方式
指标收集
经过调研,确定使用golang runtime包中的工具类和shirou/gopsutil完成golang runtime metrcis的收集,具体如下表
|
指标名
|
说明
|
收集方式
|
计算方式
|
|
heapAlloc
|
堆内存, 堆中已经分配给对象的字节数,GC内存回收后HeapAlloc取值相应减小。
|
runtime.MemStats.HeapAlloc
|
直接使用
|
|
stackInUse
|
栈内存
|
runtime.MemStats.StackInuse
|
直接使用
|
|
gcNum
|
垃圾回收-gc次数
|
runtime.MemStats.NumGC
|
计算每次收集周期内发生的gc次数
|
|
gcPauseTime
|
垃圾回收-gc时长
|
runtime.MemStats.PauseNs
|
需要计算:
PauseNs是一个循环队列,记录最近垃圾回收系统中断的时间
|
|
goroutineNum
|
协程数量,当前存在的协程数量。
|
runtime.NumGoroutine()
|
直接使用
|
|
threadNum
|
线程数量
|
runtime.ThreadCreateProfile(nil)
|
直接使用
|
|
cpuUsedRate
|
CPU使用率
|
cpu.Percent
|
直接使用
|
|
memUsedRate
|
物理内存使用率
|
mem.UsedPercent
|
直接使用
|
数据上报
设计思路:
- 考虑到skywalking已经集成了gRPC并且go2sky上报trace和JavaAgent上报metrics都是通过gRPC,因此golang runtime metrics也采取gRPC的方式进行上报。
- 考虑到一次请求只上报一个metric对象,因此采取非stream模式的gRPC连接。
- 考虑到数据收集和数据上报过程的解耦,使用“生产者-消费者”模式:启动时开启两个goroutine,一个负责收集数据并向chan中发送,一个负责从chan中取出数据,并通过gRPC上报,chan设置1000的buffer
- OAP存储指标需要通过service和serviceInstance确定指标的来源,参考trace收集方案,service由用户输入、serviceInstance根据UUID和ip地址生成
- 数据协议
skywalking的数据收集协议在skywalking-data-collect-protocol中定义,需要在language-agent目录下新增GolangMetric.proto,然后更新skywalking-goapi的代码,并在go2sky中更新依赖。
syntax = "proto3";
package skywalking.v3;
option java_package = "org.apache.skywalking.apm.network.language.agent.v3";
option go_package = "github.com/easonyipj/skywalking-goapi/collect/language/agent/v3";
import "common/Common.proto";
// Define the Golang metrics report service.
service GolangMetricReportService {
rpc collect (GolangMetricCollection) returns (Commands) {
}
}
message GolangMetricCollection {
repeated GolangMetric metrics = 1;
string service = 2;
string serviceInstance = 3;
}
message GolangMetric {
int64 time = 1;
int64 heapAlloc = 2;
int64 stackInUse = 3;
int64 gcNum = 4;
float gcPauseTime = 5;
int64 goroutineNum = 6;
int64 threadNum = 7;
float cpuUsedRate = 8;
float memUsedRate = 9;
}
服务端方案
服务端主要完成以下的工作:
- 启动时通过定义的OAL脚本动态生成Golang Metrics相关的类和对应的Dispatcher
- 启动时自动检查Golang Metrics相关的表是否已创建,如果未创建则自动创建
- 启动时注册gRPC handler 接收客户端的数据
- 将数据进行处理后持久化
- 接收前端的数据查询请求
设计服务端方案时,主要围绕如何实现上面五点工作,下面将从服务端初始化、数据接收和处理、数据持久化、数据查询四个部分来介绍服务端的设计方案(这四部分互相关联又互相独立)
服务端初始化
服务端初始化时,主要完成上面的工作1-3,在充分了解了JVM Mertics的收集流程后,完成服务端的初始化需要新增下面的内容:
- golang mertices相关的Source子类;
- golang mertices相关的OAL脚本
- 新增注册gRPC handler
- 新增golang metrics的处理类GolangSourceDispatcher
详细方案如下:
- skywalking OAP中的每个metric都对应一个org.apache.skywalking.oap.server.core.source.Source的子类和一个OAL脚本中的一行,因此需要先定义好Source类和OAL脚本;
-
- 在oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source中新增golang mertices相关的Source子类,用于保存相关的指标和生成对应的数据库表,以CPU指标为例,需新建下面的类,关键点:
- 继承Source
- @ScopeDeclaration中的id和scope()方法返回的id保持一致
@ScopeDeclaration(id = SERVICE_INSTANCE_GOLANG_CPU, name = "ServiceInstanceGolangCPU", catalog = SERVICE_INSTANCE_CATALOG_NAME) @ScopeDefaultColumn.VirtualColumnDefinition(fieldName = "entityId", columnName = "entity_id", isID = true, type = String.class) public class ServiceInstanceGolangCPU extends Source { @Override public int scope() { return DefaultScopeDefine.SERVICE_INSTANCE_JVM_CPU; } @Override public String getEntityId() { return String.valueOf(id); } @Getter @Setter private String id; @Getter @Setter @ScopeDefaultColumn.DefinedByField(columnName = "name", requireDynamicActive = true) private String name; @Getter @Setter @ScopeDefaultColumn.DefinedByField(columnName = "service_name", requireDynamicActive = true) private String serviceName; @Getter @Setter @ScopeDefaultColumn.DefinedByField(columnName = "service_id") private String serviceId; @Getter @Setter private double usePercent; }
- 新增OAL脚本
- 在oap-server/server-starter/src/main/resources/oal中新增处理golang runtime mertics的的OAL文件
instance_golang_cpu = from(ServiceInstanceJVMCPU.usePercent).doubleAvg(); - 在oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4和OALParser.g4中新增相关的关键字
- 在oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source中新增golang mertices相关的Source子类,用于保存相关的指标和生成对应的数据库表,以CPU指标为例,需新建下面的类,关键点:
- 服务端初始化时,应该注册好gRPC handler,并且使用OAL脚本动态生成处理golang runtime mertics的类:
- 在apm-network模块的proto中新增上文文GolangMetric.proto,并执行编译命令./mvnw compile -Dmaven.test.skip=true
- 在oap-server/server-receiver-plugin新增处理golang mertices的plugin模块skywalking-golang-receiver-plugin,目录结构如下:
├── skywalking-golang-receiver-plugin │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── org │ │ │ └── apache │ │ │ └── skywalking │ │ │ └── oap │ │ │ └── server │ │ │ └── receiver │ │ │ └── golang │ │ │ ├── module │ │ │ │ └── GolangModule.java │ │ │ └── provider │ │ │ ├── GolangModuleProvider.java │ │ │ ├── GolangOALDefine.java │ │ │ └── handler │ │ │ └── GolangMetricReportServiceHandler.java │ │ └── resources │ │ └── services │ │ ├── org.apache.skywalking.oap.server.library.module.ModuleDefine │ │ └── org.apache.skywalking.oap.server.library.module.ModuleProvider │ └── test │ └── java- 新增接收gRPC请求的handler GolangMetricReportServiceHandler,负责解析数据并初步封装,然后调用golang metrics的处理类GolangSourceDispatcher的sendMetric方法
- 新增相应的OALDefine - GolangOALDefine.java,需要在构造方法中传入对应OAL脚本的名称和source类所在的位置,以生成相关的表,示例如下
public class GolangOALDefine extends OALDefine { public static final GolangOALDefine INSTANCE = new GolangOALDefine(); private GolangOALDefine() { super( "oal/golang.oal", "org.apache.skywalking.oap.server.core.source" ); } } - 新增相应的GolangModule和GolangModuleProvider,并在resources/META-INF/services目录下的配置文件指定实现类的全路径名(同时在oap-server/server-starter/pom.xml中补充新增的skywalking-golang-receiver-plugin依赖,便于进行跨包SPI;在application.yml中新增),便于启动时通过SPI机制加载这些类,GolangModuleProvider中完成对gRPC handelr-GolangMetricReportServiceHandler的注册和OAL脚本的解析(动态生成类并且自动生成相关数据库表)
public class GolangModuleProvider extends ModuleProvider { @Override public String name() { return "default"; } @Override public Class<? extends ModuleDefine> module() { return GolangModule.class; } @Override public ModuleConfig createConfigBeanIfAbsent() { return null; } @Override public void prepare() throws ServiceNotProvidedException, ModuleStartException { } @Override public void start() throws ServiceNotProvidedException, ModuleStartException { getManager().find(CoreModule.NAME) .provider() .getService(OALEngineLoaderService.class) .load(GolangOALDefine.INSTANCE); GRPCHandlerRegister grpcHandlerRegister = getManager().find(SharingServerModule.NAME) .provider() .getService(GRPCHandlerRegister.class); GolangMetricReportServiceHandler golangMetricReportServiceHandler = new GolangMetricReportServiceHandler(getManager()); grpcHandlerRegister.addHandler(golangMetricReportServiceHandler); } @Override public void notifyAfterCompleted() throws ServiceNotProvidedException, ModuleStartException { } @Override public String[] requiredModules() { return new String[] { CoreModule.NAME, SharingServerModule.NAME }; } }
-
数据接收和处理
服务端初始化时,注册了gRPC handler,遵循现有的metrics处理流程,gRPC handler接收数据后,将会对servceName和InstanceName格式化,然后调用GolangSourceDispatcher的sendMetric方法:
- 在oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider中新增GolangSourceDispatcher,用于接受gRPC handler的信息,使用source包中的实体类封装这些信息,并调用sourceReceiver.receive方法
├── agent-analyzer │ ├── pom.xml │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── org │ │ │ │ └── apache │ │ │ │ └── skywalking │ │ │ │ └── oap │ │ │ │ └── server │ │ │ │ └── analyzer │ │ │ │ ├── module │ │ │ │ │ └── AnalyzerModule.java │ │ │ │ └── provider │ │ │ │ ├── AnalyzerModuleConfig.java │ │ │ │ ├── AnalyzerModuleProvider.java │ │ │ │ ├── golang │ │ │ │ │ └── GolangSourceDispatcher.java │ │ │ │ ├── jvm │ │ │ │ │ └── JVMSourceDispatcher.java
数据持久化
通过对JVM Metrics采集流程的调研发现,通过调用OALEngineLoaderService根据OAL脚本动态生成类后,会调用MetricsStreamProcessor的create方法会为每个指标创建工作任务和工作流,其中就包含了三种类型的MetricsPersistentWorker,分别每分钟、小时和天进行一次持久化;因此数据持久化部分不需要额外新增,复用现有流程并测试即可。
数据查询
- Server端启动时,当CoreModule及其依赖初始化后,会调用UITemplateInitializer(getManager()).initAll()完成UI模版的加载,UI模版的位置是:oap-server/server-starter/src/main/resources/ui-initialized-templates
- 同时也会使用SPI机制加载GraphQLQueryProvider,并调用期prepare和start方法完成查询模块的初始化
- 前端发送的GraphQL被解析到指定的服务,比如MetricsQuery的readMetricsValues方法,该方法会最终调用对应的DAO层代码(根据不同数据库),以MySQL为例,最终调用H2MetricsQueryDAO的readMetricsValue,进行SQL语句的拼接和执行。
- 前端发送的请求中具体的参数是根据UI配置文件中的metrics解析的,JVM的Metircs定义在general-instance.json中
综上,我们只需要在general-instance.json中新增golang runtime metrics的定义,前端就可以查询到相关的数据:
- 在oap-server/server-starter/src/main/resources/ui-initialized-templates的general-instance.json文件中新增相关配置,以CPU使用率指标为例:
{ "name": "Golang", "children": [ { "x": 18, "y": 0, "w": 6, "h": 13, "i": "4", // i不能和其他面板(比如JVM)的i重复 "type": "Widget", "widget": { "title": "CPU" }, "graph": { "type": "Line", "step": false, "smooth": false, "showSymbol": false, "showXAxis": true, "showYAxis": true }, "metrics": [ "instance_golang_cpu" ], "metricTypes": [ "readMetricsValues" ], "moved": false } ] } - 前端需重新加载仪表盘,更新相关UI

3. 效果如下

前端展示
根据不同的指标,采取不同的展示样式,目前暂定都以折线图形式展示
|
指标名
|
样式
|
|
heapAlloc
|
折线图
|
|
stackAlloc
|
折线图
|
|
gcNum
|
折线图
|
|
gcPauseTime
|
折线图
|
|
goroutineNum
|
折线图
|
|
threadNum
|
折线图
|
|
cpuUsedRate
|
折线图
|
|
memUsedRate
|
折线图
|
用户接入
用户使用go2sky时自动上报数据
Github链接
https://github.com/easonyipj/skywalking-goapi:go2sky的gRPC api
https://github.com/easonyipj/go2sky:主要收集指标的代码
https://github.com/easonyipj/skywalking-data-collect-protocol:collect protocol
https://github.com/easonyipj/skywalking :oap server

浙公网安备 33010602011771号