SPRING CLOUD微服务DEMO-下篇
1 Hystix
1.1 简介
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
我感觉难以解释清楚,鉴于接下来的demo项目基本不会用这个模块,就过一遍代码算了。
1.2 配置并测试
模拟一下熔断:一旦请求的接口超过1s没有响应就不再继续请求,开始执行实现定义好的回滚函数,返回一个提示信息。
注意:Ribbon的重试机制和Hystrix的熔断机制有一定关系。
1.2.1 引入依赖
往user-consume即服务调用者的pom文件里面添加hystrix依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
1.2.2 开启熔断
在UserConsumeApplication中添加@EnableCircuitBreaker
可以看到注解有点多了,可以用@SpringCloudApplication
代替上面三个注解
//@EnableDiscoveryClient // 开启EurekaClient功能
//@EnableCircuitBreaker //开启熔断
//@SpringBootApplication
@SpringCloudApplication //三合一注解
1.2.3 改造consume
改造consume即消费者的调用方法,记录调用接口耗费的时间,编写调用超时的回滚方法。
@HystrixCommand(fallbackMethod="queryUserByIdFallback")
:声明一个失败回滚处理函数queryUserByIdFallback,当queryUserById执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。
@Component
public class UserDao {
@Autowired
private RestTemplate restTemplate;
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);
@HystrixCommand(fallbackMethod = "queryUserByIdFallback")
public User queryUserById(Long id){
long begin = System.currentTimeMillis();
String url = "http://user-service/user/" + id;
User user = this.restTemplate.getForObject(url, User.class);
long end = System.currentTimeMillis();
// 记录访问用时:
logger.info("访问用时:{}", end - begin);
return user;
}
public User queryUserByIdFallback(Long id){
User user = new User();
user.setId(id);
user.setUsername("用户信息查询出现异常!");
return user;
}
}
@Service
public class UserService {
@Autowired
private UserDao userDao;
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
ids.forEach(id -> {
// 我们测试多次查询,
users.add(this.userDao.queryUserById(id));
});
return users;
}
}
1.2.4 改造service
即改造服务提供者,将方法随机延迟一段时间,模拟超时。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) throws InterruptedException {
// 为了演示超时现象,我们在这里然线程休眠,时间随机 0~2000毫秒
Thread.sleep(new Random().nextInt(2000));
return this.userMapper.selectByPrimaryKey(id);
}
}
1.2.5 结果
很明显,超过1000ms的请求被回滚方法处理掉了。
1.2.6 设置Hystrix超时时间
之前我们设置的Robbin的ReadTimeout
是1000ms,即请求一个接口超过1000ms未响应就请求另一个有相同功能的接口而Hystrix的超时时间默认也是1000ms,观察现象可知,先触发了熔断。但这样是不合理的,明明可以请求另一个接口得到结果,结果触发了熔断,返回的是找不到结果。
所以注意一点:Hystrix的超时时间一定要大于Robbin的重试时间。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 设置hystrix的超时时间为6000ms
2. Feign
2.1 简介
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
2.2 使用Feign
Feign的引入和配置全部都在consume中进行
2.2.1 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2.2 UserConsumeApplication添加注解
@EnableFeignClients
2.2.3 编写UserClient接口
在项目中添加client包,下面新建UserClient接口
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
- 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称- 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
2.2.4 使用UserClient请求数据
改造UserService,改为使用UserClient请求数据
@Service
public class UserService {
//@Autowired
//private UserDao userDao;
@Autowired
private UserClient userClient; //注入UserClient
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
ids.forEach(id -> {
//使用UserClient请求接口
users.add(userClient.queryUserById(id));
//users.add(this.userDao.queryUserById(id));
});
return users;
}
}
2.3 负载均衡
Feign是对请求的封装,本身集成了Ribbon负载均衡。可以对其进行配置,和单独使用Robbin的写法一样
user-service:
ribbon:
ConnectTimeout: 250 # 连接超时时间(ms)
ReadTimeout: 1000 # 通信超时时间(ms)
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 1 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
2.4 Hystrix支持
Feign也集成了Hystrix。
需要配置以开启Hystrix
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
回滚方法不像之前那样写了,要专门定义一个类。
@Component
public class UserFeignClientFallback implements UserClient {
@Override
public User queryUserById(Long id) {
User user = new User();
user.setId(id);
user.setUsername("用户查询出现异常");
return user;
}
}
在UserClient接口上配置回滚类
@FeignClient(value = "user-service", fallback = UserFeignClientFallback.class)
public interface UserClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
2.5.请求压缩
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:
feign:
compression:
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩
同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下限
注:上面的数据类型、压缩大小下限均为默认值。
3. Zuul网关
3.1 简介
Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon和Hystrix等组件配合使用。核心是一系列的过滤器,完成身份认证与权限管理、动态路由、压力测试等功能。
工作流程如下所示
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
3.2 搭建
3.2.1 建一个新的module
其他步骤前面已经说过,选择模块时注意选择Zuul即可。
3.2.2 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3.2.3 添加注解
@SpringBootApplication
@EnableZuulProxy //开启zuul网关
@EnableDiscoveryClient //开启Eureka客户端发现功能
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
3.3 路由功能
测试之前先把之前添加的user-service的随机延迟给注释掉
Zuul的路由功能:
- 配置
server:
port: 10010 #服务端口
spring:
application:
name: api-gateway #指定服务名
eureka:
client:
registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
zuul:
prefix: /api # 添加路由前缀
routes:
user-service: # 路由id,一般与服务名相同
path: /user-service/** # 映射路径
serviceId: user-service
-
解释:Zuul从Eureka中拉取服务列表,如果有人请求
/api/user-service/**
,将请求转发给服务列表中的user-service
服务。
坑爹的一点:之前测试的时候Zuul一直报找不到user-service服务的错误,找了一段时间后来自己好了。
没有再复现出来,不知道为啥。如果报错了,可以尝试着用maven的reimport,再等等试试。
- 由于路由id一般与服务名相同,因此zuul提供了一种简化配置,与上面实现相同功能
zuul:
prefix: /api # 添加路由前缀
routes:
user-service: /user-service/** # 这里是映射路径
访问http://localhost:10010/api/user-service/user/20即可得到结果。
3.4 过滤功能
3.4.1 ZuulFilter
ZuulFilter是过滤器的顶级父类,我们自己实现的过滤器都要继承自它。我们看看其中重要的四个方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();
Object run() throws ZuulException;
}
- shouldFilter:返回布尔值,判断过滤器是否需要执行。true表示执行,false反之。
- run:表示具体的过滤逻辑。
- filterType:返回字符串,表示本过滤器的类型
- pre:请求在被路由之前执行。
- routing:在路由请求时调用
- post:在routing和errror过滤器之后调用
- error:处理请求时发生错误调用
- filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
3.4.2 生命周期
- 正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
- 异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给post过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是post过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达post过滤器了。
3.4.3 使用场景
- 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在error类型和post类型过滤器中结合来处理。
- 服务调用时长统计:pre和post结合使用。
3.4.4 模拟登录校验
添加登录过滤器
@Component
public class LoginFilter extends ZuulFilter {
@Override
public String filterType() {
// 登录校验,肯定是在前置拦截
return "pre";
}
@Override
public int filterOrder() {
// 顺序设置为1
return 1;
}
@Override
public boolean shouldFilter() {
// 返回true,代表过滤器生效。
return true;
}
@Override
public Object run() {
// 登录校验逻辑。
// 1)获取Zuul提供的请求上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
// 2) 从上下文中获取request对象
HttpServletRequest req = ctx.getRequest();
// 3) 从请求中获取token
String token = req.getParameter("access-token");
// 4) 判断
if(token == null || "".equals(token.trim())){
// 没有token,登录校验失败,拦截
ctx.setSendZuulResponse(false);
// 返回401状态码。也可以考虑重定向到登录页。
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
// 校验通过,可以考虑把用户信息放入上下文,继续向后执行
return null;
}
}
- 访问http://localhost:10010/api/user-service/user/20,提示401未授权
- 访问http://localhost:10010/api/user-service/user/20?access-token=123 ,能正常访问
3.5 负载均衡和熔断功能
Zuul内部集成了Ribbon负载均衡和Hystrix熔断器,需要配置
zuul:
retryable: true
ribbon:
ConnectTimeout: 250 # 连接超时时间(ms)
ReadTimeout: 2000 # 通信超时时间(ms)
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔断超时时长:6000ms
总结
一直在想着如何配置,如何实现,容易犯一叶障目不见泰山的毛病,下面就概括一下。
Eureka 注册中心
服务提供者在里面注册,服务消费者拉取服务列表,根据服务列表去发请求并获取数据。
这其中涉及到最重要的一个问题:如何保证服务列表里面的服务是有效的?
- 对于提供者,这个问题是:你怎么才能知道我什么时候可用,什么时候不可用?
Eureka和提供者就形成了约定:提供者准备好后在Eureka里面注册,Eureka知道提供者可用了。注册后提供者每隔一段时间向Eureka发个消息说我还活着,Eureka就知道提供者依旧可用。Eureka每隔一段时间就会扫描整个服务列表,把很长时间没报告过的服务剔除出去。这样注册中心的服务列表就能始终更新,始终可用。
- 对于消费者,这个问题是:我怎么知道你不行了?
消费者每隔一段时间就从注册中心拉取服务列表,注册中心和服务提供者的约定保证了这个列表可以信任,我就根据这个列表发出请求,得到数据。
注意上面的三个时间段,这都是可以配置的,怎么配置就根据实际情况来了。
Robbin 负载均衡
服务的消费者在拉取服务列表后,针对同一业务可能有多个可选的服务提供者。
如果一直使用其中一个,那就是传说中的"一核有难,八核围观",表现差劲,浪费资源,这就需要负载均衡了。
Ribbon提供了诸如随机、轮询等多种策略可选。
它还提供了重试机制:选择了一个服务后,如果这个服务坏了(毕竟每隔一段时间才拉取服务列表,服务列表也是每隔一段时间才更新),或者说反应太慢,到了一定时间就考虑再换一个服务。
Hystrix 熔断机制
为了避免一个模块的错误拖垮整个系统,因此将其隔离起来。
不要让一颗老鼠屎坏了一锅汤?这个机制我不太能讲清楚。
但是注意它和Ribbon的联系,一个服务没有反应,应该先用Ribbon进行重试,而不是先熔断。然后这又引出来一个问题,一个错误如果每次都被重试机制掩盖了,那系统还会被拖垮吗?还是说这个业务的服务都坏了,试来试去找不到一个好用的,才触发熔断?想不清楚,真遇到了再说吧。
Feign 请求封装
封装了RestTemplate,让请求的编码更简单。
请求必然涉及负载均衡和熔断问题,所以Feign里面也可以配置Robbin和Hystrix。
但不光如此,Feign还提供了请求压缩等小功能。
Zuul 网关
微服务提供者的接口是暴露在外的,要是谁都能用那就乱套了。
就像学校的器材室有各种器材,但是谁都能自由取用就乱了。因此我们需要一个管理器材的老师,拿器材的人来了,先判断他有没有老师的批准,再根据老师的批准决定他能拿什么器材,最后把他带到对应的器材室拿相应的器材。
Zuul就像是这个老师,负责鉴权(判断一个人能不能拿器材,能拿什么器材)、路由(领到地方拿器材)。
Zuul虽然不发请求,但是对请求有很强的干预能力,所以它也可以配置负载均衡和熔断。