前文中我们提到 Netflix 中多项开源产品已进入维护阶段,不再开发新的版本,就目前来看是没有什么问题的。但是从长远角度出发,我们还是需要考虑是否有可替代产品使用。比如本文中要介绍的 Alibaba Sentinel 就是一款高性能且轻量级的流量控制、熔断降级可替换方案。
Sentinel 官网:https://github.com/alibaba/Sentinel
「Hystrix 目前状态」
官网提示:https://github.com/Netflix/Hystrix
❝
Hystrix is no longer in active development, and is currently in maintenance mode.
Hystrix 不再主动开发,当前处于维护模式。
Sentinel 是什么
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
「Sentinel 主要特征」
「Sentinel 开源生态」
Sentinel 目前已经针对 Servlet、Dubbo、Spring Boot/Spring Cloud、gRPC 等进行了适配,用户只需引入相应依赖并进行简单配置即可非常方便地享受 Sentinel 的高可用流量防护能力。Sentinel 还为 Service Mesh 提供了集群流量防护的能力。未来 Sentinel 还会对更多常用框架进行适配。
「Sentinel 分为两个部分」:
Sentinel 的历史
Sentinel vs Hystrix
Hystrix
官网:https://github.com/Netflix/Hystrix
❝
Hystrix is a latency and fault tolerance library designed to isolate points of access to remote systems, services and 3rd party libraries, stop cascading failure and enable resilience in complex distributed systems where failure is inevitable.
❞
Hystrix 的关注点在于隔离和熔断为主的容错机制,超时或被熔断的调用将会快速失败,并可以提供 fallback 机制。
Sentinel
官网:https://github.com/alibaba/Sentinel
Sentinel 的关注点在于:
多样化的流量控制
熔断降级
系统负载保护
实时监控和控制台
「Hystrix 迁移 Sentinel 方案」
Sentinel 提供了从 Hystrix 迁移到 Sentinel 的方案,官网:https://github.com/alibaba/Sentinel/wiki/Guideline:-从-Hystrix-迁移到-Sentinel
Sentinel 核心
Sentinel 的使用可以分为两个部分:
Sentinel 控制台
Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。
官网文档:https://github.com/alibaba/Sentinel/wiki/控制台
获取控制台
您可以从 release 页面 下载最新版本的控制台 jar 包。
您也可以从最新版本的源码自行构建 Sentinel 控制台:
启动控制台
启动命令如下,本文使用的是目前最新 1.7.1 版本:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.1.jar
「注意」:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
其中 -Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080。
从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的「登录」功能,默认用户名和密码都是 sentinel。可以参考 鉴权模块文档 配置用户名和密码。
注:若您的应用为 Spring Boot 或 Spring Cloud 应用,您可以通过 Spring 配置文件来指定配置,详情请参考 Spring Cloud Alibaba Sentinel 文档。
了方便启动,可以编写一个启动脚本 run.bat:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.1.jar
pause
访问
访问:http://localhost:8080/
输入默认用户名和密码 sentinel 点击登录。至此控制台就安装完成了。
环境准备
sentinel-demo 聚合工程。SpringBoot 2.2.4.RELEASE、Spring Cloud Hoxton.SR1。
先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。
由于我们的项目是 Spring Cloud 项目所以借助官方文档来进行学习。
Spring 官网文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
Github 文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
添加依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
子工程需要添加如下依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置文件
客户端需要启动 Transport 模块来与 Sentinel 控制台进行通信。
order-service-rest 的 application.yml
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8080
这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。
初始化客户端
「确保客户端有访问量」,Sentinel 会在「客户端首次调用的时候」进行初始化,开始向控制台发送心跳包。
简单的理解就是:访问一次客户端,Sentinel 即可完成客户端初始化操作,并持续向控制台发送心跳包。
访问
首先确保 Sentinel 是启动状态,然后依次启动 eureka-server,eureka-server02,product-service,order-service-rest。
多次访问:http://localhost:9090/order/1 然后查看控制台实时监控结果如下:
定义资源
「资源」 是 Sentinel 中的核心概念之一。我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。最常用的资源是我们代码中的 Java 方法。Sentinel 提供了 @SentinelResource 注解用于定义资源,并提供了 AspectJ 的扩展用于自动定义资源、处理 BlockException 等。
官网文档:https://github.com/alibaba/Sentinel/wiki/如何使用#定义资源
注解支持
官网文档:https://github.com/alibaba/Sentinel/wiki/注解支持
package com.example.service.impl;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 商品管理
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private RestTemplate restTemplate;
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@SentinelResource(value = "selectProductById",
blockHandler = "selectProductByIdBlockHandler", fallback = "selectProductByIdFallback")
@Override
public Product selectProductById(Integer id) {
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}
// 服务流量控制处理,参数最后多一个 BlockException,其余与原函数一致。
public Product selectProductByIdBlockHandler(Integer id, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return new Product(id, "服务流量控制处理-托底数据", 1, 2666D);
}
// 服务熔断降级处理,函数签名与原函数一致或加一个 Throwable 类型的参数
public Product selectProductByIdFallback(Integer id, Throwable throwable) {
System.out.println("product-service 服务的 selectProductById 方法出现异常,异常信息如下:"
+ throwable);
return new Product(id, "服务熔断降级处理-托底数据", 1, 2666D);
}
}
注意:注解方式埋点不支持 private 方法。
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。@SentinelResource 注解包含以下属性:
注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,「不能针对业务异常进行处理」。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandler、fallback 和 defaultFallback,则被限流降级时会将 BlockException 「直接抛出」(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。
从 1.4.0 版本开始,注解方式定义资源支持自动统计业务异常,无需手动调用 Tracer.trace(ex) 来记录业务异常。Sentinel 1.4.0 以前的版本需要自行调用 Tracer.trace(ex) 来记录业务异常。
定义规则
Sentinel 的所有规则都可以在「内存态中动态地查询及修改,修改之后立即生效」。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。
Sentinel 支持以下几种规则:「流量控制规则」、「熔断降级规则」、「系统保护规则」、「来源访问控制规则」 和 「热点参数规则」。
官网文档:https://github.com/alibaba/Sentinel/wiki/如何使用#规则的种类
流量控制规则
选择 簇点链路 找到定义好的资源 selectProductById 并点击对应的规则按钮进行设置。
比如我们设置一个流量控制规则,定义资源访问的 QPS 为 1(每秒能处理查询数目)。
「测试」
快速刷新页面多次访问:http://localhost:9090/order/1 结果如下:
熔断降级规则
「模拟服务出错」
修改 order-service-rest 项目中的核心代码,模拟服务出错。
package com.example.service.impl;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 商品管理
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private RestTemplate restTemplate;
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@SentinelResource(value = "selectProductById",
blockHandler = "selectProductByIdBlockHandler", fallback = "selectProductByIdFallback")
@Override
public Product selectProductById(Integer id) {
// 模拟查询主键为 1 的商品信息会导致异常
if (1 == id)
throw new RuntimeException("查询主键为 1 的商品信息导致异常");
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}
// 服务流量控制处理,参数最后多一个 BlockException,其余与原函数一致。
public Product selectProductByIdBlockHandler(Integer id, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return new Product(id, "服务流量控制处理-托底数据", 1, 2666D);
}
// 服务熔断降级处理,函数签名与原函数一致或加一个 Throwable 类型的参数
public Product selectProductByIdFallback(Integer id, Throwable throwable) {
System.out.println("product-service 服务的 selectProductById 方法出现异常,异常信息如下:"
+ throwable);
return new Product(id, "服务熔断降级处理-托底数据", 1, 2666D);
}
熔断降级规则支持相应时间、异常比例、异常数三种方式。
「测试」
访问:http://localhost:9090/order/1 结果如下:
动态规则扩展
官网文档:
SentinelProperties 内部提供了 TreeMap 类型的 datasource 属性用于配置数据源信息。支持:
文件配置规则
Sentinel 支持通过本地文件加载规则配置,使用方式如下(限流规则作为演示):
spring:
cloud:
sentinel:
datasource:
ds1:
file:
file: classpath:flowRule.json
data-type: json
rule-type: flow
flowRule.json 对应 com.alibaba.csp.sentinel.slots.block.RuleConstant 各属性。
[
{
"resource": "selectProductList",
"count": 1,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0
}
]
重要属性:
访问客户端以后,刷新控制台,查看流控规则如下:
RestTemplate 支持
Spring Cloud Alibaba Sentinel 支持对 RestTemplate 调用的服务进行服务保护。需要在构造 RestTemplate Bean 时添加 @SentinelRestTemplate 注解。
启动类
OrderServiceRestApplication.java
package com.example;
import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate;
import com.example.exception.ExceptionUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class OrderServiceRestApplication {
@Bean
@LoadBalanced
@SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class,
fallback = "fallback", fallbackClass = ExceptionUtil.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderServiceRestApplication.class, args);
}
}
服务熔断处理类
ExceptionUtil.java 必须使用静态方法。
package com.example.exception;
import com.alibaba.cloud.sentinel.rest.SentinelClientHttpResponse;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.example.pojo.Product;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;
public class ExceptionUtil {
// 服务流量控制处理
public static ClientHttpResponse handleException(HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution,
BlockException exception) {
exception.printStackTrace();
return new SentinelClientHttpResponse(
JSON.toJSONString(new Product(1, "服务流量控制处理-托底数据", 1, 2666D)));
}
// 服务熔断降级处理
public static ClientHttpResponse fallback(HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution,
BlockException exception) {
exception.printStackTrace();
return new SentinelClientHttpResponse(
JSON.toJSONString(new Product(1, "服务熔断降级处理-托底数据", 1, 2666D)));
}
}
访问
控制台设置流量控制规则,定义资源访问的 QPS 为 1(每秒能处理查询数目)。
快速刷新页面多次访问:http://localhost:9090/order/1 结果如下:
OpenFeign 支持
其实不管是 Hystrix 还是 Sentinel 对于 Feign 的支持,核心代码基本上是一致的,只需要修改依赖和配置文件即可。
添加依赖
<!-- spring cloud openfeign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- spring cloud alibaba sentinel 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
开启 Sentinel
server:
port: 9091 # 端口
spring:
application:
name: order-service-feign # 应用名称
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8080
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
# feign 开启 sentinel 支持
feign:
sentinel:
enabled: true
熔断降级
ProductServiceFallback.java
package com.example.fallback;
import com.example.pojo.Product;
import com.example.service.ProductService;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 服务熔断降级处理可以捕获异常
*/
@Component
public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> {
// 获取日志,在需要捕获异常的方法中进行处理
Logger logger = LoggerFactory.getLogger(ProductServiceFallbackFactory.class);
@Override
public ProductService create(Throwable throwable) {
return new ProductService() {
@Override
public Product selectProductById(Integer id) {
logger.error("product-service 服务的 selectProductById 方法出现异常,异常信息如下:"
+ throwable);
return new Product(id, "托底数据", 1, 2666D);
}
};
}
}
消费服务
ProductService.java
package com.example.service;
import com.example.fallback.ProductServiceFallbackFactory;
import com.example.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 声明需要调用的服务
@FeignClient(value = "product-service", fallbackFactory = ProductServiceFallbackFactory.class)
public interface ProductService {
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@GetMapping("/product/{id}")
Product selectProductById(@PathVariable("id") Integer id);
}
OrderServiceImpl.java
package com.example.service.impl;
import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private ProductService productService;
/**
* 根据主键查询订单
*
* @param id
* @return
*/
@Override
public Order selectOrderById(Integer id) {
return new Order(id, "order-001", "中国", 2666D,
Arrays.asList(productService.selectProductById(1)));
}
}
控制层
package com.example.controller;
import com.example.pojo.Order;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 根据主键查询订单
*
* @param id
* @return
*/
@GetMapping("/{id}")
public Order selectOrderById(@PathVariable("id") Integer id) {
return orderService.selectOrderById(id);
}
}
启动类
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
// 开启 FeignClients 注解
@EnableFeignClients
@SpringBootApplication
public class OrderServiceFeignApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceFeignApplication.class, args);
}
}
测试
控制台信息如下:
添加流量控制规则,定义资源访问的 QPS 为 1(每秒能处理查询数目)。
快速刷新页面多次访问:http://localhost:9091/order/1 结果如下:
或者关闭服务提供者,访问:http://localhost:9091/order/1 结果如下: