springCloud — 中级篇(1)

本系列笔记涉及到的代码GitHub上,地址:https://github.com/zsllsz/cloud

本文涉及知识点:

  • 服务降级熔断之hystrix;

  • 服务网关之gateway;


欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


一、服务降级&熔断&限流之Hystrix(豪猪)

1、微服务面临的问题:
将一个个的业务拆分出来,独立成一个个服务,降低了系统的耦合度,但是也面临了一些问题。比如实现业务场景一需要调用服务A,A又要调用B,B还要调用C……,这叫做“扇出”,就是各个微服务相互调用,像一把折扇一样。如果B挂了,那就导致整条链路不能用了,就出现了服务雪崩的情况。为了解决这种问题,就需要一种名为“服务降级和熔断”的办法。

2、Hystrix是什么?
Hystrix就是服务降级和熔断的一种落地实现。可以保证当某个服务超时、异常的情况下,不会导致整体服务雪崩,避免级联故障,提高分布式系统的弹性。它会在当某个服务超时、异常的时候,返回一个符合预期格式、可处理的备选响应给调用者,从而保证了服务的可用。

3、Hystrix能干嘛:

  • 服务降级:fallback,比如我们写代码的时候,有下面这种情况:
if () {

} else if () {

} else {

}

这里最后的else就是服务降级,就是给一个兜底的解决方案。 会发生服务降级的情况:程序异常、响应超时、服务熔断触发降级、线程池/信号量打满也会导致服务降级。

  • 服务熔断:break,就像保险丝熔断一样,当服务达到了所能承受的最大访问量后,就拉闸断电,调用服务降级给用户返回友好的提示。

  • 服务限流:flowlimit,在秒杀等高并发的场景下,严禁一窝蜂地访问系统,而是排队访问,一秒钟N个,有序进行。

4、cloud-provider-hystrix-payment8001项目搭建:
新建一个名为cloud-provider-hystrix-payment8001的module。

  • pom.xml:

    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-devtools
    runtime
    true


    org.projectlombok
    lombok
    true


    org.springframework.boot
    spring-boot-starter-actuator 


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client



    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix

  • application.yml:
server:
  port: 8001
  
spring:
  application:
    name: cloud-provider-hystrix-payment
    
eureka:
  client:
    register-with-eureka: true 
    fetch-registry: true 
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka #eureka server
  • 启动类:
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}
  • service:
@Service
public class PaymentService {

    public String paymentOk(Integer id) {
        return " thats ok";
    }
    
    public String paymentError(Integer id) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "somethind wrong";
    }
}

这里一个方法模拟正常情况,一个方法模拟超时的情况。然后在controller中调这两个方法。

  • controller:
@RestController
@RequestMapping("/payment")
public class PaymentController {

    @Autowired
    private PaymentService paymentService;
    @Value("${server.port}")
    private String port;
    
    @GetMapping("/ok/{id}")
    public String paymentOk(@PathVariable("id") Integer id) {
        return paymentService.paymentOk(id) + "/r/n" + port;
    }
    
    @GetMapping("/error/{id}")
    public String paymentError(@PathVariable("id") Integer id) {
        return paymentService.paymentError(id) + "/r/n" + port;
    }
}

启动7001的eureka server和这个8001,访问从controller中的两个接口,测试一下。在非高并发的情况下,两个接口都可以正常返回数据,只不过paymentOk可以立即响应,而paymentError就要等待5秒才会响应。但是,如果搞个jmeter对paymentError接口进行压测,20000个线程并发去请求paymentError,然后你再用浏览器访问paymentOk,发现paymentOk也被拖慢了。如果线程再多一点儿,可能就会访问不了了。可明明是paymentError接口响应慢,paymentOk是不应该受影响的。所以可以用hystrix对服务进行降级。下面先新建一个consumer调用这个payment服务,然后再在这个consumer中对服务进行降级。

