【Spring Cloud】Spring Cloud 组件个人最佳实践分享

前言

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 版本后已经移除对 netflixeureka 外所有组件的支持
  • spring-cloud-loadbalancer 代替 netflix-ribbon,前者由 Spring Cloud 提供,因而更加契合 Spring,且实现更加轻量级
  • spring-cloud-circuitbreaker-resilience4j 代替 spring-cloud-circuitbreaker-hystrix,前者基于 reslience4j 实现,提供更加丰富的组件功能比如:限流器、熔断器、重试服务 等
  • 服务调用依旧使用 openFeign,最佳选择,同时支持整合所有包括 ribbonspring-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

Eureka Server 的使用相对简单,本文将体现:

  • 启用 副本 模式
  • 正本副本 复用一个 jarhelm 部署方案

pom.xml

	<dependencies>

		<!-- 框架依赖 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
		</dependency>

		<!-- 二方依赖 -->

		<!-- 三方依赖 -->

	</dependencies>

	<build>
		<resources>
			<resource>
				<directory>${basedir}/src/main/resources</directory>
				<includes>
					<include>application.yaml</include>
					<include>application-dev.yaml</include>
					<include>application-${profiles.active}.yaml</include>
					<include>*.xml</include>
					<include>application-k8s4replic.yaml</include>
					<include>application-replic.yaml</include>
				</includes>
			</resource>
		</resources>
	</build>
  • 依赖 spring-cloud-starter-netflix-eureka-server
  • 基于 maven profile 可选择性打包配置文件,默认添加 application-replic.yaml application-k8s4replic.yaml 以支持副本配置打包

ConfigData(配置文件)

application-k8s

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 是否副本
  • 正副本的模板相同

service.yaml

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 来暴露服务
  • 正副本的模板相同

values.yaml

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
  • 副本针对 serviceenv 的配置自行调整即可

部署

# 部署正本
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 的个人理解:

  • 有些环境(比如 容器)下 Config ServerRepository 的连接可能涉及到网络问题,这种情况下可能会使用本地仓库等形式
  • 配置项的刷新需要通过 /actuator/refresh 端口进行,可能需要 Git Hook 或者 Spring Cloud Bus 等手段的辅助
  • 优秀的版本管理,application profile label 的级别完美契合 Git 的版本管理和 SpringConfigData 的管理

pom.xml

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-server</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
	</dependencies>
  • Config Server 端需要引入的是 spring-cloud-config-server,不同于常规的 starter 模式
  • 引入 spring-cloud-starter-netflix-eureka-client 是期望客户端基于 Eureka 发现 Config Server 服务

application.yaml

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,对应的客户端配置放在上述路径即可
  • 服务注册

helm

同样的,也是基于 默认模板 进行些许调整

deployment.yaml

containers:
 - 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

关于 Gateway 的使用:

  • Spring Cloud 栈下提供了对各服务的默认 Route 定义,前提是包含 服务发现 组件依赖,并开启配置项:spring.cloud.gateway.discovery.locator.enable=true
  • 基于 服务发现 的默认路由规则为:http://serviceId/* route to http://lb:serviceIdlb 前缀由专门的微服务 负载均衡 组件拦截器处理
  • 提供了大量现成的拦截器,本文主要示例 CircuitBreakerFilterRequestRateLimiter 的使用

pom.xml

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
		</dependency>
	</dependencies>

	<build>
		<resources>
			<resource>
				<directory>${basedir}/src/main/resources</directory>
				<includes>
					<include>application.yaml</include>
					<include>application-dev.yaml</include>
					<include>application-${profiles.active}.yaml</include>
				</includes>
			</resource>
		</resources>
	</build>
  • CircuitBreakerFilter 组件默认实现基于 spring-cloud-starter-circuitbreaker-reactor-resilience4j
  • RequestRateLimiter 组件默认实现基于 spring-boot-starter-data-redis-reactive
  • 基于 Maven Profile 机制隔离配置文件打包

Gateway CircuitBreaker

Gateway 可以针对路由提供各种拦截器,其中就包括 CircuitBreakerFilter,基于 resilience4j 提供熔断、超时等组件

	@Bean
	public Customizer<ReactiveResilience4JCircuitBreakerFactory> reactiveResilience4JCircuitBreakerCustomizer() {

		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

RequestRateLimiter

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 以支持所有路由限流,也可以使用代码形式配置

KeyResolver

	@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 部署,略

Client 组件

服务间的调用通常依赖于客户端组件进行,最好用最常用的应该就是 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<ServiceInstance> reactorLoadBalancer(Environment environment,
                                                                                       LoadBalancerClientFactory loadBalancerClientFactory) {
            String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
            return new RandomLoadBalancer(
                    loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
        }
    }
}
  • 正如官方所说,建议 客户端服务端 实现同一个接口,此处也正是如此,针对每一个 客户端 提供单独的接口,也方便我们以 静态类 的形式 高内聚 地提供对应的 自定义配置类
  • 关于配置类隔离客户端属性的原理,spring-cloud-openfeignspring-cloud-loadbalancer 的实现差不多,之前的文章有分享过
  • @FeignClient 使用同 value 属性不同 contextId 属性,可以为同一个 客户端 提供不同配置的实现,有必要可以使用
  • 自定义配置类 不需要额外使用 @Configuration 注解修饰,否则会被视为 默认配置
  • 上述示例为 eureka-client-1 提供了一个 OpenFeignClient,同时配置其日志记录级别为 FULL,其下的 LoadBalancer 策略为 RandomLoadBalancer

关于整合 CircuitBreaker

  • spring-cloud-openfeign 暂时不支持 spring-cloud-circuitbreaker-resilience4j,但后期会支持
  • 但是其实如果要使用 spring-cloud-circuitbreaker-resilience4j,也完全可以单独整合的,但是不支持接口层的注解修饰,因此可能得从 client 层提前到 service 层进行 CircuitBreakerFactory 的包装

示例

@Configuration
public class FeignCircuitBreakerConfig {

    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> 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 部署,略

Slueth

服务间的调用排查错误需要用到 链路监控 组件,此处整合 spring-cloud-slueth

  • 通常是基于 拦截器 给 响应头 中添加 traceId 信息,以方便在服务间追踪链路,这是一个通用的组件,可以抽象单独的 common 层提供该组件
  • 每个服务模块也可以添加对应的切面组件,来给整个业务逻辑链路也添加对应的 traceId 信息,以方便追踪调用链路

common

  • 正如之前提到,可以抽象单独的 common 层来提供一些通用组件,或者一些顶层抽象
  • 此处示例提供 TraceFilter 及其装配类,基于拦截器实现 traceId 记录

TraceFilter

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 组件选择的理解的
  • 对一些 核心组件 的配置、微服务组件 的组合、部署方案 等给出了个人的最佳实践分享
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值