Java基础架构设计(三)| 通用响应与异常处理(分布式应用通用方案)
- 一、前置说明
-
- [1.1 核心定位](#1.1 核心定位)
- [1.2 术语说明](#1.2 术语说明)
- 二、版本兼容&依赖总表(分布式扩展依赖)
-
- [2.1 核心依赖清单(在单体基础上新增)](#2.1 核心依赖清单(在单体基础上新增))
- [2.2 版本兼容说明(关键依赖对应关系)](#2.2 版本兼容说明(关键依赖对应关系))
- 三、分布式项目核心实现(单体基础上叠加)
-
- 场景1:分布式链路追踪(跨服务关联核心)
-
- [1. 网关配置(GatewayConfig.java,仅网关服务)](#1. 网关配置(GatewayConfig.java,仅网关服务))
- [2. 网关JWT解析过滤器(GatewayJwtFilter.java,仅网关服务)](#2. 网关JWT解析过滤器(GatewayJwtFilter.java,仅网关服务))
- [3. Feign 拦截器(FeignTraceInterceptor.java,所有微服务需添加)](#3. Feign 拦截器(FeignTraceInterceptor.java,所有微服务需添加))
- [4. 微服务上下文接收拦截器(MicroServiceTraceInterceptor.java,所有微服务)](#4. 微服务上下文接收拦截器(MicroServiceTraceInterceptor.java,所有微服务))
- [5. 微服务Web配置(WebMvcConfig.java,所有微服务)](#5. 微服务Web配置(WebMvcConfig.java,所有微服务))
- [6. 微服务链路追踪配置(application.yml,所有微服务)](#6. 微服务链路追踪配置(application.yml,所有微服务))
- [7. Zipkin 服务器部署(Docker 方式,支持ES持久化)](#7. Zipkin 服务器部署(Docker 方式,支持ES持久化))
- [8. 微服务启动类注解(所有微服务)](#8. 微服务启动类注解(所有微服务))
- 场景2:分布式日志集中收集(跨服务日志核心)
-
- [1. ELK 快速部署(docker-compose-elk.yml)](#1. ELK 快速部署(docker-compose-elk.yml))
- [2. Logstash 配置(logstash.conf)](#2. Logstash 配置(logstash.conf))
- [3. Elasticsearch 索引模板(优化检索效率)](#3. Elasticsearch 索引模板(优化检索效率))
- [4. 微服务 Logback 配置(logback-spring.xml,替换单体的本地入库配置)](#4. 微服务 Logback 配置(logback-spring.xml,替换单体的本地入库配置))
- [5. 微服务配置文件(application.yml,新增 Logstash 地址)](#5. 微服务配置文件(application.yml,新增 Logstash 地址))
- 场景3:跨服务异常统一响应(跨服务协作基础)
-
- [1. Feign 异常拦截器(FeignExceptionInterceptor.java,所有微服务)](#1. Feign 异常拦截器(FeignExceptionInterceptor.java,所有微服务))
- [2. 网关全局异常处理(GatewayExceptionHandler.java,仅网关服务)](#2. 网关全局异常处理(GatewayExceptionHandler.java,仅网关服务))
- [3. 微服务 Feign 客户端示例(OrderFeignClient.java)](#3. 微服务 Feign 客户端示例(OrderFeignClient.java))
- 场景4:熔断降级(生产环境必备)
-
- [1. Sentinel 控制台部署(Docker 方式)](#1. Sentinel 控制台部署(Docker 方式))
- [2. 网关熔断配置(application.yml,仅网关服务)](#2. 网关熔断配置(application.yml,仅网关服务))
- [3. 微服务熔断配置(application.yml,所有微服务)](#3. 微服务熔断配置(application.yml,所有微服务))
- [4. 微服务熔断注解示例(StockService.java)](#4. 微服务熔断注解示例(StockService.java))
- [5. Sentinel 熔断规则配置(通过控制台)](#5. Sentinel 熔断规则配置(通过控制台))
- 场景5:服务依赖与链路监控(可选优化)
-
- [1. Zipkin 链路告警(通过 Kibana 配置)](#1. Zipkin 链路告警(通过 Kibana 配置))
- [2. ELK 索引生命周期管理(避免磁盘占满)](#2. ELK 索引生命周期管理(避免磁盘占满))
- 四、分布式项目整合验证(跨服务流程闭环)
-
- [1. 验证流程](#1. 验证流程)
- [2. 校验清单](#2. 校验清单)
- 五、分布式项目部署指南(生产级配置)
-
- [1. 生产必查项](#1. 生产必查项)
- [2. 常见问题排查(生产环境)](#2. 常见问题排查(生产环境))
一、前置说明
1.1 核心定位
- 分布式项目:100%复用单体项目核心代码(响应统一、异常收口、链路追踪基础、双日志表设计),仅新增跨服务专属能力,无重构成本
- 核心价值:解决分布式场景下「跨服务链路断裂、日志分散、异常格式混乱、服务依赖不可视」四大痛点,实现"单体标准化+分布式可追溯"
- 架构适配:支持 Spring Cloud 微服务架构(Nacos 服务发现+Feign 远程调用+Gateway 网关),兼容 JDK1.8 + Spring Boot2.7.6 + MySQL8.0
1.2 术语说明
- 网关透传:网关路由请求时,保留前端/traceId/operatorId 等上下文字段,传递给下游服务
- Feign 拦截器:远程调用时,自动将当前服务的 traceId/bizId/operatorId 注入请求头,确保跨服务上下文一致
- 链路可视化:通过 Zipkin 展示跨服务调用链路(如订单服务→支付服务→库存服务),包含每个节点耗时
- 日志集中收集:所有微服务日志统一输出到 ELK 栈,支持按 traceId/bizId/服务名检索全链路日志
- 服务注册发现:通过 Nacos 实现微服务注册与发现,Feign 基于服务名实现无感知远程调用
- 熔断降级:通过 Sentinel 限制故障服务的调用流量,避免服务雪崩(生产环境必备)
二、版本兼容&依赖总表(分布式扩展依赖)
2.1 核心依赖清单(在单体基础上新增)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 继承单体项目依赖(或直接复用单体pom.xml) -->
<parent>
<groupId>com.example</groupId>
<artifactId>monomer-core-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>distributed-core-demo</artifactId>
<name>distributed-core-demo</name>
<description>分布式项目扩展方案示例</description>
<properties>
<!-- Spring Cloud 版本(适配 Spring Boot2.7.6) -->
<spring-cloud.version>2021.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
<zipkin.version>2.24.3</zipkin.version>
<sentinel.version>1.8.6</sentinel.version>
</properties>
<!-- 分布式依赖版本锁定(避免冲突) -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 1. 服务注册发现(Nacos) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 2. 远程调用(Feign) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 3. 分布式链路追踪(Sleuth+Zipkin) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<!-- 4. 网关(Spring Cloud Gateway)- 仅网关服务需添加 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<!-- 排除Tomcat依赖,Gateway基于Netty -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 5. 分布式配置中心(Nacos)- 统一管理配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 6. 日志集中收集(Logstash编码器) -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
<!-- 7. 熔断降级(Sentinel)- 生产环境必备 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.6</version>
</plugin>
</build>
</project>
2.2 版本兼容说明(关键依赖对应关系)
| 核心依赖 | 版本 | 适配说明 |
|---|---|---|
| Spring Boot | 2.7.6 | 分布式扩展的基础版本 |
| Spring Cloud | 2021.0.4 | 与 Spring Boot2.7.x 兼容 |
| Spring Cloud Alibaba | 2021.0.4.0 | 包含 Nacos 服务发现/配置中心、Sentinel |
| Spring Cloud Sleuth | 3.1.9 | 分布式链路追踪核心,与 Sleuth 适配 |
| Zipkin | 2.24.3 | 链路可视化平台,支持 Sleuth 数据上报 |
| Logstash-encoder | 7.4 | 日志 JSON 格式化,适配 ELK 收集 |
| Nacos | 2.1.2 | 服务注册发现+配置中心,兼容 Spring Cloud |
| Sentinel | 1.8.6 | 熔断降级,与 Spring Cloud Alibaba 适配 |
| Elasticsearch/Kibana/Logstash | 7.17.0 | ELK 栈统一版本,确保兼容性 |
三、分布式项目核心实现(单体基础上叠加)
场景1:分布式链路追踪(跨服务关联核心)
- 核心实现:网关透传+Feign 拦截器+微服务接收拦截器+Sleuth+Zipkin,复用单体 ThreadLocalUtils
- 依赖说明:依赖单体的 ThreadLocalUtils 和链路追踪基础,新增 Spring Cloud 相关依赖
1. 网关配置(GatewayConfig.java,仅网关服务)
java
package com.example.distributed.gateway.config;
import com.example.monomer.core.context.ThreadLocalUtils;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 网关全局过滤器(透传 traceId/operatorId,生成全局唯一 traceId)
*/
@Configuration
public class GatewayConfig {
// 上下文传递请求头(与微服务约定)
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String OPERATOR_ID_HEADER = "X-Operator-Id";
@Bean
@Order(-1) // 最高优先级,确保在路由前执行
public GlobalFilter traceIdGlobalFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = request.mutate();
// 1. 处理 traceId:优先获取前端传递的,无则生成
String traceId = request.getHeaders().getFirst(TRACE_ID_HEADER);
if (traceId == null || traceId.trim().isEmpty()) {
traceId = ThreadLocalUtils.generateTraceId();
}
// 注入请求头,传递给下游服务
requestBuilder.header(TRACE_ID_HEADER, traceId);
// 2. 处理 operatorId:前端/网关JWT解析后传递(默认anonymous)
String operatorId = request.getHeaders().getFirst(OPERATOR_ID_HEADER);
if (operatorId == null || operatorId.trim().isEmpty()) {
operatorId = "anonymous";
}
requestBuilder.header(OPERATOR_ID_HEADER, operatorId);
// 3. 将 traceId 存入响应头,便于前端关联
exchange.getResponse().getHeaders().add(TRACE_ID_HEADER, traceId);
return chain.filter(exchange.mutate().request(requestBuilder.build()).build())
.then(Mono.fromRunnable(() -> {
// 清理上下文(网关为响应式,避免内存泄漏)
ThreadLocalUtils.removeAll();
}));
};
}
}
2. 网关JWT解析过滤器(GatewayJwtFilter.java,仅网关服务)
java
package com.example.distributed.gateway.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* 网关JWT解析过滤器(统一解析token,提取operatorId)
*/
@Configuration
public class GatewayJwtFilter {
@Value("${app.jwt.secret}")
private String jwtSecret;
private static final String JWT_HEADER = "Authorization";
private static final String OPERATOR_ID_HEADER = "X-Operator-Id";
@Bean
@Order(-2) // 优先级低于traceId过滤器,确保先生成traceId
public GlobalFilter jwtGlobalFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = request.mutate();
// 解析JWT令牌
String token = request.getHeaders().getFirst(JWT_HEADER);
if (token != null && token.startsWith("Bearer ")) {
try {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret.getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token.substring(7))
.getBody();
String operatorId = claims.get("userId", String.class);
if (operatorId != null) {
requestBuilder.header(OPERATOR_ID_HEADER, operatorId);
}
} catch (Exception e) {
// token无效,设为anonymous
requestBuilder.header(OPERATOR_ID_HEADER, "anonymous");
}
} else {
// 无token,设为anonymous
requestBuilder.header(OPERATOR_ID_HEADER, "anonymous");
}
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
}
3. Feign 拦截器(FeignTraceInterceptor.java,所有微服务需添加)
java
package com.example.distributed.core.interceptor;
import com.example.monomer.core.context.ThreadLocalUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign 远程调用拦截器(传递 traceId/bizId/operatorId 上下文)
*/
@Configuration
public class FeignTraceInterceptor {
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String BIZ_ID_HEADER = "X-Biz-Id";
private static final String OPERATOR_ID_HEADER = "X-Operator-Id";
@Bean
public RequestInterceptor requestInterceptor() {
return template -> {
// 1. 传递 traceId(从 ThreadLocal 获取,网关生成或上游服务传递)
String traceId = ThreadLocalUtils.getTraceId();
if (traceId != null && !"UNKNOWN".equals(traceId)) {
template.header(TRACE_ID_HEADER, traceId);
}
// 2. 传递 bizId(业务ID,如订单号)
String bizId = ThreadLocalUtils.getBizId();
if (bizId != null && !"UNKNOWN".equals(bizId)) {
template.header(BIZ_ID_HEADER, bizId);
}
// 3. 传递 operatorId(操作人ID)
String operatorId = ThreadLocalUtils.getOperatorId();
template.header(OPERATOR_ID_HEADER, operatorId);
};
}
}
4. 微服务上下文接收拦截器(MicroServiceTraceInterceptor.java,所有微服务)
java
package com.example.distributed.core.interceptor;
import com.example.monomer.core.context.ThreadLocalUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 微服务请求拦截器(接收网关/Feign传递的上下文头,更新ThreadLocal)
*/
@Component
public class MicroServiceTraceInterceptor implements HandlerInterceptor {
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String BIZ_ID_HEADER = "X-Biz-Id";
private static final String OPERATOR_ID_HEADER = "X-Operator-Id";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 接收并设置traceId(网关生成或上游服务传递)
String traceId = request.getHeader(TRACE_ID_HEADER);
if (traceId != null && !traceId.trim().isEmpty()) {
ThreadLocalUtils.setTraceId(traceId);
}
// 接收并设置bizId(上游服务传递的业务ID)
String bizId = request.getHeader(BIZ_ID_HEADER);
if (bizId != null && !bizId.trim().isEmpty()) {
ThreadLocalUtils.setBizId(bizId);
}
// 接收并设置operatorId(网关解析或上游传递)
String operatorId = request.getHeader(OPERATOR_ID_HEADER);
if (operatorId != null && !operatorId.trim().isEmpty()) {
ThreadLocalUtils.setOperatorId(operatorId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ThreadLocalUtils.removeAll(); // 清理上下文,避免内存泄漏
}
}
5. 微服务Web配置(WebMvcConfig.java,所有微服务)
java
package com.example.distributed.core.config;
import com.example.distributed.core.interceptor.MicroServiceTraceInterceptor;
import com.example.monomer.core.interceptor.TraceIdInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 微服务Web配置(注册拦截器)
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 1. 注册微服务上下文接收拦截器(优先级更高)
registry.addInterceptor(new MicroServiceTraceInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/error", "/actuator/**");
// 2. 注册单体链路追踪切面拦截器
registry.addInterceptor(new TraceIdInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/error", "/swagger-ui/**", "/api-docs/**");
}
}
6. 微服务链路追踪配置(application.yml,所有微服务)
yaml
spring:
application:
name: order-service # 每个服务名称唯一(如pay-service、stock-service)
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 服务地址
config:
server-addr: localhost:8848 # Nacos 配置中心地址
file-extension: yml
shared-configs:
- data-id: spring-cloud-sleuth.properties
group: DEFAULT_GROUP
refresh: true # 支持动态刷新采样率
sleuth:
sampler:
probability: 0.1 # 生产环境采样率(0.1=10%,降低性能损耗)
rate: 1000 # 每秒最大采样数
baggage:
remote-fields: traceId,bizId,operatorId # 跨服务传递的字段
correlation-fields: traceId,bizId,operatorId # 与 MDC 关联的字段
zipkin:
base-url: http://localhost:9411 # Zipkin 服务器地址
sender:
type: web # 开发环境用HTTP上报,生产环境建议改为kafka
service:
name: ${spring.application.name} # Zipkin 中显示的服务名
# 应用自定义配置
app:
jwt:
secret: ${JWT_SECRET:your-jwt-secret-key-32bytes} # 从环境变量注入
# 复用单体的 ThreadLocalUtils,无需修改
7. Zipkin 服务器部署(Docker 方式,支持ES持久化)
bash
# 1. 确保Elasticsearch已启动(与Zipkin在同一网络)
# 2. 启动Zipkin容器(数据持久化到ES,避免重启丢失)
docker run -d -p 9411:9411 \
--network elk-network \
-e STORAGE_TYPE=elasticsearch \
-e ES_HOSTS=http://elasticsearch:9200 \
-e ES_USERNAME=elastic \
-e ES_PASSWORD=elastic123 \
openzipkin/zipkin:2.24.3
8. 微服务启动类注解(所有微服务)
java
package com.example.distributed.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 订单服务启动类(其他服务类似,仅名称不同)
*/
@SpringBootApplication(scanBasePackages = "com.example") // 扫描单体核心包+分布式扩展包
@EnableDiscoveryClient // 启用 Nacos 服务发现
@EnableFeignClients(basePackages = "com.example.distributed.feign") // 扫描 Feign 客户端
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
验证方式:
- 启动 Nacos(端口8848)、Elasticsearch(端口9200)、Zipkin(端口9411);
- 启动订单服务、支付服务、库存服务;
- 前端通过网关调用订单服务,订单服务通过 Feign 调用支付服务和库存服务;
- 访问 Zipkin 界面(http://localhost:9411),搜索 traceId,验证跨服务链路完整(订单→支付→库存);
- 查看三个服务的日志,验证 traceId 完全一致。
场景2:分布式日志集中收集(跨服务日志核心)
- 核心实现:ELK 容器部署+Logback 配置 JSON 输出+TCP 发送到 Logstash+ES 索引模板
- 依赖说明:依赖场景1的跨服务 traceId,新增 Logstash-encoder 依赖
1. ELK 快速部署(docker-compose-elk.yml)
yaml
version: '3.8'
services:
# Elasticsearch:存储日志
elasticsearch:
image: elasticsearch:7.17.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms1g -Xmx1g" # 内存配置,根据服务器调整
- "ELASTIC_PASSWORD=elastic123" # 密码
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
networks:
- elk-network
restart: always
# Logstash:日志收集与解析
logstash:
image: logstash:7.17.0
container_name: logstash
depends_on:
- elasticsearch
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
environment:
- "LS_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "5044:5044" # TCP 端口,接收微服务日志
- "9600:9600"
networks:
- elk-network
restart: always
# Kibana:日志可视化查询
kibana:
image: kibana:7.17.0
container_name: kibana
depends_on:
- elasticsearch
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- ELASTICSEARCH_USERNAME=elastic
- ELASTICSEARCH_PASSWORD=elastic123
ports:
- "5601:5601"
networks:
- elk-network
restart: always
networks:
elk-network:
driver: bridge
volumes:
es-data:
2. Logstash 配置(logstash.conf)
conf
input {
# 接收微服务 TCP 日志(JSON 格式)
tcp {
port => 5044
codec => json_lines
}
}
filter {
# 解析时间字段(与微服务日志格式对齐)
date {
match => ["timestamp", "yyyy-MM-dd HH:mm:ss.SSS"]
target => "@timestamp"
}
# 提取服务名(从日志的 appName 字段)
mutate {
rename => { "[appName]" => "serviceName" }
remove_field => ["@version", "host"] # 移除冗余字段
}
}
output {
# 输出到 Elasticsearch
elasticsearch {
hosts => ["http://elasticsearch:9200"]
user => "elastic"
password => "elastic123"
index => "distributed-log-%{+YYYY.MM.dd}" # 按日期分索引
document_type => "_doc"
}
# 开发环境输出到控制台(调试用)
stdout {
codec => rubydebug
}
}
3. Elasticsearch 索引模板(优化检索效率)
bash
# 执行curl命令创建索引模板(定义字段类型,避免自动映射异常)
curl -X PUT "http://localhost:9200/_index_template/distributed-log-template" -H "Content-Type: application/json" -u elastic:elastic123 -d '{
"index_patterns": ["distributed-log-*"],
"template": {
"mappings": {
"properties": {
"traceId": {"type": "keyword"},
"bizId": {"type": "keyword"},
"operatorId": {"type": "keyword"},
"serviceName": {"type": "keyword"},
"level": {"type": "keyword"},
"message": {"type": "text"},
"exception": {"type": "text"},
"timestamp": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSS"},
"environment": {"type": "keyword"}
}
},
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
},
"priority": 10
}'
4. 微服务 Logback 配置(logback-spring.xml,替换单体的本地入库配置)
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 上下文参数 -->
<contextName>${spring.application.name}</contextName>
<springProperty scope="context" name="appName" source="spring.application.name"/>
<springProperty scope="context" name="logstashHost" source="logstash.host" defaultValue="localhost"/>
<springProperty scope="context" name="logstashPort" source="logstash.port" defaultValue="5044"/>
<springProperty scope="context" name="environment" source="spring.profiles.active" defaultValue="prod"/>
<!-- 日志格式(JSON 格式,包含所有核心字段) -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${logstashHost}:${logstashPort}</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- 自定义字段:服务名、环境、日志类型 -->
<customFields>{"appName":"${appName}","environment":"${environment}","logType":"distributed_log"}</customFields>
<!-- 包含 MDC 字段(traceId/operatorId/bizId) -->
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>operatorId</includeMdcKeyName>
<includeMdcKeyName>bizId</includeMdcKeyName>
<!-- 时间戳格式 -->
<timestampPattern>yyyy-MM-dd HH:mm:ss.SSS</timestampPattern>
</encoder>
<!-- 重连配置 -->
<reconnectionDelay>10000</reconnectionDelay>
<queueSize>1024</queueSize>
</appender>
<!-- 控制台输出(开发环境) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-NA}] [%X{bizId:-NA}] [%p] %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 屏蔽第三方冗余日志 -->
<logger name="org.springframework" level="WARN"/>
<logger name="com.alibaba.nacos" level="WARN"/>
<logger name="feign" level="WARN"/>
<logger name="zipkin2" level="WARN"/>
<logger name="com.alibaba.csp.sentinel" level="WARN"/>
<!-- 根日志配置 -->
<root level="INFO">
<appender-ref ref="LOGSTASH"/> <!-- 发送到 ELK -->
<springProfile name="dev">
<appender-ref ref="CONSOLE"/> <!-- 开发环境启用控制台 -->
</springProfile>
</root>
</configuration>
5. 微服务配置文件(application.yml,新增 Logstash 地址)
yaml
# 日志集中收集配置
logstash:
host: localhost # 生产环境改为 ELK 服务器地址
port: 5044
# 关闭单体的本地日志入库(分布式用 ELK 集中存储)
logging:
level:
root: INFO
com.example: DEBUG
config: classpath:logback-spring.xml
# ELK 索引生命周期管理(通过 Kibana 配置,保留7天)
验证方式:
- 启动 ELK 容器(docker-compose -f docker-compose-elk.yml up -d);
- 启动所有微服务,发起跨服务调用;
- 访问 Kibana 界面(http://localhost:5601),登录(用户名elastic,密码elastic123);
- 创建索引模式(distributed-log-*),关联时间字段@timestamp;
- 按 traceId 检索,验证能查询到所有服务的日志(订单+支付+库存);
- 按 serviceName 筛选,验证能单独查看某个服务的日志。
场景3:跨服务异常统一响应(跨服务协作基础)
- 核心实现:Feign 异常拦截器(转换远程异常格式)+ 网关全局异常处理
- 依赖说明:依赖单体的 R 响应类和 BizException 异常类
1. Feign 异常拦截器(FeignExceptionInterceptor.java,所有微服务)
java
package com.example.distributed.core.interceptor;
import com.example.monomer.core.enums.BizErrorCodeEnum;
import com.example.monomer.core.exception.BizException;
import com.example.monomer.core.vo.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* Feign 异常解码器(将远程服务的异常响应转换为本地 R<T> 格式)
*/
@Slf4j
@Configuration
public class FeignExceptionInterceptor {
private final ObjectMapper objectMapper = new ObjectMapper();
@Bean
public ErrorDecoder errorDecoder() {
return (methodKey, response) -> {
try {
// 读取远程服务的异常响应体(JSON 格式)
String responseBody = Util.toString(response.body().asReader(Util.UTF_8));
log.warn("Feign 远程调用异常:methodKey={},status={},response={}",
methodKey, response.status(), responseBody);
// 解析为 R<T> 对象
R<?> errorResponse = objectMapper.readValue(responseBody, R.class);
// 匹配错误码枚举,抛出本地 BizException
BizErrorCodeEnum errorEnum = BizErrorCodeEnum.valueOf(errorResponse.getCode());
return new BizException(errorEnum, errorResponse.getMessage());
} catch (IOException | IllegalArgumentException e) {
log.error("Feign 异常解析失败", e);
// 解析失败时返回系统异常
return new BizException(BizErrorCodeEnum.SYSTEM_ERROR, "远程服务调用失败");
}
};
}
}
2. 网关全局异常处理(GatewayExceptionHandler.java,仅网关服务)
java
package com.example.distributed.gateway.config;
import com.example.monomer.core.enums.BizErrorCodeEnum;
import com.example.monomer.core.vo.R;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 网关全局异常处理器(路由失败、超时等异常标准化)
*/
@Slf4j
@RestControllerAdvice
public class GatewayExceptionHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 处理服务未找到异常(如服务未注册)
*/
@ExceptionHandler(NotFoundException.class)
public Mono<Void> handleNotFoundException(NotFoundException e, ServerWebExchange exchange) {
return buildErrorResponse(exchange, HttpStatus.NOT_FOUND,
BizErrorCodeEnum.RESOURCE_NOT_FOUND.getCode(), "服务暂不可用");
}
/**
* 处理路由超时异常
*/
@ExceptionHandler(ResponseStatusException.class)
public Mono<Void> handleResponseStatusException(ResponseStatusException e, ServerWebExchange exchange) {
if (e.getStatus() == HttpStatus.GATEWAY_TIMEOUT) {
return buildErrorResponse(exchange, HttpStatus.GATEWAY_TIMEOUT,
BizErrorCodeEnum.SYSTEM_ERROR.getCode(), "服务响应超时");
}
return buildErrorResponse(exchange, e.getStatus(),
BizErrorCodeEnum.SYSTEM_ERROR.getCode(), e.getReason());
}
/**
* 处理系统异常
*/
@ExceptionHandler(Exception.class)
public Mono<Void> handleException(Exception e, ServerWebExchange exchange) {
log.error("网关系统异常", e);
return buildErrorResponse(exchange, HttpStatus.INTERNAL_SERVER_ERROR,
BizErrorCodeEnum.SYSTEM_ERROR.getCode(), "网关繁忙,请稍后再试");
}
/**
* 构建标准化异常响应(R<T> 格式)
*/
private Mono<Void> buildErrorResponse(ServerWebExchange exchange, HttpStatus httpStatus, int code, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 构建 R<T> 异常响应
R<Void> errorResponse = R.fail(code, message);
try {
byte[] bytes = objectMapper.writeValueAsBytes(errorResponse);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
} catch (JsonProcessingException e) {
log.error("网关异常响应序列化失败", e);
return response.setComplete();
}
}
}
3. 微服务 Feign 客户端示例(OrderFeignClient.java)
java
package com.example.distributed.feign;
import com.example.distributed.core.interceptor.FeignExceptionInterceptor;
import com.example.monomer.core.vo.R;
import com.example.monomer.entity.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 订单服务 Feign 客户端(支付服务调用订单服务)
*/
@FeignClient(
name = "order-service", // 服务名(Nacos 注册的订单服务名称)
configuration = FeignExceptionInterceptor.class // 绑定异常拦截器
)
public interface OrderFeignClient {
@GetMapping("/api/order/getById")
R<Order> getOrderById(@RequestParam("id") Long id);
}
验证方式:
- 模拟支付服务调用订单服务时,订单服务抛出业务异常(如订单不存在);
- 查看支付服务的响应,验证异常格式为标准化 R 格式(code=610,message=订单不存在);
- 模拟网关路由到未启动的服务,验证网关返回 R 格式异常(code=404,message=服务暂不可用);
- 前端接收所有跨服务异常,无需区分"本地异常"和"远程异常"。
场景4:熔断降级(生产环境必备)
- 核心实现:Sentinel 控制台+网关熔断+微服务熔断
- 依赖说明:依赖 Spring Cloud Alibaba Sentinel 相关依赖
1. Sentinel 控制台部署(Docker 方式)
bash
# 启动 Sentinel 控制台(默认端口8080,账号密码:sentinel/sentinel)
docker run -d -p 8080:8080 sentinelhub/sentinel-dashboard:1.8.6
2. 网关熔断配置(application.yml,仅网关服务)
yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel控制台地址
port: 8719 # 客户端端口(默认8719,冲突时自动递增)
scg:
fallback:
mode: response # 熔断返回响应
response-status: 500
response-body: >
{
"code":500,
"success":false,
"message":"服务繁忙,请稍后再试",
"traceId":"${traceId}",
"timestamp":${timestamp}
}
datasource:
# 熔断规则持久化到Nacos(可选)
ds1:
nacos:
server-addr: localhost:8848
data-id: sentinel-gateway-rules
group-id: DEFAULT_GROUP
rule-type: gw-flow
3. 微服务熔断配置(application.yml,所有微服务)
yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719
feign:
enabled: true # 启用Feign熔断
datasource:
ds1:
nacos:
server-addr: localhost:8848
data-id: sentinel-service-rules
group-id: DEFAULT_GROUP
rule-type: flow
# Feign 超时配置(与熔断配合)
feign:
client:
config:
default:
connect-timeout: 3000 # 连接超时3秒
read-timeout: 5000 # 读取超时5秒
4. 微服务熔断注解示例(StockService.java)
java
package com.example.distributed.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.example.monomer.core.enums.BizErrorCodeEnum;
import com.example.monomer.core.exception.BizException;
import com.example.monomer.entity.Stock;
import com.example.monomer.mapper.StockMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 库存服务(含熔断注解)
*/
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
/**
* 扣减库存(添加熔断注解)
* fallback:熔断后的降级方法
*/
@SentinelResource(value = "stock-deduct", fallback = "deductFallback")
public boolean deductStock(Long productId, Integer count) {
Stock stock = stockMapper.selectById(productId);
if (stock == null) {
throw new BizException(BizErrorCodeEnum.RESOURCE_NOT_FOUND, "商品不存在");
}
if (stock.getStock() < count) {
throw new BizException(BizErrorCodeEnum.INVENTORY_INSUFFICIENT, "库存不足");
}
stock.setStock(stock.getStock() - count);
return stockMapper.updateById(stock) > 0;
}
/**
* 降级方法(与原方法参数一致)
*/
public boolean deductFallback(Long productId, Integer count) {
log.warn("库存服务熔断降级:productId={}, count={}", productId, count);
// 降级策略:返回false,或调用备用服务等
return false;
}
}
5. Sentinel 熔断规则配置(通过控制台)
- 访问 Sentinel 控制台(http://localhost:8080),登录账号密码:sentinel/sentinel;
- 网关服务规则配置:
- 选择"网关流控"→"新增网关流控规则";
- 资源名:路由ID(如order-route);
- 阈值类型:QPS;
- 单机阈值:100(每秒最多100个请求);
- 点击"新增"。
- 微服务规则配置:
- 选择服务名(如stock-service)→"流控规则"→"新增";
- 资源名:stock-deduct(注解中的value);
- 阈值类型:QPS;
- 单机阈值:50;
- 点击"新增"。
验证方式:
- 用压测工具(如JMeter)向库存服务的扣减接口发送100QPS请求(超过阈值50);
- 观察 Sentinel 控制台,验证触发熔断;
- 查看调用方(订单服务)的响应,验证返回降级提示(服务繁忙,请稍后再试);
- 停止压测后,等待熔断恢复时间(默认10秒),验证服务恢复正常。
场景5:服务依赖与链路监控(可选优化)
- 核心实现:Zipkin 告警+ELK 索引生命周期管理+Prometheus 监控
1. Zipkin 链路告警(通过 Kibana 配置)
- 访问 Kibana→Alerts→Create alert,选择"Threshold"规则;
- 配置触发条件:
distributed-log-*索引中,level:ERROR且serviceName:order-service的文档数5分钟内超过5条; - 配置通知渠道:钉钉机器人(Webhook),告警消息包含 traceId、服务名、异常信息;
- 保存规则,启用告警。
2. ELK 索引生命周期管理(避免磁盘占满)
- 访问 Kibana→Stack Management→Index Lifecycle Policies;
- 创建策略:
- 阶段1:热数据(0-7天),可写入可查询;
- 阶段2:删除(7天后),自动删除索引;
- 将策略绑定到
distributed-log-*索引模式; - 验证:7天后自动清理过期日志索引。
四、分布式项目整合验证(跨服务流程闭环)
1. 验证流程
- 前端通过网关调用订单服务的"创建订单"接口(order-service);
- 订单服务通过 Feign 调用支付服务(pay-service)完成支付,调用库存服务(stock-service)扣减库存;
- 支付服务模拟支付成功,库存服务模拟库存不足抛出业务异常;
- 订单服务捕获库存服务的异常,返回标准化响应;
- 查看 Zipkin 链路:订单→支付(成功)→库存(失败),耗时明细可见;
- 查看 ELK 日志:按 traceId 检索,三个服务的日志完整展示;
- 前端接收异常响应:code=620,message=库存不足,格式与本地异常一致;
- 压测库存服务,触发 Sentinel 熔断,验证降级响应。
2. 校验清单
| 校验项 | 标准要求 | 验证方式 |
|---|---|---|
| 跨服务 traceId 一致 | 所有服务的日志、Zipkin 链路、响应中的 traceId 完全相同 | 查看接口调用日志与异步任务日志的traceId |
| 日志集中可追溯 | ELK 能按 traceId/bizId/serviceName 快速检索 | 按 traceId 检索全链路日志 |
| 异常响应统一 | 本地异常、远程异常、网关异常均为 R 格式 | 模拟各类异常,查看响应格式 |
| 服务依赖可视 | Zipkin 能展示服务调用关系(订单依赖支付、库存) | 查看 Zipkin 链路图 |
| 熔断降级生效 | 超过阈值后触发降级,返回标准化提示 | 压测服务,验证降级响应 |
| 告警生效 | ERROR 日志量突增时,收到钉钉告警通知 | 模拟批量异常,查看告警渠道 |
五、分布式项目部署指南(生产级配置)
1. 生产必查项
安全配置
- 敏感信息加密:数据库密码、JWT密钥、Nacos账号密码通过环境变量注入,禁止硬编码;
- 接口安全:网关启用JWT鉴权,禁止未授权访问;
- 中间件安全:Nacos、Elasticsearch、Sentinel配置访问密码,限制内网访问,禁止公网暴露。
性能优化
- 连接池参数:Druid连接池
maxActive=50,Feign超时设为3~5秒; - 日志性能:Logstash队列大小调整为2048,Elasticsearch分片数按服务器核数调整;
- 采样率优化:Sleuth采样率
probability=0.3,避免高并发下性能损耗; - 熔断阈值:根据服务性能测试结果配置合理阈值(如QPS=100~500)。
监控告警
- 中间件监控:监控 Nacos/Elasticsearch/Zipkin/Sentinel 的 CPU、内存、磁盘使用率;
- 服务监控:通过 Prometheus+Grafana 监控服务响应时间、错误率、QPS;
- 日志告警:配置 ERROR 日志量、慢查询(超过500ms)告警;
- 熔断告警:Sentinel 触发熔断时,通过钉钉/邮件通知运维。
2. 常见问题排查(生产环境)
| 问题现象 | 排查方向 | 解决方案 |
|---|---|---|
| 跨服务 traceId 丢失 | 1. 网关未透传 traceId;2. Feign 拦截器未注入;3. 微服务未接收 | 1. 检查网关过滤器请求头;2. 确认 Feign 拦截器被扫描;3. 验证 MicroServiceTraceInterceptor 注册 |
| ELK 未收集到日志 | 1. Logstash 端口未映射;2. 微服务 Logstash 地址错误;3. 日志格式异常 | 1. 确认 5044 端口映射;2. 核对 logstash.host 配置;3. 查看 Logstash 日志(docker logs logstash) |
| Feign 调用异常格式混乱 | 1. 未绑定 FeignExceptionInterceptor;2. 远程服务异常未标准化 | 1. FeignClient 配置异常拦截器;2. 确保远程服务使用全局异常处理器 |
| Zipkin 无跨服务链路 | 1. 服务未注册到 Nacos;2. Sleuth 依赖缺失;3. Zipkin 地址错误 | 1. Nacos 控制台确认服务注册;2. 核对 Sleuth+Zipkin 依赖;3. 修正 zipkin.base-url |
| 熔断降级未生效 | 1. Sentinel 依赖缺失;2. 注解配置错误;3. 规则未配置 | 1. 核对 Sentinel 依赖;2. 确保 @SentinelResource 注解正确;3. 检查 Sentinel 控制台规则 |
| 网关路由失败 | 1. 路由规则错误;2. 服务名配置错误;3. 网关未排除 Tomcat 依赖 | 1. 核对网关路由 uri(lb://服务名);2. 与 Nacos 服务名一致;3. 网关依赖排除 spring-boot-starter-web |