5、cloud-consumer-feign-hystrix-order80项目搭建:
新建名为cloud-consumer-feign-hystrix-order80的module。

  • pom.xml:

    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-devtools
    runtime
    true


    org.projectlombok
    lombok
    true


    org.springframework.boot
    spring-boot-starter-actuator 


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client



    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix



    org.springframework.cloud
    spring-cloud-starter-openfeign

  • yml:
server:
  port: 80
  
eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka
  • 主启动类:
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(OrderHystrixMain80.class, args);
    }
}
  • service:
@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
    
    @GetMapping("payment/ok/{id}")
    public String paymentOk(@PathVariable("id") Integer id);
    
    @GetMapping("payment/error/{id}")
    public String paymentError(@PathVariable("id") Integer id);
}
  • controller:
@RestController
@RequestMapping("/order")
public class OrderHystrixController {

    @Autowired
    private PaymentHystrixService phService;
    
    @GetMapping("/hystrix/ok/{id}")
    public String paymentOk(@PathVariable("id") Integer id) {
        return phService.paymentOk(id);
    }
    
    @GetMapping("/hystrix/error/{id}")
    public String paymentError(@PathVariable("id") Integer id) {
        return phService.paymentError(id);
    }
}

启动后,通过浏览器访问http://localhost/order/hystrix/ok/1,发现是立即响应的,访问http://localhost/order/hystrix/error/1发现报500了,控制台报错 Read timed out。这是因为openfeign默认超时时间是1秒,而error接口又设置了线程睡5秒。可以在order80的配置文件中配置openfeign超时时间,设置大于5秒,就可以正常访问。

ribbon:
  # 建立连接后从服务器获取可用资源的时间
  ReadTimeout: 6000
  # 建立连接所有的时间
  ConnectTimeout: 6000

这是没有并发的情况,再使用jmeter进行压测,那么这两个接口都得凉凉了。

6、如何解决上面的问题?
服务提供方payment8001:

  • 如果8001超时,不能让order80一直转圈圈等待,要进行payment8001服务降级
  • 如果8001挂掉了,也不能让order80一直等待,要进行payment8001服务降级

服务调用方order80:

  • 如果payment8001没问题,order80自己出故障了,那么order80要进行服务降级

7、payment8001服务降级配置:
payment8001的paymentError方法,线程睡了5秒,所以至少5秒才会有响应。假设我们认为这个方法正常是3秒就能响应完的,超过3秒就要进行服务降级。那么我们就设置超时时间峰值为3秒,超过了3秒就要有兜底的方法。

  • 首先在启动类上加上@EnableCircuitBreaker注解激活hystrix功能;
  • 然后在paymentError方法上加上@HystrixCommand注解,并且配置超时调用的方法,如下:
@HystrixCommand(fallbackMethod = "paymentError_default", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    })
    public String paymentError(Integer id) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "somethind wrong";
    }
    
    public String paymentError_default(Integer id) {
        return "这是兜底的方法";
    }

这段代码的意思就是,paymentError方法至少要5秒才会响应,但是我加上了hystrix的注解,设置了最大响应时长为3秒,超过3秒,那就走兜底的方法。所以现在启动payment8001访问paymentError方法,应该会打出"这是兜底的方法"这句话。

hystrix服务降级

说明hystrix配置成功了,并且除了超时,如果paymentError报异常了,比如写一个int x = 10 / 0,也会走兜底的方法。

8、消费端order80的服务降级:
消费端服务降级也一样的配置,在主启动类上加@EnableCircuitBreaker注解,然后controller写成下面这样:

