一.客户端负载均衡---OpenFeign
1.什么是Feign?
Feign是一个声明式的http客户端,使用Feign可以实现声明式REST调用,它的目的就是让Web Service调用更加简单。Feign整合了Ribbon和SpringMvc注解,这让Feign的客户端接口看起来就像一个Controller。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息
。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。同时Feign整合了Hystrix,可以很容易的实现服务熔断和降级。
2.Feign编码实战
创建子工程pay模块
导入依赖
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xl</groupId>
<artifactId>springcloud-netflix-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>service-pay</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--1.导入EurekaClient的包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--2.导入Feign的包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--pojo-common-->
<dependency>
<groupId>com.xl</groupId>
<artifactId>common-pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
配置类加上注解
package com.xl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // 扫描当前包以及子包下所有包含FeginClient注解的类
public class PayApp {
public static void main(String[] args) {
SpringApplication.run(PayApp.class);
}
}
yml配置
server:
port: 1400
eureka:
instance:
hostname: localhost
prefer-ip-address: true
instance-id: service-pay:1400
client:
serviceUrl:
defaultZone: http://localhost:1000/eureka/
registry-fetch-interval-seconds: 5 #抓取服务时间为五秒
spring:
application:
name: service-pay #给应用实例加上名字
编写Feign客户端接口
Feign的客户端接口是用来调用微服务的,我这里就编写了一个用来调用用户服务的客户端接口,如下:
package com.xl.feignclients;
import com.xl.domain.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "service-user")
public interface UserServiceFeignClient {
/**
* 请求地址要和被调方保持一致
* 参数要和被调方保持一致
* 返回值要和被调方保持一致
* 请求方式要和被调方保持一致
* 方法名字可以不用一致
* @param id
* @return
*/
@GetMapping("/getUserById/{id}")
User getUser(@PathVariable("id")Long id);
}
解释
@FeignClient("service-user") : service-user是用户服务的服务名字,Feign根据服务名能够在注册中心找到目标服务的通信地址
你会发现接口中的方法跟用户服务(springcloud-user-server-1200)中的方法一样,其实Feign就是通过客户端接口里面的方法,来决定目标服务的资源路径url,参数以及返回值,这里我们可以直接把要调用的目标服务的controller方法拷贝过来,然后去掉方法体即可。
Feign可以根据@FeignClient("service-user")找到用户服务,根据方法上的 @GetMapping("/getUserById/{id}")找到目标服务的controller的方法 ,我们在使用Feign接口时传入的参数就会作为目标服务controller方法的参数,而返回值就是目标服务controller方法的返回值。
编写controller使用Feign接口
通过注入UserFeignClient ,直接发起调用
package com.xl.controller;
import com.xl.domain.User;
import com.xl.feignclients.UserServiceFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PayController {
@Autowired
private UserServiceFeignClient userFeignClient; // 注入要调用的接口
@GetMapping("/getUserById/{id}")
public User getUserById(@PathVariable("id")Long id){
return userFeignClient.getUser(id);
}
}
这里调用UserServiceFeignClient.getUserById方法,看起来是像在调用本地方法,其实该接口已经被Feign代理,我们发起调用时其实在像Feign接口配置的目标服务发起调用。
测试
依次启动注册中心springcloud-eureka-server-1000,两个用户服务springcloud-user-server-1200和1300 , 启动支付服务 springcloud-pay-server-1400 , 通过浏览器访问pay-server的controller:http://localhost:1400/getUserById/1,多次请求发现依然默认使用了轮询即可
二:Hystrix熔断器
1.Hystrix介绍
Hystrix是国外知名的视频网站Netflix所开源的非常流行的高可用架构框架。Hystrix能够完美的解决分布式系统架构中打造高可用服务面临的一系列技术难题,如雪崩。
Hystrix是处理依赖隔离的框架,将出现故障的服务通过熔断、降级等手段隔离开来
,这样不影响整个系统的主业务(比如你得了传染病是不是要把你关起来隔离呢),同时也是可以帮我们做服务的治理和监控
。
Hystrix的英文是豪猪,中文翻译为 熔断器,其思想来源于我们家里的保险开关,当家里出现短路,保险开关及时切掉电路,保证家里人员的安全,其目的就是起保护作用。
Hystrix其设计原则如下:
-
防止单个服务异常导致整个微服务故障。
-
快速失败,如果服务出现故障,服务的请求快速失败,线程不会等待。
-
服务降级,请求故障可以返回设定好的二手方案数据(兜底数据)。
-
熔断机制,防止故障的扩散,导致整个服务瘫痪。
-
服务监控,提供了Hystrix Bashboard仪表盘,实时监控熔断器状态
2.Hystrix的功能
资源隔离
资源隔离包括线程池隔离和信号量隔离
,作用是限制调用分布式服务的资源使用
,某一个调用的服务出现问题不会影响其他服务调用 ,这里可以简单的理解为资源隔离就是限制请求的数量
。
就好比在肺炎疫情爆发期间,是不是要限制人口的流动量,流动量越大可能会导致更多的肺炎患者出现。
线程池隔离:使用一个线程池来存储当前请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求先入线程池队列。这种方式要为每个依赖服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)
信号量隔离:使用一个原子计数器(或信号量)记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃该类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)
服务熔断
熔断机制是对服务链路的保护机制,如果链路上的某个服务不可访问,调用超时,发生异常等,服务会触发降级返回托底数据,然后熔断服务的调用(失败率达到某个阀值服务标记为短路状态),当检查到该节点能正常使用时服务会快速恢复。
简单理解就是当服务不可访问了或者服务器报错了或者服务调用超过一定时间没返回结果,就立马触发熔断机制配合降级返回预先准备的兜底数据返回,不至于长时间的等待服务的相应造成大量的请求阻塞,也不至于返回一些错误信息给客户端,而是返回一些兜底数据。
降级机制
超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。
简单理解就是服务降级就是当服务因为网络故障,服务器故障,读取超时等原因造成服务不可达的情况下返回一些预先准备好的数据给客户端。
在生活中服务降级到处可见,如在系统故障我们会返回友好的提示“服务暂时不可用”,或者如淘宝双11期间退款服务,留言服务暂时不可用,这个就是为了保证正常的购物流程相关服务没问题,然后人为的关闭一些不重要的服务,配合降级返回一些托底数据返回给用户(比如返回友好的提示信息“暂时不能退款”)。
缓存
提供了请求缓存、请求合并实现 , 在高并发的场景之下,Hystrix请求缓存可以方便地开启和使用请求缓存来优化系统,达到减轻高并发时请求线程的消耗、降低请求响应时间的效果。
3.Hystrix工作机制
正常情况下,断路器处于关闭状态(Closed),如果调用持续出错或者超时达到设定阈值,电路被打开进入熔断状态(Open),这时请求这个服务会触发快速失败(立马返回兜底数据不要让线程死等),后续一段时间内的所有调用都会被拒绝(Fail Fast),一段时间以后(withCircuitBreakerSleepWindowInMilliseconds=5s),保护器会尝试进入半熔断状态(Half-Open),允许少量请求进来尝试,如果调用仍然失败,则回到熔断状态,如果调用成功,则回到电路闭合状态;
4.Hystrix编码实战(配合Ribbon版)
导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
主配置类开启Hystrix
package com.xl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableCircuitBreaker // 开启Hystrix熔断
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
方法熔断
通过 @HystrixCommand 标签标记方法熔断,标签的fallbackMethod属性指定拖地方法。那么当该方法在像远程服务发起调用出现异常,或是方法本身出现异常都会触发托底方法的执行,最终结果是托底方法的返回结果。
package com.xl.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.xl.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/getUserById/{id}")
@HystrixCommand(fallbackMethod = "getUserFallBack")
public User getUser(@PathVariable("id")Long id){
String url = "http://SERVICE-USER/getUserById/1";
return restTemplate.getForObject(url, User.class);
}
//降级方法 , 参数和返回值必须和被熔断的方法一致 ,方法名要和 fallbackMethod 的值一致
public User getUserFallBack(@PathVariable("id")Long id){
return new User(-1L,"服务不可达,请稍后再试",-1);
}
}
测试熔断
依次启动:springcloud-service-eureka-1000 , springcloud-service-user-1200和1300, springcloud-service-order-1100
浏览器访问 http://localhost:1100/order/1getUserByIdhttp://localhost:1100/order/1 ,当用户服务 springcloud-service-user-1200 正常启动的时候,订单服务是可以访问,浏览器也可以收到结果 , 当关闭掉用户服务 ,订单服务会触发熔断,返回托底数据
5.OpenFeign使用Hystrix
官方文档:Spring Cloud
上面有个支付模块集成了Feign,修改该工程直接使用Hystrix
开启Hystrix
跟Ribbon不一样的是Ribbon项目是需要导入依赖才能使用Hystrix,而OpenFeign集成Feign的时候jar包里包含了Hystrix所以不需要另外倒入jar包来引入Hystrix,直接在yml里面开启即可
feign:
hystrix:
enabled: true #开启熔断支持
Fiegn接口熔断-fallbackFactory方式
服务通过Feign接口调用异常或超时需要触发降级,返回托底数据。这里有两种方式,分别是通过@FeignClient(fallback=..) ,以及@FeignClient(fallbackFactory=..) 来指定托底类,区别在于通过fallback的方式编写的托底是没办法打印出异常日志的 ,而fallbackFactory方式是可以打印出异常日志, 我们直接使用fallbackFactory方式:
package com.xl.feignclients;
import com.xl.domain.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "service-user",fallbackFactory = UserFeignFallBackFactory.class)
public interface UserServiceFeignClient {
/**
* 请求地址要和被调方保持一致
* 参数要和被调方保持一致
* 返回值要和被调方保持一致
* 请求方式要和被调方保持一致
* 方法名字可以不用一致
* @param id
* @return
*/
@GetMapping("/getUserById/{id}")
User getUser(@PathVariable("id")Long id);
}
使用fallbackFactory属性,使用工厂方式指定托底
编写托底类
工程方式的托底类需要去实现 FallbackFactory接口 ,并指定泛型为“”Feign客户端接口(UserFeignClient )。FallbackFactory的create方法返回了Feign客户端接口的实例,该方法的throwable是参数是Feign调用失败的异常信息,如下:
package com.xl.feignclients;
import com.xl.domain.User;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class UserFeignFallBackFactory implements FallbackFactory<UserServiceFeignClient> {
@Override
public UserServiceFeignClient create(Throwable throwable) {
throwable.printStackTrace();
return new UserServiceFeignClient() {
@Override
public User getUser(Long id) {
return new User(-1L,"该服务不可达,我们正在殴打程序员,请稍等",-1);
}
};
}
}
启动测试
测试方式同上 ,只是这种方式触发托底是可以在控制台看到异常信息,方便我们调试。
三:服务网关-spring cloud zuul
1.什么是zuul
Zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet(filter)应用。Zuul 在云平台上提供动态路由(请求分发),监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门,也要注册入Eureka,用一张图来理解zuul在架构中的的角色:
需要注意的是,zuul本身是一个独立的服务,默认集成了Ribbon,zuul通过Ribbon将客户端的请求分发到下游的微服务,所以zuul需要通过Eureka做服务发行,同时zuul也集成了Hystrix。
根据上图理解 ,我们需要建立独立的工程去搭建Zuul服务,同时需要把Zuul注册到EurekaServer,因为当请求过来时,zuul需要通过EurekaServer获取下游的微服务通信地址,使用Ribbon发起调用。
2.zuul的搭建
搭建模块springcloud-service-zuul-1500
修改pringcloud-service-zuul-1500,集成EurekaClient和zuul
3.导入依赖
因为Zuul需要通过Eureak做服务发现,所以我们导入了eureka-client基础依赖,和Zuul自身的基础依赖:netflix-zuul
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xl</groupId>
<artifactId>springcloud-netflix-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>service-zuul</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
</project>
4.配置开启Zuul
配置类通过 @EnableZuulProxy 注解开启zuul服务功能。
package com.xl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* 用户的启动类
* @EnableEurekaClient: 标记该应用是 Eureka客户端
* @EnableZuulProxy : 开启zuul 可以看做是 @EnableZuulServer 的增强版 ,一般用这个
* @EnableZuulServer : 这个标签也可以开启zuul,但是这个标签开启的Filter更少
*/
@SpringBootApplication
@EnableZuulProxy
public class ZuulApp {
public static void main(String[] args) {
SpringApplication.run(ZuulApp.class);
}
}
5.application.yml配置文件配置zuul
这里配置两个东西,一个是EurekaClien的配置,让zuul注册到EurekaServer,二个就是zuul的配置了
server:
port: 1500
#注册到EurekaServer
eureka:
instance:
hostname: localhost
prefer-ip-address: true
instance-id: service-zuul:1500
client:
serviceUrl:
defaultZone: http://localhost:1000/eureka/
spring:
application:
name: service-zuul #给应用实例加上名字
zuul:
prefix: "/services" #统一访问前缀
ignoredServices: "*" #禁用掉使用浏览器通过服务名的方式访问服务
routes:
service-pay: "/pay/**" #指定pay-server这个服务使用 /pay路径来访问 - 别名
service-order: "/order/**" #指定order-server这个服务使用 /order路径来访问
service-user: "/user/**" #指定order-server这个服务使用 /order路径来访问
6.测试zuul
测试如上同理