spring cloud集成sentinel实现服务限流

  1. 引入sentinel的作用
    由于近期商城需要做推广活动,为了防止流量激增导致压垮服务,因此需要对请求进行限流。经过一番搜索,最终锁定了sentinel,因为它是阿里的产品,经过多次双十一的检验,而且还有图形界面供用户设置、观察指标等。
  2. spring cloud集成sentinel
    由于我们使用的框架是spring cloud而不是cloud alibaba,所以在集成时遇到了不少的坑,很多从网上搜索的案例都模糊不清,最终还是通过官方文档(版本说明) 进行配置才搞定的。我使用的是Spring Cloud Finchley版本,因此配置文件如下在这里插入代码片
		<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
	<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.0.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
  1. 网关限流
    我们使用的网关是zuul,同样参考官方文档网关限流,增加配置,然后在启动服务是在VM中增加参数-Dcsp.sentinel.app.type=1(告诉sentinel这是一个网关服务,网关服务和普通服务的功能不同),网关服务多出一个“api管理”,该选型功能很强大也很实用,能够根据请求参数、请求路径、请求ip,请求header等进行限流、降级操作。
		<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>

网关服务

  1. 规则持久化
    sentinel默认是保存在内存中的,因此每次重启服务就会导致规则丢失(重启sentinel dashboard会导致规则丢失,但是限流不会失效;重启sentinel客户端会导致限流失效,但dashboard规则不会丢失,通过dashboard源码可以知道,sentinel是想规则保存在内存,同时通过rpc将规则发送到客户端,由客户端自行限流)。
    sentinel官方提供了多种持久化扩展(ZooKeeper,Nacos,File,Apollo,Redis…),这里我选择使用redis进行持久化,网上很多针对redis持久化的案例,但是大多出自一处,都是通过修改dashboard源码将规则持久化到redis。但是这种方式只是解决了保存的问题,没有解决读取的问题。上面我说过真正的限流是在各个客户端自行实现的,dashboard只是提供一个设置的入口而已,每个客户端在重启时需要将redis中涉及到自己的规则读取到内存中,因此这里还需要针对客户端做修改,增加如下配置
		<dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-api-gateway-adapter-common</artifactId>
        </dependency>
  • 将持久化从dashboard移到客户端
    网上很多很的方案都是在dashboard修改redis持久化配置,我按照网上的方案对dashboard源码进行了修改,确实可以把规则保存到redis中,但是网上的方案都没有涉及客户端服务启动读入规则。当我想要在客户端重启时读入dashboard写入的规则时,发现dashboard和客户端的实体类字段进行有差别,从而导致规则无法读入。因此我转变思维,将持久化操作也放在客户端,由客户端进行持久化、由客户端进行读入,这样就不会出现不兼容的问题。实现WritableDataSurce接口,从而让规则能够持久化到redis
public class RedisWritableDataSource<T> implements WritableDataSource<T> {

    private final RedisClient redisClient;
    private String key;

    public RedisWritableDataSource(RedisConnectionConfig config, String key){
        AssertUtil.notNull(config, "Redis connection config can not be null");
        AssertUtil.notEmpty(key, "Redis ruleKey can not be empty");
        this.redisClient = this.getRedisClient(config);
        this.key = key;
    }

    private RedisClient getRedisClient(RedisConnectionConfig connectionConfig) {
        if (connectionConfig.getRedisSentinels().size() == 0) {
            RecordLog.info("[RedisDataSource] Creating stand-alone mode Redis client", new Object[0]);
            return this.getRedisStandaloneClient(connectionConfig);
        } else {
            RecordLog.info("[RedisDataSource] Creating Redis Sentinel mode Redis client", new Object[0]);
            return this.getRedisSentinelClient(connectionConfig);
        }
    }

    private RedisClient getRedisStandaloneClient(RedisConnectionConfig connectionConfig) {
        char[] password = connectionConfig.getPassword();
        String clientName = connectionConfig.getClientName();
        RedisURI.Builder redisUriBuilder = RedisURI.builder();
        redisUriBuilder.withHost(connectionConfig.getHost()).withPort(connectionConfig.getPort()).withDatabase(connectionConfig.getDatabase()).withTimeout(Duration.ofMillis(connectionConfig.getTimeout()));
        if (password != null) {
            redisUriBuilder.withPassword(connectionConfig.getPassword());
        }

        if (StringUtil.isNotEmpty(connectionConfig.getClientName())) {
            redisUriBuilder.withClientName(clientName);
        }

        return RedisClient.create(redisUriBuilder.build());
    }

    private RedisClient getRedisSentinelClient(RedisConnectionConfig connectionConfig) {
        char[] password = connectionConfig.getPassword();
        String clientName = connectionConfig.getClientName();
        RedisURI.Builder sentinelRedisUriBuilder = RedisURI.builder();
        Iterator var5 = connectionConfig.getRedisSentinels().iterator();

        while(var5.hasNext()) {
            RedisConnectionConfig config = (RedisConnectionConfig)var5.next();
            sentinelRedisUriBuilder.withSentinel(config.getHost(), config.getPort());
        }

        if (password != null) {
            sentinelRedisUriBuilder.withPassword(connectionConfig.getPassword());
        }

        if (StringUtil.isNotEmpty(connectionConfig.getClientName())) {
            sentinelRedisUriBuilder.withClientName(clientName);
        }

        sentinelRedisUriBuilder.withSentinelMasterId(connectionConfig.getRedisSentinelMasterId()).withTimeout(connectionConfig.getTimeout(), TimeUnit.MILLISECONDS);
        return RedisClient.create(sentinelRedisUriBuilder.build());
    }

