如果对解决思路不感兴趣,可以直接看记录过程-下。(本篇内含从sentinel-dashboard调用sentinel-core的解析)
近日,某初级程序员在做网关限流的时候,用到了以前并不怎么用到过的sentinel。ok,代码简单使用:(为了切入重点,就不啰嗦那么多怎么引入包了,我用的是sentinel-core)
List flowRules = new ArrayList<>();
FlowRule flowRule = new FlowRule();
// 设置受保护的资源
flowRule.setResource(RESOURCE_NAME);
// 设置流控规则 QPS
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源阈值
// Set limit QPS to 1.
flowRule.setCount(1);
flowRules.add(flowRule);
// 加载配置好的规则
FlowRuleManager.loadRules(flowRules);
我的需求:流控规则放在内存中,需要给出接口动态更新。
问题出在哪了呢?问题出在了最后一步,需要动态更新。更新,也就直接指向了 loadRules加载规则这一步
FlowRuleManager.loadRules(flowRules);
对于没有经验的程序员(我)一看,哇?他怎么要load一个列表,那岂不是之前所有的规则要被覆盖,那会不会出现这样的问题?
如果再次调用loadRules,会不会影响之前已经load的rules的窗口。 比如说第一次load了个名为“a”的100qps的规则,如果"a"资源在500ms内请求了100次。这时候我又load了一下名为“a”的规则,这时候这个“a"在前500ms还能再请求吗?
虽然当时写了测试用例,不会有影响,但是没有证据证明,我还是很虚(show me)的。
我的第一步,看了一下这个方法
public static void loadRules(List rules) {
currentProperty.updateValue(rules);
}
DynamicSentinelProperty::updateValue
public boolean updateValue(T newValue) {
if (isEqual(value, newValue)) {
return false;
}
RecordLog.info("[DynamicSentinelProperty] Config will be updated to: {}", newValue);
value = newValue;
for (PropertyListener listener : listeners) {
listener.configUpdate(newValue);
}
return true;
}
注意,这里有有一个isEqual的方法,如果你两次放入的是同一个list对象,则第二次返回false。主要看进行了listener的更新,跟着debug走到了FlowRuleManager::configUpdate
public synchronized void configUpdate(List value) {
Map> rules = FlowRuleUtil.buildFlowRuleMap(value);
if (rules != null) {
flowRules = rules;
}
RecordLog.info("[FlowRuleManager] Flow rules received: {}", rules);
}
继续往下走,一直点这个build方法进入方法到FlowRuleUtil::buildFlowRuleMap(List
public static Map> buildFlowRuleMap(List list, Function groupFunction,
Predicate filter, boolean shouldSort) {
Map> newRuleMap = new ConcurrentHashMap<>();
if (list == null || list.isEmpty()) {
return newRuleMap;
}
Map> tmpMap = new ConcurrentHashMap<>();
for (FlowRule rule : list) {
if (!isValidRule(rule)) {
RecordLog.warn("[FlowRuleManager] Ignoring invalid flow rule when loading new flow rules: " + rule);
continue;
}
if (filter != null && !filter.test(rule)) {
continue;
}
if (StringUtil.isBlank(rule.getLimitApp())) {
rule.setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
}
TrafficShapingController rater = generateRater(rule);
rule.setRater(rater);
K key = groupFunction.apply(rule);
if (key == null) {
continue;
}
Set flowRules = tmpMap.get(key);
if (flowRules == null) {
// Use hash set here to remove duplicate rules.
flowRules = new HashSet<>();
tmpMap.put(key, flowRules);
}
flowRules.add(rule);
}
Comparator comparator = new FlowRuleComparator();
for (Entry> entries : tmpMap.entrySet()) {
List rules = new ArrayList<>(entries.getValue());
if (shouldSort) {
// Sort the rules.
Collections.sort(rules, comparator);
}
newRuleMap.put(entries.getKey(), rules);
}
return newRuleMap;
}
直接看return,我一看,返回了一个新的ruleMap,相当于前边定义的rule是全新的,全被覆盖掉了,我心想,完蛋,这也不能证明上边提出的可能发生的问题不会发生啊,芭比Q。
我寻思,这不该啊,一定有一个方法可以动态更新规则而不影响之前已经写好的规则(我以为FlowRuleManager.loadRules(flowRules)会影响)。我突然想到,sentinel不还有个服务端嘛,它不就是可以动态更新客户端流控规则?说干就干,找到下载sentinel-dashboard,运行查看,添加一个限流规则(这里要注意dashboard的加载属于懒加载,要请求一下接口才会显示出来我们的项目)
接着浏览器按F12查看请求的是哪个接口,如下/v1/flow/rule
紧接着在sentinel-dashboard项目中找到接口,看他做了什么
public Result apiAddFlowRule(@RequestBody FlowRuleEntity entity) {
Result checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
entity.setId(null);
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
entity.setLimitApp(entity.getLimitApp().trim());
entity.setResource(entity.getResource().trim());
try {
entity = repository.save(entity);
publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS);
return Result.ofSuccess(entity);
} catch (Throwable t) {
Throwable e = t instanceof ExecutionException ? t.getCause() : t;
logger.error("Failed to add new flow rule, app={}, ip={}", entity.getApp(), entity.getIp(), e);
return Result.ofFail(-1, e.getMessage());
}
}
上边一扫而过,直接看到publishRules。代码比较清晰,无脑往下,往return里边点就行了。最后走到了SentinelApiClient::executeCommand
private CompletableFuture executeCommand(HttpUriRequest request) {
CompletableFuture future = new CompletableFuture<>();
httpClient.execute(request, new FutureCallback() {
@Override
public void completed(final HttpResponse response) {
int statusCode = response.getStatusLine().getStatusCode();
try {
String value = getBody(response);
if (isSuccess(statusCode)) {
future.complete(value);
} else {
if (isCommandNotFound(statusCode, value)) {
future.completeExceptionally(new CommandNotFoundException(request.getURI().getPath()));
} else {
future.completeExceptionally(new CommandFailedException(value));
}
}
} catch (Exception ex) {
future.completeExceptionally(ex);
logger.error("HTTP request failed: {}", request.getURI().toString(), ex);
}
}
@Override
public void failed(final Exception ex) {
future.completeExceptionally(ex);
logger.error("HTTP request failed: {}", request.getURI().toString(), ex);
}
@Override
public void cancelled() {
future.complete(null);
}
});
return future;
}
主要是调用了一个httpclient的请求以更改客户端sentinel-core中的流控规则(我感觉我离找到‘无伤’动态修改规则越来越近了)。接下来找到服务端请求客户端的接口为/setRules
接下来找到sentinel-core中的ModifyRulesCommandHandler::handle(怀着激动的心情,我离真相越来越近了,到底是用什么方法调用的!)
然后我定睛一看!我丢,大意了(狙击手都打不住的那个男人)。竟然还是你
FlowRuleManager.loadRules(flowRules);
FlowRuleManager.loadRules(flowRules);
FlowRuleManager.loadRules(flowRules);
!!!!!!!!!!!!!!!!!!
未完待续………………