SkyWalking

1,590 阅读9分钟

1、概述

1.1 什么是 SkyWalking?

SkyWalking:是一个开源的 APM(应用性能监控)和可观测性分析平台,由 Apache 软件基金会孵化并成为顶级项目。

Skywalking 专为微服务、云原生架构和基于容器的架构设计,提供了分布式追踪、服务网格遥测分析、度量聚合和可视化一体化的解决方案。

SkyWalking 的主要功能包括:

  1. 分布式追踪:追踪分布式系统中的请求流,记录请求在各个组件之间的传递过程,识别性能瓶颈。
  2. 性能监控:监控关键性能指标,如响应时间、吞吐量等,帮助了解系统的整体性能表现。
  3. 问题排查:提供详细的跟踪信息,帮助快速定位和解决系统中的问题。
  4. 可视化界面:提供丰富的图表分析功能,如拓扑图、调用链路分析、性能趋势等。
  5. 告警和报警:设置告警规则,当系统出现异常或性能指标超过预设阈值时,及时通知相关人员。

1.2 什么是链路追踪?

链路追踪(Distributed Tracing):是一种用于监控和诊断分布式系统中请求处理的技术。

在分布式系统中,一个用户的请求可能会经过多个服务节点,每个服务节点可能是不同团队开发、部署在不同的服务器上,甚至可能使用不同的编程语言编写。链路追踪的目的是追踪请求在这些服务节点中的传播路径,以便于开发者能够理解请求的执行流程、性能瓶颈和故障点。

链路追踪系统通常包括以下几个关键概念:

  1. Trace:表示一次完整的请求处理过程,从客户端发起请求到服务器返回响应结束。每个 Trace 有一个唯一的 Trace ID 来标识。
  2. Span:是 Trace 的基本单元,代表请求在单个服务节点上的处理过程。每个 Span 有自己的 Span ID,并且包含在特定的 Trace 中。
  3. Span Context:用于在分布式系统的各个服务节点之间传播请求上下文的信息。通常包括 Trace ID 和 Span ID。
  4. Annotation/Log Tag:在 Span 中记录的时间点,用于标记特定的事件,如请求的开始和结束。日志标签可以用于记录额外的信息,如错误日志、关键业务逻辑等。
  5. Service:表示分布式系统中的一个服务节点。
  6. Service Instance:表示服务的具体实例,如一个运行中的 Pod 或一个 JVM 进程。
  7. Endpoint:表示服务中的具体操作,如 HTTP API 的路径或数据库的查询语句。

1.3 SkyWalking 的架构

1)探针(Agent) :是 SkyWalking 的客户端库,它被集成到应用程序中,负责收集应用运行时的性能数据,如方法调用、服务调用、异常等。

  1. 探针以非侵入的方式工作,对应用程序的性能影响非常小。
  2. 探针支持多种语言,包括 Java、.NET Core、Node.js、Go 等,这使得SkyWalking能够监控多种技术栈的应用程序。
  3. 探针收集的数据包括 Trace Segment(追踪段)、Log、Metric 等,这些数据被发送到 OAP 服务器进行进一步处理。

2)平台后端(OAP) :是 SkyWalking 的服务器端,负责接收探针发送的数据,并进行数据的聚合、分析和存储。

  1. OAP 支持水平扩展,可以通过增加节点来处理更多的数据。
  2. OAP 提供了丰富的 API,允许第三方系统和工具与 SkyWalking 集成。
  3. OAP 还支持多种语言的协议适配器,如 gRPC、HTTP 等。

3)展示页面(UI) :SkyWalking 的 UI 提供了一个直观的界面,用于展示监控数据和分析结果。

  1. UI 支持多种图表和视图,如拓扑图、服务地图、调用链、性能指标等,帮助用户快速了解系统的状态。
  2. UI 支持自定义和扩展,用户可以根据自己的需求定制视图和仪表板。
  3. UI 还提供了告警和通知功能,当系统出现异常时,可以及时通知运维人员。

4)存储(Storage) :通过开放/可插入的界面存储 SkyWalking 数据,SkyWalking 支持多种存储解决方案,以适应不同的部署需求和性能要求。如:

  1. H2:是 SkyWalking 自带的轻量级内存数据库,适用于演示和开发环境。
  2. MySQL:是一个稳定且广泛使用的数据库系统,但查询速度可能不如一些专门的 NoSQL 数据库,适合于长时间应用性能监控。
  3. ElasticSearch:是一个分布式、RESTful 搜索和分析引擎,通常用于处理大规模数据集。SkyWalking 官方推荐使用 ElasticSearch,因为它提供了快速的查询性能,尤其是在处理大量追踪记录时。
  4. BanyanDB:是 SkyWalking 的原生存储解决方案,旨在提供高性能和资源效率。与 ElasticSearch 相比,在 CPU、内存和磁盘使用上都有明显的优势,适合中等规模的部署。
  5. ClickHouse:是一个用于在线分析处理(OLAP)的列式数据库系统,它在处理大量数据时表现出色,尤其是在查询性能和响应时间上。

1.4 SkyWalking 的配置

1)application.yml:是 SkyWalking OAP 服务器的主配置文件,定义了 OAP 服务器的运行时参数。以下是一些常见的配置项:

# 集群配置
cluster:
    # 选择器:选择使用的集群类型,默认 standalone
    selector: ${SW_CLUSTER:standalone}
    # 独立模式:不需要额外的集群协调服务。
    standalone:
        ...
    # 与 ZooKeeper 集成时的参数
    zookeeper:
        # ZooKeeper 的命名空间,默认为空。
        namespace: ${SW_NAMESPACE:""}
        # ZooKeeper 服务的地址和端口,默认为 localhost:2181。
        hostPort: ${SW_CLUSTER_ZK_HOST_PORT:localhost:2181}
        ...
    # 与 Kubernetes 集成时的参数
    kubernetes:
        # Kubernetes 的命名空间,默认为 default。
        namespace: ${SW_CLUSTER_K8S_NAMESPACE:default}
        ...
    # 与 Consul 集成时的参数
    consul:
        # Consul 中的服务名称,默认为 SkyWalking_OAP_Cluster。
        serviceName: ${SW_SERVICE_NAME:"SkyWalking_OAP_Cluster"}
        # Consul 服务的地址和端口,默认为 localhost:8500。
        hostPort: ${SW_CLUSTER_CONSUL_HOST_PORT:localhost:8500}
        ...
    # 与 etcd 集成时的参数
    etcd:
        # etcd 服务的地址和端口,默认为 localhost:2379。
        endpoints: ${SW_CLUSTER_ETCD_ENDPOINTS:localhost:2379}
        # etcd 的命名空间,默认为 /skywalking。
        namespace: ${SW_CLUSTER_ETCD_NAMESPACE:/skywalking}
        ...
    # 与 Nacos 集成时的参数
    nacos:
        # Nacos 中的服务名称,默认为 SkyWalking_OAP_Cluster。
        serviceName: ${SW_SERVICE_NAME:"SkyWalking_OAP_Cluster"}
        # Nacos 服务的地址和端口,默认为 localhost:8848。
        hostPort: ${SW_CLUSTER_NACOS_HOST_PORT:localhost:8848}
        ...
# 核心配置
core:
    # 选择器:选择核心配置,默认为 default。
    selector: ${SW_CORE:default}
    default:
        # OAP 服务器的角色,默认为 Mixed,可以是 Mixed、Receiver 或 Aggregator。
        role: ${SW_CORE_ROLE:Mixed}
        # REST API 服务的主机地址,默认为 0.0.0.0(所有网络接口)。
        restHost: ${SW_CORE_REST_HOST:0.0.0.0}
        # REST API 服务的端口,默认为 12800。
        restPort: ${SW_CORE_REST_PORT:12800}
        # gRPC 服务的主机地址,默认为 0.0.0.0。
        gRPCHost: ${SW_CORE_GRPC_HOST:0.0.0.0}
        # gRPC 服务的端口,默认为 11800。
        gRPCPort: ${SW_CORE_GRPC_PORT:11800}
        ...
# 存储配置
storage:
    # 选择器:选择存储类型,默认为 h2。
    selector: ${SW_STORAGE:h2}
    # 使用 Elasticsearch 数据库时的参数。
    elasticsearch:
        ...
    # 使用 H2 数据库时的参数
    h2:
        ...
    # 使用 MySQL 数据库时的参数。
    mysql:
        ...
    # 使用 PostgreSQL 数据库时的参数。
    postgresql:
        ...
    # 使用 BanyanDB 时的参数。
    banyandb:
        ...

2)alarm-settings.yml:是 SkyWalking 的告警规则配置文件,它定义了告警规则和触发条件。以下是一些常见的配置项:

rules:
    # 自定义告警规则名称
    xxx:
        # 数学表达式,必须是比较操作,结果为 1 或 0,当结果为 1 时,将触发告警
        expression:
        # 评估指标的时间长度,单位为分钟
        period:
        # 在告警触发后,处于静默状态的时间(即使触发告警条件,也不会告警),默认与 period 相同
        silence-period:
        # 当告警触发时发送的消息
        message:
# 告警触发时的行为
hooks:
    # 告警触发时,发送 Webhook 请求
    webhook:
        # 默认告警行为
        default:
            # 是否作为默认的告警行为
            is-default: true
            # 一个或多个 Webhook URL,告警信息将被发送到这些 URL
            urls:
              - http://127.0.0.1/notify/
              - http://127.0.0.1/go-wechat/
              - ...
          # 其余告警行为
          xxx:

1.5 链路追踪框架对比

对比ZipkinJaegerSkyWalkingPinpoint
开发语言JavaGoJavaJava
特点轻量级,简单云原生,可扩展国产,多语言支持,UI 强大功能强大,多插件
支持语言Java, Go, Node.js 等Java, Node.js, Go, Python 等Java, .NET Core, Node.js, PHP, Go 等Java, PHP
数据存储Cassandra, Elasticsearch, MySQLCassandra, ElasticsearchElasticsearch, MySQL, H2 等HBase
UI功能简单,直观功能丰富,高级查询功能丰富,服务拓扑,依赖分析功能强大,详细调用链分析
性能相对较好性能较好,注重减少性能影响基于 Agent,性能损耗低字节码注入,性能影响
扩展性插件系统简单易于扩展支持自定义插件,灵活插件支持,不如 Agent灵活

2、快速入门

2.1 快速启动 SkyWalking

1)官网下载并解压压缩包

  • SkyWalking APM:是一个开源的、分布式追踪系统,用于监控微服务、云原生和容器化应用程序的性能。
  • Java Agent:是 SkyWalking 的一部分,它作为一个 Java 字节码增强器,用于在运行时自动收集 Java 应用程序的性能数据。

2)配置

  • 在 config 目录下,根据需要修改 application.yml(OAP 核心配置文件)和其他相关配置文件。

3)启动

  • 在 bin 目录下,执行启动脚本启动 SkyWalking。(Linux:./startup.sh、Windows:./startup.bat

4)Agent

  • 在 Java Agent 中修改 agent.config,设置服务名称和 OAP 服务地址等信息。
  • 在启动 Java 服务时,在 JVM 参数中添加 -javaagent Java Agent 的路径

5)访问 UI

  • 在浏览器中输入 http://SkyWalking_IP:SkyWalking_Port,查看监控数据和分析结果。

2.2 日志收集

SkyWalking 通过其日志分析器(log-analyzer)支持日志收集。该分析器能够从各种来源收集日志,并将其与 追踪和度量数据相关联,从而提供更全面的服务性能视图。

实现日志收集的步骤:(以 Logback 为例)

  1. 引入依赖。
