前言

在单体应用中,一次请求通常只经过一个进程;在微服务系统中,一个用户请求可能依次经过网关、订单、库存、支付、数据库和消息队列。当某个服务发版重启时,如果直接杀死旧进程,正在执行的请求就可能被中断;如果发生超时,又很难仅凭单个服务的日志判断问题出现在哪一段。

因此,生产环境通常需要同时解决两个问题:通过平滑发布保护在途请求,通过分布式链路追踪还原请求在多个服务之间的完整路径。

一、什么是分布式链路追踪?

分布式链路追踪是一种记录请求在分布式系统中传播过程的可观测性技术。它把一次端到端请求拆分为多个操作单元,记录每个操作的调用关系、开始时间、耗时、状态和标签,最终还原成一棵调用树。

例如一次下单请求可能形成如下链路:

POST /orders                         Trace
├── API Gateway                     Span
├── Order Service                   Span
│   ├── SELECT order                Span
│   ├── Inventory Service           Span
│   └── Payment Service             Span
└── Publish OrderCreated Message    Span

二、TraceId 和 SpanId 分别表示什么?

1. TraceId:一次完整调用链的标识

TraceId 用于标识一次端到端请求。同一请求经过多个服务时,只要上下文传播正常,各服务产生的 Span 都属于同一个 Trace,并共享相同的 TraceId。

TraceId = a1b2c3

网关 Span       traceId=a1b2c3
订单服务 Span   traceId=a1b2c3
库存服务 Span   traceId=a1b2c3
支付服务 Span   traceId=a1b2c3

因此,在日志中搜索 TraceId,通常可以串起该请求在不同服务中的相关记录。

2. SpanId:链路中一次具体操作的标识

SpanId 用于标识 Trace 中的一次操作,例如一次 HTTP 调用、数据库查询或消息发送。每个 Span 有自己的 SpanId,并通过 parentSpanId 指向父操作,从而形成调用树。

TraceId = a1b2c3

SpanId=01  网关接收请求
├── SpanId=02  调用订单服务,parentSpanId=01
│   ├── SpanId=03  查询数据库,parentSpanId=02
│   └── SpanId=04  调用库存服务,parentSpanId=02

三、SkyWalking 和 Zipkin 的区别

SkyWalking 和 Zipkin 都能展示分布式调用链,但产品定位并不完全相同。

对比维度Apache SkyWalkingZipkin
核心定位面向分布式系统的综合可观测平台专注分布式链路收集与查询
接入方式Java Agent、语言探针、SDK、OpenTelemetry 等依赖 Brave、OpenTelemetry 等 instrumentation 上报 Span
数据范围Trace、Metrics、Logs、Events、拓扑、部分 Profiling 能力以 Trace/Span 为核心
服务拓扑原生能力较强,可分析服务与端点依赖功能更聚焦于链路查询,整体能力相对轻量
后端架构Probe、OAP 后端、Storage、UICollector、Storage、Search、Web UI
存储支持 Elasticsearch、BanyanDB、MySQL、H2 等多种实现支持内存、MySQL、Cassandra、Elasticsearch 等
部署和运维功能多,组件和配置相对更复杂架构清晰,入门和轻量场景更简单
适用场景希望统一观察链路、指标、日志和拓扑的大中型系统重点需要调用链查询、快速搭建追踪系统的团队

选择时不应只看功能数量:

  • 如果团队主要需要 Trace 查询,希望组件少、学习成本低,可以优先考虑 Zipkin;

  • 如果系统规模较大,还需要拓扑、指标、日志关联和性能分析,SkyWalking 更接近完整 APM 平台;

  • 如果企业已经采用 OpenTelemetry,应重点评估协议兼容、存储成本、查询体验和现有运维体系,而不是只比较客户端 SDK。

四、微服务发版为什么会中断请求?

假设某个实例正在处理一个耗时 8 秒的订单请求,此时发布平台直接终止进程:

  1. 进程不再执行剩余业务逻辑;

  2. TCP 连接可能被断开,调用方收到连接重置或超时;

  3. 如果数据库已经提交但响应尚未返回,调用方重试可能造成重复处理;

  4. 消费中的 MQ 消息、运行中的定时任务也可能被中断。

仅使用多个副本和滚动更新,并不能自动保证在途请求安全。完整平滑发布需要解决两个不同问题:

  • 流量排空:停止把新请求发送到即将下线的实例;

  • 优雅停机:等待该实例中已经接收的请求完成,再关闭进程。

五、实现平滑发布的核心流程

1. 保证发布期间仍有可用实例

部署至少需要多个副本,并采用滚动更新。Kubernetes Deployment 可以配置:

spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1

maxUnavailable: 0 表示更新期间不主动降低可用副本数,maxSurge: 1 允许先多启动一个新 Pod。新实例只有通过 Readiness Probe 后才应接收业务流量。

2. 先停止新流量

实例下线时,应先让注册中心、网关或 Kubernetes Service 不再选择该实例:

  • 注册中心场景:将实例标记为下线或主动注销;

  • Kubernetes 场景:Pod 终止时 EndpointSlice 状态会变化,服务代理不再把新流量发送给终止中的端点;

  • 应用场景:把 Readiness 状态改为拒绝流量,使实例退出负载均衡候选集合。

摘除信息传播到网关、客户端缓存和节点代理可能需要时间,因此不能在发出摘除操作后立即杀死进程。必要时可保留一小段连接排空时间,但这段时间应根据实际传播延迟验证,而不是机械地写死一个很大的 sleep

3. Spring Boot 开启并配置优雅停机

现代 Spring Boot 对内嵌 Tomcat、Jetty、Reactor Netty 等支持优雅停机,并允许配置关闭阶段的超时时间:

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

management:
  endpoint:
    health:
      probes:
        enabled: true

收到正常的 SIGTERM 后,应用关闭上下文并进入优雅停机阶段:不再接受新请求,并给已有请求留出完成时间。超过超时仍未结束的任务可能被强制终止。

要注意,IDE 的停止按钮或 kill -9 不一定触发正常的优雅关闭。生产发布应向容器主进程发送 SIGTERM,避免使用 SIGKILL 作为常规下线手段。

参考资料

更多推荐