问题
在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
也实现了对热点数据的限流,对保障业务稳定提供支持。