草庐IT

使用SkyWalking-go2sky收集Golang运行时指标技术

easonyi 2023-03-28 原文

需求理解

本次项目,是将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是一个循环队列,记录最近垃圾回收系统中断的时间
float64(rtm.PauseNs[(rtm.NumGC+255)%256]) / float64(1000000),
goroutineNum
协程数量,当前存在的协程数量。
 
runtime.NumGoroutine()
直接使用
threadNum
线程数量
runtime.ThreadCreateProfile(nil)
直接使用
cpuUsedRate
CPU使用率
cpu.Percent
直接使用
memUsedRate
物理内存使用率
mem.UsedPercent
直接使用

数据上报

设计思路:
  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

详细方案如下:

  1. skywalking OAP中的每个metric都对应一个org.apache.skywalking.oap.server.core.source.Source的子类和一个OAL脚本中的一行,因此需要先定义好Source类和OAL脚本;
      1. 在oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source中新增golang mertices相关的Source子类,用于保存相关的指标和生成对应的数据库表,以CPU指标为例,需新建下面的类,关键点:
        1. 继承Source
        2. @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;
          }
      2. 新增OAL脚本
      3. 在oap-server/server-starter/src/main/resources/oal中新增处理golang runtime mertics的的OAL文件
        instance_golang_cpu = from(ServiceInstanceJVMCPU.usePercent).doubleAvg();
      4. 在oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4和OALParser.g4中新增相关的关键字 
         
    1. 服务端初始化时,应该注册好gRPC handler,并且使用OAL脚本动态生成处理golang runtime mertics的类:
       
      1. 在apm-network模块的proto中新增上文文GolangMetric.proto,并执行编译命令./mvnw compile -Dmaven.test.skip=true
      2. 在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
         
        1. 新增接收gRPC请求的handler GolangMetricReportServiceHandler,负责解析数据并初步封装,然后调用golang metrics的处理类GolangSourceDispatcher的sendMetric方法
        2. 新增相应的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"
                  );
              }
          }
        3. 新增相应的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方法:
  1. 在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,分别每分钟、小时和天进行一次持久化;因此数据持久化部分不需要额外新增,复用现有流程并测试即可。

数据查询

根据之前的调研,数据查询是通过/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的定义,前端就可以查询到相关的数据:
  1. 在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
        }
      ]
    }
  2. 前端需重新加载仪表盘,更新相关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

有关使用SkyWalking-go2sky收集Golang运行时指标技术的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

随机推荐