SpringCloud 服务保护 & 分布式事务

书接上回:SpringCloud 微服务

一、服务保护

1.1 雪崩问题

微服务调用链路中的某个服务故障,导致整个链路中的所有微服务都不可用。

如图所示:购物车服务调用商品服务,若商品服务出现了宕机或者处理速度太慢(单个请求要好几秒),就会出现商品服务上级资源得不到释放,使得上级的 Tomcat 线程资源被耗尽。在多级服务相互调用的情况下,这种阻塞的问题会不断向上传递,进而产生雪崩问题:

1.1.1 问题分析

① 雪崩问题产生的原因是什么?

  • 微服务相互调用,服务提供者(下级微服务)出现故障或阻塞;
  • 服务调用者没有做好异常处理,导致自身故障(如例子中的购物车服务及其上层服务);
  • 调用链路中的所有服务级联失败,导致整个集群故障。

② 解决问题的思路有哪些?

  • 尽量避免服务出现故障或阻塞。
    • 保证代码的健壮性;
    • 保证网络通畅;
    • 能应对较高的并发请求;
  • 服务调用者做好远程调用异常的后备方案,避免故障扩散。
1.1.2 解决方案

① 请求限流:限制访问微服务请求的并发量,避免服务因流量激增出现故障。

这种方式也称为流量整形:

② 线程隔离:通过限定每个业务能使用的线程数量将故障业务隔离

③ 失败处理:给业务编写一个调用失败时的处理的逻辑,称为fallback。当调用出现故障(比如无线程可用)时,按照fallback处理业务,而不是直接抛出异常

④ 服务熔断:由断路器统计请求的异常比例慢调用比例,如果超出阈值则会熔断该业务,拦截该接口的请求。熔断期间,所有请求快速失败,全都走fallback逻辑

例如一个广告业务,如果查询广告失败,可以返回一个默认的广告

1.1.3 服务保护技术

1.2 Sentinel

Sentinel是阿里巴巴开源的一款面向分布式、多语言异构化服务架构的流量治理组件。官网地址: home | Sentinel

1.2.1 运行 Sentinel

在 Github 上下载到 jar 包,用以下命令执行:

java -Dserver.port=8090 -Dscp.sentienl.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

更多启动参数 - 参考官方文档:startup-configuration

启动后访问网址:http://localhost:8090/,默认的用户名和密码都是sentinel

1.2.2 微服务整合依赖

① 引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

② 添加配置

# application.yml (cart-service)
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090  # sentinel 控制台地址

③ 重启 cart-service

但是此时 sentinel dashboard 里并不会显示 cart-service,因为它做的是流量监控,所以我们访问购物车几次,就可以在控制台看到了。

1.2.3 簇点链路

簇点链路,就是单机调用链路。是一次请求进入服务器后经过的每一个被sentinel监控的资源链。默认sentinel会监控springMVC的每一个Endpoint(http接口)。限流、熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径:

简单来说,簇点链路(Cluster Node Link)就是在单台应用实例上,对一次入站请求从进入到离开的全过程中,所有被 Sentinel 监控的“资源”节点的调用顺序的汇总。它帮助我们在单机层面实时查看该请求走过了哪些接口或方法,以及每个节点的统计数据(如 QPS、响应时间、阻塞或异常数等)。例如:

HTTP GET /api/order -> OrderController.createOrder() -> 
                            InventoryService.reserve() -> PaymentService.process()

不过对于 Restful 风格的接口,这种 Endpoint 的监控方式是有问题的,因为不同的接口路径都是 /carts,只有请求方式不一样,例如:

package com.hmall.cart.controller;

@RestController
@RequestMapping("/carts")
@RequiredArgsConstructor
public class CartController {

    private final ICartService cartService;

    @PostMapping
    public void addItem2Cart(@Valid @RequestBody CartFormDTO cartFormDTO) {
        cartService.addItem2Cart(cartFormDTO);
    }

    @PutMapping
    public void updateCart(@RequestBody Cart cart) {
        cartService.updateById(cart);
    }
    ...
}

Restful 风格的 API 请求路径一般都相同,这会导致簇点资源名称重复。因此我们要修改配置,把请求方式+请求路径作为簇点资源名称。

# application.yml (cart-service)
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090  # sentinel 控制台地址
      http-method-specify: true  # <--- 使簇点资源区分请求方式

重启 cart-service,重新查看控制台:

1.2.4 请求限流

每一个被访问的接口都会在簇点链路显示,找到需要限流的接口,按【➕流控】

限流的规则中,有一个阈值类型:

  • QPS:控制单位时间内允许进入系统的请求数,主要用于防止流量洪峰。
  • 并发线程数:系统中同时处理的请求数量,受 QPS 和平均响应时间的共同影响。

点击新增之后,可以在这里找到和编辑新增的规则:

验证配置结果:

HTTP 响应状态码:429(flow limiting)表示被限流

Sentinel Dashboard:

1.2.5 线程隔离

商品服务出现阻塞或者故障时,调用商品服务的购物车服务可能因此被拖慢,甚至资源耗尽。所以必须限制购物车服务中查询商品服务的可用线程数,实现线程隔离。

为什么需要线程隔离?

即便对 QPS 做了限制,只要每个请求的平均响应时间(latency)较长,系统中的并发量依然会很高——因为并发量近似等于 QPS 与平均响应时间的乘积:

