1. 阿里熔断限流Sentinel研究

1.1. 功能特点

  1. 丰富的应用场景:例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用
  2. 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  3. 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  4. 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

JAVA熔断实现不了 java 熔断_List

开源生态

JAVA熔断实现不了 java 熔断_ide_02

Sentinel 分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

1.2. 快速开始

1.2.1. 公网接入

  • 根据该步骤,建立demo
  • 控制台效果图

JAVA熔断实现不了 java 熔断_JAVA熔断实现不了_03

JAVA熔断实现不了 java 熔断_List_04

JAVA熔断实现不了 java 熔断_JAVA熔断实现不了_05

1.3. 手动接入

  1. 引入pom
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.6.2</version>
</dependency>
  1. 定义资源:也就是对哪个资源进行流量控制,现在已经提供了注解形式,所以新的接入直接用注解,@SentinelResource
  • 关于SentinelResource注解,这里列出几个好用和必填的参数,具体参考这里

1.3.1. apollo接入

由于我的系统使用的是apollo管理配置,所以我用apollo来管理规则,官方也提供了apollo的接入说明,然后我想到需要自动提交规则,而不是自己手动去配,又找到了官方的推送例子,可是该例子还是存在问题的,或者说要运行该例子需要对apollo的开放设置有进一步了解

期间我遇到了401问题,是因为需要apollo授权第三方应用,配置token后才能起效;

之后又遇到400问题,是因为openItemDTO.setDataChangeCreatedBy("apollo");namespaceReleaseDTO.setReleasedBy("apollo");该配置项,例子并不是这么写的,需要将参数改成apollo才行

下面给出完整配置,结合apollo读取配置

@Configuration
@ConditionalOnProperty(name = "sentinel.enabled",havingValue = "true")
@Slf4j
public class SentinelAspectConfiguration {

    @Autowired
    private ApolloOpenApiClient apolloOpenApiClient;

    @Autowired(required = false)
    private IRuleManage ruleManage;

    @Value("${appId:}")
    private String appId;

    /**
     * 已有配置,可直接使用
     */
    @Value("${profile:}")
    private String env;

    /**
     * 没有profile属性配置,则必须配置env
     */
    @Value("${env:pro}")
    private String envReal;

    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        pushlish();
        return new SentinelResourceAspect();
    }

    private void pushlish(){
        List<FlowRule> flowRules = null;
        if (ruleManage != null) {
            flowRules = ruleManage.getFlowRules();
        }

        if (appId == null) {
            return;
        }

        String flowDataId = appId+"-flow-rules";
        String degradeDataId = appId+"-degrade-rules";
        if("".equals(env)){
            env = envReal;
        }
        env = env.toUpperCase();

        //代码级别的规则,初始化加载,可不实现IRuleManage接口
        setRules(flowRules, flowDataId);

        //流控
        flowConfig(flowDataId);

        //降级
        degradeConfig(degradeDataId);

    }

    private void degradeConfig(String degradeDataId) {
        //读取
        String namespaceName = "application";
        String defaultFlowRules = "[]";
        ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new ApolloDataSource<>(namespaceName,
                degradeDataId, defaultFlowRules, source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
        }));
        DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());

        //写入配置
        WritableDataSource<List<DegradeRule>> wds = new WritableDataSource<List<DegradeRule>>() {
            @Override
            public void write(List<DegradeRule> rules) throws Exception {
                setRules(rules, degradeDataId);
            }

            @Override
            public void close() throws Exception {
                log.info("WritableDataSource DegradeRule close");
            }
        };
        WritableDataSourceRegistry.registerDegradeDataSource(wds);
    }

    private void flowConfig(String flowDataId) {
        //读取
        String namespaceName = "application";
        String defaultFlowRules = "[]";
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ApolloDataSource<>(namespaceName,
                flowDataId, defaultFlowRules, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
        }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());

        //写入配置
        WritableDataSource<List<FlowRule>> wds = new WritableDataSource<List<FlowRule>>() {
            @Override
            public void write(List<FlowRule> rules) throws Exception {
                setRules(rules, flowDataId);
            }

            @Override
            public void close() throws Exception {
                log.info("WritableDataSource FlowRule close");
            }
        };
        WritableDataSourceRegistry.registerFlowDataSource(wds);
    }

    private void setRules(List rules, String flowDataId) {
        if (rules != null && rules.size() > 0) {
            // Increase the configuration
            OpenItemDTO openItemDTO = new OpenItemDTO();
            openItemDTO.setKey(flowDataId);
            openItemDTO.setValue(JSON.toJSONString(rules));
            openItemDTO.setComment("Program auto-join");
            openItemDTO.setDataChangeCreatedBy("apollo");
            apolloOpenApiClient.createOrUpdateItem(appId, env, "default", "application", openItemDTO);

            // Release configuration
            NamespaceReleaseDTO namespaceReleaseDTO = new NamespaceReleaseDTO();
            namespaceReleaseDTO.setEmergencyPublish(true);
            namespaceReleaseDTO.setReleaseComment("Modify or add configurations");
            namespaceReleaseDTO.setReleasedBy("apollo");
            namespaceReleaseDTO.setReleaseTitle("Modify or add configurations");
            apolloOpenApiClient.publishNamespace(appId, env, "default", "application", namespaceReleaseDTO);
        }
    }
}

