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
也實現了對熱點數據的限流,對保障業務穩定提供支持。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM