掘金 后端 ( ) • 2024-03-07 11:25

theme: scrolls-light

此篇笔记包含的技术:Consul,OpenFeign,LoadBancer,Resilience4J,Micrometer+ZipKin,Gateway。

=======注意,这里有用!!!!!=======

笔记案例代码已经同步到:https://gitee.com/yanghaokun0312/cloud-demo

=======注意,这里有用!!!!!=======

Consul服务发现与注册中心

官网:https://www.consul.io/

Consul软件安装

Docker方式:

docker run -d --name consul -p 8500:8500 consul agent -dev -server -bootstrap -client 0.0.0.0 -ui

安装验证:

访问:http://你的安装地址:8500

image.png

安装成功。以上安装没有解决数据持久化,数据都存储在内存中。

客户端注册到Consul中

导入坐标:

<!--consul服务发现注册-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>


<!--web健康检测-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

yaml配置:

# consul服务地址
consul:
  host: cloud.demo
  port: 8500
# consul注册发现配置
spring:
  cloud:
    consul:
      host: ${consul.host}
      port: ${consul.port}
      discovery:
        # 注册到consul的服务名称
        service-name: ${spring.application.name}
        prefer-agent-address: true
        # 心跳检查
        heartbeat:
          # 开启
          enabled: true
        # 在注册时使用ip地址而不是主机名
        prefer-ip-address: true

启动类开启服务注册注解:

@EnableDiscoveryClient

启动微服务组件项目,观察Consul控制台:

image.png

Consul分布式配置中心(浅用)

Consul配置云yaml文件内容

image.png

image.png

image.png

解释:在Consul服务中配置了微服务 cloud-product 开发环境(dev)的云配置信息。

客户端读取云配置内容

导入云配置坐标:

<!--consul云配置读取-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

创建 bootstrap.yaml 系统级别项目启动文件

内容如下:

---
spring:
  application:
    name: cloud-product
  profiles:
    active: dev

---
# consul服务地址
consul:
  host: cloud.demo
  port: 8500

---
# consul注册发现配置
spring:
  cloud:
    consul:
      host: ${consul.host}
      port: ${consul.port}
      discovery:
        # 注册到consul的服务名称
        service-name: ${spring.application.name}
        prefer-agent-address: true
        # 心跳检查
        heartbeat:
          # 开启
          enabled: true
        # 在注册时使用ip地址而不是主机名
        prefer-ip-address: true

---
spring:
  cloud:
    consul:
      # 配置中心
      config:
        # 配置文件名称已 - 进行分割
        profile-separator: '-'
        # 配置文件类型
        format: yaml
        watch:
          # 监听更新时间间隔 默认 55秒 测试 设置为 1秒
          wait-time: 1

配置解释:项目一启动就去注册到Consul服务中,并且读取cloud-product-dev.yaml 文件的内容到本地。

image.png

Spring中读取配置文件内容实现动态刷新的两种方式:

  • @Value()注解 + @RefreshScope注解 实现动态刷新。

  • @Component注解 + @ConfigurationProperties()注解,实现动态刷新,例子如下:

@ConfigurationProperties(prefix = "dateformat")
@Component
@Data
public class DateFormat {
    private String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
    private String dateFormat = "yyyy-MM-dd";
}

创建一个访问接口进行测试,代码如下:

@RestController("/yun")
public class YunConfigController {

    @Resource
    private DateFormat dateFormat;

    @GetMapping("/getInfo")
    public String getInfo(){
        return dateFormat.getDateTimeFormat();
    }
}

访问:

image.png

更新Consul上的云配置文件内容:

image.png

观察idea控制台打印:

image.png

再次访问测试接口:

image.png

已实现云配置更新影响程序结果的效果。

OpenFeign远程调用接口

维护base-openfeign-api项目接口

根据以往项目开发经验,将OpenFeign维护的接口,单独创建出一个工程项目进行维护,步骤如下:

创建一个base-openfeign-api的项目。

image.png

导入openfeign坐标,以及基础公共实体坐标。

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

创建维护某个微服务的openfeign接口:

@FeignClient("cloud-product")
public interface ProductApi {

    /**
     * 根据商品id查询商品信息
     */
    @GetMapping("/product/getProductById/{id}")
    public ResponseResult<ProductVO> getProductById(@PathVariable("id") Integer id);

}

接口上核心注解:@FeignClient() ,表明次api接口面向注册中心哪个服务。

使用base-openfeign-api项目进行远程访问

导入base-openfeign-api坐标到使用工程中:

<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--远程调用服务接口项目-->
<dependency>
    <groupId>com.haokun</groupId>
    <artifactId>base-openfeign-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

