一, Service Provider对外界提供服务,基于QPS模式限流

Service Provider用于对外提供服务, 处理各个消费者的调用请求。为了保护自己作为Provider端的服务不被激增的流量拖垮影响稳定性,可以给 Provider 配置 QPS 模式的限流,这样当每秒的请求量超过设定的阈值时会自动拒绝多的请求。

Sentinel的限流粒度可以是 服务接口 和 服务方法 两种粒度。

① 服务接口资源粒度:若希望整个服务接口的 QPS 不超过一定数值,则可以为对应服务接口资源(resourceName 为接口全限定名)配置 QPS 阈值;

② 服务方法资源粒度:若希望服务的某个方法的 QPS 不超过一定数值,则可以为对应服务方法资源(resourceName 为接口全限定名:方法签名)配置 QPS 阈值。

为什么一定要配置为对应服务接口的权限定名呢?

@Activate(group = "provider")
public class SentinelDubboProviderFilter extends AbstractDubboFilter implements Filter {

    public SentinelDubboProviderFilter(){
        RecordLog.info("Sentinel Dubbo provider filter initialized");
    }

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // Get origin caller.
        String application = DubboUtils.getApplication(invocation, "");

        Entry interfaceEntry = null;
        Entry methodEntry = null;
        try {
            String resourceName = getResourceName(invoker, invocation);
            String interfaceName = invoker.getInterface().getName();
            ContextUtil.enter(resourceName, application);
            // 服务接口级别限流
            interfaceEntry = SphU.entry(interfaceName, EntryType.IN);
            // 服务方法级别限流
            methodEntry = SphU.entry(resourceName, EntryType.IN, 1, invocation.getArguments());
            // 真正执行
            Result result = invoker.invoke(invocation);
            if (result.hasException()) {
                Tracer.trace(result.getException());
            }
            return result;
        } catch (BlockException e) {
            return DubboFallbackRegistry.getProviderFallback().handle(invoker, invocation, e);
        } catch (RpcException e) {
            Tracer.trace(e);
            throw e;
        } finally {
            if (methodEntry != null) {
                methodEntry.exit(1, invocation.getArguments());
            }
            if (interfaceEntry != null) {
                interfaceEntry.exit();
            }
            ContextUtil.exit();
        }
    }
}

通过查看Sentinel提供的对dubbo框架的适配源码可知, 通过getSourceName()方法, 从Invoker和invocation对象中获取本次调用服务接口的全限定名称, 方法名称, 方法参数类型.

abstract class AbstractDubboFilter implements Filter {

    protected String getResourceName(Invoker<?> invoker, Invocation invocation) {
        StringBuilder buf = new StringBuilder(64);
        buf.append(invoker.getInterface().getName())
            .append(":")
            .append(invocation.getMethodName())
            .append("(");
        boolean isFirst = true;
        for (Class<?> clazz : invocation.getParameterTypes()) {
            if (!isFirst) {
                buf.append(",");
            }
            buf.append(clazz.getName());
            isFirst = false;
        }
        buf.append(")");
        return buf.toString();
    }
}

getSourceName方法: 通过Invoker对象获取服务接口全限定名称, 方法名称, 再从invocation对象中获取本次调用的服务方法的参数类型, 最终拼接成形如 : com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)这样格式的字符串, 并返回作为Sentinel限流的资源名称.

了解了配置原理后, 下面就分为服务方法粒度和服务接口粒度配置即可.

 

1 服务方法粒度

① 首先按照上面介绍的规则定义服务方法级别的资源名称, 如下:

private static final String sourceName = "com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)";

② 初始化该服务方法的流控规则; 阈值为10, 基于QPS流控, 超过阈值的流量采用默认的直接拒绝方式, 

即 flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); 

private static void initFlowRule() {
        FlowRule flowRule = new FlowRule();
        flowRule.setResource(sourceName);
        flowRule.setCount(10);
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        flowRule.setLimitApp("default");
        flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 默认:快速失败/立即拒绝
        FlowRuleManager.loadRules(Collections.singletonList(flowRule));
    }

并在构造方法中调用上面定义的流控规则:

public Demo1FooServiceImpl(){
        // 初始化接口级别限流
        // initFlowRuleInterface();
        // 服务方法级别限流
        initFlowRule();
    }

③ 业务方法

@Override
    public String sayHello(String name) {
        return String.format("Hello, %s at %s", name, LocalDateTime.now());
    }

