分布式系统中重要概念:
服务发现,配置管理,网关路由,负载均衡,断路器,分布式追踪
服务发现和配置管理
Eureka
注意:看Github,Eureka已停止维护,最新为2.0.4版本,最近提交时间为2024年1月
Java语言写的服务发现框架,基于REST服务
主要功能:
服务注册(服务向Eureka Server 发起HTTP PUT 请求进行注册,信息会存在Eureka Server的内存中形成注册表)
**服务续约(心跳检查) ** (服务默认每隔30s 发送心跳,默认90s内Eureka Server没收到心跳会T掉该服务)
服务剔除 默认90s内没收到心跳检查T掉该服务实例,也可主动T掉
获取注册表信息 消费者服务可定期默认30s获得注册表信息缓存在本地进行通信
自我保护 如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,并自动进入自我保护模式。此时,Eureka Server将不再从注册列表中移除长时间未收到心跳的服务实例,同时仍然接受新服务的注册和查询请求,但这些更新不会立即同步到其他节点上。只有在网络恢复正常后,新的注册信息才会被同步到集群中的其他节点,默认是开启的
高可用 多个Eureka Server可组成集群
SpringBoot3.3.6
JDK17
Eureka Server搭建,默认http://localhost:8761/eureka/ 可以访问Eureka
//依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
//启动类
@SpringBootApplication
@EnableEurekaServer // 启用 Eureka Server 功能
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
//配置文件
server:
port: 8761 # 设置服务器监听端口
spring:
application:
name: eureka-server # 定义服务名称
eureka:
instance:
hostname: localhost # 设置实例主机名
client:
register-with-eureka: false # 不注册自己到 Eureka Server
fetch-registry: false # 不从 Eureka Server 获取服务列表
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
Client配置
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaClient // 或者使用 @EnableDiscoveryClient
public class SystemServerApp {
public static void main(String[] args) {
SpringApplication.run(SystemServerApp.class, args);
}
}
//配置文件
server:
port: 10010 # 设置客户端监听端口
spring:
application:
name: system-service # 定义服务名称
eureka:
instance:
prefer-ip-address: true # 使用 IP 地址而不是主机名注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # 自定义实例 ID
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 指定 Eureka Server 的 URL
使用到的注解
@EnableEurekaServer
@EnableEurekaClient
配置文件示例:
## server配置
server:
port: 8761 # 设置服务器端口
spring:
application:
name: eureka-server # 定义服务名称
profiles:
active: peer1,peer2 # 激活多profile配置,用于集群部署
eureka:
instance:
hostname: ${HOST_NAME} # 使用环境变量设置主机名
preferIpAddress: true # 使用IP地址而不是主机名注册
client:
register-with-eureka: false # 不向自己注册
fetch-registry: false # 不从其他节点获取服务列表
service-url:
defaultZone: http://${PEER_HOST_2}:8762/eureka/ # 指定对等节点的服务URL
server:
enable-self-preservation: false # 关闭自我保护模式,确保不可用实例能被及时移除
eviction-interval-timer-in-ms: 3000 # 缩短清理任务的时间间隔至3秒
response-cache-update-interval-ms: 5000 # 缩短缓存更新时间间隔至5秒
renewal-threshold-update-interval-ms: 120000 # 缩短续约阈值更新时间间隔至2分钟
use-read-only-response-cache: false # 禁用只读响应缓存以减少延迟
environment:
ribbon:
ReadTimeout: 30000 # 设置Ribbon读取超时时间为30秒
ConnectTimeout: 30000 # 设置Ribbon连接超时时间为30秒
management:
endpoints:
web:
exposure:
include: "*" # 允许暴露所有管理端点
## client配置
server:
port: 8081 # 设置客户端应用端口
spring:
application:
name: my-service # 定义服务名称
eureka:
client:
service-url:
defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/ # 指定Eureka Server集群地址
registry-fetch-interval-seconds: 5 # 缩短客户端拉取注册表信息的时间间隔至5秒
instance-info-replication-interval-seconds: 5 # 缩短实例状态复制时间间隔至5秒
instance:
lease-expiration-duration-in-seconds: 15 # 缩短服务过期时间至15秒
lease-renewal-interval-in-seconds: 5 # 缩短心跳频率至5秒
prefer-ip-address: true # 使用IP地址注册
ribbon:
ServerListRefreshInterval: 2000 # 缩短Ribbon刷新时间间隔至2秒
NaCos
java写的开源服务注册及配置的平台
主要功能:
服务发现
动态配置管理
配置版本管理
多环境支持
高可用 集群
nacos版本2.4.3
默认情况http://localhost:8848/nacos 进入网站,账号和密码均为nacos
具体文档参照官网,写的一般但够用
网关路由
Gateway
主要特性:
路由Route
断言Predicate
过滤Filter
每个Route有以下四个元素
- ID: 每个路由必须有一个唯一的标识符。
- URI: 目标服务或资源的位置。
- Predicates: 用于匹配 HTTP 请求的条件。
- Filters: 在请求被发送到下游之前或响应返回给客户端之后修改请求和响应的内容
配置考虑身份验证,限流熔断,日志,动态路由,健康检查,SSL/TLS配置,WebSocket支持
代码示例:
SpringBoot3.3.6 + Gateway + Nacos
<!-- Spring Cloud Gateway 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
## 配置文件示例
server:
port: 8080 # 网关服务监听端口
ssl:
enabled: true # 启用SSL/TLS支持
key-store: classpath:keystore.p12 # 密钥库路径
key-store-password: changeit # 密钥库密码
keyStoreType: PKCS12 # 密钥库类型
keyAlias: tomcat # 别名
spring:
application:
name: api-gateway # 应用名称
cloud:
gateway:
discovery:
locator:
enabled: true # 开启基于服务发现的路由配置
lower-case-service-id: true # 将服务ID转换为小写形式
routes: # 定义静态路由规则
- id: user-service-route # 路由唯一标识符
uri: lb://user-service # 使用负载均衡器指向的服务名称
predicates:
- Path=/api/users/** # 匹配以/api/users/开头的请求路径
filters:
- StripPrefix=1 # 移除最前面的一个前缀,即/api
- name: RequestRateLimiter # 添加请求速率限制过滤器
args:
redis-rate-limiter.replenishRate: 10 # 每秒补充令牌数
redis-rate-limiter.burstCapacity: 20 # 最大突发容量
key-resolver: "#{@ipKeyResolver}" # 自定义键解析器bean名称
- id: product-service-route
uri: lb://product-service
predicates:
- Path=/api/products/**
filters:
- AddResponseHeader=X-Product-Service, "true" # 在响应头中添加X-Product-Service:true
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos服务地址
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr} # Nacos配置中心地址
file-extension: yaml # 配置文件扩展名
namespace: public # 命名空间,默认为空字符串
group: DEFAULT_GROUP # 组名,默认为DEFAULT_GROUP
loadbalancer:
configurations:
health-check: true # 对上游服务执行健康探测
management:
endpoints:
web:
exposure:
include: "*,"refresh"" # 暴露所有端点,包括refresh
endpoint:
health:
show-details: always # 显示详细的健康信息
metrics:
export:
enabled: true # 启用指标导出功能
tags:
application: ${spring.application.name} # 添加标签用于区分不同应用
logging:
level:
org.springframework.cloud.gateway: DEBUG # 设置Spring Cloud Gateway的日志级别为DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger : %msg%n" # 控制台日志格式
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://login.example.com/oauth2/default # JWT颁发者URI
负载均衡
Ribbon
客户端负载均衡器
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
## 配置文件
service1:
ribbon:
listOfServers: http://localhost:8081,http://localhost:8082
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 使用随机规则
## 配置FeignClient或者RestTemplate
@Configuration
public class MyConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
使用注解:@LoadBalanced
## 比较详细一点的配置文件示例
spring:
application:
name: ribbon-consumer # 定义应用程序名称
cloud:
consul:
discovery:
instanceId: ${spring.application.name}:${server.port}
host: localhost
port: 8500
config:
enabled: true # 启用Consul配置,默认为true
format: YAML # Consul上配置文件的格式
data-key: data # Consul上的KEY值或文件名
prefix: # 配置值的基本文件夹路径
defaultContext: # 所有应用程序使用的文件夹名称
profileSeparator: # 配置文件名称分隔符
server:
port: 8804 # 应用程序监听端口
# Ribbon全局配置
# 正常配置好注册中心Eureka或者Nacos时会自动从注册中心拉取实例服务列表
ribbon:
eager-load:
enabled: true # 禁止懒加载,立即初始化客户端
clients: mima-cloud-producer,mima-cloud-producer2 # 指定需要预加载的服务列表
# 为单个服务配置负载均衡策略和其他参数
mima-cloud-producer:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 使用随机选择算法
MaxAutoRetries: 1 # 对当前服务器的最大重试次数(不包括首次尝试)
MaxAutoRetriesNextServer: 1 # 切换到下一个服务器时的最大重试次数
OkToRetryOnAllOperations: true # 是否允许所有操作都可重试
ServerListRefreshInterval: 2000 # 从源刷新服务器列表的时间间隔(毫秒)
ConnectTimeout: 3000 # 连接超时时间(毫秒)
ReadTimeout: 3000 # 读取超时时间(毫秒)
mima-cloud-producer2:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule # 使用基于区域和可用性的复合规则
HTTP请求工具
Feign
使用示例
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.8</version> <!-- 请根据实际情况调整版本号 -->
</dependency>
//注意feign和springboot,springcloud版本兼容性
@SpringBootApplication
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@FeignClient(name = "example-service", url = "http://localhost:8080")
public interface ExampleServiceClient {
@GetMapping("/api/example/{id}")
ExampleResponse getExampleById(@PathVariable("id") Long id);
@PostMapping("/api/example")
ExampleResponse createExample(@RequestBody ExampleRequest request);
}
@RestController
@RequestMapping("/feign")
public class FeignController {
private final ExampleServiceClient exampleServiceClient;
@Autowired
public FeignController(ExampleServiceClient exampleServiceClient) {
this.exampleServiceClient = exampleServiceClient;
}
@GetMapping("/get/{id}")
public ResponseEntity<ExampleResponse> getExample(@PathVariable("id") Long id) {
return ResponseEntity.ok(exampleServiceClient.getExampleById(id));
}
@PostMapping("/create")
public ResponseEntity<ExampleResponse> createExample(@RequestBody ExampleRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(exampleServiceClient.createExample(request));
}
}
远程服务调用失败时
package com.example.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "example-service", fallbackFactory = ExampleServiceFallbackFactory.class)
public interface ExampleServiceClient {
@GetMapping("/api/user/{id}")
User getUserById(@PathVariable("id") Long id);
}
package com.example.feign;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ExampleServiceFallbackFactory implements FallbackFactory<ExampleServiceClient> {
@Override
public ExampleServiceClient create(Throwable cause) {
return new ExampleServiceClient() {
@Override
public User getUserById(Long id) {
log.error("Fallback for getUserById with id: {}, due to: {}", id, cause.getMessage());
return new User(id, "Fallback User", "N/A");
}
};
}
}
配置文件
# application.yml
server:
port: 8080 # 应用程序监听端口
spring:
application:
name: feign-client-app # 应用名称
profiles:
active: prod # 激活的环境配置,默认为生产环境
feign:
client:
config:
default: # 默认配置适用于所有Feign Clients,也可以针对特定服务进行单独配置
connectTimeout: 5000 # 连接超时时间(毫秒)
readTimeout: 10000 # 读取超时时间(毫秒)
loggerLevel: HEADERS # 日志级别:NONE, BASIC, HEADERS, FULL
retryer: com.example.feign.SimpleRetryer # 自定义重试策略实现类
errorDecoder: com.example.feign.CustomErrorDecoder # 自定义错误解码器
requestInterceptors: # 请求拦截器列表
- com.example.feign.LoggingRequestInterceptor
- com.example.feign.AuthRequestInterceptor
decode404: true # 是否将HTTP 404响应视为正常业务逻辑的一部分
encoder: com.example.feign.CustomEncoder # 自定义编码器
decoder: com.example.feign.CustomDecoder # 自定义解码器
contract: com.example.feign.MyContract # 自定义契约处理
client: com.example.feign.OkHttpClientFactory # 使用OkHttp作为HTTP客户端
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/xml,application/xml,application/json # 支持的MIME类型
min-request-size: 2048 # 最小压缩大小(字节)
response:
enabled: true # 开启响应压缩
okhttp:
enabled: true # 启用OkHttp客户端
httpclient:
enabled: false # 禁用Apache HttpClient
hystrix:
enabled: true # 启用Hystrix熔断机制
sentinel:
enabled: false # 如果使用Sentinel而非Hystrix,则启用此选项
logging:
level:
com.example.feign: DEBUG # 设置Feign相关包的日志级别为DEBUG,方便调试
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 更改负载均衡策略为随机选择
management:
endpoints:
web:
exposure:
include: health,info,hystrix.stream # 暴露健康检查和其他管理端点
endpoint:
health:
show-details: always # 始终显示详细的健康信息
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos服务器地址
loadbalancer:
ribbon:
enabled: false # 禁用Ribbon,推荐使用Spring Cloud LoadBalancer代替
分布式追踪
Zipkin
工作流程
- 生成追踪信息:当一个请求进入系统时,Zipkin 客户端会在该请求中插入追踪上下文(通常是 HTTP Headers),包括
X-B3-TraceId
、X-B3-SpanId
和X-B3-ParentSpanId
等字段。这些头部信息会随着请求传递给下游的服务,确保所有相关联的服务调用都能够共享相同的追踪上下文 - 采集跟踪信息:各个服务内部集成有 Zipkin 提供的 SDK 或者中间件,在每次服务调用前后记录下必要的元数据(如开始时间和结束时间),形成 Span 对象。同时,它们还会检查传入请求是否已经包含了追踪上下文,如果没有则创建新的追踪上下文
- 传输追踪数据:一旦某个 Span 结束,即服务完成了其职责并返回了结果,就会将此 Span 作为异步任务发送到 Zipkin Server。为了不影响业务逻辑的执行效率,通常采用异步方式提交追踪数据。支持多种传输协议,如 HTTP POST、Kafka 或者 RabbitMQ 等
- 收集与存储:Zipkin Server 接收到追踪数据后,首先通过 Collector 模块验证并解析追踪信息,然后将其保存到持久化层(如 MySQL、Cassandra 或 Elasticsearch)。此外,Collector 还负责为追踪数据建立索引,以便后续查询
- 查询与展示:最后,用户可以通过 Web UI 或 API 查询特定条件下的追踪记录,比如按照服务名称、操作名或者标签过滤。Web UI 提供了直观的图表界面,使得开发者可以轻松地浏览整个系统的依赖关系图以及各个服务节点上的性能指标
常用header
- X-B3-TraceId:这是一个全局唯一的标识符,用来表示整个请求的调用链路。所有的 Span 在同一个 Trace 中都会共享相同的
traceId
,它帮助我们识别出属于同一请求的所有服务调用 - X-B3-SpanId:这是当前 Span 的唯一标识符。每当一个新的服务调用发生时,就会生成一个新的 Span ID。这个 ID 与
traceId
一起,可以精确地定位到某一次特定的服务调用 - X-B3-ParentSpanId:当一个 Span 是另一个 Span 的子级时(即存在父子关系),这个字段会包含父 Span 的 ID。如果一个 Span 没有父 Span,则该字段为空或不存在。通过这种方式,Zipkin 可以构建出完整的调用树结构
- X-B3-Sampled:这是一个布尔值(通常为 “1” 或 “0”),指示是否对本次请求进行采样。采样决策决定了哪些请求会被记录下来用于后续分析。设置为 “1” 表示采样,而 “0” 则表示不采样。为了不影响系统的性能,通常不会对所有请求都进行采样,而是根据一定的概率来选择部分请求
- X-B3-Flags:这是一个长整型数值,默认情况下为 “0”。它可以用来传递一些特殊的标志位,比如开启调试模式等。一般情况下,除非特别需要,否则这个字段很少被使用
注意SpringBoot2 是以Sleuth作为分布式追踪基础组件,3.x移除Sleuth 转为Zipkin
配置服务端
1.下载Zipkin的可执行jar包 部署即可得到一个单机环境,实际看公司情况决定单机还是集群部署
配置客户端
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
Springboot2.x配置文件
spring:
sleuth:
web:
client:
enabled: true # 开启Web请求跟踪,默认为false
sampler:
probability: 0.1 # 设置采样比例,默认值为0.1(即10%),生产环境中建议调整至合适的范围
zipkin:
base-url: http://zipkin-server:9411 # 指定Zipkin Server的URL,确保与实际部署位置一致
sender:
type: kafka # 使用Kafka作为消息传输方式;也可以选择rabbitmq或其他支持的方式
tracing:
endpoint: http://zipkin-server:9411/api/v2/spans # 当sender.type为web时指定此参数
## 配置Actuator端点
management:
endpoints:
web:
exposure:
include: "*" # 允许暴露所有端点
endpoint:
health:
show-details: always # 显示详细的健康状态信息
## 如果使用kafka
spring:
kafka:
bootstrap-servers: kafka-broker1:9092,kafka-broker2:9092 # 列出所有Kafka broker的地址
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: io.zipkin.reporter2.sender.kafka.ZipkinKafkaMessageConverter
consumer:
group-id: zipkin-consumer-group # 定义消费者组ID
auto-offset-reset: earliest # 如果没有初始偏移量或当前偏移量不再存在,则从最早的消息开始读取
## 如果使用RabbitMQ
spring:
rabbitmq:
host: rabbitmq-host # RabbitMQ主机名或IP地址
port: 5672 # RabbitMQ默认端口
username: guest # 登录用户名
password: guest # 登录密码
virtual-host: / # 虚拟主机路径,默认为空
SpringBoot3.x
management:
zipkin:
tracing:
endpoint: http://zipkin-server:9411/api/v2/spans # 当sender.type为web时指定此参数
tracing:
sampling:
probability: 0.1 # 设置采样比例,默认值为0.1(即10%),生产环境中建议调整至合适的范围
## kafka
spring:
kafka:
bootstrap-servers: kafka-broker1:9092,kafka-broker2:9092 # 列出所有Kafka broker的地址
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: io.zipkin.reporter2.sender.kafka.ZipkinKafkaMessageConverter
consumer:
group-id: zipkin-consumer-group # 定义消费者组ID
auto-offset-reset: earliest # 如果没有初始偏移量或当前偏移量不再存在,则从最早的消息开始读取
zipkin:
sender:
type: kafka # 使用Kafka作为消息传输方式
## RabbitMQ
spring:
rabbitmq:
host: rabbitmq-host # RabbitMQ主机名或IP地址
port: 5672 # RabbitMQ默认端口
username: guest # 登录用户名
password: guest # 登录密码
virtual-host: / # 虚拟主机路径,默认为空
zipkin:
sender:
type: rabbit # 使用RabbitMQ作为消息传输方式
spring:
cloud:
openfeign:
trace:
enabled: true # 开启Feign链路追踪
##持久化存储
spring:
data:
elasticsearch:
cluster-nodes: es-node1:9300,es-node2:9300 # Elasticsearch集群节点列表
cluster-name: my-es-cluster # Elasticsearch集群名称
zipkin:
storage:
type: elasticsearch # 指定存储类型为Elasticsearch
任务调度
xxl-job
国内用Java写的一个分布式任务调度平台,参考官网: 文档写的很全