启动类上激活你需要的api接口所在包:

@EnableFeignClients(basePackages = {"com.haokun.api.product"})

业务中使用:

@Resource
private ProductApi productApi;

// 调用接口
ResponseResult<ProductVO> productRes = productApi.getProductById(order.getProductId());

image.png

image.png

上述步骤已实现,在微服务一个体系内,可以使用openfeign的方式进行远程调用。

复制一份商品微服务程序,形成集群服务,使用openfeign远程调用商品服务,测试 LoadBancer 负载均衡:

image.png

启动:

image.png

LoadBancer 默认实现的是轮训算法进行访问:

image.png

经过上述测试,已经实现openfeign + LoadBancer + consul 服务的注册发现,负载均衡,远程访问功能。

image.png

LoadBancer的依赖由consul带入。

经过上面的步骤已经完成了案例的第一个版本,下面逐步加入微服务组件,代码地址如下: https://gitee.com/yanghaokun0312/cloud-demo/tree/v1.0

openfeign的高级特性

调用时间超时机制

yaml配置内容:

# openfeign超时时间配置
spring:
  cloud:
    openfeign:
      client:
        config:
          # 默认调用全部接口的时间为1.5秒
          default:
            connect-timeout: 1500
            read-timeout: 1500
          # 指定微服务组件的超时时间
          cloud-product:
            connect-timeout: 3000
            read-timeout: 3000

超时会报错抛出异常,内容如下:

image.png

重试机制:

/**
 * openfeign配置
 */
@SpringBootConfiguration
public class FeignConfig {

    /**
     * 重试配置
     *
     * @return
     */
    @Bean
    public Retryer myRetryer() {
        // openfeign 默认配置是不走重试策略的
        // return Retryer.NEVER_RETRY;

        // 最大请求次数为3 (1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
        return new Retryer.Default(100, 1, 3);
    }

}

可以实现在请求超时时,重新发送两次请求。

请求日志

image.png

观察请求日志:

image.png

请求连接池

openfeign 默认使用的是 java.base java.net.HttpURLConnection,没有连接池,替换为 httpClient 连接池,为openfeign来提供连接供给。

引入连接池坐标:

<!--请求连接池-->
<!-- httpclient5-->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
    <version>13.1</version>
</dependency>

yaml配置内容:

# 配置请求连接池
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true

请求观察:

image.png

开启请求响应压缩

yaml配置:

# 开启请求响应压缩模式
spring:
  cloud:
    openfeign:
      # 压缩
      compression:
        # 请求
        request:
          enabled: true
          # 最小压缩要求
          min-request-size: 2048
          # 压缩数据类型
          mime-types: text/xml, application/xml, application/json
        # 响应
        response:
          enabled: true

image.png

Resilience4j熔断降级,限流,隔离

我对熔断降级,限流,隔离的理解:熔断就是在请求访问一个服务时,触发了设置的阈值,然后开始熔断,不再请求,并且快速返回数据,称为降级处理,隔离和限流很类似,是设置访问一个服务的请求数量保持在多少,多的请求会进行降级处理。

  • 设定一个需求:在访问一个请求时,请求数量6个,出现了3次异常信息,错误率达到50%,进行熔断降级处理,并返回系统繁忙请稍后再试的数据内容。

  • 设定第二个需求:访问一个请求,这个请求1秒只能并发5个,多的请求会降级处理,并返回当前人数较多,请稍后再试的数据。

  • 设定第三个需求:访问一个请求,这个请求1秒只能通过10个,多的请求会降级处理,并返回当前人数较多,请稍后再试的数据。

针对resilience4j熔断的方式两种:超时,异常,我将使用openfeign来控制超时,若超时抛出异常,让resilience4j来统计,达到阈值时熔断。

熔断降级

导入坐标:

<!--熔断降级resilience4j-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!--由于断路保护等需要AOP实现,所以必须导入AOP包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

yaml配置内容:

# 熔断降级前提配置
spring:
  cloud:
    openfeign:
      # 断路器
      circuitbreaker:
        enabled: true
        group:
          enabled: true

坑:

# 延长TimeLimiter默认的请求超时时长 它 1秒的
resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s

主要内容:

