Sentinel实现热点数据限流

问题

在Sentinel社区里看到一个问题,CommonFilter是否支持热点限流?
问题链接:https://github.com/alibaba/Sentinel/issues/2014

答案是不支持。
因为CommonFilter源码里标记资源SphU.entry(String, int, EntryType),并没有像sentinel-dubbo-adaptor里的SentinelDubboProviderFilter那样通过4个参数重载的方法SphU.entry(java.lang.String, int, EntryType, java.lang.Object[])来标记资源,即传递接口相关的参数,因此它不能使用热点参数规则。

场景

实际项目中对热点数据限流的需求很常见。比如电商业务里的商品详情页,通过流量高峰来源于热点商品,如做促销活动的商品、预约抢购的商品、最近关注度高卖的很火的商品等,它的某时段访问量会比普通商品高很多。

假设商品详情页接口为/product/detail
我们对它设置限流规则,保护接口不被突发的流量击垮。
如果对整个接口设置,假定接口支持最大qps=1000/s,那么有2个问题:

  1. 当流量高峰来临,接口达到或者超过1000/s时,这时部分请求会被限流然后快速失败,但因为是对整个接口做的限流,这时访问非热点商品,也可能出现限流,影响了用户体验
  2. 可能项目里会对热点商品查询做单独的优化,比如缓存等,它比普通商品详情接口而言能承担更高的qps阈值,对整个接口设置限流阈值粒度太粗,设高了可能应用拖垮,设低了优化后的程序没利用起来

原因

Sentinel的热点限流规则本来是用于热点数据场景的,但目前对sentinel-web-servlet(基于普通servlet)和sentinel-spring-webmvc-adapter(基于springmvc)两种适配都不支持。
不支持的原因可能是:
对于http request请求,不同项目可能获取参数的方式不一样。比如:
有的是get请求,参数在url里;
有的是post请求,参数在body里;
有的参数是form data形式;
有的参数是json格式;
有的参数就一个,比如body里有个data参数,data里面是具体的json格式参数;
有的不区分get/post;
理论上说,如果项目的请求参数格式统一,应该可以按某个标准统一获取参数,最后转换为Object[] args形式。

思路

sentinel-web-servlet模块提供了UrlCleaner扩展,
参考:https://github.com/alibaba/Sentinel/wiki/主流框架的适配#web-servlet

