Spring Cloud 微服务系列 - 微服务的链路追踪工具 - skywalking

微服务技术的历程和展望

微服务发展的历程

从最初的单体架构到如今的云原生、服务网格,微服务经历了长达十余年的演进。它是技术发展、组织架构变革以及云计算普及共同催生的产物。下面我们来回顾一下微服务技术发展历程中的四个主要阶段。

  • 阶段 1:单体与 SOA 阶段(前微服务时代)。

    • 在早期,所有的业务逻辑、数据库访问、前端页面都被打包在同一个 War 包中(单体架构)。随着业务变复杂,单体架构变得臃肿、难以维护、编译缓慢。随后出现了 SOA(面向服务架构),试图通过 ESB(企业服务总线) 来串联各个系统。
    • 但 ESB 终归是过于笨重、协议繁琐(大量使用 XML 和 SOAP),属于中心化的重型架构,越来越无法应对互联网爆发式的高并发需求。
  • 阶段 2:第一代微服务基础设施。

    • 2014 年左右,马丁·福勒(Martin Fowler)正式提出 “微服务” 概念。互联网公司需要去中心化、轻量级的治理方案。以 Eureka、Ribbon、Hystrix、Zuul 为核心的 Spring Cloud 早期生态爆发。以 Netflix / Spring Cloud OSS 为代表,微服务提倡 “智能端点与管道哑巴”,服务间通过轻量级的 RESTful API (HTTP) 或 RPC 通信。
    • 第一代组件许多由 Java 语言编写,存在强语言绑定问题。此外,这代组件大多属于 “代码侵入式” 治理,开发者必须在项目中引入大量依赖、编写繁琐的配置。
  • 阶段 3:第二代微服务与国内微服务组件的崛起。

    • 随着 Netflix 宣布部分核心组件停更,以及阿里巴巴等国内大厂在双十一高并发场景下的沉淀,以 Nacos、Sentinel、Seata 为代表的 Spring Cloud Alibaba 生态全面崛起,成为了国内主流的选择。
    • 这套全新的微服务组件拥有更强的高可用与性能,例如 Nacos 完美将注册中心与配置中心合二为一,Sentinel 提供了更强大的非阻塞流控与可视化激增流量防护。
    • 随着容器化全面普及,Docker + K8S (Kubernetes) 成为微服务部署的标准底座。K8S 提供了底层的服务发现、弹性伸缩、健康检查能力,让微服务天然具备了 “云原生” 属性。
  • 阶段 4:服务网格阶段(Service Mesh / 下一代微服务)。

    • 为了彻底解决 “代码侵入” 和 “多语言支持(Go、Python、Java 混编)” 的痛点,服务网格(Service Mesh) 应运而生,代表技术为 Istio 和 Linkerd。
    • 它们的特点是通过 Sidecar(边车代理)模式,将服务治理逻辑(限流、熔断、路由、安全加密)完全从业务代码中剥离出来,下沉到基础设施层。业务开发者只需关注纯粹的业务逻辑,真正的 “零代码侵入” 和 “语言无关性”。


微服务技术的发展趋势

更多微服务演进趋势,推荐参考:


链路追踪工具的横向对比

好,回到本文正题,聚焦链路追踪工具。我归纳了几个当前链路追踪工具的几个扛把子,对比如下:

技术的选型:

  • 如果你的核心体系是 Java / Spring Cloud (如你的 Pro-Cloud 体系):直接闭眼选 SkyWalking。它的零代码侵入能让你在半小时内把探针挂载到 Gateway、User、Order 以及各大中间件(Redis/MQ)上,且功能极其全面,是国内微服务生态的绝对霸主。
  • 如果你需要监控方法级、甚至是某一行代码的执行耗时:可以考虑 Pinpoint,它的调用树细致到了方法栈级别,但注意准备好足够的存储资源(HBase)。
  • 如果你的微服务是多语言混编(Java + Go + Python),且瞄准了云原生 Service Mesh 架构:长远来看一定要布局 OpenTelemetry。它正在终结全行业的链路追踪标准,各大厂商(包括 SkyWalking 最新版)都在全面兼容它的协议。


Skywalking 的原理

