一、简介
sentinel限流支持流量控制(flow control)和热点参数限流(ParamFlowRule)。
流量控制的原理是监控应用流量的 QPS 或并发线程数等指标,当达到的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
二、流量控制(flow control)
官方文档:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
(一)基本元素
一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
1.resource:资源名,即限流规则的作用对象
2.count: 限流阈值
3.grade: 限流阈值类型(0代表根据并发数量限流,1代表根据 QPS 来进行流量控制)
4.limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
5.strategy: 调用关系限流策略
6.controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)
(二)QPS流量控制
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。
1.直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
2.Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
3.匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
三、热点参数限流(ParamFlowRule)
官网文档:https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
1.商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
2.用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
在我们的系统中,如果想通过ip或者用户来对接口进行限流,可以使用filter获取ip或者用户信息,并将IP和用户信息放入上下文,代码示例在ReqContextFilter中;
四、使用方法
(一)引入依赖jar
流量控制和热点参数限流均只需要引入以下依赖即可:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>1.8.0</version>
</dependency>
(二)初始化规则
1.在Apollo中配置规则,示例如下
配置限流开关:deploy.trafficControl.enabled = true
(1)# 热点参数限流
sentinel.paramFlowRules[0].resource = /dgg/index/gasprice
sentinel.paramFlowRules[0].count = 199
### 0=thread、1=qps
sentinel.paramFlowRules[0].grade = 1
### 0=直接拒绝、1=预热、2=匀速排队、3=预热匀速排队
sentinel.paramFlowRules[0].controlBehavior = 0
### 统计窗口时间长度,单位(秒)
sentinel.paramFlowRules[0].durationInSec = 60
### 热点参数的索引,0=IP、1=USER
sentinel.paramFlowRules[0].paramIdx = 0
(2)# 流量控制
sentinel.flowRules[0].resource = /dgg/activity/compro/fun
sentinel.flowRules[0].count = 5
### 0=thread,1=qps
sentinel.flowRules[0].grade = 1
### 0=直接拒绝,1=预热,2=匀速排队,3=预热匀速排队
sentinel.flowRules[0].controlBehavior = 0
2.使用SentinelConfig读取Apollo配置并初始化规则,代码如下:
@Data
@Component("sentinelConfig")
@ConfigurationProperties("sentinel")
public class SentinelConfig {
/**
* 熔断规则集合
*/
private List<DegradeRule> degradeRules;
/**
* 基本限流规则集合
*/
private List<FlowRule> flowRules;
/**
* 参数限流规则
*/
private List<ParamFlowRule> paramFlowRules;
@PostConstruct
private void init() {
DegradeRuleManager.loadRules(degradeRules);
FlowRuleManager.loadRules(flowRules);
ParamFlowRuleManager.loadRules(paramFlowRules);
}
}
(三)异常统一处理
@Slf4j
@Component
public class GlobalExceptionHandler implements HandlerExceptionResolver {
private final ModelAndView emptyMV = new ModelAndView();
public static final ObjectMapper JSON_MAPPER = new ObjectMapper();
static {
JSON_MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS);
}
/**
* Sentinel熔断限流异常处理
*/
public void handleBlockException(BlockException be) {
if (be instanceof DegradeException) {
/* 熔断异常处理 */
DegradeException de = (DegradeException) be;
DegradeRule rule = de.getRule();
String type = 0 == rule.getGrade() ? "SLOW_REQUEST_RATIO" : 1 == rule.getGrade() ? "ERROR_RATIO" : "ERROR_COUNT";
String value = rule.getResource();
log.error("[熔断降级]触发熔断", be);
} else if (be instanceof FlowException) {
/* 基本限流流控处理 */
log.warn("[本地流量控制]触发流控", be);
} else if (be instanceof ParamFlowException) {
/* 参数限流流控处理 */
String ip = ReqContext.get().getIp();
Long userId = ReqContext.get().getUserId();
String uri = ReqContext.get().getRequest().getRequestURI();
ParamFlowException pfe = (ParamFlowException) be;
ParamFlowRule rule = (ParamFlowRule) be.getRule();
String type = rule.getParamIdx() == 1 ? TrafficControlType.USER.name() : TrafficControlType.IP.name();
String value = rule.getParamIdx() == 1 ? String.valueOf(userId) : ip;
try {
TrafficInterceptor.insertIntoMDC(type, value);
log.warn("[本地参数限流]触发流控", be);
} finally {
TrafficInterceptor.clearMDC();
}
} else {
log.error("未处理的BlockException", be);
}
}
@Override
public ModelAndView resolveException( HttpServletRequest request,
HttpServletResponse response, Object handler,
Exception ex) {
try {
R result = handle(request, ex);
response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString());
response.getWriter().write(JSON_MAPPER.writeValueAsString(result));
} catch (IOException e) {
log.warn("resolveException", e);
}
return emptyMV;
}
public R<Void> handle(HttpServletRequest request, Exception e) {
int code = 500;
log.warn("全局异常, uri={}, errorMessage={}", request.getRequestURI(), e.getMessage(), e);
if (e instanceof BlockException) {
// 限流
handleBlockException((BlockException) e);
code = 510;
} else {
String uri = request.getRequestURI();
log.error("服务器错误, uri={}, reqContent={}", uri, e);
code = 500;
}
return R.failed(code, "接口限流");
}
}
(四)使用拦截器统计限流规则
@Component
@Slf4j
public class TrafficInterceptor extends HandlerInterceptorAdapter {
@Value("${deploy.trafficControl.enabled:true}")
private boolean enabled;
@Autowired
private GlobalExceptionHandler exceptionHandler;
@Override
public boolean preHandle( HttpServletRequest req, HttpServletResponse resp, Object handler) {
boolean result = true;
if (enabled && (handler instanceof HandlerMethod)) {
String uri = String.valueOf(req.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
if (ParamFlowRuleManager.hasRules(uri)) {
// 判断是否执行本地参数限流[内存计数,规则apollo配置动态加载]
result = controlParamFlow(req, resp, uri);
}
if (result && FlowRuleManager.hasConfig(uri)) {
result = controlFlow(req, resp, uri);
}
}
return result;
}
// 热点参数限流
private boolean controlParamFlow(HttpServletRequest request, HttpServletResponse response, String uri) {
String ip = ReqContext.get().getIp();
Long userId = ReqContext.get().getUserId();
Entry entry = null;
try {
// 如果需要对其他参数进行限流,可在userid后追加参数,同时在热点参数索引中设置限流的热点参数:sentinel.paramFlowRules[0].paramIdx
entry = SphU.entry(uri, EntryType.IN, 1, ip, userId);
} catch (BlockException e) {
exceptionHandler.resolveException(request, response, null, e);
return false;
} finally {
if (entry != null) {
entry.exit(1, ip, userId);
}
}
return true;
}
private boolean controlFlow(HttpServletRequest request, HttpServletResponse response, String uri) {
Entry entry = null;
try {
entry = SphU.entry(uri, EntryType.IN, 1);
} catch (BlockException e) {
exceptionHandler.resolveException(request, response, null, e);
return false;
} finally {
if (entry != null) {
entry.exit(1);
}
}
return true;
}
public static void insertIntoMDC(String type, String value) {
MDC.put("traffic_type", type);
MDC.put("traffic_value", value);
}
public static void clearMDC() {
MDC.remove("traffic_type");
MDC.remove("traffic_value");
}
}
(五)Apollo配置刷新
@Slf4j
@Component
public class SpringBootApolloRefreshConfig {
private SentinelConfig sentinelConfig;
// 监听Apollo配置变化
@ApolloConfigChangeListener(value = {"application", "dubbo", "huobiee-defilayer-app-common", "application-uc"}, interestedKeyPrefixes = {"sentinel."})
public void onSentinelChange(ConfigChangeEvent event) {
// 刷新配置
refreshScope.refresh("sentinelConfig");
log.info("sentinel config init, config={}", sentinelConfig.toString());
}
}
(六)获取ip并将IP放入上下文
@Slf4j
public class ReqContextFilter extends OncePerRequestFilter {
@Autowired
private GlobalExceptionHandler globalExceptionHandler;
private static final String UNKNOWN_IP = "unknown";
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) {
ReqContext ctx = ReqContext.get();
try {
String ip = getIp(req);
ctx.setIp(ip);
ctx.setRequest(req);
ctx.setResponse(resp);
ctx.setUri(req.getRequestURI());
chain.doFilter(req, resp);
} catch (Exception e) {
log.error("ReqContextFilter Exception", e);
globalExceptionHandler.resolveException(req, resp, null, e);
} catch (Throwable e) {
log.error("ReqContextFilter Exception", e);
}finally {
if (null != ReqContext.get() ) {
ReqContext.remove();
}
}
}
public static String getIp(HttpServletRequest req) {
String ip = StringUtils.trimAllWhitespace(req.getHeader("HB-REAL-IP"));
if (null != ip) {
return ip;
}
ip = req.getHeader("x-connecting-ip");
if (null != ip && !UNKNOWN_IP.equalsIgnoreCase(ip)) {
return ip;
}
// 获取第一个有效ip
ip = StringUtils.trimAllWhitespace(req.getHeader("X-Forwarded-For"));
if (null != ip && !UNKNOWN_IP.equalsIgnoreCase(ip)) {
for (String item : StringUtils.split(ip.replaceAll("[ \t]", ""), ",")) {
if (UNKNOWN_IP.equalsIgnoreCase(item)) {
continue;
}
return item;
}
}
ip = StringUtils.trimAllWhitespace(req.getHeader("X-Real-IP"));
if (null != ip && !UNKNOWN_IP.equalsIgnoreCase(ip)) {
return ip;
}
// 对端IP
return req.getRemoteAddr();
}
}