@Configuration
@EnableApolloConfig(value = "application", order = 10)
public class AppBaseConfig {

    @Value("${apollo.token}")
    private String token;

    @Value("${apollo.portalUrl}")
    private String portalUrl;

    @Value("${sentinel.project.name:}")
    private String projectName;

    @Value("${spring.application.name:}")
    private String applicationName;

    @Value("${sentinel.console.server:}")
    private String consoleServer;

    @Value("${sentinel.heartbeatClient:}")
    private String heartbeatClient;

    @Bean
    @ConditionalOnProperty(name = "sentinel.enabled",havingValue = "true")
    public ApolloOpenApiClient apolloOpenApiClient() {
        System.setProperty(AppNameUtil.APP_NAME, "".equals(projectName) ? applicationName : projectName);
        System.setProperty(TransportConfig.CONSOLE_SERVER, consoleServer);
        if (!"".equals(heartbeatClient)) {
            String[] split = heartbeatClient.split(":");
            System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, split[0].trim());
            System.setProperty(TransportConfig.SERVER_PORT, split[1].trim());
        }
        ApolloOpenApiClient client = ApolloOpenApiClient.newBuilder()
                .withPortalUrl(portalUrl)
                .withToken(token)
                .build();
        return client;
    }
}

1.4. 控制台修改规则apollo写入

  1. 这个功能是可以有的,只是官方文档我没找到,我直接debug源码查看哪里可以把apollo写入加进去,果然发现它是提供了写入接口的
  2. 写入接口为WritableDataSource,参考上面的完整代码

1.5. 控制台

  1. 下载启动控制台,下载地址
  2. 启动命令java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

JAVA熔断实现不了 java 熔断_JAVA熔断实现不了_06

登录名密码都是sentinel

1.6. 工作原理

  1. 官方说明

1.7. 问题

1.7.1. 将控制台部署在公网,本机启动连接出现错误日志

2019-07-23 14:57:33.256 ERROR 14788 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher   : Failed to fetch metric from <http://172.16.100.141:8721/metric?startTime=1563865044000&endTime=1563865050000&refetch=false> (ConnectionException: Connection refused: no further information)

说明发送了内网地址,导致fetch拉取埋点信息不通

我通过System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, split[0].trim());设置心跳地址为外网地址解决这个问题

本质上是因为控制台主动通过接口来客户端拉信息,但若是访问不通,也是没辙,所以本地测试部在服务器上的控制台,除非外网映射

1.7.2. 部署上去后发现可以访问通,且项目注册进来了,但没有任何调用信息,且没有任何规则信息

我的这个问题基础是因为我部署到docker上的,之后debug源码,发现控制台调用客户端的地址是我根本没配过的,深入后发现如下代码段

Runnable serverInitTask = new Runnable() {
            int port;

            {
                try {
                    port = Integer.parseInt(TransportConfig.getPort());
                } catch (Exception e) {
                    port = DEFAULT_PORT;
                }
            }

            @Override
            public void run() {
                boolean success = false;
                ServerSocket serverSocket = getServerSocketFromBasePort(port);

                if (serverSocket != null) {
                    CommandCenterLog.info("[CommandCenter] Begin listening at port " + serverSocket.getLocalPort());
                    socketReference = serverSocket;
                    executor.submit(new ServerThread(serverSocket));
                    success = true;
                    port = serverSocket.getLocalPort();
                } else {
                    CommandCenterLog.info("[CommandCenter] chooses port fail, http command center will not work");
                }

                if (!success) {
                    port = PORT_UNINITIALIZED;
                }

                TransportConfig.setRuntimePort(port);
                executor.shutdown();
            }

        };

        new Thread(serverInitTask).start();

该代码段的作用是为客户端在分配一个socketServer,之后的信息交互都是通过该服务提供的端口来提供;

这样一来客户端需要额外提供一个端口了,而我的docker只暴露了1个服务端口,所以不可避免的会出现问题,以上是我到目前的思路,正在验证中

至于端口如何决定,它是用了一个简单的技巧,若设置了csp.sentinel.api.port配置项,则会取该配置端口,若没有设,则是默认端口8719;但如果你用的是官网的启动方式,那8719应该是被控制台占用了,所以进入小技巧getServerSocketFromBasePort方法,内容如下

private static ServerSocket getServerSocketFromBasePort(int basePort) {
        int tryCount = 0;
        while (true) {
            try {
                ServerSocket server = new ServerSocket(basePort + tryCount / 3, 100);
                server.setReuseAddress(true);
                return server;
            } catch (IOException e) {
                tryCount++;
                try {
                    TimeUnit.MILLISECONDS.sleep(30);
                } catch (InterruptedException e1) {
                    break;
                }
            }
        }
        return null;
    }

它会循环尝试端口是否被占用,每个端口尝试三次,若被占用则取下一个+1端口,一直到可用的端口返回;所以如果我们的客户端应用放到了docker,而开放的端口只有一个,那就获取不了信息了

这里csp.sentinel.api.port配置项很容易理解成客户端的端口地址,因为启动也不会报错啥的,会误让我们误会这个参数可以不填,虽然文档上写着必填,但本地测试的时候可没影响-_-||,所有都注意了,这个配置项是必填的

  • 还要注意一点,因为是socket连接,两边端口要一致,所以docker端口号映射需要一样