@GetMapping("/hystrix/error/{id}")
@HystrixCommand(fallbackMethod = "paymentError_default", commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentError(@PathVariable("id") Integer id) {
    return phService.paymentError(id);
}
    
public String paymentError_default(@PathVariable("id")Integer id) {
    return "这是消费端兜底的方法";
}

配置超过3秒就走兜底的方法。而服务端payment8001对应的方法设置了线程睡5秒,超过了3秒才会有响应,所以这里会走兜底的方法。注意ribbon默认超时时间是1秒,所以如果修改ribbon超时时间的话,即使payment8001中设置线程睡2秒,order80中hystrix配置超时时间3秒,也会走兜底的方法,因为是否超时优先取的是ribbon配置的。

9、全局降级配置:
上面针对需要服务降级的两个方法加上了hystrix相关配置,如果还有大量的方法也需要服务降级,在每个方法上都加上这么一段注解,不太方便,所以就出现了全局降级配置。比如要在order80上做全局服务降级配置,只需要将controller改成下面这样即可:

@RestController
@RequestMapping("/order")
@DefaultProperties(defaultFallback = "defaultMethod", commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public class OrderHystrixController {

    @Autowired
    private PaymentHystrixService phService;
    
    @GetMapping("/hystrix/ok/{id}")
    @HystrixCommand
    public String paymentOk(@PathVariable("id") Integer id) {
        return phService.paymentOk(id);
    }
    
    @GetMapping("/hystrix/error/{id}")
    @HystrixCommand(fallbackMethod = "paymentError_default", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    })
    public String paymentError(@PathVariable("id") Integer id) {
        return phService.paymentError(id);
    }
    
    public String paymentError_default(@PathVariable("id")Integer id) {
        return "这是消费端兜底的方法";
    }
    
    public String defaultMethod() {
        return "我是全局默认的服务降级配置";
    }
}

首先在类上加注解,并配置超时时间,然后需要用全局降级的方法上加@HystrixCommand注解。这里用paymentOk方法来演示,payment8001端的paymentOk方法也设置线程睡4秒钟,paymentOk方法应该会打印出“我是全局默认的服务降级配置”这句话。

全局降级配置

然后将payment8001的paymentOk方法改成睡2秒钟,那么就会正常返回:

正常访问

然后paymentError方法,因为在方法上配置了hystrix相关配置,所以还是走方法对应的配置,而不是走全局。

但是以上做法还是不够完美,因为order80调用payment8001,所有接口都是通过openfeign去调用的,擒贼先擒王,所以最好的做法就是在openfeign调用payment8001的接口中添加实现去做降级。具体做法如下:

  • 修改yml:
feign:
  hystrix:
    enabled: true
  • order80的service改成这样:
@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentHystrixServiceImpl.class)
public interface PaymentHystrixService {
    
    @GetMapping("payment/ok/{id}")
    public String paymentOk(@PathVariable("id") Integer id);
    
    @GetMapping("payment/error/{id}")
    public String paymentError(@PathVariable("id") Integer id);
}
  • 新建service的实现:
@Component
public class PaymentHystrixServiceImpl implements PaymentHystrixService{

    @Override
    public String paymentOk(Integer id) {
        return "paymentOk-default-return";
    }

    @Override
    public String paymentError(Integer id) {
        return "paymentError-default-return";
    }

}
  • 最后把controller中hystrix相关注解都去掉。

这样就搞定了,当访问8001发生异常、或者8001压根儿就没启动时,就会执行serviceImpl里面的方法。

10、服务熔断:
当微服务调用链路的某个服务不可用了或者响应时间太长了,会对服务进行降级,快速返回友好的响应信息,当检测到该节点正常了之后,又会恢复调用。即一开始是close状态,检测到异常就变成open状态,然后再变成half open状态,恢复一部分调用,如果没问题再变成close。下面就演示服务熔断。

  • 首先在payment8001的service中加如下代码
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), // 失败率达到多少后跳闸
            // 10次请求有6次是失败的,就进行服务熔断,10秒后会变为half open状态,看看能否请求成功,如果成功就变成close状态
    })
    public String paymentCircuitBreaker(Integer id) {
        if (id < 0) {
            throw new RuntimeException("id不能为负数");
        }
        return "调用成功";
    }
    
    public String paymentCircuitBreaker_fallback(Integer id) {
        return "id不能为负数";
    }

