[toc]

1 Hystix

1.1 简介

Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。

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的请求被回滚方法处理掉了。

SPRING CLOUD微服务DEMO-下篇-LMLPHP

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等组件配合使用。核心是一系列的过滤器,完成身份认证与权限管理动态路由、压力测试等功能。

工作流程如下所示

SPRING CLOUD微服务DEMO-下篇-LMLPHP

不管是来自于客户端(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 路由功能

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服务。

  • 由于路由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;
    }
}

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知道提供者可用了。注册后提供者<u>每隔一段时间</u>向Eureka发个消息说我还活着,Eureka就知道提供者依旧可用。Eureka<u>每隔一段时间</u>就会扫描整个服务列表,把很长时间没报告过的服务剔除出去。这样注册中心的服务列表就能始终更新,始终可用。

  • 对于消费者,这个问题是:我怎么知道你不行了?

消费者<u>每隔一段时间</u>就从注册中心拉取服务列表,注册中心和服务提供者的约定保证了这个列表可以信任,我就根据这个列表发出请求,得到数据。

注意上面的三个时间段,这都是可以配置的,怎么配置就根据实际情况来了。

Robbin 负载均衡

服务的消费者在拉取服务列表后,针对同一业务可能有多个可选的服务提供者。

如果一直使用其中一个,那就是传说中的"一核有难,八核围观",表现差劲,浪费资源,这就需要负载均衡了。

Ribbon提供了诸如随机、轮询等多种策略可选。

它还提供了重试机制:选择了一个服务后,如果这个服务坏了(毕竟每隔一段时间才拉取服务列表,服务列表也是每隔一段时间才更新),或者说反应太慢,到了一定时间就考虑再换一个服务。

Hystrix 熔断机制

为了避免一个模块的错误拖垮整个系统,因此将其隔离起来。

不要让一颗老鼠屎坏了一锅汤?这个机制我不太能讲清楚。

但是注意它和Ribbon的联系,一个服务没有反应,应该先用Ribbon进行重试,而不是先熔断。然后这又引出来一个问题,一个错误如果每次都被重试机制掩盖了,那系统还会被拖垮吗?还是说这个业务的服务都坏了,试来试去找不到一个好用的,才触发熔断?想不清楚,真遇到了再说吧。

Feign 请求封装

封装了RestTemplate,让请求的编码更简单。

请求必然涉及负载均衡和熔断问题,所以Feign里面也可以配置Robbin和Hystrix。

但不光如此,Feign还提供了请求压缩等小功能。

Zuul 网关

微服务提供者的接口是暴露在外的,要是谁都能用那就乱套了。

就像学校的器材室有各种器材,但是谁都能自由取用就乱了。因此我们需要一个管理器材的老师,拿器材的人来了,先判断他有没有老师的批准,再根据老师的批准决定他能拿什么器材,最后把他带到对应的器材室拿相应的器材。

Zuul就像是这个老师,负责鉴权(判断一个人能不能拿器材,能拿什么器材)、路由(领到地方拿器材)。

Zuul虽然不发请求,但是对请求有很强的干预能力,所以它也可以配置负载均衡和熔断。

03-14 04:46