在正式切入 SkyWalking 的源码和实战之前,必须要先吃透它的地基——探针(Agent)和代理(Proxy)。


代理技术及其缺陷

在 Java 生态中,代理模式的核心思想是 为一个已知对象提供一个替身(代理对象),由替身来控制对原对象的访问,并在访问前后悄悄加入一些额外操作(如打印日志、开启事务、Sentinel 限流等)。分布式链路追踪需要捕获微服务之间的每一次方法调用。Java 中的代理技术主要分为两大类:

  • JDK 动态代理:要求目标类必须实现接口。它利用 java.lang.reflect.Proxy 在内存中动态生成一个接口的实现类。
  • CGLIB 动态代理:通过继承目标类,利用字节码技术(ASM)在运行时动态生成一个目标类的子类。Spring AOP 默认就采用了这种机制。

动态代理是代码层面的技术。如果想给项目里成百上千个类加上代理,必须显式编写 Spring AOP 配置,或者手动用 Proxy.newProxyInstance 去包一层。对于分布式组件(如捕获远程 Redis、MySQL 驱动的内部调用),代码侵入性极大。


探针技术的原理

如果说动态代理是在应用代码内部做手脚,那么探针技术(JavaAgent)则是直接站在 JVM 虚拟机的高度,在类加载阶段进行 “降维打击”。JavaAgent 是 Java 1.5 引入的技术。它本质上是一个独立于业务系统的外挂 Jar 包。
利用 JVM 提供的 Instrumentation 接口,JavaAgent 可以在业务类的字节码(.class 文件)被 JVM 加载到内存之前,强行拦截并肆意修改其内容。

探针的核心工作原理就是 字节码注入,当微服务启动时,JVM 类加载器准备加载一个类(例如 Spring Boot 的 UserController.class):

$$\text{UserController.class(磁盘)} \xrightarrow{\text{JVM加载}} \mathbf{[JavaAgent 拦截器]} \xrightarrow{\text{魔改/注入代理代码}} \text{魔改后的字节码(内存)}$$

探针技术在类加载阶段,直接通过修改操作码,在原方法体的前后硬编码塞入统计耗时、记录链路 ID 的逻辑。

JavaAgent 的两大核心入口点:

  • premain(静态加载,最常用):在项目的 main 方法执行之前触发。SkyWalking 挂载探针使用的就是这种模式(启动参数加 -javaagent:/path/to/skywalking-agent.jar)。
  • agentmain(动态热挂载):在项目运行过程中,利用虚拟机动态工具(Attach API)在不重启服务的情况下,强行把探针注入到正在运行的 JVM 进程中。


Skywalking 的底层实现

直接去改类的二级制字节码(操作字节码指令集,如 aload_0, invokevirtual)极其痛苦且容易出错。早期框架如 Pinpoint 使用了较为底层的 ASM 或 Javassist。而 SkyWalking 的内核则采用了目前业内最优雅、性能最好的高级字节码框架——Byte Buddy。Byte Buddy 封装了复杂的 JVM 指令,允许开发者用纯粹的 Java 流式代码(Fluent API)来改写字节码。例如,SkyWalking 内部要拦截并魔改一个方法,其核心伪代码极其优雅:

1
2
3
4
5
6
7
// SkyWalking 探针底层的 Byte Buddy 语法缩影
new AgentBuilder.Default()
.type(ElementMatchers.named("com.koohub.user.service.UserService")) // 1. 拦截目标类
.transform((builder, typeDescription, classLoader, module) ->
builder.method(ElementMatchers.named("getUserInfo")) // 2. 拦截目标方法
.intercept(MethodDelegation.to(MyInterceptor.class)) // 3. 把这个方法委托给探针的拦截器
).installOn(instrumentation);


Skywalking 的安装部署

官网下载GitHub官方文档中文文档

使用 docker 安装部署

我们在此使用 9.6.0 作为安装版本,使用 Docker-Compose 启动后端的核心大本营(OAP Server + WebUI),数据存储采用最轻量、适合开发联调的内置内存/H2 模式(生产环境可一键切换为 Elasticsearch 或官方自研的 BanyanDB)。由于 docker 比较轻量,我经常使用这种方式安装单机版的 skywalking,追踪和调试本地开发环境,非常 nice。