这几个注解的意思就是在10次访问中,如果失败了6次,那就熔断,返回“id不能为负数这句话”,这个时候,即使你传的id是正数,也会返回这句话,因为这个时候服务熔断了。10000 milliseconds后,会变成半开状态,试着放行一个请求,如果此时你请求的id是正数,那么就返回“调用成功”,并且解除熔断。

  • 在controller中新增如下方法:
@GetMapping("/breaker/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
        return paymentService.paymentCircuitBreaker(id);
    }

可以访问该接口,先传入负数id,然后满足10次有6次失败的情况下,再将id变为正数传入,发现也是返回“id不能为负数”这句话。过一段时间后才恢复正常,说明服务熔断起作用了。

11、hystrix的图形化界面:

  • 新建一个cloud-consumer-hystrix-dashboard9001的module
  • pom.xml:

    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-devtools
    runtime
    true


    org.projectlombok
    lombok
    true


    org.springframework.boot
    spring-boot-starter-actuator 



    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix-dashboard

  • 主启动类:
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashBoardMain9001 {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(HystrixDashBoardMain9001.class, args);
    }
}
  • 如果要监控8001,那么需要在8001中配置如下bean:
@Configuration
public class DashBoardConfig {

    @Bean
    public ServletRegistrationBean getBean() {
        HystrixMetricsStreamServlet bean = new HystrixMetricsStreamServlet();
        ServletRegistrationBean register = new ServletRegistrationBean(bean);
        register.setLoadOnStartup(1);
        register.addUrlMappings("/hystrix.stream");
        register.setName("HystrixMetricsStreamServlet");
        return register;
    }
}
  • 然后访问 localhost:9001/hystrix,在页面中填写如下信息:

    dashboard

    填好后回车,然后再去访问8001,那么8001的访问信息就会出现在dashboard中了。

    dashboard

二、服务网关之spring gateway

1、服务网关是来干嘛的?
一般来说,用户访问首先是到nginx,nginx会做负载均衡,然后并不是直接访问各位微服务应用,而是先到gateway。gateway可以做路由转发、权限校验、流量控制等。功能类似的框架还有zuul和zuul2,zuul停更了,zuul2还没完善出来,所以spring自己搞了一套gateway。

2、gateway核心概念:

  • 路由Route:简单的理解为url,如果断言为true就匹配该url
  • 断言Predicate:路由转发的判断条件
  • 过滤Filter:过滤器

3、gateway工作流程:
核心逻辑是路由转发 + 执行过滤链。客户端向gateway发请求,然后在gateway handler mapping 找到相匹配的路由,将请求发送到gateway web handler,handler再通过过滤链将请求发送到微服务中,然后返回。

4、代码演示:

  • 新建名为cloud-gateway-gateway9527的module;
  • pom.xml:

    org.springframework.boot
    spring-boot-devtools
    runtime
    true


    org.projectlombok
    lombok
    true


    org.springframework.boot
    spring-boot-starter-test



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client



    org.springframework.cloud
    spring-cloud-starter-gateway

  • yml:
server:
  port: 9527
  
spring:
  application:
    name: cloud-gateway
    
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
       defaultZone: http://eureka7001.com:7001/eureka
  instance:
    instance-id: cloud-gateway
    prefer-ip-address: true
  • 主启动:
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(GatewayMain9527.class, args);
    }
}

8001的controller中有两个接口,现在我们可以直接通过接口路由访问到,但是我不想暴露8001端口,想通过9527端口去访问,做法如下:

  • 将9527的yml改成下面这样:
