問題
在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個問題:
- 當流量高峰來臨,接口達到或者超過1000/s時,這時部分請求會被限流然后快速失敗,但因為是對整個接口做的限流,這時訪問非熱點商品,也可能出現限流,影響了用戶體驗
- 可能項目里會對熱點商品查詢做單獨的優化,比如緩存等,它比普通商品詳情接口而言能承擔更高的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-servlet
和sentinel-spring-webmvc-adapter
本身也不支持熱點參數限流,我們換一種思路通過擴展UrlCleaner
也實現了對熱點數據的限流,對保障業務穩定提供支持。