第一步,扫除之前的安装垃圾:

1
2
3
4
5
# 1. 停止并删除 9.6.0 的容器、网络、卷
docker compose down -v

# 2. (可选)删除 9.6.0 的旧镜像,释放磁盘空间
docker rmi apache/skywalking-oap-server:9.6.0 apache/skywalking-ui:9.6.0

第二步:编写全新、干净的 docker-compose.yml

注意,如果之后修改了 docker-compose.yml,执行 docker compose up -d skywalking-oap 重启容器之后,因为使用的是 H2 内存数据库,刚才的临时指标会清空,需要再跑几笔 curl 重新喂一下数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: '3.8'
services:
# 1. SkyWalking 9.6.0 核心计算后端
skywalking-oap:
image: apache/skywalking-oap-server:9.6.0
container_name: skywalking-oap
restart: always
ports:
- "11800:11800" # 探针 gRPC 数据上报端口
- "12800:12800" # UI 查询 GraphQL 端口
environment:
SW_STORAGE: h2 # 开发联调模式:使用内置轻量 H2 内存数据库,不需要额外装 ES
TZ: Asia/Shanghai
# 解锁仪表盘在线编辑和保存权限
SW_ENABLE_UPDATE_UI_TEMPLATE: "true"

# 2. SkyWalking 9.6.0 经典的 Booster 全新大屏
skywalking-ui:
image: apache/skywalking-ui:9.6.0
container_name: skywalking-ui
restart: always
ports:
- "8686:8080" # 映射到宿主机 8686,避免与微服务网关 8080 端口撞车
environment:
SW_OAP_ADDRESS: http://skywalking-oap:12800
TZ: Asia/Shanghai
depends_on:
- skywalking-oap

第三步:一键启动 9.6.0 后端,在当前 docker-compose.yml 目录下执行:

1
docker compose up -d

启动完成后,你可以直接在浏览器打开: localhost:8686 就可以看到 9.6.0 版本的干净大屏。


被追踪的应用挂载探针

第一步:先去官网下载对应版本的探针 agent。下载页面

1
2
3
4
5
6
7
8
# 下载并完成解压
$ wget https://dlcdn.apache.org/skywalking/java-agent/9.6.0/apache-skywalking-java-agent-9.6.0-src.tgz
$ tar -xzvf apache-skywalking-java-agent-9.6.0-src.tgz -C .
$ ls

LICENSE bootstrap-plugins licenses optional-reporter-plugins
NOTICE config logs plugins
activations expired-plugins optional-plugins skywalking-agent.jar

第二步:由于我们的被追踪应用包含 spring cloud gateway 这种底层是 netty + webflux 的组件,所以我们还需要将 optional-plugins 目录中的三个 jar 包(根据你实际的版本)拷贝到 plugins 目录(都是泪啊同志们😭)。

1
2
3
$ cp optional-plugins/apm-spring-cloud-gateway-4.x-plugin-9.6.0.jar plugins/
$ cp optional-plugins/apm-spring-webflux-6.x-plugin-9.6.0.jar plugins/
$ cp optional-plugins/apm-netty-http-4.1.x-plugin-9.6.0.jar plugins/

第三步:被追踪应用的启动。被追踪应用集群的构建请参考 《Spring Cloud Gateway - 使用测试案例》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 网关服务(127.0.0.1 是 skywalking 服务端IP)
-javaagent:/xxx/skywalking-agent/skywalking-agent.jar
-Dskywalking.agent.service_name=zdemo-gateway
-Dskywalking.collector.backend_service=127.0.0.1:11800
-Djava.net.preferIPv4Stack=true

# 下游订单服务
-javaagent:/xxx/skywalking-agent/skywalking-agent.jar
-Dskywalking.agent.service_name=zdemo-scloud-order
-Dskywalking.collector.backend_service=127.0.0.1:11800
-Djava.net.preferIPv4Stack=true

# 上游用户服务
-javaagent:/xxx/skywalking-agent/skywalking-agent.jar
-Dskywalking.agent.service_name=zdemo-scloud-user
-Dskywalking.collector.backend_service=127.0.0.1:11800
-Djava.net.preferIPv4Stack=true