④ 以main方法的方式启动应用, 对外暴露接口服务即可. 按照上面的4个步骤就完成了对provider端服务基于QPS流控方式对服务方法的限流.

public static void main(String[] args) throws IOException {
        // Users don't need to manually call this method.
        // InitExecutor.doInit();

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Demo1ProviderConfiguration.class);
        context.refresh();

        System.out.println("Service provider is ready");
        System.in.read();
    }

⑤ 下面以main方法方式启动consumer端, 间隔50ms持续调用该provider端服务方法, 根据50ms的休眠时间调用一次, 理论上total_qps:20, p_qps:10, b_qps:10左右;

public class Demo1FooConsumerBootstrap {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext();
        consumerContext.register(ConsumerConfiguration.class);
        consumerContext.refresh();
        FooServiceConsumer service = consumerContext.getBean(FooServiceConsumer.class);
        // 连续调用
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(50);
                String message = service.sayHello("world");
                System.out.println("Success: " + message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

⑤ 效果展示

如下图, p_qps稳定在10, 而b_qps为基本也是9或者10, 这与上面步骤④的理论计算结果基本一致

dubbo 熔断原理 dubbo sentinel 熔断降级 filter_Dubbo

描述: 当在provider端使用基于QPS限流的方式时, 设置最大阈值为10, 则剩余的请求全部block, 如果按照50ms调用一次服务方法来计算的话, 1s大概会产生20次调用, 所以基本上10次p_qps, 10次b_qps左右;

2 服务接口粒度

① 按照上面介绍的规则定义服务接口级别的资源名称, 即服务接口的全限定名称.

private static final String sourceNameInterface = "com.alibaba.csp.sentinel.demo.dubbo.FooService";

② 初始化流控规则, 这里仅需修改资源名称为上面定义sourceNameInterface, 其他配置同服务方法粒度的配置一致.

private static void initFlowRuleInterface() {
        FlowRule flowRule = new FlowRule();
        flowRule.setResource(sourceNameInterface);
        flowRule.setCount(10);
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        flowRule.setLimitApp("default");
        flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 默认:快速失败/立即拒绝
        FlowRuleManager.loadRules(Collections.singletonList(flowRule));
    }

③ 效果展示

客户端还是间隔50ms调用服务接口中的sayHello()方法, 如下图显示, 按照服务接口级别的粒度, 也是达到了同样的流控效果, p_qps=10, b_qps约等于10.

dubbo 熔断原理 dubbo sentinel 熔断降级 filter_Dubbo_02

二,Service Consumer调用别人的服务, 基于并发线程数

Service Consumer对服务端的限流可以分为访问资源的并发线程数, 和服务降级两个维度.

1 并发线程数

服务消费方作为客户端去调用远程服务。每一个服务都可能会依赖几个下游服务,若某个服务A依赖的下游服务B出现了不稳定的情况,服务A请求服务B的响应时间变长,从而服务A调服务B的线程就会产生堆积,最终可能耗尽服务A的线程数。我们通过用并发线程数来控制对下游服务B的访问,来保证下游服务不可靠的时候,不会拖垮服务自身。采用基于线程数的限制模式后,我们不需要再去对线程池进行隔离,Sentinel 会控制资源的线程数,超出的请求直接拒绝,直到堆积的线程处理完成。限流粒度同样可以是服务接口和服务方法两种粒度。

① 服务方法级别粒度, 定义资源名称;

private static final String sourceName = "com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)";

② 初始化流控规则为, 基于并发线程数RuleConstant.FLOW_GRADE_THREAD流控, 阈值为5, 超过阈值的请求线程直接失败block;

private static void initFlowRule() {
        FlowRule flowRule = new FlowRule();
        flowRule.setResource(sourceName);
        flowRule.setCount(5);
        flowRule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
        flowRule.setLimitApp("default");
        FlowRuleManager.loadRules(Collections.singletonList(flowRule));
    }

③ 启动10个线程, 模拟调用FooService.sayHello()方法, 再启动10个线程, 模拟FooService.doAnother()方法, 由于对sayHello()方法做了基于线程数的限流, 最大允许阈值为5, 而对doAnother()方法不做任何限制.

// 启动10个线程, 模拟调用sayHello()服务方法
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    String message = service.sayHello("world");
                    System.out.println("Success: " + message);
                } catch (SentinelRpcException ex) {
                    System.out.println(Thread.currentThread().getName() + "调用sayHello()超出阈值线程被blocked");
                    // System.out.println("Blocked");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }).start();
        }