concurrency ≈ QPS × 平均响应时间

这意味着,QPS 限制只能控制“到达速率”;如果处理慢或阻塞,允许流入的请求都会在系统中累积,形成高并发。

① Sentinel 控制台配置线程隔离

如果这个接口的处理能力是 500ms/个,那么 5 个线程的处理能力就是 10 QPS

这里的规则先不急着新增,第 ③ 步再新增

② 修改商品服务,模拟延时

package com.hmall.item.controller;

@RestController
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {

    private final IItemService itemService;

    @ApiOperation("根据id批量查询商品")
    @GetMapping
    public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids) {
        // 模拟业务延迟 500ms
        ThreadUtil.sleep(500);
        return itemService.queryItemByIds(ids);
    }
    ...
}

重启 item-service

Spring boot 集成的 Tomcat 默认最大连接数高达8192:

为了使用 Jmeter 进行测试,需要对 Tomcat 线程数和最大连接数进行限制(因为我们单机 Jmeter 无法达到这么高的并发量)。

server:
  port: 8082
  tomcat:
    threads:
      max: 50
    # accept-count:允许排队等待的连接数,50个用户可立即建立连接,50个用户等待连接释放,
    # 其余用户建立不了连接直接失败
    accept-count: 50
    max-connections: 100

重启 cart-service,测试查询购物车的请求速度500ms+:

启动 Jmeter 测试:

再次去购物车进行增减数量操作:

发现商品查询操作>1s,增减数量操作由十几ms变为500+ms,明显被拖慢。(下面的请求是恢复正常的)。

③ 利用线程隔离:防止购物车查询影响购物车的别的业务

④ 再次用 Jmeter 开启压力测试,参数与上面相同,继续测试:

现在只有查询购物车的接口有问题了(被流控了),但是商品的增减是正常的(增减按钮被遮住了)。后端返回 429 Too Many Requests。

1.2.6 Fallback

① 定义 FallbackFactory

package com.hmall.api.client.fallback;

import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;

import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
    @Override
    public ItemClient create(Throwable cause) {
        /*log.error("item-service服务调用失败", cause);*/
        return new ItemClient() {
            @Override
            public List<ItemDTO> queryItemsByIds(Collection<Long> ids) {
                log.error("查询商品失败!", cause);
                return Collections.emptyList();
            }

            @Override
            public void deductStock(List<OrderDetailDTO> items) {
                log.error("扣减库存失败!", cause);
                throw new RuntimeException(cause);
            }
        };
    }
}

② 将其注册为 Bean

package com.hmall.api.config;

import com.hmall.api.client.fallback.ItemClientFallbackFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FallbackConfig {
    @Bean
    public ItemClientFallbackFactory itemClientFallbackFactory() {
        return new ItemClientFallbackFactory();
    }
}

③ 给 FeignClient 的注解添加新属性

package com.hmall.api.client;

import com.hmall.api.client.fallback.ItemClientFallbackFactory;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.Collection;
import java.util.List;

// 旧的注解配置
//@FeignClient("item-service")
// 新的注解配置
@FeignClient(value = "item-service", 
        fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemsByIds(@RequestParam("ids") Collection<Long> ids);

    @PutMapping("/items/stock/deduct")
    void deductStock(@RequestBody List<OrderDetailDTO> items);

}

④ 修改 cart-service 的 application.yml

这里是把 Feign 的远程调用作为簇点资源,这是比较推荐的 Sentinel 监控/限流方式,而非监控/限制整个 /carts 请求路径

feign:
  okhttp:
    enabled: true
  sentinel:
    # If true, an OpenFeign client will be wrapped with a Sentinel circuit breaker.
    enabled: true

⑤ config文件,cart-service默认扫描不到新加的 Configuration,因为和启动类不在同一个包下面,所以要做一下配置:

package com.hmall.cart;

import com.hmall.api.config.DefaultFeignConfig;
import com.hmall.api.config.FallbackConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(
        basePackages = "com.hmall.api.client",
        /*defaultConfiguration = DefaultFeignConfig.class) 这一行是旧配置 */
        defaultConfiguration = {DefaultFeignConfig.class, FallbackConfig .class})
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }
}

⑥ 添加流控规则

⑦ 开启 Jmeter 压力测试,不再直接失败,而是走 fallback 逻辑,只是查不到最新的商品信息:

ERROR 40596 --- [8082-exec-48] c.h.a.c.f.ItemClientFallbackFactory: 查询商品失败!
[
  {
    "id": "7",
    "itemId": "100000006163",
    "num": 1,
    "name": "巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)",
    "spec": "{}",
    "price": 67100,
    "newPrice": null,    // <---- 调用 item-service 但是失败了,所以为 null 
    "status": 1,
    "stock": 10,
    "image": "....",
    "createTime": "2023-05-20T21:07:09"
  }
]
1.2.7 服务熔断

以一个 Feign 远程调用为例,服务熔断是指当请求数超过流控规则的限制,就会熔断远程调用服务,即不再走远程调用,而是直接走 Fallback 逻辑。

熔断是解决雪崩问题的重要手段。思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求(直接走fallback);而当服务恢复时,断路器会放行访问该服务的请求。

Sentinel 配置如下:

注:熔断状态(Open)下,并不是直接失败,而是走 fallback 逻辑

二、分布式事务

 

This article was updated on