完成以上三步,从网关作为入口发几个请求:

1
2
3
4
5
# 调用 feign 接口
$ curl -H "Authorization: my-jwt-token-xxx" http://192.168.1.5:8880/user-serv/feign/user/order?orderNo=1

# 调用 dubbo 接口
$ curl -H "Authorization: my-jwt-token-xxx" http://192.168.1.5:8880/user-serv/dubbo/user/order?orderNo=2

这时候再次访问 Skywalking 网页端,在服务项中就可以观察到请求数据和集群的链路拓扑图了。✌🏻


Skywalking 探针整合 logback

建议直接参考官方文档:Skywalking Logback 工具包官方配置文档(GitHub 源码站)

在生产环境中,把 SkyWalking 的 tid 注入到应用日志(如 Logback)中,是实现 “指标、链路、日志三合一” 的最核心步骤。通过它,你不需要编写任何拦截器或手动往 MDC 里塞 traceId 或者移除 traceId,探针会自动通过字节码技术把 traceId 悄悄塞进你的日志框架中。而且如果你的 Spring 项目中开启了异步线程池(如线程池复用),你只需要在配置中额外追加一个 SkyWalking 的线程池装饰器(如 RunnableWrapper.of(runnable)),探针就会跨线程把这个 TID 自动复制到子线程的 Logback 框架中,彻底解放双手!


实现日志打印全局 traceId

第一步:给应用引入依赖包(版本请根据实际的 skywalking 探针版本):

1
2
3
4
5
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>9.6.0</version>
</dependency>

第二步:日志配置文件的更改,编辑 logback-spring.xml(关键是 TraceIdPatternLogbackLayout 和 %tid)。

示例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" 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>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%tid] %logger{36} - %msg%n</pattern>
</layout>
<charset>UTF-8</charset>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<property name="LOG_PATH" value="./logs"/>
<property name="APP_NAME" value="order"/>

<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- [%15.15thread] [%tid] %-40.40logger{39} : %m%n"/>

<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- [%thread] [%tid] %logger{50} - [%method,%line] - %m%n"/>

<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>
<charset>UTF-8</charset>
</encoder>
</appender>

<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-sys-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archive/${APP_NAME}-sys-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>30GB</totalSizeCap>
</rollingPolicy>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${FILE_LOG_PATTERN}</pattern>
</layout>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
</appender>

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-sys-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archive/${APP_NAME}-sys-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${FILE_LOG_PATTERN}</pattern>
</layout>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>

第三步:携带探针重新启动相关的应用。这时候就可以观察到:

1
2
3
4
5
6
7
8
9
10
11
# user 模块正常
2026-06-23 03:19:39.703 [http-nio-8180-exec-10] INFO [TID:f7b0b115a22544d28461d875834dffed.160.17821559796900683] z.s.user.controller.UserController - 使用 dubbo 发起 RPC 请求,订单号: 1111
# order 模块正常
2026-06-23 03:19:39.730 INFO 75998 --- [:20881-thread-5] [TID:f7b0b115a22544d28461d875834dffed.160.17821559796900683] z.scloud.order.service.OrderServiceImpl : 收到 RPC 请求,订单号: 1111

# user 模块出现了异常
2026-06-23 03:12:20.234 [http-nio-8180-exec-3] ERROR [TID:f7b0b115a22544d28461d875834dffed.155.17821555361440483] o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.apache.dubbo.rpc.RpcException: java.util.concurrent.ExecutionException: org.apache.dubbo.rpc.StatusRpcException: DEADLINE_EXCEEDED : Waiting server-side response timeout by scan timer. start time: 2026-06-23 03:12:16.867, end time: 2026-06-23 03:12:19.949, timeout: 3000 ms, service: zdemo.scloud.api.service.dubbo.OrderService, method: getOrderDetails] with root cause
org.apache.dubbo.rpc.StatusRpcException: DEADLINE_EXCEEDED : Waiting server-side response timeout by scan timer. start time: 2026-06-23 03:12:16.867, end time: 2026-06-23 03:12:19.949, timeout: 3000 ms, service: zdemo.scloud.api.service.dubbo.OrderService, method: getOrderDetails
at org.apache.dubbo.rpc.TriRpcStatus.asException(TriRpcStatus.java:213)
at org.apache.dubbo.rpc.protocol.tri.DeadlineFuture$TimeoutCheckTask.notifyTimeout(DeadlineFuture.java:162)
...