它可用于清洗或者过滤资源(比如将满足 /foo/:id 的 URL 都归到 /foo/* 资源下,比如通过返回""排除某个URL)
如果换个思路,基于它扩展也可实现热点参数限流。比如想对某个热点商品限流,实现一个自定义的UrlCleaner接口,
里面获取到热点商品id参数,返回带上商品id的特定URL,这样生成新的资源,结合控制台就可以单独设置该URL的限流规则
如:普通商品详情页的URL为:/product/detail,热点商品详情页URL为:/product/detail?id=xxx
然后对两个URL设置普通流控规则就好。
因为热点商品是单独的资源了,也可设置其它规则,比如降级规则。

实战

定义接口UrlParser:

/**
 * @author cdfive 
 */
public interface UrlParser {
    
    /**
     * 需要处理的url
     * 这里返回一个列表,因为可能一个业务对应多个接口,而处理逻辑一致
     * 如商品详情页,APP呈现该页面调了多个商品详情相关的接口,如:
     * /product/detail 获取商品详情信息
     * /prdouct/detail/promotion 获取商品可参与的促销活动
     * /product/detail/evalution 获取商品的评论
     */    
    List<String> getUrls();

    /**
     * 解析url生成需要的资源名
     * 这里可根据业务情况灵活处理,如调另接口查询哪些是热点商品,从缓存中取数据等
     */
    String parseUrl(String originUrl);
}

定义抽象类AbstractUrlParser,包括获取HttpServletRequest对象,拼接参数等公共方法:

/**
 * base class for UrlParser
 *
 * @author cdfive
 */
public abstract class AbstractUrlParser implements UrlParser {

    protected Logger log = LoggerFactory.getLogger(getClass());

    /**
     * separator between url and parameter
     */
    protected static final String URL_SEPERATOR = "#";

    /**
     * separator between parameter name and value
     */
    protected static final String PARAM_VALUE_SEPERATOR = "=";

    /**
     * separator between different parameters
     */
    protected static final String OTHER_PARAM_SEPERATOR = "&";

    /**
     * append parameters after url, including parameter name and parameter value
     */
    protected String appendUrlParam(String originUrl, String paramName, String paramValue) {
        return originUrl + URL_SEPERATOR + paramName + PARAM_VALUE_SEPERATOR + paramValue;
    }

    /**
     * batch append parameters after url, including parameter name and parameter value
     */
    protected String appendUrlParams(String originUrl, List<String> paramNames, List<String> paramValues) {
        StringBuilder newUrl = new StringBuilder(originUrl).append(URL_SEPERATOR);
        for (int i = 0; i < paramNames.size(); i++) {
            if (i > 0) {
                newUrl.append(OTHER_PARAM_SEPERATOR);
            }
            newUrl.append(paramNames.get(i)).append(PARAM_VALUE_SEPERATOR).append(paramValues.get(i));
        }
        return newUrl.toString();
    }

    /**
     * get HttpServletRequest object
     */
    protected HttpServletRequest getHttpServletRequest() {
        try {
            return ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        } catch (Exception e) {
            log.error("get HttpServletRequest error", e);
            return null;
        }
    }
}

对商品详情页接口定制处理,调商品服务ProductService查询哪些是热点商品:

/**
 * Custom UrlParser for urls of product detail.
 *
 * @author cdfive
 */
@Component
public class ProductDetailUrlParser extends AbstractUrlParser {

    private static final String URL_PRODUCT_DETAIL = "/product/detail";
    private static final String URL_PRODUCT_DETAIL_PROMOTION = "/product/detail/promotion";
    private static final String URL_PRODUCT_DETAIL_EVALUTION = "/product/detail/evalution";

    private static final List<String> URLS = new ArrayList<String>() {
        {
            add(URL_PRODUCT_DETAIL);
            add(URL_PRODUCT_DETAIL_PROMOTION);
            add(URL_PRODUCT_DETAIL_EVALUTION);
        }
    };

    private static final String PARAM_NAME_PRODUCT_ID = "productId";
    private static final String PARAM_URL_PRODUCT_ID = "id";

    @Autowired
    private ProductService productService;

    @Override
    public List<String> getUrls() {
        return URLS;
    }

    @Override
    public String parseUrl(String originUrl) {
        HttpServletRequest request = super.getHttpServletRequest();
        if (request == null) {
            return originUrl;
        }

        String productIdStr = request.getParameter(PARAM_NAME_PRODUCT_ID);
        if (StringUtil.isBlank(productIdStr)) {
            log.error("ProductDetailUrlParser parameter productId is blank");
            return originUrl;
        }

        Long productId;
        try {
            productId = Long.parseLong(productIdStr);
        } catch (NumberFormatException e) {
            log.error("ProductDetailUrlParser parameter productId is invalid");
            return originUrl;
        }

        if (!productService.isHotProduct(productId)) {
            return originUrl;
        }

        return super.appendUrlParam(originUrl, PARAM_URL_PRODUCT_ID, String.valueOf(productId));
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    private static class IdVo implements Serializable {

        private String id;
    }
}

定义CustomUrlCleaner类,实现UrlCleaner接口,里面通过UrlParser来处理:

/**
 * @author cdfive
 */
public class CustomUrlCleaner implements UrlCleaner {

    private static final Logger log = LoggerFactory.getLogger(CustomUrlCleaner.class);

    private List<UrlParser> urlParsers = new ArrayList<>();

    @Override
    public String clean(String originUrl) {
        if (urlParsers.isEmpty()) {
            return originUrl;
        }

        for (UrlParser urlParser : urlParsers) {
            if (urlParser.getUrls() != null && urlParser.getUrls().contains(originUrl)) {
                try {
                    return urlParser.parseUrl(originUrl);
                } catch (Exception e) {
                    log.error("urlParser[{}] parse url[{}] error", urlParser.getClass().getSimpleName(), originUrl, e);
                    return originUrl;
                }
            }
        }

        return originUrl;
    }

    public List<UrlParser> getUrlParsers() {
        return urlParsers;
    }

    public void setUrlParsers(List<UrlParser> urlParsers) {
        this.urlParsers = urlParsers;
    }
}

通过以上步骤后,再访问商品详情页接口(假设热点商品id=2001,普通商品id=2002),在sentinel控制台的簇点链路菜单里可以看到,
当商品是热点商品时生成了单独的资源/product/detail#id=2001
当商品是普通商品时资源名为/prdouct/detail/
我们可对/product/detail#id=2001单独设置流控、降级等流控规则,并不会影响到普通商品。

跟sentinel的热点参数限流相比,缺点是需要编码优点是处理时灵活,通过UrlParser的抽象,不同业务可单独实现自己需要的定制逻辑而相互
不影响,如商品详情用ProductDetailUrlParser实现,提交订单用SubmitOrderUrlParser实现。

因为sentinel-web-servletsentinel-spring-webmvc-adapter本身也不支持热点参数限流,我们换一种思路通过扩展UrlCleaner
也实现了对热点数据的限流,对保障业务稳定提供支持。

原文地址:https://www.cnblogs.com/cdfive2018/p/14704992.html