需求
网关有两种路由类型
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;
}
至此结束,功能实现,得看文档,最好看看源码