# 配置断路器
resilience4j:
  circuitbreaker:
    configs:
      # 默认断路器配置
      default:
        # 断路器方式 计数的方式
        sliding-window-type: count_based
        # 统计数量周期
        sliding-window-size: 6
        # 最小统计数量
        minimum-number-of-calls: 6
        # 阈值 百分比
        failure-rate-threshold: 50
        #熔断时长 秒
        wait-duration-in-open-state: 10s
        # 需要开启半开状态来测试
        automatic-transition-from-open-to-half-open-enabled: true
        # 半开测试访问请求数量
        permitted-number-of-calls-in-half-open-state: 2
        # 计数的异常
        record-exceptions:
          - java.lang.Exception
    instances:
      cloud-product:
        base-config: default

Java代码入侵加强:

image.png

@CircuitBreaker(name = "cloud-product", fallbackMethod = "nextOrderFallbackMethod")
@Override
public Boolean nextOrder(Order order) {
}
public Boolean nextOrderFallbackMethod(Order order,Throwable throwable){
    if (true){
        throw new ServiceException("系统繁忙请稍后再试");
    }
    return false;
}

降级的方法参数有要求,如上:需要与配置了断路器的方式参数一致时,后面还需要加入Throwable参数类型。

开始压测:

image.png

其他请求在熔断的时间内将直接降级处理:

image.png

image.png

隔离

限制对下游的并发数量。

导入坐标:

<!--对下游访问的访问隔离-->
<!--resilience4j-bulkhead-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
</dependency>

yaml配置:

# 对下游访问的访问隔离
resilience4j:
  bulkhead:
    configs:
      default:
        # 最大允许数量
        max-concurrent-calls: 5
        # 时间周期
        max-wait-duration: 1s
    instances:
      cloud-product:
        base-config: default

Java入侵增强:

image.png

@Bulkhead(name = "cloud-product",fallbackMethod = "nextOrderBulkheadFallbackMethod",type = Bulkhead.Type.SEMAPHORE)

降级方法:

image.png

开始压测:

允许的范围内成功:

image.png

失败:

image.png

image.png

限流

导入坐标:

<!--限流-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
</dependency>

yaml配置内容:

# 限流
resilience4j:
  ratelimiter:
    configs:
      # 默认每秒只能通过1个请求
      default:
        limit-for-period: 10
        limit-refresh-period: 1s
        timeout-duration: 1
    instances:
      cloud-product:
        base-config: default

Java代码入侵增强:

image.png

压测触发,降级处理:

image.png

总结:resilience4j不好用,配置繁琐,代码入侵,难维护!

分布式链路追踪

micrometer(收集数据) + zipkin(数据展示)

搭建 zipkin

Docker的方式搭建:

docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin

浏览器访问:你的服务地址:9411

image.png

引入micrometer

父工程版本控制内容:

<properties>
    
    <!--链路追踪版本控制-->
    <micrometer-tracing.version>1.2.0</micrometer-tracing.version>
    <micrometer-observation.version>1.12.0</micrometer-observation.version>
    <feign-micrometer.version>12.5</feign-micrometer.version>
    <zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>

</properties>

<dependencyManagement>
    <dependencies>

        <!--链路追踪版本控制-->
        <!--micrometer-tracing-bom导入链路追踪版本中心  1-->
        <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing-bom</artifactId>
                <version>${micrometer-tracing.version}</version>
                <type>pom</type>
                <scope>import</scope>
        </dependency>
        <!--micrometer-tracing指标追踪  2-->
        <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing</artifactId>
                <version>${micrometer-tracing.version}</version>
        </dependency>
        <!--micrometer-tracing-bridge-brave适配zipkin的桥接包 3-->
        <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing-bridge-brave</artifactId>
                <version>${micrometer-tracing.version}</version>
        </dependency>
        <!--micrometer-observation 4-->
        <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-observation</artifactId>
                <version>${micrometer-observation.version}</version>
        </dependency>
        <!--feign-micrometer 5-->
        <dependency>
                <groupId>io.github.openfeign</groupId>
                <artifactId>feign-micrometer</artifactId>
                <version>${feign-micrometer.version}</version>
        </dependency>
        <!--zipkin-reporter-brave 6-->
        <dependency>
                <groupId>io.zipkin.reporter2</groupId>
                <artifactId>zipkin-reporter-brave</artifactId>
                <version>${zipkin-reporter-brave.version}</version>
        </dependency>

    </dependencies>
</dependencyManagement>

微服务项目工程引入坐标:

<!--链路追踪-->
<!--micrometer-tracing指标追踪  1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!--micrometer-observation 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer 4-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave 5-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>

yaml配置内容:

# 链路追踪zipkin配置
management:
  zipkin:
    tracing:
      # zipkin服务端地址
      endpoint: http://cloud.demo:9411/api/v2/spans
  tracing:
    sampling:
      # 默认0.1 10次请求采集一次,值越大采集越及时
      probability: 1.0

