因项目需求,在有大流量访问时,需要能够对流量进行降级,熔断,防止项目崩溃.目前常用的熔断工具有 Hystrix 和阿里的 Sentinel,这篇主要介绍Spring Boot项目中 Sentinel 和控制台的使用,以及搭配 Sentinel Dashboard 对流量进行视图化监控和降级规则设置.

Sentinel 简介

Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性, 是分布式系统的流量防卫兵. Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

Sentinel 的主要特性:

容器流量监控 流量监控功能_java

Spring Boot项目中配置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.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

<!-- 使用Web Filter过滤用到 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-web-servlet</artifactId>
    <version>1.8.0</version>
</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 中配置自定义降级熔断规则.

Sentinel Dashboard监控原理

  1. 首先我们自己项目的 sentinel-core 向 dashboard 发送心跳包
  2. dashboard 将 sentinel-core 的机器信息保存在内存中
  3. dashboard 根据 sentinel-core 的机器信息通过 httpClient 获取实时的数据
  4. sentinel-core 接收到请求之后,会找到具体的 CommandHandler 来处理
  5. sentinel-core 将处理好的结果返回给 dashboard
  6. dashboard 以图形化的形式展示数据

也就是说外界的请求是不经过 dashboard 的,dashboard 只负责向我们的项目请求访问数据并进行展示. 另外,我们在 dashboard 页面上配置的降级熔断等规则其实最终保存在我们自己项目的内存中, dashboard 只负责数据和规则的展示, 对流量进行降级的操作是在我们自己项目中进行的,所以如果重启 dashboard 项目,规则还是在的,但是如果重启我们自己项目,规则就清空了.

总结

  • 优点: Sentinel 使用还是比较方便的,对现有代码的改动也不大,熔断规则可以通过 dashboard 实时修改配置,并且还能实时看到接口的访问量,很适合非 IT 人员,像网管他们使用.
  • 缺点: 不得不吐槽的一点是对于不同的项目,Spring Web项目,Spring Cloud项目,Spring Boot项目等,想在 dashboard 里面成功监控到,对于新手太难了,哪里有个问题,就死活监控不到,需要自己慢慢去摸索.
  • 还有个问题就是 dashboard 页面上的监控数据只有短短几分钟,如果没有访问了,页面就一片空白,当然网上也有对应的解决方案,但使用成本又增加了.
  • 另外配置的熔断规则在重启项目后会丢失,如果监控的接口多就比较麻烦了,网上也有相应的持久化工具,可自行百度.

11/02/2020补充

目前使用注解形式的监控只能是一个 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 = "";


}
  • 主要是参照了 CommonFilter 这个Sentinel 提供的类,然后最关键的地方是下面这行代码的修改.
//urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
urlEntry = SphU.entry("/System/*", ResourceTypeConstants.COMMON_WEB, EntryType.IN);
  • 将 target ("/System/getName") 确切地址替换为 “/System/*”,这样我们在 dashboard 控制台上面配置"/System/ *"就能生效了,从而可以控制 /System/ 下的所有请求地址.
  • 而且如果要配置其他的 url 监控组,只要再新建一个 Web Filter 类,里面改下 监控 URL 就行了,是不是很酷.

dashboard 的流量控制规则配置如下

容器流量监控 流量监控功能_java_02

簇点链路展示如下:

容器流量监控 流量监控功能_System_03