本次项目,是将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的收集,具体如下表  
float64(rtm.PauseNs[(rtm.NumGC+255)%256]) / float64(1000000),

数据上报

设计思路:
  1. 考虑到skywalking已经集成了gRPC并且go2sky上报trace和JavaAgent上报metrics都是通过gRPC,因此golang runtime metrics也采取gRPC的方式进行上报。
  2. 考虑到一次请求只上报一个metric对象,因此采取非stream模式的gRPC连接。
  3. 考虑到数据收集和数据上报过程的解耦,使用“生产者-消费者”模式:启动时开启两个goroutine,一个负责收集数据并向chan中发送,一个负责从chan中取出数据,并通过gRPC上报,chan设置1000的buffer
  4. 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;
}

 

服务端方案

服务端主要完成以下的工作:
  1. 启动时通过定义的OAL脚本动态生成Golang Metrics相关的类和对应的Dispatcher
  2. 启动时自动检查Golang Metrics相关的表是否已创建,如果未创建则自动创建
  3. 启动时注册gRPC handler 接收客户端的数据
  4. 将数据进行处理后持久化
  5. 接收前端的数据查询请求
设计服务端方案时,主要围绕如何实现上面五点工作,下面将从服务端初始化、数据接收和处理、数据持久化、数据查询四个部分来介绍服务端的设计方案(这四部分互相关联又互相独立)

服务端初始化

服务端初始化时,主要完成上面的工作1-3,在充分了解了JVM Mertics的收集流程后,完成服务端的初始化需要新增下面的内容:
  1. golang mertices相关的Source子类;
  2. golang mertices相关的OAL脚本
  3. 新增注册gRPC handler
  4. 新增golang metrics的处理类GolangSourceDispatcher

详细方案如下:

@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;
}
instance_golang_cpu = from(ServiceInstanceJVMCPU.usePercent).doubleAvg();
├── 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
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"
        );
    }
}

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方法:
├── 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,分别每分钟、小时和天进行一次持久化;因此数据持久化部分不需要额外新增,复用现有流程并测试即可。

数据查询

根据之前的调研,数据查询是通过/graphql接口,采用的是Armeria框架和GraphQL,在初步看了这两个技术的介绍后,总结出前端数据查询的大概流程:
  1. Server端启动时,当CoreModule及其依赖初始化后,会调用UITemplateInitializer(getManager()).initAll()完成UI模版的加载,UI模版的位置是:oap-server/server-starter/src/main/resources/ui-initialized-templates
  2. 同时也会使用SPI机制加载GraphQLQueryProvider,并调用期prepare和start方法完成查询模块的初始化
  3. 前端发送的GraphQL被解析到指定的服务,比如MetricsQuery的readMetricsValues方法,该方法会最终调用对应的DAO层代码(根据不同数据库),以MySQL为例,最终调用H2MetricsQueryDAO的readMetricsValue,进行SQL语句的拼接和执行。
  4. 前端发送的请求中具体的参数是根据UI配置文件中的metrics解析的,JVM的Metircs定义在general-instance.json中
综上,我们只需要在general-instance.json中新增golang runtime metrics的定义,前端就可以查询到相关的数据:
{
  "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
    }
  ]
}

   3. 效果如下

 

前端展示

根据不同的指标,采取不同的展示样式,目前暂定都以折线图形式展示
指标名 样式
heapAlloc 折线图
stackAlloc 折线图
gcNum 折线图
gcPauseTime 折线图
goroutineNum 折线图
threadNum 折线图
cpuUsedRate 折线图
memUsedRate 折线图

用户接入

用户使用go2sky时自动上报数据 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