<dependency>
 <groupId>org.apache.skywalking</groupId>
 <artifactId>apm‐toolkit‐logback‐1.x</artifactId>
 <version>x.x.x</version>
</dependency>
  1. 配置日志框架。
<?xml version="1.0" encoding="UTF‐8"?>
<configuration>
  <!‐‐ 引入 Spring Boot 默认的 logback XML 配置文件 ‐‐>
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <!‐‐ 日志的格式化 ‐‐>
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout  class= "org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
        <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
      </layout>
    </encoder>
  </appender>

  <!‐‐ 设置 Appender ‐‐>
  <root level="INFO">
    <appender‐ref ref="console"/>
  </root>

</configuration>

2.3 告警通知

SkyWalking 的告警系统允许用户定义告警规则,并在满足特定条件时触发告警。以下是告警通知功能使用步骤:

  1. 告警规则配置:通过 alarm-settings.yml 文件,用户可以定义告警规则,包括告警表达式(expression)、周期(period)、静默周期(silence-period)和告警消息(message)。
  2. 告警通知:触发的告警可以通过多种方式通知用户,如 Webhook、邮件、消息队列等。

案例:当服务的平均响应时间超过 1000ms(即 1s)时,通过 Webhook 将告警信息发送到飞书。

rules:
  service_resp_time_rule:
    expression: avg(service_resp_time) > 1000
    period: 10
    silence-period: 5
    message: '{name} 的平均响应时间超过1秒,持续了 {period} 分钟。'

hooks:
  feishu:
    default:
      is-default: true
      text-template: |-
        {
          "msg_type": "text",
          "content": {
            "text": "Apache SkyWalking Alarm: \n %s."
          },
          "ats":"feishu_user_id_1,feishu_user_id_2"
        }      
    webhooks:
      # 飞书机器人的 Webhook URL
    - url: https://open.feishu.cn/open-apis/bot/v2/hook/dummy_token 
      # 飞书机器人的签名验证
      secret: dummysecret

2.4 自定义 SkyWalking 链路追踪

自定义 SkyWalking 链路追踪是指根据特定的业务需求,通过编程方式扩展或修改 SkyWalking 的默认链路追踪行为。以下是具体流程:

1)引入依赖

<dependency>
   <groupId>org.apache.skywalking</groupId>
   <artifactId>apm-toolkit-trace</artifactId>
   <version>8.9.0</version>
</dependency>

2)自定义链路追踪

  • 使用 @Trace 标记需要追踪的方法。
  • 使用 @Tag 添加自定义标签,以存储额外的信息。
public class UserService {
    @Trace
    @Tag(key="userId", value="arg[0])
    @Tag(key="result.name", value="returnedObj.name)
    public User getUser(long userId) {
        // ...
    }
}

3、SkyWalking 原理

3.1 数据结构

1)Trace:表示一个完整的请求链路,涵盖分布式系统中所有相关组件的调用关系和性能信息。每个Trace都有一个全局唯一的ID,用于标识整个请求链路。

2)TraceSegment:是组成 Trace 的基本单元,一个 Trace 可以包含多个 TraceSegment,一个 TraceSegment 包含多个 Span。

3)Span:一次操作的单位,通常对应于分布式事务中的一个逻辑工作单元。

在 SkyWalking 中,根据 Span 的作用和上下文,可以将它们分为以下三种类型:

  1. Entry Span:服务的入口点,通常是处理客户端请求的开始。
  2. Local Span:在单个服务实例内部的操作,不涉及跨网络的远程调用。
  3. Exit Span:从本服务调用另一个服务的请求,通常是服务间的远程调用。

3.2 Java Agent

1)Java Agent 的入口