异常的情况,就可以直接复制 tid 到 skywaking dashboard 查询对应的全局链路了。


需要注意的地方

你之前如果编写了 TraceIdMdcFilter 去维护 MDC,但在复杂的微服务异步调用(比如:你代码里用了 @Async 异步线程池、或者编程式线程拉起、或者使用 WebFlux 响应式框架)中,手动维护的 MDC 会因为线程切换直接发生断裂,导致下游线程日志里抓不到 ID,甚至由于未 remove 清理造成线程池相互污染。所以在实际的生产环境中,请彻底移除 MDC 的逻辑,全面拥抱 skywaking!


日志要不要灌到 skywalking

在实际生产架构中,绝大多数企业不会将 SkyWalking 作为应用日志的核心存储和查询中心,而是依然选择直接打到 ES或专门的日志系统。

为了既不冲垮 SkyWalking,又能享受“看图点进日志”的丝滑体验,最成熟的做法是:链路走 SkyWalking,日志走 ES,二者通过 TraceId 强强联合。架构拓扑:

  • 链路数据:应用挂载 SkyWalking Agent $\rightarrow$ 直接上报给 SkyWalking OAP $\rightarrow$ 存入 APM 专用的 ES/BanyanDB(只保留 3 天)。
  • 全量日志:应用控制台打印日志(带有 TraceId) $\rightarrow$ Filebeat/Logstash 异步采集 $\rightarrow$ 经过 Kafka 削峰 $\rightarrow$ 存入独立的日志 ES 集群(保留 30 天以上)。

总结来说,把全量业务日志打给 SkyWalking 是 “小材大用”,还会把监控拖垮;让它专注做链路追踪,日志交给 ES,通过 TraceId 做桥梁,才是生产标配。


日志灌入 ES 的标准做法

把日志灌入 ES(Elasticsearch)通常有两种完全不同的架构路线:

  • 一种是轻量侵入式,直接通过 SkyWalking Logback GRPC Appender 上报,只适合处理小规模日志;
  • 另一种是生产标准级异步解耦 Filebeat/Logstash + ES 上报,是大规模日志处理的标准做法。


不太好的方案

虽然不推荐第一种做法,但这里还是做一个介绍,实现该方案总共三步走:

  1. 引入 GRPC 日志上报依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-logback-1.x</artifactId>
    <version>9.6.0</version>
    </dependency>
  2. 在 logback-spring.xml 中追加 GRPC Appender

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <appender name="SKYWALKING_GRPC" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
    <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
    <pattern>${FILE_LOG_PATTERN}</pattern>
    </layout>
    </encoder>
    </appender>

    <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="INFO_FILE" />
    <appender-ref ref="ERROR_FILE" />
    <appender-ref ref="SKYWALKING_GRPC" />
    </root>
  3. 在 SkyWalking 中开启 ES 存储

    1
    2
    3
    4
    5
    storage:
    selector: ${SW_STORAGE:elasticsearch}
    elasticsearch:
    namespace: ${SW_STORAGE_ES_NAMESPACE:""}
    clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:127.0.0.1:9200}

    最终效果:服务启动后,日志会随 GRPC 协议发送给 OAP 写入 ES。你在 SkyWalking 的 UI 界面上,点击某条分布式链路,就能直接在右侧看到和该 TraceId 绑定的高亮业务日志,实现真正的“链路-日志一体化。


更推荐的方案

如果微服务并发极高,直接用方案一的 skywalking GRPC 上报会占用微服务的 JVM 内存和网络带宽,产生性能抖动。还是强烈推荐第二种方案,采用 异步落地本地磁盘 $\rightarrow$ 采集器异步监听 $\rightarrow$ 灌入 ES 的解耦架构。

配置 Filebeat 采集器:在服务器上部署 Filebeat,修改 filebeat.yml 收集你的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
filebeat.inputs:
- type: log
enabled: true
paths:
- /path/to/logs/order-sys-*.log # 💡 监控你的订单服务日志目录

