- 前言
- Eureka Server
- pom.xml
- ConfigData(配置文件)
- application-k8s
- application-k8s4replic
- helm
- deployment.yaml
- service.yaml
- values.yaml
- 部署
- 小结
- Config Server
- pom.xml
- application.yaml
- helm
- deployment.yaml
- 小结
- Gateway
- pom.xml
- Gateway CircuitBreaker
- RequestRateLimiter
- KeyResolver
- 小结
- Client 组件
- 最佳实践
- 关于整合 CircuitBreaker
- 示例
- 小结
- Slueth
- common
- TraceFilter
- TraceFilterConfiguration
- 自动装配
- 总结
Spring Cloud 的学习分享有一段时间了,之前多是从代码、配置角度去了解,最终还是要落地的,打算使用个人倾向的组件搭建一套微服务并给出容器化部署方案,顺便总结一下学习内容
个人倾向组件选择:
- eureka:服务注册、发现
- spring-cloud-loadbalancer:负载均衡
- spring-cloud-circuitbreaker-resilience4j:服务熔断
- openFeign:服务调用,同时整合 spring-cloud-loadbalance,当前版本暂不能整合 spring-cloud-circuitbreaker-resilience4j,但后者完全可以单独使用
- spring-cloud-gateway:网关
- spring-cloud-config:配置中心
- spring-cloud-slueth:链路排查、监控
- 基于 docker + k8s + helm 技术栈部署
说明:
- Spring Cloud BOM 定义版本号为 2020.0.2
- 弃用 netflix 全家桶是因为 Spring Cloud 2020 版本后已经移除对 netflix 除 eureka 外所有组件的支持
- spring-cloud-loadbalancer 代替 netflix-ribbon,前者由 Spring Cloud 提供,因而更加契合 Spring,且实现更加轻量级
- spring-cloud-circuitbreaker-resilience4j 代替 spring-cloud-circuitbreaker-hystrix,前者基于 reslience4j 实现,提供更加丰富的组件功能比如:限流器、熔断器、重试服务 等
- 服务调用依旧使用 openFeign,最佳选择,同时支持整合所有包括 ribbon、spring-cloud-loadbalancer 等组件,当前版本暂不能整合 spring-cloud-circuitbreaker-resilience4j,后续会支持
- spring-cloud-gateway 代替 zuul,前者由 Spring Cloud 提供,更加契合 Spring,同时更加契合 WebFlux
- 个人觉得容器时代下配置中心的作用没以前大,同时 spring-cloud-config 的配置更新还需要 Spring Cloud Bus 或者 Github Hook 的协助,更加倾向于使用类似 diamond 的监听式配置中心,但此处还是保持 Spring Cloud 生态完整性吧
- spring-cloud-slueth 负责链路的记录,方便排查错误、分析性能
- 分享个人基于 docker + k8s + helm 技术栈的部署方案,helm 版本 v3.6.2
Eureka Server 的使用相对简单,本文将体现:
- 启用 副本 模式
- 正本 与 副本 复用一个 jar 的 helm 部署方案
org.springframework.cloud spring-cloud-starter-netflix-eureka-server ${basedir}/src/main/resources application.yaml application-dev.yaml application-${profiles.active}.yaml *.xml application-k8s4replic.yaml application-replic.yaml
- 依赖 spring-cloud-starter-netflix-eureka-server
- 基于 maven profile 可选择性打包配置文件,默认添加 application-replic.yaml application-k8s4replic.yaml 以支持副本配置打包
eureka:
instance:
prefer-ip-address: true
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://eureka-server-replic.default:8762/eureka/
application-k8s4replic
eureka:
instance:
prefer-ip-address: true
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://eureka-server.default:8761/eureka/
- 开启副本模式互相注册
- k8s 环境下我们以 DNS 的方式获取注册地址
使用 DNS 方式而不是 环境变量 方式,是因为本文会采用 helm 部署,通常 helm chart 是以 应用 为单位的,因此无法保证 service 组件的启动顺序而导致 环境 变量 无法正确获取到 当然也可以通过多创建几个 chart 解决上述问题以使用 环境变量 方式获取地址helm
事实上,基于 helm 默认 chart 就能满足部署了,下面简单描述下调整到 默认模板 的地方
deployment.yaml env:
{{- with .Values.env }}
{{- range . }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- end }}
- containers 模块下增加 env 模块支持镜像的 环境变量 配置
- 基于此,我们可以使用 spring.profiles.active 顺序来控制启动的 jar 是否副本
- 正副本的模板相同
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
{{- if eq .Values.service.type "NodePort" }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
- 简单调整 service 模板,以支持 NodePort 类型
- 本文将采用 NodePort 来暴露服务
- 正副本的模板相同
image:
repository: shuiniudocker/sc-max-eureka-server
pullPolicy: Never
tag: "1.0"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: "eureka-server"
service:
type: NodePort
port: 8761
nodePort: 30001
env:
- name: spring.profiles.active
value: k8s
- 使用本地镜像,故拉取策略为 Never
- fullnameOverride 覆盖 service name,呼应配置文件中的注册中心 DNS 地址
- 正本 NodePort 配置转发 30001 端口至容器 8761 端口
- 正本使用配置文件 application-k8s.yaml
- 副本针对 service 和 env 的配置自行调整即可
# 部署正本 helm install eureka-server ./eureka-server # 部署副本 helm install eureka-server-replic ./eureka-server-replic
也可以复用同一个 chart,通过不同的 values.yaml 启动小结
- Eureka Server 的使用整体相对简单
- 因为 Eureka Client 最终肯定是体现到各个服务中,故此处没有提到
关于 Config Server 的个人理解:
- 有些环境(比如 容器)下 Config Server 与 Repository 的连接可能涉及到网络问题,这种情况下可能会使用本地仓库等形式
- 配置项的刷新需要通过 /actuator/refresh 端口进行,可能需要 Git Hook 或者 Spring Cloud Bus 等手段的辅助
- 优秀的版本管理,application profile label 的级别完美契合 Git 的版本管理和 Spring 对 ConfigData 的管理
org.springframework.cloud spring-cloud-config-server org.springframework.cloud spring-cloud-starter-netflix-eureka-client
- Config Server 端需要引入的是 spring-cloud-config-server,不同于常规的 starter 模式
- 引入 spring-cloud-starter-netflix-eureka-client 是期望客户端基于 Eureka 发现 Config Server 服务
spring:
application:
name: configserver
profiles:
active: native
cloud:
config:
server:
native:
search-locations: classpath:/, classpath:/config, file:./, file:./config
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
server:
port: 8888
- 这里是基于本地仓库管理配置,spring.profiles.active=native,默认配置文件路径为 classpath:/, classpath:/config, file:./, file:./config,对应的客户端配置放在上述路径即可
- 服务注册
同样的,也是基于 默认模板 进行些许调整
deployment.yamlcontainers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /client1/dev
port: http
readinessProbe:
httpGet:
path: /client1/dev
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- with .Values.env }}
{{- range . }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- end }}
- Config Server 的 存活探针 需要额外配置下,可以指定一个 自定义的 endpoint、存在的配置文件路径 或 不存在的配置文件 等,如上述示例中是一个测试配置文件的访问路径 /client1/dev
- 支持 env 属性的指定,主要用来覆盖 application.yaml,对应 values.yaml 中的 env 配置如下:
env:
- name: eureka.client.service-url.defaultZone
value: http://eureka-server.default:8761/eureka/,http://eureka-server-replic.default:8762/eureka/
有必要的话也可以覆盖对应 配置仓库 等属性
小结
- Config Server 的使用也相对简单,重点是关注仓库的相关配置
- 其他未提到的 helm 配置,即跟之前 Eureka Server 的配置基本无异
- Config Client 的配置体现在对应的服务中,此处没有体现
关于 Gateway 的使用:
- Spring Cloud 栈下提供了对各服务的默认 Route 定义,前提是包含 服务发现 组件依赖,并开启配置项:spring.cloud.gateway.discovery.locator.enable=true
- 基于 服务发现 的默认路由规则为:http://serviceId/* route to http://lb:serviceId,lb 前缀由专门的微服务 负载均衡 组件拦截器处理
- 提供了大量现成的拦截器,本文主要示例 CircuitBreakerFilter 和 RequestRateLimiter 的使用
org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-circuitbreaker-reactor-resilience4j org.springframework.boot spring-boot-starter-data-redis-reactive ${basedir}/src/main/resources application.yaml application-dev.yaml application-${profiles.active}.yaml
- CircuitBreakerFilter 组件默认实现基于 spring-cloud-starter-circuitbreaker-reactor-resilience4j
- RequestRateLimiter 组件默认实现基于 spring-boot-starter-data-redis-reactive
- 基于 Maven Profile 机制隔离配置文件打包
Gateway 可以针对路由提供各种拦截器,其中就包括 CircuitBreakerFilter,基于 resilience4j 提供熔断、超时等组件
@Bean public CustomizerreactiveResilience4JCircuitBreakerCustomizer() { return factory -> { factory.configureDefault( id -> new Resilience4JConfigBuilder(id) .circuitBreakerConfig( CircuitBreakerConfig.custom() .recordException(e -> e instanceof RuntimeException) .minimumNumberOfCalls(2) .failureRateThreshold(50L) .build() ) .build() ); }; }
- 创建对应的 ReactiveResilience4JCircuitBreakerFactory(而不是 Resilience4JCircuitBreakerFactory)
- 示例中提供的是一个默认配置,因此对所有 CircuitBreaker 生效
@Bean
public RouteLocator routeLocator(ConfigurableApplicationContext applicationContext) {
return new RouteLocatorBuilder(applicationContext)
.routes()
.route("circuit-breaker-route"
, p -> p.path("/test/circuitBreaker")
.filters(f -> f
.circuitBreaker(
c -> c.setName("circuitBreaker").setFallbackUri("forward:/fallback")
)
.stripPrefix(1)
)
.uri("http://localhost:9000")
)
.build();
}
- 提供一个路由配置,匹配路径 /test/circuitBreaker,创建对应的 CircuitBreaker,基于之前的配置进行熔断、降级处理
- 指定降级处理路径为 fallback
spring:
cloud:
gateway:
filter:
request-rate-limiter:
deny-empty-key: false
routes:
- id: rateLimiter
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 30
redis-rate-limiter.requestedTokens: 30
uri: lb://CLIENT1
predicates:
- Path=/rateLimiter
- 限流的默认是基于 Redis 的 令牌桶算法 实现
- 这里提供一个基于 ConfigurationProperties 的路由配置,其中:redis-rate-limiter.replenishRate 即每秒令牌生成数、redis-rate-limiter.burstCapacity 即令牌桶最大容量、redis-rate-limiter.requestedTokens 即每个请求消耗的令牌数,综上配置含义为:每三十秒允许一次请求
- 这里是针对单个路由的限流配置,当然也可以配置为 default-filters 以支持所有路由限流,也可以使用代码形式配置
@Bean
public KeyResolver keyResolver() {
return exchange -> Mono.justOrEmpty(
Optional.ofNullable(exchange)
.map(ServerWebExchange::getRequest)
.map(ServerHttpRequest::getQueryParams)
.map(map -> map.getFirst("limitKey"))
.orElse(null)
);
}
- 上述为 KeyResolver 的配置示例,以参数 limitKey 作为限流 Key
- 如果解析结果为 null,则基于 spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key 配置决定是否拒绝该次请求
- Gateway 针对基于 服务发现 的路由,提供了一套默认配置
- Gateway 提供了大量的 拦截器 路由断言 实现,可基于 代码 或者 配置文件 直接配置使用,详情可参考 官方文档
- 常规 helm 部署,略
服务间的调用通常依赖于客户端组件进行,最好用最常用的应该就是 OpenFeign 了
- 基于 Spring 的整合使用方便,注解式声明即可
- 基于单个客户端,可以提供自定义配置类进行个性化配置
- 无缝整合 spring-cloud-loadbalancer(ribbon)
- 暂时不能整合 spring-cloud-circuitbreaker-resilience4j(但是可以整合 hystrix)
@FeignClient(contextId = "eureka-client-1-1", value = "eureka-client-1", configuration = EurekaClient1Client.EurekaClient1ClientFeignConfig.class)
@LoadBalancerClient(name = "eureka-client-1", configuration = EurekaClient1Client.EurekaClient1LoadBalancerConfig.class)
public interface EurekaClient1Client {
// @Configuration
static class EurekaClient1ClientFeignConfig {
@Bean
Logger.Level level() {
return Logger.Level.FULL;
}
}
static class EurekaClient1LoadBalancerConfig {
@Bean
public ReactorLoadBalancer reactorLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
}
- 正如官方所说,不建议 客户端 与 服务端 实现同一个接口,此处也正是如此,针对每一个 客户端 提供单独的接口,也方便我们以 静态类 的形式 高内聚 地提供对应的 自定义配置类
- 关于配置类隔离客户端属性的原理,spring-cloud-openfeign 和 spring-cloud-loadbalancer 的实现差不多,之前的文章有分享过
- @FeignClient 使用同 value 属性不同 contextId 属性,可以为同一个 客户端 提供不同配置的实现,有必要可以使用
- 自定义配置类 不需要额外使用 @Configuration 注解修饰,否则会被视为 默认配置
- 上述示例为 eureka-client-1 提供了一个 OpenFeignClient,同时配置其日志记录级别为 FULL,其下的 LoadBalancer 策略为 RandomLoadBalancer
- spring-cloud-openfeign 暂时不支持 spring-cloud-circuitbreaker-resilience4j,但后期会支持
- 但是其实如果要使用 spring-cloud-circuitbreaker-resilience4j,也完全可以单独整合的,但是不支持接口层的注解修饰,因此可能得从 client 层提前到 service 层进行 CircuitBreakerFactory 的包装
@Configuration
public class FeignCircuitBreakerConfig {
@Bean
public Customizer circuitBreakerFactoryCustomizer1() {
return factory -> factory.configure(
resilience4JConfigBuilder -> resilience4JConfigBuilder
.circuitBreakerConfig(
CircuitBreakerConfig
.custom()
.slowCallDurationThreshold(Duration.ofSeconds(1))
.slowCallRateThreshold(50)
.minimumNumberOfCalls(4)
.build()
)
.timeLimiterConfig(
TimeLimiterConfig
.custom()
.timeoutDuration(Duration.ofSeconds(3))
.build()
)
, "eureka-client"
);
}
}
=================================
@Service
public class EurekaClientServiceImpl implements EurekaClientService {
@Autowired
EurekaClient1Client eurekaClient1Client;
@Autowired
CircuitBreakerFactory circuitBreakerFactory;
@Override
public String delay(int time) {
return circuitBreakerFactory
.create("eureka-client")
.run(() -> eurekaClient1Client.delay(time));
}
}
- 上述示例提供 spring-cloud-circuitbreaker 配置类(基于 resilience4j):3s 的超时熔断
- 在 client 上层的 service 层进行熔断包装(也可以基于 注解 包装,但 注解 并不是 SpringCloud 原生提供,之前有文章分享过)
当然,如果使用旧版本的 SpringCloud,直接使用 hystrix 即可,跟 openfeign 也是无缝整合的小结
- spring-cloud-openfeign 实现服务间的调用
- 整合 spring-cloud-loadbalancer 实现 负载均衡 相关
- 有必要的话,单独依赖 spring-cloud-circuitbreaker-resilience4j 提供 熔断 等功能(或整合 hystrix)
- 常规 helm 部署,略
服务间的调用排查错误需要用到 链路监控 组件,此处整合 spring-cloud-slueth
- 通常是基于 拦截器 给 响应头 中添加 traceId 信息,以方便在服务间追踪链路,这是一个通用的组件,可以抽象单独的 common 层提供该组件
- 每个服务模块也可以添加对应的切面组件,来给整个业务逻辑链路也添加对应的 traceId 信息,以方便追踪调用链路
- 正如之前提到,可以抽象单独的 common 层来提供一些通用组件,或者一些顶层抽象
- 此处示例提供 TraceFilter 及其装配类,基于拦截器实现 traceId 记录
public class TraceFilter implements Filter {
private Tracer tracer;
public TraceFilter(Tracer tracer) {
this.tracer = tracer;
}
private static final String TRACE_ID = "traceId";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Optional.ofNullable(response)
.map(rep -> (HttpServletResponse) rep)
.ifPresent(rep -> rep.addHeader(TRACE_ID, Optional.ofNullable(tracer)
.map(t -> t.currentSpan())
.map(span -> span.context())
.map(context -> context.traceId())
.orElse("")));
chain.doFilter(request, response);
}
}
将 traceId 加入 响应头 中
TraceFilterConfiguration@Configuration
public class TraceFilterConfiguration {
@Bean
@ConditionalOnMissingBean
public TraceFilter traceFilter(Tracer tracer) {
return new TraceFilter(tracer);
}
}
- TraceFilter 组件注册
- 也支持组件的覆盖
- 其实如果 common 层包含了大量组件并且想代入 模块化 思想,其实也可以提供 ConfigurationProperties 来控制各个组件功能的开关,当然基本上 SpringCloud 组件都已经有自己的开关了,此处也就不示例了
业务依赖于 common 层的组件包路径不可控,可以使用 自动装配 的方式加载组件,在 src/main/resources/meta-INF/spring.factories 文件中添加对应的组件类路径,比如:
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.xsn.demo.sc.max.common.config.sleuth.TraceFilterConfiguration总结
- 算是个 just run 分享吧,但是其中还是有一些个人对 Spring Cloud 组件选择的理解的
- 对一些 核心组件 的配置、微服务组件 的组合、部署方案 等给出了个人的最佳实践分享