public static void premain(String agentArgs, Instrumentation instrumentation) {
    final PluginFinder pluginFinder;
    // 1. 初始化核心配置。
    try {
        SnifferConfigInitializer.initializeCoreConfig(agentArgs);
    } catch (Exception e) {
        return;
    }
    // 2. 检查 SkyWalking Agent 是否被启用。
    if (!Config.Agent.ENABLE) {
        return;
    }
    // 3. 创建一个 PluginFinder 实例,加载所有插件。
    try {
        pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
    } catch (AgentPackageNotFoundException ape) {
        return;
    } catch (Exception e) {
        return;
    }
    // 4. 注册类转换器,以便在类加载时进行字节码增强。
    try {
        installClassTransformer(instrumentation, pluginFinder);
    } catch (Exception e) {
        LOGGER.error(e, "Skywalking agent installed class transformer failure.");
    }
    // 5. 启动 SkyWalking Agent 的内部服务。
    try {
        ServiceManager.INSTANCE.boot();
    } catch (Exception e) {
        LOGGER.error(e, "Skywalking agent boot failure.");
    }
    // 6. 在 JVM 关闭时注册一个关闭钩子,优雅地关闭 SkyWalking Agent 的内部服务。
    Runtime.getRuntime()
           .addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
}

2)Java Agent Plugin

在 SkyWalking 中,Java Agent 是通过 Plugin(插件)来实现具体组件的链路追踪。如:

  • HttpClient:httpclient-4.x-plugin、httpclient-5.x-plugin
  • MongoDB:mongodb-3.x-plugin、mongodb-4.x-plugin
  • MySQL:mysql-5.x-plugin、mysql-8.x-plugin
  • Tomcat:tomcat-10x-plugin
  • ...

以 Tomcat 为例:

public class TomcatInvokeInterceptor implements InstanceMethodsAroundInterceptor {
    /**
      * 在目标方法执行之前调用,用于初始化追踪 Span。
      */
    @Override
    public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) {
        // 1. 从 Request 对象的头部中提取上下文信息,并设置到 ContextCarrier 中
        Request request = (Request) allArguments[0];
        ContextCarrier contextCarrier = new ContextCarrier();
        CarrierItem next = contextCarrier.items();
        while (next.hasNext()) {
            next = next.next();
            next.setHeadValue(request.getHeader(next.getHeadKey()));
        }
        // 2. 根据请求的方法和请求 URI 构造操作名称
        String operationName =  String.join(":", request.getMethod(), request.getRequestURI());
        // 3. 构造一个新的 Entry Span,并将 Span 的组件设置为 Tomcat
        AbstractSpan span = ContextManager.createEntrySpan(operationName, contextCarrier);
        Tags.URL.set(span, request.getRequestURL().toString());
        Tags.HTTP.METHOD.set(span, request.getMethod());
        span.setComponent(ComponentsDefine.TOMCAT);
        SpanLayer.asHttp(span);

        if (TomcatPluginConfig.Plugin.Tomcat.COLLECT_HTTP_PARAMS) {
            collectHttpParam(request, span);
        }
    }
    
    /**
      * 在目标方法执行之后调用,用于处理 Span 的结束逻辑。
      */
    @Override
    public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,  Object ret) {
        // 1. 从上下文管理器中获取当前的 Span,为 Span 设置 HTTP 响应状态码标签
        Request request = (Request) allArguments[0];
        HttpServletResponse response = (HttpServletResponse) allArguments[1];
        AbstractSpan span = ContextManager.activeSpan();
        Tags.HTTP_RESPONSE_STATUS_CODE.set(span, response.getStatus());
        
        // 2. 如果响应状态码大于或等于 400,标记 Span 为发生错误的 Span
        if (response.getStatus() >= 400) {
            span.errorOccurred();
        }
        // 3. 移除与请求相关的特定上下文信息,停止并结束当前的 Span
if (!TomcatPluginConfig.Plugin.Tomcat.COLLECT_HTTP_PARAMS && span.isProfiling()) {
            collectHttpParam(request, span);
        }
        ContextManager.getRuntimeContext().remove(Constants.FORWARD_REQUEST_FLAG);
        ContextManager.stopSpan();
        // 4. 返回原始方法的返回值
        return ret;
    }
}