server:
  port: 9527
  
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
      - id: payment_route # id,要求唯一
        uri: http://localhost:8001 #提供服务的路由地址
        predicates:
        - Path=/payment/ok/** # 路径匹配的进行路由
        
      - id: payment_route # id,要求唯一
        uri: http://localhost:8001 #提供服务的路由地址
        predicates:
        - Path=/payment/error/** # 路径匹配的进行路由
          
    
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
       defaultZone: http://eureka7001.com:7001/eureka
  instance:
    instance-id: cloud-gateway
    prefer-ip-address: true

现在依次启动7001的eureka、8001的payment和9527的gateway,然后访问呢localhost:9527/payment/ok/1,发现可以访问成功,说明路由转发成功。
这是配置网关的第一种方式,还有硬编码的方式,如下:

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        RouteLocatorBuilder.Builder routes = builder.routes();
        // 意思就是访问localhost:9527/domestic将会转发到https://tuijian.hao123.com/domestic中去
        routes.route("path_id", r -> r.path("/domestic").uri("https://tuijian.hao123.com/domestic")).build();
        return routes.build();
    }
}

现在访问localhost:9527/domestic,结果如下:

gateway路由转发

5、动态路由:
提供payment服务的有8001和8002,但是上面那样配置只写了8001,现在要实现通过微服务名称来做动态路由。即在配置那里不写死8001或8002,而是写8001和8002的微服务名称,这样就可以实现动态路由。

  • 首先确保启动了8001和8002;
  • 改yml:
server:
  port: 9527
  
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能
      routes:
      - id: payment_route1 # id,要求唯一
        #uri: http://localhost:8001 #提供服务的路由地址
        uri: lb://CLOUD-PAYMENT-SERVICE #微服务名称
        predicates:
        - Path=/payment/** # 路径匹配的进行路由
        
      - id: payment_route2 # id,要求唯一
        #uri: http://localhost:8001 #提供服务的路由地址
        uri: lb://CLOUD-PAYMENT-SERVICE #微服务名称
        predicates:
        - Path=/payment/** # 路径匹配的进行路由
          
    
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
       defaultZone: http://eureka7001.com:7001/eureka
  instance:
    instance-id: cloud-gateway
    prefer-ip-address: true

就这样就行了,然后访问http://localhost:9527/payment/1,就可以成功访问到8001和8002,并且会进行负载均衡,一次8001,一次8002,轮着来。

6、常用断言:

 predicates:
 - Path=/payment/** # 路径匹配的进行路由
 - After=2020-05-04T20:47:11.281+08:00[Asia/Shanghai] # 配置的规则要在2020年5月4日20点20分后才生效,有after就有before和between
 - Cookie=username,test # 必须带cookie访问,cookie名叫username,值为test,值也可以写正则表达式
 - Header=X-Request-Id,/d+ # 必须带请求头访问,请求头名为X-Request-Id,值要满足 /d+ 这个正则表达式
 - Host=**.somehost.com,**.otherhost.com #请求域名必须匹配这两个,即请求时添加header,名为host,值匹配这两个即可
 - Method=GET #必须是get请求
 - Query=id,/d+ #必须有名为id的参数且值匹配 /d+ 这个正则

7、常用filter:
filter生命周期有两种,一种是在业务逻辑之前,一种是在业务逻辑之后;种类也是两种,一种gatewayFilter,一种globalFilter。用法和predicates一样,比如:

filters:
- AddRequestParameter=X-Request-Id,1024 # 请求必须带名为X-Request-Id的请求头,值为1024

8、自定义过滤器:
可以实现全局日志记录,统一网关鉴权等功能。

  • 新建自定义过滤器类:
@Component
@Slf4j
public class MyFilter implements GlobalFilter, Ordered{
    /**
     * 表示这个过滤器的优先级,数字越小优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("=================进去自定义全局过滤器=============");
        // 获取id参数
        String id = exchange.getRequest().getQueryParams().getFirst("id");
        if (!"1".equals(id)) {
            log.error("================= id不存在==================");
            // 设置返回码
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }
}

也就是说,如果没有id这个参数或者id参数值不为1的,都会被拦截。实际生产中可以用这个来验证请求是否携带token。

版权声明:
作者:玉兰
链接:https://www.techfm.club/p/24013.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>