发送请求,观察zipkin控制台

image.png

可以发现第一次请求访问很慢。

image.png

点击一个请求详细内容:SHOW按钮点击

image.png

发送一个会报异常的请求:

image.png

image.png

Gateway网关

网关HelloWorld

创建网关微服务独立项目,引入坐标:

<!--gateway-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

需要引入所使用的服务注册中心,并注册进入。

yaml配置一个路由:

# 配置网关路由
spring:
  cloud:
    gateway:
      routes:
          # 唯一id
        - id: cloud-order
          # 动态访问服务地址
          uri: lb://cloud-order
          # 断言
          predicates:
            - Path=/order/**

网关三大组件:路由:routes,断言:predicates,过滤器:filters

通过网关的服务地址+端口号进行访问,有效的屏蔽了真正微服务服务的地址。

网关动态获取服务地址

uri: lb://cloud-order

这里的lb,就是:LoadBancer,从服务注册中心拉去下来,负载均衡。

断言predicates

断言官网介绍 https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-mvc/gateway-request-predicates.html

image.png

项目启动也加载了这些断言。

image.png

最常用的就是Path断言了。

测试一下头信息需要携带的内容:需要请求携带头信息authentication,值为存数字才可以断言成功

predicates:
  - Header=authentication, \d+

image.png

测试不携带authentication头信息:

image.png

携带:

image.png

自定义断言 与 自定义过滤器类似。

过滤器

对已断言成功的请求,锦上添花:

参考官网地址: https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-mvc/gateway-handler-filter-functions.html

使用内置过滤器工厂往请求头中添加信息,证明请求是经过了网关,若客户端直接绕开网关,则请求失败:

spring:
  cloud:
    gateway:
      routes:
          # 唯一id
        - id: cloud-order
          # 动态访问服务地址
          uri: lb://cloud-order
          # 断言
          predicates:
            - Path=/order/**
            # - Header=authentication, \d+
          filters:
            - AddRequestHeader=me, yang

image.png

自定义全局过滤器

往请求头中添加客户端请求的IP地址。

@Component
public class ClientIpFilter implements GlobalFilter, Ordered {

    private static final String CLIENT_IP_HEADER = "X-Client-IP";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // 获取客户端IP
        String clientIp = exchange.getRequest().getRemoteAddress().getHostString();

        // ReadOnlyHttpHeaders 报错
        // exchange.getRequest().getHeaders().add(CLIENT_IP_HEADER, clientIp);

        ServerHttpRequest request = exchange.getRequest().mutate().header(CLIENT_IP_HEADER, clientIp).build();
        return chain.filter(exchange.mutate().request(request).build());
    }

    /**
     * 值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return -1;
    }
}

网关发送到微服务时:

image.png

统计接口调用时间

@Component
@Slf4j
public class GlobalTimeTestFilter implements GlobalFilter, Ordered {

    //开始访问时间
    private static final String BEGIN_VISIT_TIME = "begin_visit_time";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //先记录下访问接口的开始时间
        exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
            if (beginVisitTime != null) {
                log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
                log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
                log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
                log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
                log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
                log.info("分割线: ###################################################");
                System.out.println();
            }
        }));
    }

    @Override
    public int getOrder() {
        return -2;
    }
}

image.png

自定义条件过滤器

请求头中携带指定id

@Component
@Slf4j
public class CheckGatewayFilterFactory extends AbstractGatewayFilterFactory<CheckGatewayFilterFactory.Config> {

    public CheckGatewayFilterFactory() {
        super(CheckGatewayFilterFactory.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                log.info("经过了校验条件过滤器");
                List<String> ids = List.of("2670", "2690");
                String id = exchange.getRequest().getHeaders().getFirst("id");
                if (Objects.isNull(id) || id.isEmpty() || !ids.contains(id)) {
                    exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                    return exchange.getResponse().setComplete();
                }
                return chain.filter(exchange);
            }
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("id");
    }

    @Data
    public static class Config {
        private String id;
    }

}

yaml配置它:

# 配置网关路由
spring:
  cloud:
    gateway:
      routes:
          # 唯一id
        - id: cloud-order
          # 动态访问服务地址
          uri: lb://cloud-order
          # 断言
          predicates:
            - Path=/order/**
            # - Header=authentication, \d+
          filters:
            - AddRequestHeader=me, yang
            - Check

请求访问:

失败:

image.png

成功:

image.png

以上的笔记就结束了对原生SpringCloud的一些组件学习与练习。代码已标签: https://gitee.com/yanghaokun0312/cloud-demo/tree/v2.0