sentinel限流

一、简介

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();

    }

}

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值