        // 启动10个线程, 模拟调用doAnother()服务方法
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println("Another: " + service.doAnother());
            }).start();
        }

④ 结果展示

分析下面结果可知, 对于超出阈值线程数的5个请求线程, 访问sayHello()方法时被blocked, 同时也不影响另外10个请求线程, 访问该服务接口中的其他方法, 如doAnother()方法.

dubbo 熔断原理 dubbo sentinel 熔断降级 filter_熔断降级_03

2 降级

当调用链路中某个资源出现不稳定的情况,如平均 RT 增高、异常比例升高的时候,Sentinel 会使对此调用资源进行降级操作。

① 服务方法级别粒度, 定义资源名称;

private static final String sourceName = "com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)";

② 初始化降级规则, 基于RT响应时间熔断降级, 设置响应时间阈值RT为200ms, 时间窗口为10ms, 当服务处于不稳定状态时, 就是按照这个时间窗口的时间, 每次放入5个请求进来, 判断这5个请求的响应时间是否超过阈值, 一直这样重复下去.

// 初始化RT响应时间降级
    private static void initRtDegradeRule() {
        List<DegradeRule> rules = new ArrayList<DegradeRule>();
        DegradeRule rule = new DegradeRule();
        rule.setResource(sourceName);
        // set threshold rt, 10 ms
        rule.setCount(200);
        rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
        rule.setTimeWindow(10);
        rules.add(rule);
        DegradeRuleManager.loadRules(rules);
    }

③ 启动consumer端应用, 并创建线程用于模拟调用服务, 这里创建一个线程, 当其他服务处于不稳定状态时, 能直观的看见每隔一个时间窗口, 就会放入5个请求进来, 判断这5个请求的响应时间, 做出熔断, 接下来的请求就走降级逻辑.

for(int i = 0; i < 1; i++){
        	new Thread(()->{
            	long start = 0L;
            	long end = 0L;
            	for(;;){
            		try {
            			TimeUnit.MILLISECONDS.sleep(5);
            			
            			start = System.currentTimeMillis();
                        String message = service.sayHello("world");
                        end = System.currentTimeMillis();
                        
                        System.out.println(Thread.currentThread().getName() + " Success: " + message+", 耗时:"+ (end - start) );
                    } catch (SentinelRpcException ex) {
                    	end = System.currentTimeMillis();
                        System.out.println(Thread.currentThread().getName() + " Blocked, 耗时:" + (end - start));
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
            	}
            }).start();
        }

④ 这里模拟服务处理不稳定状态, 设置其休眠300ms > 200ms的阈值, 所以consumer端调用服务, 会在接下来的时间窗口内, 对该服务的请求会进行熔断, 在熔断期间, 所有的请求将直接走降级逻辑.

@Override
    public String sayHello(String name) {
    	// 休眠300ms, 模拟业务耗时
    	try {
			TimeUnit.MILLISECONDS.sleep(300);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
        return String.format("Hello, %s at %s", name, LocalDateTime.now());
    }

⑤ 结果展示:

如下图, 由于被调用服务, 执行业务时间300ms > 200ms阈值, 所以一开始被调用的服务就处于一种不稳定的状态, 这是该服务就是就进入到了准降级状态, 这是Sentinel会放入5个请求, 如下图, 判断这5个请求的响应时间是否还是大于200ms的阈值, 是的话, 在接下来的时间窗口, 就会对该服务的请求进行熔断, 直接走降级.

dubbo 熔断原理 dubbo sentinel 熔断降级 filter_dubbo 熔断原理_04

下图, 则展示了在基于RT响应时间, 熔断降级判断, 时间窗口的作用, Sentinel框架会按照配置的时间窗口, 每隔一个时间窗口, 就会再次放入5个请求, 一直重复下去, 一直到服务资源正常. 这样其实就实现了当后台服务恢复稳定时, Sentinel框架能自动恢复对其的正常请求.

dubbo 熔断原理 dubbo sentinel 熔断降级 filter_Sentinel_05

三, 总结

Sentinel结合dubbo实现服务流控, 熔断降级, 保证服务高可用, 通过不同的流控策略针对不同的业务方, 如果是服务提供者, 为保证自己的服务不被激增的流量打死, 可以使用基于的QPS限流; 假如是服务调用方, 为保证自己的服务不被不稳定的服务拖死, 使用基于并发线程数限流; 同时使用Dubbo结合Sentinel框架来实现基于RT响应时间, 判断服务的稳定性, 从而进行熔断降级处理;