# 处理多行日志(如 Spring 的 Exception 堆栈),让它聚合成一条记录
multiline.type: pattern
multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}' # 匹配 yyyy-MM-dd 开头的行
multiline.negate: true
multiline.match: after

# 直接灌入 Elasticsearch
output.elasticsearch:
hosts: ["http://127.0.0.1:9200"]
index: "order-service-log-%{+yyyy.MM.dd}" # ES 索引命名规则

在 Kibana 中根据 TraceId 检索。当 Filebeat 把日志灌入 ES 后,你可以直接在 Kibana 或者是现有的日志数据大屏中:

  • 创建名为 “order-service-log-*” 的索引模式。
  • 遇到线上故障时,直接在 Kibana 搜索框中输入你在前面日志里看到的 tid。
  • 此时整个分布式系统中所有流经服务的、包含此 ID的业务日志、报错信息、上下文,都将被检索出来。


让 skywalking 支持告警

关于这部分功能的实现,也可以直接参考官方文档 Skywalking Alerting 说明。

配置 OAP 告警规则

SkyWalking 的所有告警规则都定义在后端 OAP 容器内的 config/alarm-settings.yml 文件中。对这个配置文件进行修改,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 告警规则定义
rules:
# 规则 1:服务响应时间
service_resp_time_rule:
# 9.x MQE 表达式:10分钟内响应时间 > 1000ms 的次数 >= 3次则触发
expression: sum(service_resp_time > 1000) >= 3
period: 10
silence-period: 5
message: 【SkyWalking告警】服务 {name} 响应时间过长,最近 10 分钟内已连续 3 次超时!

# 规则 2:服务成功率(SLA)
service_sla_rule:
# 9.x MQE 表达式:10分钟内成功率 < 80% (8000) 的次数 >= 2次则触发
expression: sum(service_sla < 8000) >= 2
period: 10
silence-period: 3
message: 【SkyWalking告警】服务 {name} 成功率太低,最近 10 分钟内已有 2 次跌破 80%!

# 以下为系统默认的其他规则,保持不变...
service_resp_time_percentile_rule:
expression: sum(service_percentile{_='0,1,2,3,4'} > 1000) >= 3
period: 10
silence-period: 5
message: Percentile response time of service {name} alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000
service_instance_resp_time_rule:
expression: sum(service_instance_resp_time > 1000) >= 2
period: 10
silence-period: 5
message: Response time of service instance {name} is more than 1000ms in 2 minutes of last 10 minutes
database_access_resp_time_rule:
expression: sum(database_access_resp_time > 1000) >= 2
period: 10
message: Response time of database access {name} is more than 1000ms in 2 minutes of last 10 minutes
endpoint_relation_resp_time_rule:
expression: sum(endpoint_relation_resp_time > 1000) >= 2
period: 10
message: Response time of endpoint relation {name} is more than 1000ms in 2 minutes of last 10 minutes

# 核心改造点:9.x 规范的 Webhook 配置
# 注意:解除原本的注释,并把默认的 127.0.0.1 改成你真实的微服务接收接口!
hooks:
webhook:
default:
is-default: true
urls:
- http://192.168.1.5:8081/sys/alarm/receive # 填入你在订单服务或网关中写的控制层接口地址

你可以直接将容器内的配置文件拉取到本地宿主机,然后在宿主机改完配置文件,再塞到容器中。

1
2
3
4
5
6
7
8
9
10
11
# 拷贝容器文件到宿主机
$ docker cp skywalking-oap:/skywalking/config/alarm-settings.yml ./alarm-settings.yml

# 在宿主机进行编辑
$ vim alarm-settings.yml

# 编辑完成再塞入到容器对应位置
$ docker cp ./alarm-settings.yml skywalking-oap:/skywalking/config/alarm-settings.yml

# 重启 OAP 容器让配置生效
$ docker compose restart skywalking-oap

或者直接进入到容器内部进行修改:

1
2
3
4
5
6
7
8
9
10
$ docker exec -it skywalking-oap bash

# 刷新 Debian/Ubuntu 的软件源索引
apt-get update

