因项目需求,在有大流量访问时,需要能够对流量进行降级,熔断,防止项目崩溃.目前常用的熔断工具有 Hystrix 和阿里的 Sentinel,这篇主要介绍Spring Boot项目中 Sentinel 和控制台的使用,以及搭配 Sentinel Dashboard 对流量进行视图化监控和降级规则设置.
Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性, 是分布式系统的流量防卫兵.
Sentinel 具有以下特征:
Sentinel 分为两个部分:
1.去 github 官网下载Sentinel Dashboard 的可运行 jar 包
https://github.com/alibaba/Sentinel/releases
2.使用命令窗口启动 Sentinel Dashboard jar 包
java -jar sentinel-dashboard-1.8.0.jar --server.port=18080
3.在自己项目中引入 Sentinel 的 Spring Cloud jar 包
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
<version>2.2.1.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-web-servletartifactId>
<version>1.8.0version>
dependency>
4.在自己项目里配置 Sentinel Dashboard 的项目访问地址
#指定dashboard项目端口
spring.cloud.sentinel.transport.dashboard=192.168.1.166:18080
#默认8721,集群下使用,指定自己本项目的通信端口,防止和其他需要被监控项目冲突
spring.cloud.sentinel.transport.port=8722
#设置本项目名称
spring.application.name=JMBInterface
5.使用 @SentinelResource 注解在自己项目中配置需要进行监控的接口.
注意事项: 配置的两个 blockHandler 和 fallback 方法的形参和返回值需要与被标注的 service 方法完全一致,否则会报错或不能执行熔断后的方法.
// blockHandler对应降级熔断后执行的方法, fallback对应捕获异常后执行的方法
@SentinelResource(value = "hiService", blockHandler = "exceptionHandler", fallback = "cut")
public String hiService(String name) throws Exception {
//throw new Exception("test exception");
Thread.sleep(3000);
System.out.println("testHystrix");
return "success";
}
// Block 降级熔断后执行的方法.
public String exceptionHandler(String name, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return "exceptionHandler";
}
// Fallback 捕获异常后执行的方法.
public String cut(String name, Throwable throwable) {
throwable.printStackTrace();
return "cut";
}
6.启动自己项目,手动调用一次被标注的 service 方法,这样 Sentinel Dashboard 控制台才能监控到我们的项目,另外 Sentinel Dashboard 部署的服务器和我们自己项目部署的服务器的系统时间不一致也会导致监控不到,其次,不要在自己项目中手动配置降级熔断规则,如果配置了,Sentinel Dashboard 也会监控不到我们的项目.
7.在 Sentinel Dashboard 中配置自定义降级熔断规则.
也就是说外界的请求是不经过 dashboard 的,dashboard 只负责向我们的项目请求访问数据并进行展示.
另外,我们在 dashboard 页面上配置的降级熔断等规则其实最终保存在我们自己项目的内存中, dashboard 只负责数据和规则的展示, 对流量进行降级的操作是在我们自己项目中进行的,所以如果重启 dashboard 项目,规则还是在的,但是如果重启我们自己项目,规则就清空了.
目前使用注解形式的监控只能是一个 url 对应一个流量控制规则, 而如果我想一个流量控制规则同时监控两个 url,就无能为力了,这时需要使用 Web Filter 来达成目的.
过滤器代码如下:
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.ResourceTypeConstants;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager;
import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig;
import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(urlPatterns = "/System/*")
public class Filter2 implements Filter {
/**
* Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}).
*/
public static final String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY";
/**
* If enabled, use the default context name, or else use the URL path as the context name,
* {@link WebServletConfig#WEB_SERVLET_CONTEXT_NAME}. Please pay attention to the number of context (EntranceNode),
* which may affect the memory footprint.
*
* @since 1.7.0
*/
public static final String WEB_CONTEXT_UNIFY = "WEB_CONTEXT_UNIFY";
private final static String COLON = ":";
private boolean httpMethodSpecify = false;
private boolean webContextUnify = true;
@Override
public void init(FilterConfig filterConfig) {
httpMethodSpecify = Boolean.parseBoolean(filterConfig.getInitParameter(HTTP_METHOD_SPECIFY));
if (filterConfig.getInitParameter(WEB_CONTEXT_UNIFY) != null) {
webContextUnify = Boolean.parseBoolean(filterConfig.getInitParameter(WEB_CONTEXT_UNIFY));
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest sRequest = (HttpServletRequest) request;
Entry urlEntry = null;
try {
String target = FilterUtil.filterTarget(sRequest);
//target = target.su
// Clean and unify the URL.
// For REST APIs, you have to clean the URL (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or
// the amount of context and resources will exceed the threshold.
/*UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner();
if (urlCleaner != null) {
target = urlCleaner.clean(target);
}*/
// If you intend to exclude some URLs, you can convert the URLs to the empty string ""
// in the UrlCleaner implementation.
if (!StringUtil.isEmpty(target)) {
// Parse the request origin using registered origin parser.
String origin = parseOrigin(sRequest);
String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target;
ContextUtil.enter(contextName, origin);
if (httpMethodSpecify) {
// Add HTTP method prefix if necessary.
String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target;
urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
} else {
//urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
urlEntry = SphU.entry("/System/*", ResourceTypeConstants.COMMON_WEB, EntryType.IN);
}
}
chain.doFilter(request, response);
} catch (Exception e) {
e.printStackTrace();
// 将熔断信息返回给调用者
ServletOutputStream servletOutputStream = response.getOutputStream();
response.setCharacterEncoding("UTF-8");
servletOutputStream.write("CUT".getBytes("UTF-8"));
servletOutputStream.flush();
servletOutputStream.close();
} finally {
if (urlEntry != null) {
urlEntry.exit();
}
ContextUtil.exit();
}
}
private String parseOrigin(HttpServletRequest request) {
RequestOriginParser originParser = WebCallbackManager.getRequestOriginParser();
String origin = EMPTY_ORIGIN;
if (originParser != null) {
origin = originParser.parseOrigin(request);
if (StringUtil.isEmpty(origin)) {
return EMPTY_ORIGIN;
}
}
return origin;
}
@Override
public void destroy() {
}
private static final String EMPTY_ORIGIN = "";
}
//urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
urlEntry = SphU.entry("/System/*", ResourceTypeConstants.COMMON_WEB, EntryType.IN);