Zuul整合Sentinel

该博客详细介绍了如何将Sentinel与Zuul结合,实现动态路由和固定路由的接口级别限流及降级策略。通过EtcdReadDataSource和EtcdWriteDataSource实现规则的读写,SentinelPreFilter、SentinelPostFilter和SentinelErrorFilter分别处理请求前加权、请求后统计和异常情况。此外,还展示了如何在SentinelDashboard中配置和持久化规则,以及如何处理BlockException。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求

网关有两种路由类型
1.动态路由url配置
2.固定路由到执行器微服务,由他分发
现在要支持对这两种策略精确到接口层面的限流,并且支持客户端和Sentinel Dashboard配置配置限流策略

实现

请求前加权 -> 请求 ->请求后统计(异常)
配置三种filter,sentinel提供了这三种filter,这里写出来加深理解

@Slf4j
@Configuration
public class SentinelZuulConfig {

    @Resource
    EtcdZuulConfig etcdConfig;

    @Resource
    RedisUtil redisUtil;

    @Value("${sentinel-dashboard-config}")
    private boolean sentinelDashboardEnable = false;

    @Bean
    public ZuulFilter sentinelPreFilter() {
        return new SentinelPreFilter();
    }

    @Bean
    public ZuulFilter sentinelPostFilter() {
        return new SentinelPostFilter();
    }

    @Bean
    public ZuulFilter sentinelErrorFilter() {
        return new SentinelErrorFilter();
    }

    /**
     * 初始化限流规则监听
     */
    @PostConstruct
    public void init() throws Exception {
        // 流量控制规则 (FlowRule)
        ReadableDataSource<String, List<FlowRule>> flowRuleDs = new EtcdReadDataSource<>(Constant.SENTINEL_FLOW_RULE_KEY,
                (rule) -> JSON.parseArray(rule, FlowRule.class), etcdConfig.client());
        // 熔断降级规则 (DegradeRule)
        ReadableDataSource<String, List<DegradeRule>> degradeRuleDs = new EtcdReadDataSource<>(Constant.SENTINEL_DEGRADE_RULE_KEY,
                (rule) -> JSON.parseArray(rule, DegradeRule.class), etcdConfig.client());

        //SentinelDashboard 控制是否持久化
        if (sentinelDashboardEnable) {
            //注册持久化数据源,保存SentinelDashboard规则到etcd
            EtcdWriteDataSource<List<FlowRule>> flowDataSource =
                    new EtcdWriteDataSource<>(Constant.SENTINEL_FLOW_RULE_KEY, etcdConfig.client(), redisUtil);
            EtcdWriteDataSource<List<DegradeRule>> degradeDataSource =
                    new EtcdWriteDataSource<>(Constant.SENTINEL_DEGRADE_RULE_KEY, etcdConfig.client(), redisUtil);
            WritableDataSourceRegistry.registerFlowDataSource(flowDataSource);
            WritableDataSourceRegistry.registerDegradeDataSource(degradeDataSource);
        }

        FlowRuleManager.register2Property(flowRuleDs.getProperty());
        log.info("7.从ETCD中初始化流量控制规则, size=[{}]", FlowRuleManager.getRules().size());

        DegradeRuleManager.register2Property(degradeRuleDs.getProperty());
        log.info("8.从ETCD中初始化熔断降级规则, size=[{}]", DegradeRuleManager.getRules().size());
    }
}

实现读和写的数据源接口,用来往etcd中写限流降级规则

@Slf4j
public class EtcdReadDataSource<T> extends AbstractDataSource<String, T> {

    private final Client client;

    private Watch.Watcher watcher;

    private final String key;

    private Charset charset = StandardCharsets.UTF_8;

    public EtcdReadDataSource(String key, Converter<String, T> parser, Client client) {
        super(parser);
        this.key = key;
        this.client = client;
        loadInitialConfig();
        initWatcher();
    }

    private void loadInitialConfig() {
        try {
            T newValue = loadConfig();
            if (newValue == null) {
                log.warn("[EtcdDataSource] Initial configuration is null, you may have to check your data source");
            }
            getProperty().updateValue(newValue);
        } catch (Exception ex) {
            log.warn("[EtcdDataSource] Error when loading initial configuration", ex);
        }
    }

    private void initWatcher() {
        watcher = client.getWatchClient().watch(ByteSequence.from(key, charset), response -> {
            for (WatchEvent event : response.getEvents()) {
                WatchEvent.EventType eventType = event.getEventType();
                if (eventType == WatchEvent.EventType.PUT) {
                    try {
                        String info = event.getKeyValue().getValue().toString(StandardCharsets.UTF_8);
                        getProperty().updateValue(loadConfig(info));
                    } catch (Exception e) {
                        log.warn("[EtcdDataSource] Failed to update config", e);
                    }
                } else if (eventType == WatchEvent.EventType.DELETE) {
                    log.info("[EtcdDataSource] Cleaning config for key <{}>", key);
                    getProperty().updateValue(null);
                }
            }
        });
    }

    @Override
    public String readSource() throws Exception {
        CompletableFuture<GetResponse> responseFuture = client.getKVClient().get(ByteSequence.from(key, charset));
        List<KeyValue> kvs = responseFuture.get().getKvs();
        return kvs.isEmpty() ? null : kvs.get(0).getValue().toString(charset);
    }

    @Override
    public void close() {
        if (watcher != null) {
            try {
                watcher.close();
            } catch (Exception ex) {
                log.info("[EtcdDataSource] Failed to close watcher", ex);
            }
        }
        if (client != null) {
            client.close();
        }
    }
}

@Slf4j
public class EtcdWriteDataSource<T> implements WritableDataSource<T> {
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    private final Client client;
    private final String key;
    private final RedisUtil redisUtil;

    public EtcdWriteDataSource(String key, Client client, RedisUtil redisUtil) {
        this.client = client;
        this.key = key;
        this.redisUtil = redisUtil;
    }

    @Override
    public void write(T value) throws Exception {
        //分布式锁服务互斥持久化数据
        String identify = UUID.randomUUID().toString();
        Boolean getLock = redisUtil.tryLock(Constant.LOCK_DASHBOARD, identify, 2000);
        try {
            if (Boolean.TRUE.equals(getLock)) {
                String str = JSON.toJSONString(value);
                client.getKVClient().put(ByteSequence.from(key, DEFAULT_CHARSET), ByteSequence.from(str, DEFAULT_CHARSET));
                if (log.isDebugEnabled()) {
                    log.info("ETCD添加SentinelDashboard配置的规则成功, key=[{}], value=[{}]", key, str);
                }
            }
        } finally {
            redisUtil.releaseLock(Constant.LOCK_DASHBOARD, identify);
        }
    }

    @Override
    public void close() throws Exception {
        if (Objects.nonNull(client)) {
            client.close();
        }
    }
}

三个Filter
请求前

public class SentinelPreFilter extends ZuulFilter {
    public static final String BLOCK_EXCEPTION_FLAG = "SentinelBlockException";

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 10000;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        //获取动态路由id,服务路由为服务id,其他服务访问为dsp-deployment-unit
        String routeId = (String) ctx.get(FilterConstants.PROXY_KEY);
        String appId = (String) ctx.get(UriConverterFilter.X_APP_ID);
        Deque<Entry> holders = new ArrayDeque<>();
        try {
            if (StringUtil.isNotBlank(routeId)) {
                //分类为部署单元和路由服务两类
                if (ExecutorRule.DSP_DEPLOYMENT_UNIT.equals(routeId)) {
                    ContextUtil.enter(ExecutorRule.DSP_DEPLOYMENT_UNIT);
                    routeId = (String) ctx.get(UriConverterFilter.SERVICE_CACHE_ID);
                } else {
                    ContextUtil.enter("router");
                }
                //异步加权
                AsyncEntry entry = SphU.asyncEntry(appId + Constant.TENANTED_LINK_CHAR + routeId,
                        ResourceTypeConstants.COMMON_API_GATEWAY,
                        EntryType.IN);
                holders.push(entry);
            }
        } catch (BlockException ex) {
            //抛出异常,在ExceptionController那处理,BLOCK_EXCEPTION_FLAG用于限流判断
            throw new ZuulException(ex, BLOCK_EXCEPTION_FLAG, 429, "Sentinel block exception" + routeId);
        } finally {
            if (!holders.isEmpty()) {
                ctx.put(Constant.ZUUL_CTX_SENTINEL_ENTRIES_KEY, holders);
            }
        }
        return null;
    }
}

请求后

public class SentinelPostFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return 1000;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
    	//退出
        SentinelEntryUtils.tryExitFromCurrentContext();
        return null;
    }
}

异常

public class SentinelErrorFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.ERROR_TYPE;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return ctx.getThrowable() != null;
    }

    @Override
    public int filterOrder() {
        return -1;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        Throwable throwable = ctx.getThrowable();
        if (throwable != null) {
            if (!BlockException.isBlockException(throwable)) {
                SentinelEntryUtils.tryTraceExceptionThenExitFromCurrentContext(throwable);
                RecordLog.info("[SentinelZuulErrorFilter] Trace error cause", throwable.getCause());
            }
        }
        return null;
    }
}

用到的util,异步加权

public class SentinelEntryUtils {
    static void tryExitFromCurrentContext() {
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.containsKey(Constant.ZUUL_CTX_SENTINEL_ENTRIES_KEY)) {
            Deque<Entry> holders = (Deque<Entry>) ctx.get(Constant.ZUUL_CTX_SENTINEL_ENTRIES_KEY);
            Entry entry;
            while (!holders.isEmpty()) {
                entry = holders.pop();
                entry.exit();
            }
            ctx.remove(Constant.ZUUL_CTX_SENTINEL_ENTRIES_KEY);
        }
        ContextUtil.exit();
    }
    static void tryTraceExceptionThenExitFromCurrentContext(Throwable t) {
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.containsKey(Constant.ZUUL_CTX_SENTINEL_ENTRIES_KEY)) {
            Deque<Entry> holders = (Deque<Entry>) ctx.get(Constant.ZUUL_CTX_SENTINEL_ENTRIES_KEY);
            Entry entry;
            while (!holders.isEmpty()) {
                entry = holders.pop();
                Tracer.traceEntry(t, entry);
                entry.exit();
            }
            ctx.remove(Constant.ZUUL_CTX_SENTINEL_ENTRIES_KEY);
        }
        ContextUtil.exit();
    }
}

异常判断

 //Sentinel熔断
 if (BlockException.isBlockException(throwable)) {
     map.put(WriteErrorMsgUtil.KEY_CODE, ErrorCode.HYSTRIX_ERROR.getCode());
     map.put(WriteErrorMsgUtil.KEY_MSG, ErrorCode.HYSTRIX_ERROR.getMsg());
     status = HttpStatus.SC_FORBIDDEN;
 }

至此结束,功能实现,得看文档,最好看看源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值