# 安装 vim 编辑器(-y 代表全自动同意)
apt-get install -y vim

# 再次尝试打开配置文件
vim alarm-settings.yml


编写告警接收接口

上面我们在 config/alarm-settings.yml 配置了 hooks.webhook.default.urls,那我们就来实现这个告警接收器。根据 SkyWalking 9.x 的官方标准规范,告警数据的 JSON 结构映射如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package zdemo.scloud.order.dto;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;

@Data
public class SkyWalkingAlarmDTO {
private int scopeId; // 作用域ID(如服务、实例、端点)
private String scope; // 作用域名称 (如 SERVICE, INSTANCE)
private String name; // 发生告警的组件名称 (如 zdemo-scloud-user)
private String id0; // 发生告警的实体内部ID
private String id1;
private String ruleName; // 触发的规则名称 (对应 service_sla_rule)
private String alarmMessage; // 告警文本内容
private long startTime; // 告警触发的时间戳

// 直接使用通用节点 JsonNode,不管是 {} 还是 [] 都能完美兼容不报错!
private JsonNode tags;
}

编写 Webhook 接收接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package zdemo.scloud.order.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import zdemo.scloud.order.dto.SkyWalkingAlarmDTO;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/sys/alarm")
public class AlarmController {

@PostMapping("/receive")
public void receiveAlarm(@RequestBody List<SkyWalkingAlarmDTO> alarmList) {
if (alarmList == null || alarmList.isEmpty()) {
return;
}

for (SkyWalkingAlarmDTO alarm : alarmList) {
log.error("【SkyWalking 系统告警】触发规则: {}, 目标服务: {}, 告警内容: {}, 标签数据: {}",
alarm.getRuleName(),
alarm.getName(),
alarm.getAlarmMessage(),
alarm.getTags() != null ? alarm.getTags().toString() : "{}");

// 生产拓展拓展:在这里调用钉钉/企业微信群机器人的 SDK 发送群通知,或者调用邮件服务报警
// TODO
}
}
}


测试验证

在我们的 OrderController 里故意写一段延迟,或者直接把目标依赖服务强行断网/下线:

1
2
3
4
5
6
7
8
9
10
@GetMapping("/details")
public OrderDTO getUserOrderDetails(@RequestParam String orderNo) {
log.info("收到客户端订单:{}", orderNo);
try {
Thread.sleep(1500); // 故意延迟 1.5 秒,100% 触发 service_resp_time_rule (阈值 1000ms)
} catch (InterruptedException e) {
e.printStackTrace();
}
return new OrderDTO(orderNo, new BigDecimal("200.00"), "Owlias教程:" + port);
}

使用循环命令连续请求该慢接口 至少 1~2 分钟,确保满足配置中的 count: 3(连续多次超时)以及 period 的时间滑窗要求:

1
for i in {1..100}; do curl -H "Authorization: my-jwt-token-xxx" http://192.168.1.5:8880/user-serv/feign/user/order?orderNo=1; sleep 0.1; done

这时候来到 SkyWalking 的前端大盘,查看告警页,就会看到告警记录已经显示出来了。并且在应用端接收器也可以收到告警消息:

