# Spring Cloud 介绍 (opens new window)
微服务(Microservices (opens new window))是一种架构风格,它提倡将单体应用划分成一组小的服务,每个微服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相协作(通常是基于 HTTP 协议的 RESTful API),这些服务围绕着具体业务进行构建,并且能够通过自动化部署机制来独立的部署
优点:技术异构性、弹性、易扩展、易部署 、与组织结构对齐 、可组合性 、可替代性
Spring Cloud offers a simple and accessible programming model to the most common distributed system patterns, helping developers build resilient, reliable, and coordinated applications. Spring Cloud is built on top of Spring Boot, making it easy for developers to get started and become productive quickly.
Spring Cloud 下的 Spring Cloud Netflix 模块,主要封装了 Netflix 的以下项目:Eureka、Hystrix、Ribbon、Zuul
除了 Spring Cloud Netflix (opens new window) 模块外,Spring Cloud 还包括以下几个重要的模块:Spring Cloud Config (opens new window)、Spring Cloud OpenFeign (opens new window)、Spring Cloud Sleuth (opens new window)、Spring Cloud Stream (opens new window)、Spring Cloud Bus (opens new window)、Spring Cloud Security (opens new window) 等
Spring Cloud 主要功能
- 服务发现
- 服务熔断
- 配置服务
- 服务安全
- 服务网关
- 分布式消息
- 分布式跟踪
- 各种云平台支持
Spring Cloud 的基础依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.RELEASE</spring-cloud.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> </dependencies> </dependencyManagement> <!--<build>--> <!--<plugins>--> <!-- SpringBoot 应用打包插件 --> <!--<plugin>--> <!--<groupId>org.springframework.boot</groupId>--> <!--<artifactId>spring-boot-maven-plugin</artifactId>--> <!--</plugin>--> <!--</plugins>--> <!--</build>-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Spring Cloud Context
应用程序上下文服务
Bootstrap 应用程序上下文
- Spring Cloud 应用程序在进行容器初始化时会先建立一个“引导上下文”(bootstrap context),再创建主应用上下文(application context),两个上下文共享同一个
Environment
- 默认情况下,bootstrap 属性(在引导阶段加载的属性,不是 bootstrap.properties)以高优先级添加,因此本地配置无法覆盖它们
- 引导程序在进行引导上下文创建时,会去读取外部的属性,并且会进行属性解密等工作
- 引导上下文会读取 bootstrap.yml(或 .properties)文件,主应用程序上下文通常读取的是 application.yml(或 .properties)文件
- 因为 application.yml 的配置会在 bootstrap. yml 后加载,所以如果两份配置文件同时存在,且存在 key 相同的配置,则 application.yml 的配置会覆盖 bootstrap.yml 的配置
- Spring Cloud 应用程序在进行容器初始化时会先建立一个“引导上下文”(bootstrap context),再创建主应用上下文(application context),两个上下文共享同一个
覆盖远程属性的值
- 默认情况下,bootstrap 上下文添加到应用程序的属性源(如从 Spring Cloud Config Server 读取到的远程配置)不能被本地属性覆盖,除非在远程配置中设置
spring.cloud.config.override-none=true
、spring.cloud.config.override-system-properties=false
- 默认情况下,bootstrap 上下文添加到应用程序的属性源(如从 Spring Cloud Config Server 读取到的远程配置)不能被本地属性覆盖,除非在远程配置中设置
环境变化
- 当 ApplicationListener 监听到 EnvironmentChangeEvent 发生时,应用程序会:
- Re-bind any
@ConfigurationProperties
beans in the context - Set the logger levels for any properties in
logging.level.*
- Re-bind any
- 可通过访问 /configprops 端点验证更改
- 已知 EnvironmentChangeEvent 事件监听器:ConfigurationPropertiesRebinder、LoggingRebinder、EurekaServerAutoConfiguration.RefreshablePeerEurekaNodes、ArchaiusAutoConfiguration
- 当 ApplicationListener 监听到 EnvironmentChangeEvent 发生时,应用程序会:
刷新作用域
- 对于只在初始化时才注入配置的有状态 Bean,或只能初始化一次的 Bean,需要在该 Bean 上使用 @RefreshScope 标记,或设置
spring.cloud.refresh.extra-refreshable=其全限定类名
,才能在刷新时重建并重新注入其依赖项 - 刷新作用域 Bean 是在使用它们时初始化的惰性代理(即在调用方法时),并且作用域充当初始化值的缓存,要强制 Bean 在下一个方法调用上重新初始化,必须使其缓存条目无效(GenericScope#cache)
- RefreshScope 是上下文中的一个 Bean,其一个公共 refreshAll() 方法,通过清除目标缓存来刷新作用域中的所有 Bean,/refresh 端点公开此功能(通过 HTTP 或 JMX),要按名称刷新单个 Bean,还有一个 refresh(String) 方法
- 暴露 /refresh 端点:
management.endpoints.web.exposure.include=refresh
- 对于只在初始化时才注入配置的有状态 Bean,或只能初始化一次的 Bean,需要在该 Bean 上使用 @RefreshScope 标记,或设置
端点
- 对于 Spring Boot Actuator 应用,可以使用一些其它管理端点:
- /env:更新 Environment 并重新绑定 @ConfigurationProperties Bean 和设置日志级别
- /refresh:重新加载引导上下文并刷新 @RefreshScope Bean。
- /restart:关闭并重新启动 application context(默认情况下禁用)
- /pause 和 /resume:用于调用 Lifecycle.stop() 和 Lifecycle.start() 方法(默认情况下禁用)
如果禁用 /restart 端点,则 /pause 和 /resume 端点也将被禁用,因为它们只是 /restart 的特殊情况
management.endpoint.≶id>.enabled=true- 对于 Spring Boot Actuator 应用,可以使用一些其它管理端点:
# Spring Cloud Commons
- 通用的抽象
- @SpringCloudApplication,组合了 @SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker
- @EnableDiscoveryClient
- 查找 DiscoveryClient 接口的实现
- 健康指示器 DiscoveryClientHealthIndicator
- 排序 Discovery Client 实例,
spring.cloud.{clientIdentifier}.discovery.order
- ServiceRegistry
- 服务注册接口,提供 register(Registration)、deregister(Registration) 等方法
- ServiceRegistry 自动注册,可以设置:@EnableDiscoveryClient(autoRegister=false) 永久禁用自动注册,或通过配置
spring.cloud.service-registry.auto-registration.enabled=false
禁用注册行为 - ServiceRegistry 自动注册事件:InstancePreRegisteredEvent、InstanceRegisteredEvent
- Service Registry Actuator 端点:/service-registry,GET 调用返回 Registration 的状态,POST 调用可更改 Registration 的状态
- RestTemplate 作为负载均衡客户端
- 创建RestTemplate @Bean 时使用@LoadBalanced 修饰
- URI 使用虚拟主机名(即服务名称,而不是主机名),Ribbon 客户端用来创建完整的物理地址
- WebClient 作为负载均衡客户端
- 创建 WebClient.Builder @Bean 时使用 @LoadBalanced 修饰
- URI 使用虚拟主机名(即服务名称,而不是主机名),Ribbon 客户端用来创建完整的物理地址
- 重试失败的请求
- 通过将 Spring Retry 添加到应用程序的类路径启用,当存在 Spring Retry 时,负载均衡的 RestTemplates、Feign 和 Zuul 会自动重试任何失败的请求
- 可通过设置
spring.cloud.loadbalancer.retry.enabled=false
禁用 - 相关 Ribbon 配置:
client.ribbon.MaxAutoRetries
、client.ribbon.MaxAutoRetriesNextServer
、client.ribbon.OkToRetryOnAllOperations
- 相关 Zuul 配置:
zuul.retryable
、zuul.routes.<routename>.retryable
- 多个 RestTemplate 对象
- 创建普通 RestTemplate @Bean 时使用 @Primary 修饰
- 忽略网络接口
spring.cloud.inetutils.ignoredInterfaces[0]=veth.*
:忽略以 veth 开头的所有网卡接口名以便从 Service Discovery 注册中排除它们spring.cloud.inetutils.preferredNetworks[0]=192.168
:仅使用指定的网络地址spring.cloud.inetutils.useOnlySiteLocalInterfaces=true
:仅使用站点本地地址
- HTTP 客户端工厂
- Spring Cloud 会自动创建 Ribbon、Feign 和 Zuul 使用的 HTTP 客户端,也可以自定义 HTTP 客户端
- ApacheHttpClientFactory、OkHttpClientFactory
spring.cloud.httpclientfactories.apache.enabled
、spring.cloud.httpclientfactories.ok.enabled
- 启用特性
- /features 端点,返回类路径上可用的特性以及它们是否已启用,返回的信息包括 type、name、version、vendor
- 通过定义 HasFeatures Bean 声明特性
# 服务注册与发现 Eureka
- 将业务组件注册到 Eureka 服务器中,其他客户端组件可以向服务器获取服务并且进行远程调用
- 作为 Eureka 客户端存在的服务提供者,主要进行以下工作:
- Register:向服务器注册服务(发送自己的服务信息,包括 IP 地址、端口、ServiceId 等)
- Renew:发送心跳给服务器(即服务续约,默认每隔 30 秒发送一次心跳来保持其最新状态)
- Fetch Registry:向服务器获取注册列表(客户端同样在内存中保存了注册表信息,因此每次服务的请求都不必经过服务器端的注册中心)
- Cancel:向服务器发送一个取消请求
# Eureka 服务器 (opens new window)
添加依赖 spring-cloud-starter-netflix-eureka-server(会自动引入 spring-boot-starter-web,具有 Web 容器的功能)
添加配置
server.port: 8761 # 声明服务器的 HTTP 端口 eureka: client: registerWithEureka: false # 是否将自己向自己或别的 Eureka Server 进行注册,默认值为 true fetchRegistry: false # 是否到 Eureka 服务器中获取注册信息,默认值为 true
1
2
3
4
5在启动类上添加 @EnableEurekaServer,声明该应用是一个 Eureka 服务器
访问 http://localhost:8761
# Eureka 客户端 (opens new window)(服务提供者、服务调用者)
添加依赖 spring-cloud-starter-netflix-eureka-client(已包含 spring-cloud-starter-netflix-ribbon)
添加配置
server.port: 9000 spring.application.name: xxx eureka: # 实例信息配置 instance: instanceId: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} # 注册到 Eureka 的实例 id hostname: localhost # 主机名,不配置的时候将根据操作系统的主机名来获取 preferIpAddress: true # 优先使用 IP 地址方式进行注册服务 # appname: ${spring.application.name} # 服务名,默认取 spring.application.name 的配置值,如果没有则为 unknown # 客户端信息配置 client: serviceUrl.defaultZone: http://localhost:8761/eureka/ # 服务注册地址
1
2
3
4
5
6
7
8
9
10
11
12在启动类上添加 @EnableEurekaClient,声明该应用是一个 Eureka 客户端(该注解已经包含 @EnableDiscoveryClient,即具有发现服务的能力)(Greenwich 版后不需要)
获得服务地址:注入 EurekaClient、DiscoveryClient
# Eureka 集群搭建
Eureka 服务器之间需要互相注册
Eureka 服务器配置文件
server.port: 8761 spring: profiles: discovery1 application.name: cloud-server eureka: instance: hostname: discovery1 instanceId: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} client: serviceUrl.defaultZone: http://discovery2:8762/eureka/ --- server.port: 8762 spring: profiles: discovery2 application.name: cloud-server eureka: instance: hostname: discovery2 instanceId: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} client: serviceUrl.defaultZone: http://discovery1:8761/eureka/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21修改 Eureka 客户端的配置文件,defaultZone 改为
http://discovery1:8761/eureka/
,将服务注册到 discovery1 上,discovery1 的注册列表信息会自动同步到 discovery2 节点
# Eureka 的自我保护模式
默认情况下,如果 Eureka Server 在一定时间内(默认 90 秒
eureka.instance.leaseExpirationDurationInSeconds=90
)没有接收到某个微服务实例的心跳,Eureka Server 将会移除该实例(失效剔除)如果在 15 分钟内 Eureka Serve 接收到的心跳低于 85%,那么 Eureka 就认为客户端与注册中心出现了网络故障,而微服务本身是正常运行的,此时进入自我保护机制:
- Eureka Server 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
- Eureka Server 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用
- 当网络稳定时当前 Eureka Server 新的注册信息会被同步到其它节点中
Renews threshold:Eureka Server 期望在每分钟中收到的心跳次数:threshold 初始值为 1,client 个数为 n,threshold = 1+2*n(此为禁止自注册的情况,这里的乘以 2 是因为默认每分钟发两次心跳)
Renews (last min):上一分钟内收到的心跳次数:renews = 2*n
当 renews/threshold < 0.85
eureka.server.renewalPercentThreshold=0.85
时,就会进入自我保护机制eureka.server.enableSelfPreservation=true
# Eureka Rest 接口 (opens new window)
- 获取所有注册信息:GET /eureka/apps
- 获取应用信息:GET /eureka/apps/{appname}
- 服务注册:POST /eureka/{appname}
- 心跳续约: PUT /eureka/apps/{appname}/{instanceId}?status=UP
- 服务下线:DELETE /eureka/apps/{appname}/{instanceId}
- 强制修改健康状态(overriddenstatus):PUT /eureka/apps/{appname}/{instanceId}?status={status} (status 的值:UP; DOWN; STARTING; OUT_OF_SERVICE; UNKNOWN)(通过手动或使用 Asgard 等管理工具设置 OUT_OF_SERVICE 暂时禁止某些实例的流量,可实现红黑部署)
- 删除修改的健康状态(让 Eureka 服务器开始遵守实例本身发布的状态):DELETE /eureka/apps/{appname}/{instanceId}/status
# 信息监控与健康检查
在 Eureka 客户端的项目中添加依赖 spring-boot-starter-actuator
信息监控:修改配置文件,添加 info 信息;访问 Eureka客户端的 /info 端点
info: app.name: ${spring.application.name} company.name: www.example.com build.artifactId: @project.artifactId@ build.version: @project.version@
1
2
3
4
5健康检查:修改配置文件,添加健康状态检查配置;访问 Eureka客户端的 /health 端点
(可通过 EurekaClient#registerHealthCheck() API 插入自定义的 HealthCheckHandlers)eureka.client.healthcheck.enabled: true
1
# 服务注册与发现机制
- spring-cloud-commons
- 服务注册抽象:ServiceRegistry<R extends Registration>
- 客户发现抽象:DiscoveryClient、LoadBalancerClient
# 其它服务注册中心
- ZooKeeper (opens new window):spring-cloud-starter-zookeeper-discovery
- Consul (opens new window):spring-cloud-starter-consul-discovery
- Nacos (opens new window)
- etcd:A distributed, reliable key-value store for the most critical data of a distributed system
# 负载均衡器 Ribbon (opens new window)
- 常见的负载均衡方式:
- 集中式 LB,独立进程单元,通过负载均衡策略,将请求转发到不同的执行单元上,如 F5、Nginx
- 进程内 LB,将负载均衡逻辑以代码的形式封装到服务消费者的客户端上(服务消费者客户端维护了一份服务提供者的信息列表),如 Ribbon
# Ribbon 介绍
Ribbon 是 Netflix 下的负载均衡项目,提供以下特性:
- 负载均衡器,可支持插拔式的负载均衡规则
- 对多种协议提供支持,例如 HTTP、TCP、UDP
- 集成了负载均衡功能的客户端
Ribbon 的主要子模块:
- ribbon-core:Ribbon 的核心 API
- ribbon-loadbalancer:可以独立使用或与其他模块一起使用的负载均衡器 API
- ribbon-eureka: Ribbon 结合 Eureka 客户端的 API,为负载均衡器提供动态服务注册列表信息
Ribbon 的负载均衡器的三大子模块
- Rule:负载均衡策略,决定从 server 列表中返回哪个 server 实例(由 IRule 接口的 choose 方法来决定)
- Ping:心跳检测,该组件主要使用定时器每隔一段时间会去 Ping server ,判断 server 是否存活(IPing 接口)
- ServerList:服务列表,可以通过静态的配置确定负载的 server ,也可以动态指定 server 列表(如果动态指定 server 列表,则会有后台的线程来刷新该列表)
Ribbon 自带的负载规则
- RoundRobinRule:默认的规则,通过简单的轮询服务列表来选择 server
- AvailabilityFilteringRule:过滤掉由于多次访问故障而处于“短路”状态的 server 以及并发的连接数量超过阈值的 server ,再对剩余的 server 列表按照轮询规则选择
- WeightedResponseTimeRule:根据响应时间为每个 server 分配一个权重值,响应时间越短,权重值越大,被选中的可能性越高(当刚开始运行权重没有初始化时使用轮询规则选择)
- ZoneAvoidanceRule:使用 Zone 对 server 进行分类,复合判断 server 所在区域的运行性能和 server 的可用性选择 server
- BestAvailableRule:忽略“短路”的 server ,再选择并发数较低的 server
- RandomRule:随机选择可用的 server
- RetryRule:按照轮询规则选择 server ,如果选择的 server 无法连接则在指定时间内进行重试,再重新选择 server
# 在 Spring Cloud 中使用 Ribbon
- 在 Spring 容器启动时,会为被 @LoadBalanced 修饰过的 RestTemplate 添加拦截器 LoadBalancerInterceptor,在拦截器的方法中,将远程调用方法交给了 Ribbon 的负载均衡器 LoadBalancerClient 去处理(选择 server,将原来请求的 URI 进行改写)
在服务调用者的项目中添加依赖 spring-cloud-starter-netflix-ribbon
配置 Ribbon,指定使用的负载规则、Ping 类或者 server 列表
使用代码配置 Ribbon
@Configuration public class MyConfig { @Bean public IRule getRule() { // return new RandomRule(); return new MyRule(); } @Bean public IPing getPing() { return new MyPing(); } } @RibbonClient(name="cloud-provider", configuration=MyConfig.class) public class CloudProvider { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16使用配置文件 (opens new window)设置 Ribbon,修改服务调用者配置文件,可配置的属性见 CommonClientConfigKey 类
<clientName>.ribbon: # clientName 为设置服务名称(当缺省时设置的是所有的 Ribbon),ribbon 为默认的 nameSpace # MaxAutoRetries: 0 # Max number of retries on the same server (excluding the first try) # MaxAutoRetriesNextServer: 1 # Max number of next servers to retry (excluding the first server) # OkToRetryOnAllOperations: false # Whether all operations can be retried for this client # ConnectTimeout: 2000 # 连接超时时间 # ReadTimeout: 5000 # 读取超时时间 # ServerListRefreshInterval: 30s # 刷新服务列表源的间隔时间 # NFLoadBalancerRuleClassName: com.example.cloud.MyRule # NFLoadBalancerPingClassName: com.example.cloud.MyPing # listOfServers: localhost:8080,localhost:8081
1
2
3
4
5
6
7
8
9
10
Ribbon 默认会自动重试 GET 请求 1 次
在程序的 IoC 容器中配置 RestTemplate Bean,并在这个 Bean 上加上 @LoadBalanced 注解
@Configuration public class RibbonConfig { @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } }
1
2
3
4
5
6
7
8服务调用者使用 RestTemplate 根据服务提供者的应用名称 serviceId 来进行调用其发布的 REST 服务(也可以通过注入 LoadBalancerClient 选择服务)
# 声明式调用 Feign (opens new window)
- 在使用 Feign 时,可以使用注解来修饰接口,被注解修饰的接口具有访问 Web Service 的能力
- Feign 使用的是 JDK 的动态代理,生成的代理类会将请求的信息封装,交给 Feign 客户端(即 Client 接口的实现类)发送请求(SynchronousMethodHandler#invoke --> Client#execute)
- Client 接口的默认实现类 Client.Default 使用 java.net.HttpURLConnection 来发送 HTTP 请求,也可以通过添加依赖使用其它 HttpClient:
- 使用 Apache HttpClient feign 客户端 ClosableHttpClient:通过设置
feign.httpclient.enabled=true
feign.okhttp.enabled=false
,并添加依赖 io.github.openfeign:feign-httpclient - 使用 Ok HttpClient feign 客户端 OkHttpClient:通过设置
feign.httpclient.enabled=false
feign.okhttp.enabled=true
,并添加依赖 io.github.openfeign:feign-okhttp
- 使用 Apache HttpClient feign 客户端 ClosableHttpClient:通过设置
- 默认使用 SpringEncoder 编码 Request,使用 ResponseEntityDecoder 解码 Response
- 默认 Feign 客户端配置类:FeignClientsConfiguration,用于定义 Feign Client 的 Decoder、Encoder、Contract、Feign.Builder、Retryer、FeignLoggerFactory 等
- 默认 Feign 自动配置类:FeignAutoConfiguration
- 默认 InvocationHandlerFactory.MethodHandler:SynchronousMethodHandler
# 在 Spring Cloud 中使用 Feign (opens new window)
# 整合 Feign
在服务调用者的项目中添加依赖 spring-cloud-starter-openfeign
在服务调用者的启动类上添加 @EnableFeignClients,打开 Feign 开关
编写客户端接口,由于 Spring Cloud 提供了注解翻译器(SpringMvcContract),使得 Feign 可以解析 @RequestMapping、@RequestParam、@RequestHeader、@PathVariable 注解的含义,因此在服务接口上可以直接使用这些注解
/** * 应用程序上下文中 Bean 的名称是该接口的全限定类名 * @FeignClient 属性: * 1. name 声明需要调用的服务名称(The service id with optional protocol prefix. It will be used used to create a Spring Cloud LoadBalancer client)。如果应用程序是 Eureka 客户端,则会在 Eureka 服务注册表中解析该服务。 * 2. path 指定所有方法级映射将使用的路径前缀。 * 3. contextId // String alias = contextId + "FeignClient"; * 3. qualifier // if (StringUtils.hasText(qualifier)) {alias = qualifier;} */ @FeignClient(name = "stores"/*, path = "store"*/) // @FeignClient(name = "stores", url = "http://127.0.0.1:8001") public interface StoreClient { // 动态设置请求地址的 Host:在定义的接口的方法中如果包含**URI 类型**的参数,Feign 将使用该值作为请求目标主机 @GetMapping("/stores") List<Store> getStores(); @PostMapping(value = "/stores/{storeId}") Store update(@PathVariable("storeId") Long storeId, Store store); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18注入编写的客户端接口调用方法
@Autowired private StoreClient storeClient; storeClient.getStores();
1
2
3
4
注意
数据默认使用 Payload 提交方式来提交(
Content-Type: application/json;charset=UTF-8
),即使没有 @RequestBody如果参数需要通过拼接在 URL 后以查询字符串的方式来提交,需要在参数前加上注解:@RequestParam(参数只能是简单类型,且需要设置注解中的 name 属性 (opens new window))或 @SpringQueryMap(参数只能是复杂类型)
如果数据需要通过 Form 提交方式来提交(
Content-Type: application/x-www-form-urlencoded
或Content-Type: multipart/form-data
),需要创建 SpringFormEncoder Bean,并在方法的 @RequestMapping 上指定 consumes// 解决 POST 时 URL Encoder 问题,支持 multipart/form-data 多文件上传 @Bean @Primary public Encoder multipartFormEncoder(ObjectFactory<HttpMessageConverters> messageConverters) { return new SpringFormEncoder(new SpringEncoder(messageConverters)); }
1
2
3
4
5
6
添加拦截器:自定义 RequestInterceptor Bean
# 配置 Feign
feign:
# httpclient:
# enabled: true
# okhttp:
# enabled: false
client:
config:
feignName: # If you prefer using configuration properties to configured all @FeignClient, you can create configuration properties with `default` feign name.
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意事项:
- 默认情况下 Feign 的连接超时、读取超时都是 1s,见 RibbonClientConfiguration#ribbonClientConfig
- 必须同时配置 Feign 的连接超时、读取超时,才能生效,见 FeignClientFactoryBean#configureUsingProperties
- 单独的超时可以覆盖全局超时
- 可以配置 Feign,也可以配置 Ribbon 组件的参数来修改两个超时时间
- 同时配置 Feign 和 Ribbon 的超时,以 Feign 为准
# Feign 负载均衡
- Feign 的 stater 依赖已包含 Ribbon,所提供的 Client 的实现类为 LoadBalancerFeignClient(FeignRibbonClientAutoConfiguration),在该类中,维护着与 SpringClientFactory 相关的实例,通过 SpringClientFactory 可以获取负载均衡器,负载均衡器会根据一定的规则来选取处理请求的服务器,最终实现负载均衡的功能
# 熔断器 Hystrix
- 在分布式环境中,总会有一些被依赖的服务会失效,Hystrix 通过隔离服务间的访问点、停止它们之间的级联故障、提供可回退操作来实现容错,使得不会因为单点故障而降低整个集群的可用性
- 当某基础服务模块不可用时,服务调用者将对其进行“熔断”,在一定的时间内,服务调用者都不会再调用该基础服务,以维持本身的稳定
- 如果客户端没有容错机制,客户端将一直等待服务端返回,直到网络超时或者服务有响应,而外界会一直不停地发送请求给客户端,最终导致客户端因请求过多而瘫痪
- 雪崩效应:如果一个服务出现了故障或者是网络延迟,在高并发的情况下,会导致线程阻塞,在很短的时间内该服务的线程资源会消耗殆尽,使得该服务不可用。在分布式系统中,由于服务之间的相互依赖,最终可能会导致整个系统的不可用。
- 基本的容错模式
- 超时:主动超时
- 限流:限制最大并发数(RateLimiter)
- 熔断:调用失败触发阈值后,后续调用直接由断路器返回错误,不再执行实际调用(断路器模式,CircuitBreaker)
- 隔离:隔离不同的依赖调用(舱壁隔离模式,Bulkhead)
- 降级:服务降级
# Hystrix 介绍
Hystrix 的主要功能:
- 防止单个服务的故障耗尽整个服务的 Servlet 容器的线程资源
- 快速失败机制,如果某个服务出现了故障,则调用该服务的请求快速失败,而不是线程等待
- 提供回退(fallback)方案,在请求发生故障时,提供设定好的回退方案
- 使用熔断机制,防止故障扩散到其他服务
- 提供熔断器的监控组件 Hystrix Dashboard,可以实时监控熔断器的状态
Hystrix 的运作流程
- 在命令开始执行时,会做一些准备工作,例如为命令创建相应的线程池等
- 判断是否打开了缓存,打开了缓存就直接查找缓存并返回结果
- 判断断路器是否打开,如果打开了,就表示链路不可用,直接执行回退方法
- 判断线程池、信号量(计数器)等条件,例如像线程池超负荷,则执行回退方法,否则,就去执行命令的内容
- 执行命令,计算是否要对断路器进行处理,执行完成后,如果满足一定条件,则需要开启断路器。如果执行成功,则返回结果,反之则执行回退
- 每次调用创建一个新的 HystrixCommand,把依赖调用封装在 run() 方法中.
- 执行 execute()/queue 做同步或异步调用.
- 当前调用是否已被缓存,是则直接返回结果,否则进入步骤 4
- 判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤 8,进行降级策略,如果关闭进入步骤 5
- 判断线程池/队列/信号量是否满载,如果满载进入降级步骤 8,否则继续后续步骤 6
- 调用 HystrixCommand 的 run 方法,运行依赖逻辑:
- 调用是否出现异常,否:继续,是:进入步骤 8
- 调用是否超时,否:返回调用结果,是:进入步骤 8
- 搜集 5、6 步骤所有的运行状态(成功、失败、拒绝、超时)上报给熔断器,用于统计从而判断熔断器状态
- getFallback() 降级逻辑。四种触发 getFallback() 调用情况(图中步骤 8 的箭头来源):返回执行成功结果
触发回退的情况:1. 断路器被打开;2. 线程池、队列或信号量满载;3. 实际执行命令失败
回退的模式:在 A 命令的回退方法中执行 B 命令,如果 B 命令也执行失败,同样也会触发 B 命令的回退,形成一种链式的命令执行
断路器开启要满足的两个条件:
- 整个链路达到一定阈值,默认情况下,10 秒内产生超过 20 次请求,则符合第一个条件
- 满足第一个条件的情况下,如果请求的错误百分比大于阈值,则会打开断路器,默认为 50%
断路器关闭
- 断路器打开后,在一段时间内,命令不会再执行(一直触发回退),这段时间称作“休眠期”
- 休眠期的默认值为 5 秒,休眠期结束后,Hystrix 会尝试性地执行一次命令,此时断路器的状态不是开启,也不是关闭,而是一个半开的状态,如果这一次命令执行成功,则会关闭断路器并清空链路的健康信息;如果执行失败,断路器会继续保持打开的状态
隔离策略
- THREAD:默认值,由线程池来决定命令的执行,如线程池满载,则不会执行命令(Hystrix 使用了 ThreadPoolExecutor 来控制线程池行为,线程池的默认大小为 10);适用场景:不受信服务、有限扇出
- SEMAPHORE:由信号量来决定命令的执行,当请求的并发数高于阈值时,就不再执行命令(相对于线程策略,信号量策略开销更小,但是该策略不支持超时以及异步,除非对调用的服务有足够的信任,否则不建议使用该策略进行隔离);适用场景:受信服务、高扇出(网关)、高频高速调用(cache)
合并请求:在一次请求的过程中,可以将一个时间段内的相同请求(URL 相同,参数不同),收集到同一个命令中执行,这样就节省了线程的开销,减少了网络连接,从而提升执行的性能
请求缓存:如果在一次请求的过程中,多个地方调用同一个接口,可以使用缓存
# Hystrix 主要配置项 (opens new window)
配置项(前缀 hystrix.command.*.) | 含义 |
---|---|
execution.isolation.strategy | 线程“THREAD”或信号量“SEMAPHORE”隔离(默认:THREAD) |
execution.isolation.thread.timeoutInMilliseconds | run() 方法执行超时时间(默认:1000) |
execution.timeout.enabled | run() 方法执行是否开启超时(默认:true) |
execution.isolation.semaphore.maxConcurrentRequests | 信号量隔离最大并发数(默认:10) |
circuitBreaker.errorThresholdPercentage | 熔断的错误百分比阀值(默认:50) |
circuitBreaker.requestVolumeThreshold | 断路器生效必须满足的流量阀值(默认:20) |
circuitBreaker.sleepWindowInMilliseconds | 熔断后重置断路器的时间间隔(默认:5000) |
circuitBreaker.forceOpen | 设 true 表示强制熔断器进入打开状态(默认:false) |
circuitBreaker.forceClosed | 设 true 表示强制熔断器进入关闭状态(默认:false) |
配置项(前缀 hystrix.threadpool.*.) | 含义 |
---|---|
coreSize | 使用线程池时的最大并发请求(默认:10),建议:QPS * TP99 + 冗余线程 |
maxQueueSize | 最大 LinkedBlockingQueue 大小,-1 表示用 SynchronousQueue(默认: -1) |
default.queueSizeRejectionThreshold | 队列大小阀值,超过则拒绝(默认:5) |
其中
*
可以为:default
、@FeignClient注解的name属性值
、Feign客户端接口名#方法名()
com.netflix.hystrix.HystrixCommandProperties
# 在 Spring Cloud 中使用 Hystrix
# 在需受保护的方法上使用 Hystrix
在服务调用者的项目中添加依赖 spring-cloud-starter-netflix-hystrix
在服务调用者的启动类上添加 @EnableHystrix 或 @EnableCircuitBreaker,启用断路器
在服务方法上添加 @HystrixConanand(fallbackMethod = "xxxx"),并配置回退方法等属性
- 被 @HystrixCommand 修饰的方法,Hystrix(javanica) 会使用 AspectJ 对其进行代理,Spring 会将相关的代理类转换为 Bean 放到容器中
- @HystrixConanand 属性 fallbackMethod、groupKey、commandKey、threadPoolKey
@Component public class StoreIntegration { @HystrixCommand(fallbackMethod = "defaultStores") public Object getStores(Map<String, Object> parameters) { //do stuff that might fail } public Object defaultStores(Map<String, Object> parameters) { return /* something useful */; } }
1
2
3
4
5
6
7
8
9
10
11
12
# 在 Feign 上使用 Hystrix
- 在服务调用者的项目中添加依赖 spring-cloud-starter-openfeign(已包含 spring-cloud-starter-netflix-hystrix)
- 在配置文件中打开 Feign 的 Hystrix 开关
feign.hystrix.enabled=true
,默认 false - 在 Feign 客户端接口的 @FeignClient 注解中添加 fallback 属性(如果需要获取导致回退触发的原因则使用 fallbackFactory 属性),设置处理回退的类(回退类需要实现该接口,回退工厂类需要实现 FallbackFactory<T>,并且需要以 Bean 的方式注入 IoC 容器,即添加 @Component)
hystrix.command.@FeignClient注解的name属性值.circuitBreaker.requestVolumeThreshold
:针对全局设置默认时间段内发生的请求数hystrix.command.Feign客户端接口名#方法名().circuitBreaker.requestVolumeThreshold
:针对某个客户端默认时间段内发生的请求数hystrix.command.@FeignClient注解的name属性值.execution.isolation.thread.timeoutlnMilliseconds
:针对全局设置超时时间hystrix.command.Feign客户端接口名#方法名().execution.isolation.thread.timeoutlnMilliseconds
:针对某个客户端设置超时时间
# 缓存注解、合并请求注解
合并请求、请求缓存,在一次请求的过程中才能实现,因此需要先初始化请求上下文
@WebFilter(urlPatterns = "/*", filterName = "hystrixFilter") // 需要在启动类上添加 @ServletComponentScan public class HystrixFilter implements Filter { public void init(FilterConfig filterConfig) throws ServletException { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 创建 Hystrix 的请求上下文 HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { chain.doFilter(request, response); } finally { // 销毁 Hystrix 的请求上下文 context.shutdown(); } } public void destroy() { } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19请求缓存相关注解
- @CacheResult:用于修饰方法,表示被修饰的方法返回的结果将会被缓存,需要与 @HystrixCommand 一起使用
- @CacheRemove:用于修饰方法让缓存失效,需要与 @CacheResult 的缓存 key 关联
- @CacheKey:用于修饰方法参数,表示该参数作为缓存的 key
@CacheResult @HystrixCommand(commandKey = "cacheKey") public String cacheMethod(String name) { return "hello"; } @CacheRemove(commandKey = "cacheKey") @HystrixCommand public String updateMethod(String name) { return "update"; }
1
2
3
4
5
6
7
8
9
10
11合并请求相关注解:@HystrixCollapser
@Component public class CollapseService { // 配置收集 1 秒内的请求 @HystrixCollapser(batchMethod = "getPersons", collapserProperties ={@HystrixProperty(name = "timerDelayInMilliseconds", value = "1000")}) public Future<Person> getSinglePerson(Integer id) { System.out.println("执行单个获取的方法"); return null; } @HystrixCommand public List<Person> getPersons(List<Integer> ids) { System.out.println("收集请求,参数数量:" + ids.size()); List<Person> ps = new ArrayList<Person>(); for (Integer id : ids) { Person p = new Person(); p.setId(id); p.setName("crazyit"); ps.add(p); } return ps; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 监控熔断器状态
# 使用 Hystrix Dashboard 监控熔断器状态
- 在服务调用者的项目中添加依赖 spring-boot-starter-actuator
- 添加配置
management.endpoints.web.exposure.include: hystrix.stream
- 访问 /actuator/hystrix.stream 端点,可以看到 Hystrix 输出的 stream 数据
- 新建监控的 Web 项目 hystrix-dashboard
- 添加依赖 spring-cloud-starter-netflix-hystrix-dashboard
- 在启动类中添加 @EnableHystrixDashboard,开启 Hystrix 控制台
- 访问 http://hostname:port/hystrix,在文本框中输入需要监控的 Hystrix 流
# 使用 Turbine 集群监控
- Turbine 用于聚合多个 Hystrix 流,将多个 Hystrix Dashboard 组件的数据放在一个页面上展示,进行集中监控
- 在服务调用者的项目中添加依赖 spring-boot-starter-actuator
- 新建监控的 Web 项目 turbine-monitor
- 添加依赖 spring-cloud-starter-netflix-turbine(已包含 Eureka Client)
- 添加配置
spring: application.name: turbine-monitor server.port: 8769 security.basic.enabled: false turbine: aggregator.clusterConfig: default # 指定聚合哪些集群,多个使用“,”分割,默认为 default,可使用 http://.../turbine.stream?cluster={clusterConfig 之一} 访问 appConfig: eureka-ribbon-client, eureka-feign-client # 配置 Eureka 中的 serviceId 列表,表明监控哪些服务 clusterNameExpression: new String("default") # 1. clusterNameExpression 指定集群名称,默认表达式 appName;此时:turbine.aggregator.clusterConfig 需要配置想要监控的应用名称 # 2. 当 clusterNameExpression: default 时,turbine.aggregator.clusterConfig 可以不写,因为默认就是 default # 3. 当 clusterNameExpression: metadata['cluster']时,假设想要监控的应用配置了 eureka.instance.metadata-map.cluster: ABC,则需要配置,同时 turbine.aggregator.clusterConfig: ABC eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/
1
2
3
4
5
6
7
8
9
10
11
12 - 在启动类中添加 @EnableTurbine
- 访问 http://localhost:8769/hystrix,在文本框中输入需要要监控 Turbine 流 http://localhost:8769/turbine.stream
# 其它熔断器
# 路由网关 Zuul
# Zuul 介绍
- 为微服务集群提供代理、过滤、路由等功能
- Zuul 的核心是一系列过滤器,可以在 Http 请求的发起和响应返回期间执行一系列的过滤器 ZuulServlet#service
- Zuul 包括以下 4 种过滤器:
- pre 过滤器:在请求路由到具体的服务之前执行(可以做安全验证,例如身份验证、参数验证等)
- routing 过滤器:用于将请求路由到具体的微服务实例(默认使用 http client 进行网络请求)
- post 过滤器:在请求已被路由到微服务后执行的(一般用作收集统计信息、指标,以及将响应传输回客户端)
- error 过滤器:在其它过滤器发生错误时执行
- 每个请求都会创建一个 RequestContext 对象,过滤器之间过 RequestContext 对象来共享数据
- 注意:Spring Cloud Zuul 去掉了过滤器动态加载
- Zuul 1.x 采用的是同步阻塞模型,适用计算密集型(CPU bound)场景(可使用异步 AsyncServlet 优化连接数)
- Zuul 2.x 采用的是异步非阻塞模型,基于 Netty 处理请求,适用 IO 密集型(IO bound)场景
# 在 Spring Cloud 中使用 Zuul
- 在 zuul-gateway 项目中添加依赖 spring-cloud-starter-netflix-zuul
- 添加配置,可用属性见 ZuulProperties.java
spring.application.name: zuul-gateway eureka: instance.hostname: localhost client.serviceUrl.defaultZone: http://localhost:8761/eureka/ # 网关作为 Eureka 客户端注册到 Eureka 服务器 zuul: ignoredPatterns: /*-server/** routes: xxx: # routeld path: /xxx/** # 默认情况下使用 routeld 作为 path serviceId: xxx-server # Ribbon 路由,声明所有的 /xxx/** 请求将会被转发到 Id 为 xxx-server 的服务进行处理,默认情况下使用 routeld 作为 serviceld # url: https://www.baidu.com # 简单路由(以 http: 或 https: 开头) # url: forward:/yyy/hello # 跳转路由(以 forward: 开头),当外部访问网关的 A 地址时,会跳转到 B 地址 ribbon.eager-load.enabled: true # 预加载 Ribbon,默认为 false(在第一次调用集群服务时才初始化 Ribbon 的客户端) # prefix: /v1 # 给每一个服务的 API 接口加前缀 /v1
1
2
3
4
5
6
7
8
9
10
11
12
13
14 - 在启动类中添加 @EnableZuulProxy 注解,开启对 Zuul 的支持
# 路由配置
- 简单路由:将 HTTP 请求全部转发到“源服务”(HTTP 服务),处理跳转路由的过滤器为 SimpleHostRoutingFilter
- 跳转路由:当外部访问网关的 A 地址时,会跳转到 B 地址,处理跳转路由的过滤器为 SendForwardFilter
- Ribbon 路由:当网关作为 Eureka 客户端注册到 Eureka 服务器时,可以通过配置 serviceld 将请求转发到集群的服务中
- 对应的过滤器为 RibbonRoutingFilter 的过滤器,该过滤器会调用 Ribbon 的 API 来实现负载均衡,默认情况下用 HttpClient 来转发请求
- 为了保证转发的性能,使用了 HttpClient 的连接池功能,可以修改 HttpClient 连接池的属性
zuul.host.maxTotalConnections
:目标主机的最大连接数,默认值为 200zuul.host.maxPerRouteConnections
:每个主机的初始连接数,默认值为 20
- 自定义的路由规则
- 忽略路由:使用 zuul.ignoredServices 设置排除的服务,使用 zuul.ignoredPattems 设置不进行路由的 URL
# 请求头配置
zuul:
sensitiveHeaders: # 在默认情况下 HTTP 请求头的 Cookie、Set-Cookie、Authorization 属性不会传递到“源服务”,即默认值为 [Cookie, Set-Cookie, Authorization]
ignoredHeaders: accept-language # 忽略请求与响应中该头信息
2
3
# 路由端点
- 在网关项目中提供了一个 /routes 服务,可以访问该端点查看路由映射信息
- 需要手动开启该服务
- 在 zuul-gateway 项目中添加依赖 spring-boot-starter-actuator
- 在配置文件中将
management.security.enabled
属性值设置为 false,关闭安全认证 - 在启动类中添加 @EnableZuulProxy 注解
- 访问 http://localhost:8080/routes
# 在 Zuul 上配置熔断器
- RibbonRoutingFilter 在进行转发时会封装为一个 Hystrix 命令予以执行,即具有容错的功能
- 在 zuul-gateway 项目中建立一个网关处理类实现 ZuulFallbackProvider 的接口,并使用 @Component 注入 IoC 容器
- 在网关处理类的 getRoute() 方法指定熔断功能应用于哪些路由的服务(返回“*”表示所有的路由服务都加熔断功能)
- 在网关处理类的 fallbackResponse() 方法处理回退逻辑
# 自定义的过滤器
- 类型 Type:定义在路由流程中,过滤器被应用的阶段
- 执行顺序 Execution Order:在同一个 Type 中,定义过滤器执行的顺序
- 条件 Criteria:过滤器被执行必须满足的条件
- 动作 Action:如果条件满足,过滤器中将被执行的动作
@Component
@Slf4j
public class AuthZuulFilter extends ZuulFilter {
@Override
public String filterType() {
// 过滤器的类型,可以为 "pre", "post", "routing", "error"
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
// 过滤顺序,值越小,越早执行该过滤器
return 0;
}
@Override
public boolean shouldFilter() {
// 是否过滤逻辑,如果为 true,则执行 run() 方法;如果为 false,则不执行 run() 方法
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("${} >>> ${}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getParameter("token");
// 检查请求的参数中是否传了 token 这个参数
// 如果没有传,则请求不被路由到具体的服务实例,直接返回响应,状态码为 401
if (accessToken == null) {
log.warn("token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
try {
ctx.getResponse().getWriter().write("token is empty");
} catch (Exception e) {
}
return null;
}
log.info("ok");
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# API 网关 Spring Cloud Gateway (opens new window)
- 添加依赖 spring-cloud-starter-gateway
# 网关处理流程
- 请求分发:请求发送到网关,DispatcherHandler 是 HTTP 请求的中央分发器,将请求匹配到相应的 HandlerMapping
- 路由定位:请求与处理器之间有一个映射关系,网关将会对请求进行路由,handler 此处会匹配到 RoutePredicateHandlerMapping,以匹配请求所对应的 Route
- 路由过滤:随后到达网关的 Web 处理器,该 WebHandler 代理了一系列网关过滤器和全局过滤器的实例,如对请求或者响应的头部进行处理(增加或者移除某个头部)
- 最后,转发到具体的代理服务
RoutePredicateHandlerMapping 用于匹配具体的 Route,并返回处理 Route 的 FilteringWebHandler
FilteringWebHandler 通过创建所请求 Route 对应的 GlobalFilterChain,在网关处进行过滤处理
转发时使用的是 Netty HttpClient
@Bean
public RouterFunction<ServerResponse> testFunRouterFunction() {
RouterFunction<ServerResponse> route = RouterFunctions.route(
RequestPredicates.path("/testfun"),
request -> ServerResponse.ok().body(BodyInserters.fromObject("hello")));
return route;
}
2
3
4
5
6
7
# 路由定义定位器
- RouteDefinitionLocator,作用:获取路由定义
- 实现类:PropertiesRouteDefinitionLocator(基于属性配置)、DiscoveryClientRouteDefinitionLocator(基于服务发现)、CompositeRouteDefinitionLocator(组合方式)、CachingRouteDefinitionLocator(缓存方式)、RouteDefinitionRepository(对路由定义进行增、删、查)
# 路由定位器
RouteLocator,作用:获取路由
路由 Route,组成:id(路由 ID)、uri(路由地址)、order(路由优先级)、predicate(路由断言)、gatewayFilters(网关过滤器)
@Bean
public RouteLocator customRouteLocator1(RouteLocatorBuilder builder) {
return builder.routes()
.route("my_route", r -> r
.host("**.abc.org").and().path("/get")
.filters(f -> f
.addResponseHeader("X-TestHeader", "foobar")
.hystrix(config -> config
.setName("mycmd")
.setFallbackUri("forward:/fallback")))
.uri("http://httpbin.org:80"))
.build();
}
2
3
4
5
6
7
8
9
10
11
12
13
# 路由断言
- Predicate<ServerWebExchange>、AsyncPredicate<ServerWebExchange>
- 根据请求时间、请求的远端地址、Host 地址、路由权重、请求头部、请求方法、请求 URL 中的路径和请求参数等匹配请求对应的 Route
- 一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发
- 路由断言工厂 RoutePredicateFactory 实现类
- 自定义 RoutePredicateFactory:继承 AbstractRoutePredicateFactory
- 断言定义 PredicateDefinition
# 过滤器
- 过滤器定义 FilterDefinition
# 网关过滤器
- 网关过滤器 GatewayFilter 用于拦截和链式处理 Web 请求
- 对路由请求进行过滤,对符合条件的请求进行一些操作,如增加请求头、增加请求参数、增加响应头、断路器、修改传入的 HTTP 请求或输出的 HTTP 响应等功能
- 网关过滤器来自两部分:网关配置默认的过滤器、路由定义中的过滤器
- GatewayFilter 实现类:
- ModifyResponseGatewayFilter(修改响应体内容)
- OrderedGatewayFilter
- GatewayFilterAdapter
- 每个 GatewayFilterFactory 实现类的
GatewayFilter apply(C config)
方法里声明的内部类(即路由过滤器,作用于特定路由)
# 网关过滤器工厂
- GatewayFilterFactory
- 自定义 GatewayFilterFactory:实现 GatewayFilter 或继承 AbstractGatewayFilterFactory,注意:自定义过滤器类名称应以 GatewayFilterFactory 结尾
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config> {
public CustomGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// grab configuration from Config object
return (exchange, chain) -> {
// If you want to build a "pre" filter you need to manipulate the
// request before calling chain.filter
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
// use builder to manipulate the request
return chain.filter(exchange.mutate().request(builder.build()).build())
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
// Manipulate the response in some way
}));
};
}
public static class Config {
// Put the configuration properties for your filter here
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 全局过滤器
全局过滤器 GlobalFilter 会根据条件应用到所有的路由上
当请求进入(并匹配到一个路由)时,FilteringWebHandler 会将 GlobalFilter 的所有实例和 GatewayFilter 的所有路由特定实例添加到过滤器链中
GlobalFilter 实现类:
- 路由转发 ForwardRoutingFilter,处理请求转发 forward
- 负载均衡 LoadBalancerClientFilter,处理 lb://myservice
- Netty 路由 NettyRoutingFilter,URL 使用 http 或 https 协议
- Netty 写响应 NettyWriteResponseFilter,将代理响应写回网关客户端响应
- 路由到指定请求 URL RouteToRequestUrlFilter
- ws 路由 WebsocketRoutingFilter,URL 使用 ws 或 wss 协议
- 缓存请求体 AdaptCachedBodyGlobalFilter
- ForwardPathFilter
- 网关度量 GatewayMetricsFilter
自定义全局过滤器:实现 GlobalFilter
@Bean public GlobalFilter customGlobalFilter() { return (exchange, chain) -> exchange.getPrincipal() .map(Principal::getName) .defaultIfEmpty("Default User") .map(userName -> { // adds header to proxied request // exchange.getResponse().getHeaders() 获取的是 ReadOnlyHttpHeaders 类型的实例 exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build(); return exchange; }) .flatMap(chain::filter); } @Bean public GlobalFilter customGlobalPostFilter() { return (exchange, chain) -> chain.filter(exchange) .then(Mono.just(exchange)) .map(serverWebExchange -> { // adds header to response serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER", HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked": "It did not work"); return serverWebExchange; }) .then(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Actuator API
management.endpoints.web.exposure.include=gateway
- 包括三类:路由操作、获取过滤器和路由刷新
- /actuator/gateway/routes/{id} , methods=[DELETE],删除单个路由
- /actuator/gateway/routes/{id} , methods=[POST],增加单个路由
- /actuator/gateway/routes/{id} , methods=[GET],查看单个路由
- /actuator/gateway/routes], methods=[GET],获取路由列表
- /actuator/gateway/refresh, methods=[POST],路由刷新,RefreshRoutesEvent
- /actuator/gateway/globalfilters, methods=[GET],获取全局过滤器列表
- /actuator/gateway/routefilters, methods= [GET],路由过滤器工厂列表
- /actuator/gateway/routes/{id}/combinedfilters , methods=[GET ],获取单个路由的联合过滤器
# 配置
spring:
cloud:
gateway:
discovery.locator:
enabled: true # 是否自动根据服务发现为每个 serviceId 创建路由,将以 serviceId 开头的请求路径转发到对应的服务,默认 false
lowerCaseServiceId: true # 使用小写 serviceId(Eureka 自动将 serviceId 转成大写),默认 false
globalcors:
corsConfigurations: # 跨域配置
'[/**]':
allowedOrigins: "docs.spring.io"
allowedMethods: [GET, HEAD, POST]
maxAge: 1800
httpclient:
ssl:
useInsecureTrustManager: true # 使用使用不安全的信任管理器,信任所有下游证书
filter:
remove-hop-by-hop:
headers: # 默认删除的请求头 RemoveHopByHopHeadersFilter.HEADERS_REMOVED_ON_REQUEST
default-filters: # 定义全局过滤器
- AddResponseHeader=X-Response-Default-Foo, Default-Bar # 为所有的响应加上头部 X-Response-Default-Foo:Default-Bar
- RemoveRequestHeader=Origin
# - Hystrix=myCommandName
- name: Hystrix # 需添加依赖 spring-cloud-starter-netflix-hystrix
args:
name: myCommandName # 使用 myCommandName 作为名称生成 RouteHystrixCommand 对象包装剩余的其它过滤器,当转发服务出现问题时,执行失败逻辑(当无 fallbackUri 时,返回 504 Gateway Timeout)
fallbackUri: forward:/fallbackcontroller # 只支持 forward:格式的 URI
- name: RequestRateLimiter # Redis 请求速率限制器,使用令牌桶算法,需添加依赖 spring-boot-starter-data-redis-reactive,Redis key:request_rate_limiter.{xxx}.timestamp、request_rate_limiter.{xxx}.tokens
args:
# 每个用户的请求率限制是 10,允许突发 20,但下一秒只有 10 个请求可用
# key-resolver: "#{@principalNameKeyResolver}" # 限流键(实现 KeyResolver 接口的 Bean),使用 SpEL 按名称引用 Bean,默认使用 PrincipalNameKeyResolver
redis-rate-limiter.replenishRate: 10 # 令牌桶每秒填充速率,即允许用户每秒执行多少请求,而不会丢弃任何请求
redis-rate-limiter.burstCapacity: 20 # 令牌桶容量,即用户在一秒钟内允许执行的最大请求数,设置为 0 将阻止所有请求
- name: Retry # 重试过滤器
args:
retries: 3 # 重试次数
# statuses: BAD_GATEWAY # 将会重试的 HTTP 状态代码,取值参考 HttpStatus
series: SERVER_ERROR # 将会重试的系列状态代码,取值参考 HttpStatus.Series,默认值 SERVER_ERROR
methods: GET # 将会重试的 HTTP 方法,取值参考 HttpMethod
- name: RequestSize
args:
maxSize: 5000000 # 请求的允许大小限制,当请求因大小而被拒绝时响应状态设置为 413 Payload Too Large,缺省为 5MB
routes: # 定义路由
- id: demo_route # routeld
uri: http://example.org
order: 0 # 定义优先级
predicates: # XxxRoutePredicateFactory
- After=2018-04-20T06:06:06+08:00[Asia/Shanghai] # ZonedDateTime
- Cookie=chocolate, ch.p # 请求的 cookie 名为 chocolate,且其值与正则表达式 ch.p 相匹配
- Header=X-Request-Id, \d+ # 请求的头部中有 X-Request-Id,且其值与正则表达式 \d+ 相匹配
- Host=**.somehost.org # 以“·”作为分隔符的 Ant 风格的模式
- RemoteAddr=192.168.1.1/24 # 请求的远程地址,其中 192.168.1.1 是 IP 地址,24 是子网掩码,当没有子网掩码时,默认为 /32,使用的远程地址解析器 RemoteAddressResolver
- Method=GET
- Path=/foo/**, /foo2/**
- Query=foo, ba. # 请求包含一个 foo 查询参数,且值与 ba. 正则表达式匹配
# - Weight=group3, 1 # 所属权重分组 group3,设置权重为 1
# - Weight=group3, 9 # 所属权重分组 group3,设置权重为 9
filters: # XxxGatewayFilterFactory
- AddRequestParameter=foo, bar
- AddRequestHeader=X-Request-Foo, Bar
- AddResponseHeader=X-Response-Foo, Bar
- RemoveRequestHeader=X-Request-Foo # 删除请求头
- RemoveResponseHeader=X-Response-Foo # 删除响应头
- SetRequestHeader=X-Response-Foo, Bar # 替换原来请求头的值
- SetResponseHeader=X-Response-Foo, Bar # 替换原来响应头的值
- PreserveHostHeader # 设置路由过滤器将检查的请求属性,以确定是否应发送原始 host,而不是 http 客户端确定的 host
- Hystrix=myCommandName # 设置断路器
- RedirectTo=302, http://acme.org # 重定向
- RewritePath=/foo/(?<remaining>.*), /$\{remaining} # 对于 /foo/bar 的请求路径,将在生成下游请求之前设置路径为 /bar
- SetPath=/{segment} # 对于 /foo/bar 的请求路径,将在进行下游请求之前将路径设置为 /bar
- PrefixPath=/mypath # 请求 /hello 将被发送到 /mypath/hello,即在请求路径前加上自定义路径
- StripPrefix=2 # 设置在将请求发送到下游之前从请求中截掉的路径中的元素数量,如当请求 /name/bar/foo 时,对 nameservice 的请求将是 http://nameservice/foo
- SaveSession # 在转发下游调用之前强制执行 WebSession::save 操作,当使用类似于 Spring Session MongoDB 这种惰性数据存储时可使用这种操作,并且需要确保在转发之前会话的数据已经被存储
- SetStatus=401
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
在 yml 文档中
$
要写成$\
过滤器的执行顺序和配置定义的顺序有关
# 其它
# 权重算法
- 权重的算法实现过程:
- 首先构造 weights(group3)数组:weights=[1, 9]
- 规范化(Normalize):weights= weights/sum(weights) = [0.1, 0.9]
- 计算区间范围:ranges= weights .collect(0, (s, w) -> s + w) = [0, 0.1, 1.0]
- 生成随机数:r =random()
- 搜索随机数所在的区间:i = integer s.t. r>=ranges [i] && r <ranges [i+1]
- 选择相应的路由:routes[i]
# 限流算法
# 配置中心 Spring Cloud Config
- 支持的后端存储 :Git(默认)、SVN、本地文件系统、Vault、JDBC 等
# Spring Cloud Config 的使用
# 配置服务器
在 config-server 项目中添加依赖 spring-cloud-config-server(可同时添加服务发现依赖及相关配置)
添加配置
- spring-cloud-config-server 项目提供了 4 种配置,可以通过设置 spring.profiles.active 不同的值来激活
- git:默认值,表示去 Git 仓库读取配置文件
- subversion:表示去 SVN 仓库读取配置文件
- native:将去本地的文件系统中读取配置文件
- vault:去 Vault 中读取配置文件,Vault 是一款资源控制工具,可对资源实现安全访问
server.port: 8888
spring:
application.name: config-server
# 从 Git 仓库读取配置文件
profiles.active: git
cloud.config.server.git:
uri: https://github.com/abc/efg # 配置 Git 仓库的地址
search-paths: config-repo # 搜索仓库的文件夹地址,可以配置多个,用“,”分割
username: # Git 仓库的账号(公开的 Git 仓库不需要)
password: # Git 仓库的密码(公开的 Git 仓库不需要)
2
3
4
5
6
7
8
9
10
server.port: 8888
spring:
application.name: config-server
# 从本地读取配置文件
profiles.active: native
# 读取配置的路径为 classpath 下的 config 目录
cloud.config.server.native.search-locations: classpath:/config
2
3
4
5
6
7
在启动类中添加 @EnableConfigServer 注解,开启 Config Server
访问 http://localhost:8888/文件名,访问规则:{application} 表示微服务的名称,{label} 对应 Git 仓库的分支,默认是 master
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
# 配置客户端
在客户端项目中添加依赖 spring-cloud-starter-config
由于客户端要在引导程序中读取配置服务器的配置,配置文件名为 bootstrap.yml(或 .properties)
spring.cloud.config: uri: http://localhost:8888/ # 读取的是 master 分支 config-repo 目录下的 neo-config-dev.yml(或. properties) label: master name: neo-config # 默认值为 spring.application.name 的值 profile: dev fail-fast: true|retry
1
2
3
4
5
6
7可同时添加服务发现依赖及相关配置,发现配置中心,再添加配置
spring.cloud.config.discovery: enabled: true serviceId: config-server
1
2
3
# 刷新配置
版本控制系统的配置文件修改后,需要通知客户端来改变配置
使用 POST 访问客户端的 /refresh 端点进行刷新
客户端的 refresh 服务在接收到请求后,会重新到配置服务器获取最新的配置
此时,容器中使用了 @RefreshScope 注解进行修饰的 Bean 都会在缓存中进行销毁 ContextRefresher#refresh,当这些 Bean 被再次引用时,就会创建新的实例,以此达到一个“刷新”的效果
所有 Bean 被销毁后,会发布 RefreshScopeRefreshedEvent 事件,已知监听器:EurekaDiscoveryClientConfiguration.EurekaClientConfigurationRefresher
可以与 Spring Cloud Bus 整合实现集群全部配置的刷新:当版本控制系统的配置文件修改后,只需要使用 POST 访问某一个微服务实例的 /bus-refresh 端点,发布 RefreshRemoteApplicationEvent 事件,监听器将消息发送到消息组件(RabbitMQ 等),通过消息组件通知其他微服务实例重新拉取配置文件
# 配置的加密和解密
- Spring Cloud Config 为配置文件中的敏感数据提供了加密和解密的功能,加密后的密文在传输给客户端前会进行解密
- 配置服务器支持对称加密和非对称加密,对称加密使用 AES 算法,非对称加密使用 RSA 算法
为配置服务器安装 JCE(Java Cryptography Extension)
配置加密
- 对称加密,修改配置服务器(spring-config-server)的 application. yml 文件
encrypt.key: myKey # 设置加密和解密的 key
1 - 非对称加密
- 使用 keytool 工具生成一对密钥用于加密和解密:
keytool -genkeypair -alias "testKey" -keyalg "RSA" -keystore "D:\myTest.keystore"
(创建一个别名为 testKey 的证书,该证书存放在 D:\myTest. keystore 密钥库中) - 将生成的 myTest. keystore 复制到配置服务器 spring-configserver 的 classpath 下
- 修改配置服务器(spring-config-server)的 application. yml 文件
encrypt.keyStore: location: classpath:/myTest.keystore # keystore 位置 alias: testKey # 密钥对的别名 password: root # 密钥库的密码 secret: admin # 密钥口令
1
2
3
4
5 - 使用 keytool 工具生成一对密钥用于加密和解密:
- 对称加密,修改配置服务器(spring-config-server)的 application. yml 文件
加密和解密端点
- 访问的 /encrypt 端点会使用配置的 key 对明文进行加密并返回密文
- 使用 HTTP 的 POST 方法访问配置服务器 /encrypt 端点,该端点会使用配置的 key 对明文进行加密并返回密文
- 使用 HTTP 的 POST 方法访问配置服务器 /decrypt 端点,该端点会使用配置的 key 对密文进行解密并返回明文
存储加密数据
- 在版本控制系统的配置文件中,使用“{cipher}密文”的格式来保存加密后的数据
- yml 文件在配置时需要加单引号,properties 文件在配置时需要把单引号去掉,如
mysql.passwd: '{cipher}797e316ce5cldc9ce02d6eca929ea48c577efd2e379d79171d454a9b816818cd'`
1
# Spring Cloud 的配置抽象
在上下文中增加 Spring Cloud Config 的 PropertySource
通过 PropertySourceLocator 提供 PropertySource
配置中心会以如下的顺序给属性设值(优先级从高到低)
- 应用名-profile.{profile}.properties 和 YAML 变量
- 应用名.{profile}.properties 和 YAML 变量
- application-profile.{profile}.properties 和 YAML 变量
- application.{profile}.properties 和 YAML 变量
# 其它配置中心
- ZooKeeper (opens new window):spring-cloud-starter-zookeeper-config
- Consul:spring-cloud-starter-consul-config
- Nacos
- Apollo (opens new window)
# 消息微服务
# Spring Cloud Stream (opens new window)
- 一个用于构建消息驱动微服务的框架
- Spring Cloud Stream 在消息生产者和消息消费者之间加入了一个类似代理的角色,它直接与消息代理中间件进行交互,消息生产者与消息消费者不再需要直接调用各个消息代理框架的 API
# 开发消息微服务
在生产者、消费者项目中添加依赖 spring-cloud-starter-stream-rabbit 或 spring-cloud-starter-stream-kafka
在配置文件中添加 RabbitMQ 的连接信息
rabbitmq: host: localhost port: 5672 username: guest password: guest
1
2
3
4
5在生产者、消费者的启动类上添加 @EnableBinding(通道接口名.class),绑定通道
- 由于 Spring Cloud Stream 内置了 3 个接口 Source、Sink 与 Processor(Processor 接口继承于 Source 与 Sink),可以使用内置的 output 与 input 两个通道发送消息、接受消息
// 生产者 @Autowired Source source; / / 创建消息 Message msg = MessageBuilder.withPayload("Hello World".getBytes()).build(); / / 发送消息,调用 output 方法得到 SubscribableChannel(通道)实例,再调用 send 方法 source.output().send(msg);
1
2
3
4
5
6
7
8# 消费者 @StreamListener(Sink.INPUT) // 声明订阅 Sink.INPUT 通道的消息 public void receivelnput(byte[] msg) { System.out.println("receivelnput 方法接收到的消息:" + new String(msg)); }
1
2
3
4
5
# 服务链路追踪 Spring Cloud Sleuth
用于跟踪微服务的调用过程
Google Dapper 相关术语:
Span(跨度):基本的工作单元,表示一次调用的过程
Trace(跟踪):由一组共享“Root Span”的 Span 构成的树形结构,表示整个跟踪过程,从用户发起请求到最终的响应,一次跟踪包含多个跨度,这些跨度以树状结构进行保存
Annotation(标注):用于记录事件的存在,其中核心 Annotation 用来定义请求的开始和结束:Client Start、Server Start、Client Finish、Server Finish
Sleuth 可以与 Zipkin、Apache HTrace、Brave 和 ELK 等整合进行服务跟踪、数据分析
日志输出:[appname,traceId,spanId,exportable],如 [order-server,c323c72e7009c077,fba72d9c65745e60,false]
# Sleuth 整合服务跟踪系统 Zipkin
- 启动 Zipkin 服务器,下载地址 https://zipkin.io/
- 配置微服务,让各个微服务向 Zipkin 服务器报告过程数据
添加依赖 spring-cloud-starter-zipkin(已包含 spring-cloud-starter-sleuth),可同时添加服务发现依赖及相关配置,并设置
spring.zipkin.discoveryClientEnabled=true
默认使用 HTTP 埋点,如需通过 MQ 埋点,需增加 RabbitMQ 或 Kafka 依赖,并设置
spring.zipkin.sender.type=web | rabbit | kafka
添加配置
# 配置 Zipkin 服务器 spring: zipkin: baseUrl: http://localhost:9411 sender.type: web sleuth: sampler.probability: 0.1 # 跨度数据的采样百分比,默认值为 0.1 # 配置日志级别(可选) logging.level: root: INFO org.springframework.cloud.sleuth: DEBUG
1
2
3
4
5
6
7
8
9
10
11
- 访问 http://localhost:9411
# Sleuth 整合数据分析平台 ELK
- ELK 包括 Elasticsearch、Logstash、Kibana 这 3 个项目:
- Elasticsearch:一个分布式、RESTful 风格的搜索和数据分析引擎
- Logstash:主要用于数据收集、转换,并将数据发送到指定的存储库中
- Kibana:可视化的数据管理平台,主要用于操作 Elasticsearch 的数据,它提供了多种图表展示数据,支持动态报表
- 微服务所产生的日志数据会被 Logstash 读取,最终保存到 Elasticsearch 仓库进行保存
- 下载 ELK
- 运行 Elasticsearch
- 使用 Logstash 读取 JSON,分为 3 个阶段:输入(input)、过滤(filter)和输出(output)
- 使用 Kibana 展示数据
- 让微服务产生相应的 JSON 日志(使用 Logback 转换 JSON)
# 服务链路追踪
- 使用 Sleuth 收集的链路数据
- 使用 RabbitMQ 传输链路数据
- 使用 ElasticSearch 存储链路数据
- 使用 Kibana 展示链路数据
# 其它链路追踪
- Skywalking
- Pinpoint
- Cat
← 09 Kafka