    @Override
    public void write(T t) throws Exception {
        RedisCommands<String, String> stringRedisCommands = this.redisClient.connect().sync();
        stringRedisCommands.set(key, JSON.toJSONString(t, SerializerFeature.WriteClassName));
    }

    @Override
    public void close() throws Exception {
        this.redisClient.shutdown();
    }
}

  • 客户单启动时读取规则配置
@Configuration
public class GatewayRuleConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private Integer port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.cloud.sentinel.transport.port}")
    private String sentinelPort;

    //限流规则key前缀
    public final String RULE_FLOW = "sentinel_rule_flow_";
    public final String RULE_FLOW_CHANNEL = "sentinel_rule_flow_channel";
    //降级规则key前缀
    public final String RULE_DEGRADE = "sentinel_rule_degrade_";
    public final String RULE_DEGRADE_CHANNEL = "sentinel_rule_degrade_channel";
    //系统规则key前缀
    public final String RULE_SYSTEM = "sentinel_rule_system_";
    public final String RULE_SYSTEM_CHANNEL = "sentinel_rule_system_channel";
    //API分组
    public final String RULE_API = "sentinel_rule_api_";
    public final String RULE_API_CHANNEL = "sentinel_rule_api_channel";
    //gateway规则
    public final String RULE_GATEWAY_FLOW = "sentinel_rule_gateway_flow_";
    public final String RULE_GATEWAY_FLOW_CHANNEL = "sentinel_rule_gateway_flow_channel";

    @PostConstruct
    public void doInit() throws Exception {
        RedisConnectionConfig config = RedisConnectionConfig.builder()
                .withHost(host)
                .withPort(port)
                .withPassword(password)
                .build();

        InetAddress host = InetAddress.getLocalHost();
        initFlowRule(config, host);
        initDegradeRule(config, host);
        initSystemRule(config, host);
        initApiDefinition(config, host);
        initGatewayFlowRule(config, host);
    }

    private void initFlowRule(RedisConnectionConfig config, InetAddress host){
        Converter<String, List<FlowRule>> parser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
        ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<>(config, RULE_FLOW+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_FLOW_CHANNEL, parser);
        FlowRuleManager.register2Property(redisDataSource.getProperty());

        WritableDataSource<List<FlowRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_FLOW+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
        WritableDataSourceRegistry.registerFlowDataSource(writableDataSource);
    }

    private void initDegradeRule(RedisConnectionConfig config, InetAddress host){
        Converter<String, List<DegradeRule>> parser1 = source -> JSON.parseObject(source,new TypeReference<List<DegradeRule>>() {});
        ReadableDataSource<String, List<DegradeRule>> redisDataSource1 = new RedisDataSource<>(config, RULE_DEGRADE+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_DEGRADE_CHANNEL, parser1);
        DegradeRuleManager.register2Property(redisDataSource1.getProperty());

        WritableDataSource<List<DegradeRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_DEGRADE+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
        WritableDataSourceRegistry.registerDegradeDataSource(writableDataSource);
    }

    private void initSystemRule(RedisConnectionConfig config, InetAddress host){
        Converter<String, List<SystemRule>> parser2 = source -> JSON.parseObject(source,new TypeReference<List<SystemRule>>() {});
        ReadableDataSource<String, List<SystemRule>> redisDataSource2 = new RedisDataSource<>(config, RULE_SYSTEM+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_SYSTEM_CHANNEL, parser2);
        SystemRuleManager.register2Property(redisDataSource2.getProperty());

        WritableDataSource<List<SystemRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_SYSTEM+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
        WritableDataSourceRegistry.registerSystemDataSource(writableDataSource);
    }

    private void initApiDefinition(RedisConnectionConfig config, InetAddress host) throws Exception {
        Converter<String, Set<ApiDefinition>> parser3 = source -> JSON.parseObject(source,new TypeReference<Set<ApiDefinition>>() {});
        ReadableDataSource<String, Set<ApiDefinition>> redisDataSource3 = new RedisDataSource<>(config, RULE_API+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_API_CHANNEL, parser3);
        GatewayApiDefinitionManager.register2Property(redisDataSource3.getProperty());

        WritableDataSource<Set<ApiDefinition>> writableDataSource = new RedisWritableDataSource<>(config, RULE_API+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
        UpdateGatewayApiDefinitionGroupCommandHandler.setWritableDataSource(writableDataSource);
    }

    private void initGatewayFlowRule(RedisConnectionConfig config, InetAddress host) throws Exception {
        Converter<String, Set<GatewayFlowRule>> parser4 = source -> JSON.parseObject(source,new TypeReference<Set<GatewayFlowRule>>() {});
        ReadableDataSource<String, Set<GatewayFlowRule>> redisDataSource4 = new RedisDataSource<>(config, RULE_GATEWAY_FLOW+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_GATEWAY_FLOW_CHANNEL, parser4);
        GatewayRuleManager.register2Property(redisDataSource4.getProperty());

        WritableDataSource<Set<GatewayFlowRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_GATEWAY_FLOW+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
        UpdateGatewayRuleCommandHandler.setWritableDataSource(writableDataSource);
    }

}

这样就形成了一个闭环,既能够持久化,又能读入内存,修改即时生效。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值