1
2
3
4
5
6
7
8
9
10
11
2026-02-13 22:04:25.078 ERROR 92973 --- [nio-8081-exec-7] [TID:26eb1323cd5343af8229b5ba067e4863.96.17821910649510009] z.s.order.controller.AlarmController     : 【SkyWalking 系统告警】触发规则: endpoint_relation_resp_time_rule, 目标服务: User in User to Netty-http:/user-serv/feign/user/order?orderNo=1111 in zdemo-gateway, 告警内容: Response time of endpoint relation User in User to Netty-http:/user-serv/feign/user/order?orderNo=1111 in zdemo-gateway is more than 1000ms in 2 minutes of last 10 minutes, 标签数据: []
2026-02-13 22:04:25.081 ERROR 92973 --- [nio-8081-exec-7] [TID:26eb1323cd5343af8229b5ba067e4863.96.17821910649510009] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_instance_resp_time_rule, 目标服务: 3397cd08f9de45c5b91e95bd3f280986@192.168.64.1 of zdemo-scloud-user, 告警内容: Response time of service instance 3397cd08f9de45c5b91e95bd3f280986@192.168.64.1 of zdemo-scloud-user is more than 1000ms in 2 minutes of last 10 minutes, 标签数据: []
2026-02-13 22:04:25.082 ERROR 92973 --- [nio-8081-exec-7] [TID:26eb1323cd5343af8229b5ba067e4863.96.17821910649510009] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_instance_resp_time_rule, 目标服务: dc4f04eb2082451eaa627b572926971b@192.168.64.1 of zdemo-scloud-order, 告警内容: Response time of service instance dc4f04eb2082451eaa627b572926971b@192.168.64.1 of zdemo-scloud-order is more than 1000ms in 2 minutes of last 10 minutes, 标签数据: []
2026-02-13 22:04:25.082 ERROR 92973 --- [nio-8081-exec-7] [TID:26eb1323cd5343af8229b5ba067e4863.96.17821910649510009] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_instance_resp_time_rule, 目标服务: 532846803edc44cab8ef72b10ab61c33@192.168.64.1 of zdemo-gateway, 告警内容: Response time of service instance 532846803edc44cab8ef72b10ab61c33@192.168.64.1 of zdemo-gateway is more than 1000ms in 2 minutes of last 10 minutes, 标签数据: []
2026-02-13 22:04:25.083 ERROR 92973 --- [nio-8081-exec-7] [TID:26eb1323cd5343af8229b5ba067e4863.96.17821910649510009] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_instance_resp_time_rule, 目标服务: cd75011f711847fbb0566bb1127b738a@192.168.64.1 of zdemo-scloud-order, 告警内容: Response time of service instance cd75011f711847fbb0566bb1127b738a@192.168.64.1 of zdemo-scloud-order is more than 1000ms in 2 minutes of last 10 minutes, 标签数据: []
2026-02-13 22:05:24.916 ERROR 92973 --- [nio-8081-exec-9] [TID:26eb1323cd5343af8229b5ba067e4863.98.17821911249140011] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_resp_time_rule, 目标服务: zdemo-scloud-user, 告警内容: 【SkyWalking告警】服务 zdemo-scloud-user 响应时间过长,最近 10 分钟内已连续 3 次超时!, 标签数据: []
2026-02-13 22:05:24.916 ERROR 92973 --- [nio-8081-exec-9] [TID:26eb1323cd5343af8229b5ba067e4863.98.17821911249140011] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_resp_time_rule, 目标服务: zdemo-scloud-order, 告警内容: 【SkyWalking告警】服务 zdemo-scloud-order 响应时间过长,最近 10 分钟内已连续 3 次超时!, 标签数据: []
2026-02-13 22:05:24.917 ERROR 92973 --- [nio-8081-exec-9] [TID:26eb1323cd5343af8229b5ba067e4863.98.17821911249140011] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_resp_time_rule, 目标服务: zdemo-gateway, 告警内容: 【SkyWalking告警】服务 zdemo-gateway 响应时间过长,最近 10 分钟内已连续 3 次超时!, 标签数据: []
2026-02-13 22:05:24.917 ERROR 92973 --- [nio-8081-exec-9] [TID:26eb1323cd5343af8229b5ba067e4863.98.17821911249140011] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_resp_time_percentile_rule, 目标服务: zdemo-scloud-user, 告警内容: Percentile response time of service zdemo-scloud-user alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000, 标签数据: []
2026-02-13 22:05:24.917 ERROR 92973 --- [nio-8081-exec-9] [TID:26eb1323cd5343af8229b5ba067e4863.98.17821911249140011] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_resp_time_percentile_rule, 目标服务: zdemo-scloud-order, 告警内容: Percentile response time of service zdemo-scloud-order alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000, 标签数据: []
2026-02-13 22:05:24.917 ERROR 92973 --- [nio-8081-exec-9] [TID:26eb1323cd5343af8229b5ba067e4863.98.17821911249140011] z.s.order.controller.AlarmController : 【SkyWalking 系统告警】触发规则: service_resp_time_percentile_rule, 目标服务: zdemo-gateway, 告警内容: Percentile response time of service zdemo-gateway alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000, 标签数据: []