上一篇简单介绍了sentinel限流的基本配置和使用,这一篇我们来稍微深入一点,看看如何将zookeeper继承进来,用以保存添加的流控规则。

上一篇中我们启动了dashboard.jar,然后在客户端中指定了dashboard的地址。之后启动项目,随便访问个接口,之后就能在dashboard的界面上看到相应的请求了,并且能在控制台上添加一些规则,保存后客户端就能生效了。

基于内存的推送

那么它的内部原理是什么呢?来简单了解一下。

从官方文档可以看到,客户端在引入了Sentinel后,并指定dashboard的地址,启动后,将会在客户端启动一个http服务,默认占用8719端口。由于我们是引入的SpringCloud的模块,就已经包含了下面的引入。

引入这个transport模块的原因就是为了接收dashboard推送过来的配置规则。可以看看官方文档的介绍,默认就是“原始模式”。所谓的原始模式,就是指客户端启动web服务,连上dashboard后,在dashboard配置的规则,由dashboard发起http请求来修改。修改后的规则,直接保存在客户端内存中,并即时生效。

这种方式原理简单,一般用于入门测试使用,生产环境不能用。基于内存存储,在客户端重启后,所有规则都会丢失,需要重新配置。而且不适用于客户端多个实例,因为彼此之间不共享规则,倘若启动多个实例,需要多次重复配置。很明显,这不是我们想要的那种结果。

官方提供了三种模式,上面的“原始模式”、“pull模式”、“push模式”。pull模式就是搞个文件存着,隔一会去请求一下,看看有没有变化,如果变了,就更新到内存,很明显这种模式存在延迟,也不建议上生产。那就来看看“push模式”吧。

基于zookeeper的推送

从上面可以看到,要想能持久化规则的存储,并且在多个实例间共享,就需要一个第三方的存储。让dashboard对规则的修改能及时存储到第三方并及时通知客户端完成修改。官方给了三种示例推荐,Apollo、Nacos、Zookeeper,它们的使用类似,我们以zookeeper为例来看看怎么使用。

客户端pom文件添加zookeeper的依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-zookeeper</artifactId>
        </dependency>

然后客户端修改获取规则的地方为从zookeeper获取规则。

import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.zookeeper.ZookeeperDataSource;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;

/**
 * @author wuweifeng wrote on 2019/7/1.
 */
@Component
public class ZookeeperSentinelConfig {
    @Value("${spring.application.name}")
    private String appName;


    @PostConstruct
    public void loadRules() {
        final String remoteAddress = "127.0.0.1:2181";
        final String path = "/sentinel_rule_config/" + appName;

        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<>(remoteAddress, path,
                source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
                }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }
}

这一步很简单,大家自行去下载个zookeeper的安装文件,启动即可。在方法里指定zookeeper的地址和要监听变化的path,然后注册一下就好了。客户端到这里就完毕了。

重新启动客户端后,就会变成从zookeeper的固定path里获取rule规则。之后对该path做的变化,都会即时更新到客户端,并应用新的规则。

这里我们测试一下:

@RestController
public class TestController {

    @GetMapping(value = "/hello")
    public String hello() {
        return "Hello Sentinel";
    }

    @GetMapping(value = "/test")
    @SentinelResource(value = "TestResource", blockHandler = "handleException")
    public String test() {
        return "Hello TestResource";
    }

    // Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
    public String handleException(BlockException ex) {
        return "handleException";
    }
}

上面是一个简单的Controller,里面定义了一个resource。之后,我们通过对zookeeper的path推送该resource的规则,来测试是否生效。

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;

/**
 * @author wuweifeng wrote on 2019/7/1.
 */
public class ZookeeperConfigSender {
    private static final int RETRY_TIMES = 3;
    private static final int SLEEP_TIME = 1000;

    public static void main(String[] args) throws Exception {
        final String remoteAddress = "localhost:2181";
        final String rule = "[\n"
                + "  {\n"
                + "    \"resource\": \"TestResource\",\n"
                + "    \"controlBehavior\": 0,\n"
                + "    \"count\": 1.0,\n"
                + "    \"grade\": 1,\n"
                + "    \"limitApp\": \"default\",\n"
                + "    \"strategy\": 0\n"
                + "  }\n"
                + "]";

        CuratorFramework zkClient = CuratorFrameworkFactory.newClient(remoteAddress, new ExponentialBackoffRetry
                (SLEEP_TIME, RETRY_TIMES));
        zkClient.start();
        String appName = "your-app-name";
        String path = "/sentinel_rule_config/" + appName;
        Stat stat = zkClient.checkExists().forPath(path);
        if (stat == null) {
            zkClient.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path, null);
        }
        zkClient.setData().forPath(path, rule.getBytes());

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        zkClient.close();
    }
}

这就是测试代码,运行后,就会往zookeeper写入一个rule,设置名为TestResource的qps为1,并应用到客户端。无论启动多少个客户端实例,都会生效这个rule。

看到这里,其实已经完成了基于push的动态规则功能了,可以通过zkui这种zookeeper界面工具,或者通过代码来查询、修改zookeeper里的rule配置(json)来完成对客户端规则的控制。

那么可能有人会问了,dashboard呢?用那个界面操作不是更方便吗?

事实上,对客户端的限流,与dashboard没一点关系,只用zookeeper就能完成了。那么这时,你再启动dashboard,当然也是能用的,因为客户端的web服务还是启动着的,也能接收到来自于dashboard的推送。只是来自于dashboard的在客户端重启后会失效,在zookeeper里的会仍然存在。

那么我们应该改造一下dashboard,让在界面上的操作也推送到zookeeper里去,这样就方便多了。

在GitHub上下载Sentinel的源码,里面有dashboard的工程,我们来修改一下它的代码就好了。

先修改一下pom文件,把scope注释掉。

找到rule包,添加个zookeeper文件夹,里面有4个类。

可以直接从工程的test测试代码里,直接把zookeeper包抄过去就行,并把rule下原来的FlowRuleApiProvider和FlowRuleApiPublisher给注释掉。

test源码里已经提供了基于三种中间件的配置代码了,抄过去就行。

抄过去后,修改一下RULE_ROOT_PATH,保持和客户端配置的是一致的。

之后找到Controller包下的v2包,如果你设置的FlowRuleZookeeperProvider和publisher两个bean有名字,可以在autowired时指定为你设置的名字,或者用@Resource。

最后修改一下sidebar.html,将原来的flowV1改为如图。

这样就ok了。

重新启动dashboard项目,重启客户端。这样dashboard就已经和zookeeper关联起来了,dashboard的操作就由原来的操作客户端的api,变成了操作zookeeper。你所有在dashboard界面上做的配置,都会存储到zookeeper中,并实时推送到客户端。客户端重启后,dashboard不受影响。这样就完成了多实例共享流控规则。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