scservers(六) 网关-路由

前言

前面的五篇内容已经构成了微服务的基本核心框架,已经可以结合业务,加上存储提供服务,并且整个系统内部服务之间也具备相互识别调用路由能力,那么再将这些服务汇总提供给其他第三方(不管是自己客户端(如h5,mobile app)还是作为开放平台供第三方使用),还是需要一个汇总的接口出口点。网关的需求自然形成。我们需要再提供了一个外部路由的能力。

业务网关

网关我们可以分位接入网关(连接的保持、消息的解析(卸载证书等)、消息的分发)+应用网关(路由,鉴权,限流等功能性网关的能力基础安全通用能力)。方案挺多:

* nginx(f5) + zuul(or) gateway:

nginx卸载https安全证书,高并发连接能力,反向代理能力。

gateway 功能路由鉴权,限流等包含业务功能的网关能力。

* kong(nginx基础上的包含功能性网关及业务网关能力)

nginx 我们应该用得多,比较熟悉,这里主要讲gateway的业务网关附带的一些其他能力。

sprincloud gateway

之前springcloud 一直使用zuul1.0作为其网关,后来鉴于性能(同步模型)及zuul2跳票等可能原因,社区推出了自己的网关,名字直接明了,gateway,同时还支持webflux,整合stream流等功能特性。那么我们就直接使用它来讲解吧。

  • 配置

    依赖

    <!--api网关-->
        <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--redis限流-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    属性配置(静态路由配置):

    spring: 
      cloud:
    gateway:
    discovery:
    locator:
    enabled: true
    routes:
    #网关路由到订单服务order-service
    - id: demo-ribbon-consumer
    uri: lb://center-ribbon-demo
    predicates:
    - Path=/test/ribbon/**
    default-filters:
    - name: Retry
    args:
    retries: 3
    statuses: BAD_GATEWAY
    - AddResponseHeader=X-Response-Default-Foo, Default-Bar
    - name: RequestRateLimiter
    args:
    redis-rate-limiter.replenishRate: 1 #令牌桶的容积
    redis-rate-limiter.burstCapacity: 1 #流速 每秒
    rate-limiter: "#{@defaultRedisRateLimiter}" #SPEL表达式去的对应的bean
    key-resolver: "#{@remoteAddressKeyResolver}" #SPEL表达式去的对应的bean
  • 使用

    限流

    限流可以在nginx接入就做.gateway 也提供了限流的组件集成,基于redis做的。基本使用:

    主要查看属性配置中的:

    default-filters:
    - name: RequestRateLimiter   
    

    限流基于令牌桶算法,参数有

    * 令牌桶总容量:redis-rate-limiter.replenishRate

    * 令牌桶每秒填充平均速率:流速 每秒:redis-rate-limiter.burstCapacity

    上面两个参数只需要配置,下面两个这两个参数

    * 限流关键字对应:key-resolver:

    * 限流

    需要编写类代码:

    @Component
    public class RequestRateLimiterConfig {
    
  • 令牌桶算法

    令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。

scservers(五) 服务调用——安全,熔断,限流,降级

前言

服务调用是分布式系统基础,因为微服务的理念,不管粒度如何划分,系统功能间交互调用都会复杂起来,基础服务不管什么原因挂掉,容易就造成整个系统的不可用,这就是雪崩效应。原因是服务不可用,不断重试,造成服务调用者因为同步等待造成的资源耗尽也不可用,扩大后就造成所有服务都不可用了。

解决的策略基本有如下几种:

  • 限流

    按照漏斗模型,从前向后的限流

    • 用户交互限流
    • 网关限流
    • 服务调用关闭重试
  • 改进缓存
    • 缓存预加载
    • 同步改异步刷新
  • 服务自动扩容
    • 可以在docker 容器层面解决
  • 降级

    服务调用者降级服务

    • 资源即调用服务的线程池的隔离
    • 熔断,通过阈值设置失败的服务不再继续请求
    • 快速失败:通过超时机制, 熔断器 进行快速失败
    • 快速失败根据分类可以是应答错误也可以是应答缓存or默认数据

在上述策略中,微服务系统框架可以做的主要:

  • 可用情况下预防:超时处理,进行限流,资源隔离,分类,将不重要业务降级。
  • 不可用情况下直接隔离,熔断,降级等的操作。

而Springcloud 就为我们提供了Hystrix来保护我们的系统,可以将请求隔离,针对服务限流,当服务不可用时能够熔断并降级,防止级联故障。

Hystrix处理策略

当我们使用了Hystrix时,Hystrix将所有的外部调用都封装成一个HystrixCommand或者HystrixObservableCommand对象,这些外部调用将会在一个独立的线程中运行。我们可以将出现问题的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务。

  • 资源隔离

    通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池。这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。
  • 超时机制

    如果我们加入超时机制,例如2s,那么超过2s就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题

  • 服务限流

    通过线程池+队列的方式,通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中

  • 服务熔断

    这个熔断可以理解为我们自己家里的电闸。

    当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费

  • 服务降级

    有服务熔断,必然要有服务降级。

    所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景

Hystrix配置使用

  • hystrix & ribbon

    • 配置

      依赖:

       <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-hystrix</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-consul-discovery</artifactId>
      </dependency>

      属性设置:

    • 注解式使用

      • step1:使用注解 @EnableHystrix 启用豪猪断路器功能。
      • step2:在需要降级的接口上添加注解,并添加参数类型相同的降级函数,返回自定义信息

        @HystrixCommand(fallbackMethod = "ribbonfallback")
        @RequestMapping("/test/ribbon")
        public String testconfigrpc(String name) {
        return "RestTemplate+Ribbon get username <<==>> "+restTemplate.getForObject("http://center-demo/test/config/username",String.class);
        }
        public String ribbonfallback(String name) {
        return "ribbonFallback default name :ribbonFallback";
        }
    • 监控

      host:port/hystrix

      此处只是简单的单个应用监控,显然不适合大规模集群监控,后续的多个客户端监控放到后面专门的监控章节去讲吧。

  • hystrix & feign

    • 配置

      依赖

      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
      </dependency>

      属性配置

      feign: 
        hystrix:
      enabled: true
    • 注解式使用

      • step1: 引入注解 @EnableFeignClients,其已经包含启用断路器注解
      • step2:在ribbon中我们使用注解降级函数,在feign中我们使用注解降级类

        @FeignClient( name = "center-demo", path = "/test", decode404 = true,fallback = DemoFallbackFeign.class)
        public interface DemoFeignclient {  @GetMapping("/config")
        String selectconfigByName(@RequestParam String name);
        @GetMapping("/token/back")
        String gettokenback();
        }
        @Component
        public class DemoFallbackFeign implements DemoFeignclient {
        @Override
        public String selectconfigByName(String name) {
        // TODO Auto-generated method stub
        return "FeignFallbackName";
        }
        @Override
        public String gettokenback() {
        // TODO Auto-generated method stub
        return "FeignFallbackToken";
        }
        }
  • 源码

scservers(四)服务调用——接口与负载均衡

前言

前面的三篇搭起微服务的基本应用框架:服务发现,分布式动态配置,消息总线的通知能力等。就如人的身体基本上已经有了头脑四肢,基本成形。

头脑四肢之间需要互联互通,相当于服务调用,并且需要一定的协议接口及基于协议的序列化反序列化,比如dubbo,grpc甚至原生的别人不了解的协议等。目前我们这里只介绍下http的,其他基本类同。服务的调用还要负载均衡,不然只用右手,容易起茧。

http-rest

目前http协议所使用的远程调用范式基本以REST为范式,spring3之后就提供RestTemplate来执行rest http接口调用,并提供序列化反序列的能力。但没有负载均衡,负载均衡依赖于服务注册,所以springcloud 提供了Ribbon作为软负载。

而Feign,则是及大成者,包含软件负载(ribbon)的http远程调用及序列化反序列化能力。即有如下方式:

* 原生RestTemplate+Ribbon

* Feign

RestTemplate+Ribbon

  • 配置

    我们需要添加Ribbon的依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>

    但是在示例里面我们并没有在pom.xml中看到这个依赖配置,那是因为我们已经存在如下依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    前面我们说过,负载还依赖于服务注册,而如果使用eureka作为服务注册,那么其已经包含了Ribbon的依赖了。如果使用其他服务注册发现中心,如consul、zookeeper、etcd,那就要加上Ribbon的依赖。嗯,全家桶确实名不虚传。

    eureka的配置这里就不再复述了。

  • 使用

    • step1:@LoadBalanced

      在RestTemplate加上注解@LoadBalanced,让RestTemplate支持负载客户端。
    @Configuration
     class MyConfiguration {
    @LoadBalanced
    @Bean
    RestTemplate restTemplate() {
    return new RestTemplate();
    }
    }
    • step2:配置负载模式

      标明Ribbon负载的工作模式,如果是轮训可以跳过这一步(默认支持轮训),如果想该用其他方式按照如下方式。已经实现了随机模式,我们也可以通过重写类支持其他模式。
    @Configuration
    public class RibbonRuleConfiguration {
    @Bean
    public IRule ribbonRule() {
    return new RandomRule();
    }
    }

    在使用到的control或者service加上使用的ribbon客户端的注解:

    @RibbonClient(name = "center-ribbon-Rule",configuration = RibbonRestTemplateConfiguration.class)
    public class AppApplication {
    
    • step3:使用 RestTemplate远程调用
    restTemplate.getForObject("http://serviceid/restpath", String.class);
    

    Eureka根据 spring.application.name 设置 serviceId,我们只要设置serviceid就可以解析出ip:port。

Feign

Feign 通过注解及拦截器让 Java HTTP 客户端编写更方便。支持可插拔编码器和解码器,降低 HTTP API 的复杂度,通过最少的资源和代码来实现和 HTTP API 的连接。通过可定制的解码器和错误处理,可以编写任意的HTTP API。Spring Cloud Feign 封装了 Ribbon 这一组件,所以在使用 Feign 同时还能提供负载均衡的功能,这一切只需要一个 @FeignClient 即可完成。

  • 配置

    新增依赖 spring-cloud-starter-openfeign

    <dependencies>
        <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    </dependencies>
  • 使用

    1. feign client 注解引入

      在application引入 feign注解

      @EnableFeignClients
      
    2. feign client 接口

      定义rpc服务的接口

      @FeignClient( name = "center-demo", path = "/test", decode404 = true)
      public interface DemoFeignclient {
      @GetMapping("/config")
      String selectconfigByName(@RequestParam String name);
      }
    3. 调用接口

      在service中实例化该接口并使用

          @Autowired
      private DemoFeignclient feignClient;
      
      @RequestMapping("/test/feign")
      public String testconfigrpcByFeign(String name) {
      return "Feign get username <<==>> "+feignClient.selectconfigByName(name);
      }

      我们看到,所有服务提供者供外部调用的接口是一样的,如果每个不同消费者都些feign client 接口,就做了重复的工作,这就是坏味道,所以我们可以像dubbo一样在写服务提供者的时候,就可以把接口单独整个架包出来,供消费者使用。具体可见后面章节的最佳实践。

  • 拦截器

    上面是使用 Feign 来调用简单的远程服务,但实际上远程服务一般会有权限验证(这部分后续章节也会出),需要在 header 中传递 token之类的。在方法中显示传递又过于麻烦了,这时候就可以考虑使用 Feign 提供的RequestInterceptor 接口,只要实现了该接口,那么Feign每次做远程调用之前都可以被它拦截下来在进行包装。示例如下,就可以方便的传递header字段了。

    @Configuration
    public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
    requestTemplate.header("token", "ok");
    }
    }
  • 源码

  • PS注意

    Feign与显示声明的Ribbon不要混合在一个controller下使用,不然使用Feign的时候会间歇性提示404错误

          {"timestamp":"2019-03-21T05:37:41.540+0000","status":404,"error":"Not Found","message":"No message available","path":"/test/token/back"}