motan源码分析十:流量切换

motan提供了流量切换的功能,可以实现把一个group的流量切换到另一个group(一个或多个服务都可以)。大家可以使用tomcat部署motan的管理工具,并设置几个组,例如可以参考demo代码:motan_demo_server_commandRegistry.xml。分析源码时可以发现,流量切换是在客户端完成的,与服务端没什么关系,在实际的工作中,可以解决很多问题,例如:某个集群出了问题,可以马上将流量切换到其它集群;在系统升级的过程中,将带升级集群的流量切换到其它集群,实现了24小时随时升级等。

1.motan的流量切换是通过command来实现的,每次我们在motan管理器上进行设置的时候,其实是写入信息到注册中心的command节点,而motan又监听了这些command节点,下面是motan的客户端监听command相关的代码

复制代码
    protected void subscribeCommand(final URL url, final CommandListener commandListener) {
        try {
            clientLock.lock();//对clientLock进行上锁
            ConcurrentHashMap<CommandListener, IZkDataListener> dataChangeListeners = commandListeners.get(url);//数据变更监听器
            if (dataChangeListeners == null) {
                commandListeners.putIfAbsent(url, new ConcurrentHashMap<CommandListener, IZkDataListener>());
                dataChangeListeners = commandListeners.get(url);
            }
            IZkDataListener zkDataListener = dataChangeListeners.get(commandListener);
            if (zkDataListener == null) {
                dataChangeListeners.putIfAbsent(commandListener, new IZkDataListener() {//增加新的listener
                    @Override
                    public void handleDataChange(String dataPath, Object data) throws Exception {
                        commandListener.notifyCommand(url, (String) data);//调用commandListener的notifyCommand方法
                        LoggerUtil.info(String.format("[ZookeeperRegistry] command data change: path=%s, command=%s", dataPath, (String) data));
                    }

                    @Override
                    public void handleDataDeleted(String dataPath) throws Exception {
                        commandListener.notifyCommand(url, null);
                        LoggerUtil.info(String.format("[ZookeeperRegistry] command deleted: path=%s", dataPath));
                    }
                });
                zkDataListener = dataChangeListeners.get(commandListener);
            }

            String commandPath = ZkUtils.toCommandPath(url);
            zkClient.subscribeDataChanges(commandPath, zkDataListener);//向zookeeper注册监听事件
            LoggerUtil.info(String.format("[ZookeeperRegistry] subscribe command: path=%s, info=%s", commandPath, url.toFullStr()));
        } catch (Throwable e) {
            throw new MotanFrameworkException(String.format("Failed to subscribe %s to zookeeper(%s), cause: %s", url, getUrl(), e.getMessage()), e);
        } finally {
            clientLock.unlock();
        }
    }
复制代码

2.CommandServiceManager实现了上节中的commandListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public void notifyCommand(URL serviceUrl, String commandString) {
    LoggerUtil.info("CommandServiceManager notify command. service:" + serviceUrl.toSimpleString() + ", command:" + commandString);
 
    if (!MotanSwitcherUtil.isOpen(MOTAN_COMMAND_SWITCHER) || commandString == null) {//判断命令开关是否打开
        LoggerUtil.info("command reset empty since swither is close.");
        commandString = "";
    }
 
    List<URL> finalResult = new ArrayList<URL>();
    URL urlCopy = serviceUrl.createCopy();//serviceurl的副本
 
    if (!StringUtils.equals(commandString, commandStringCache)) {
        commandStringCache = commandString;
        commandCache = RpcCommandUtil.stringToCommand(commandStringCache);//将字符串转换为命令
        Map<String, Integer> weights = new HashMap<String, Integer>();
 
        if (commandCache != null) {
            commandCache.sort();
            finalResult = discoverServiceWithCommand(refUrl, weights, commandCache);
        } else {
            // 如果是指令有异常时,应当按没有指令处理,防止错误指令导致服务异常
            if (StringUtils.isNotBlank(commandString)) {
                LoggerUtil.warn("command parse fail, ignored! command:" + commandString);
                commandString = "";
            }
            // 没有命令时,只返回这个manager实际group对应的结果
            finalResult.addAll(discoverOneGroup(refUrl));
 
        }
 
        // 指令变化时,删除不再有效的缓存,取消订阅不再有效的group
        Set<String> groupKeys = groupServiceCache.keySet();
        for (String gk : groupKeys) {
            if (!weights.containsKey(gk)) {
                groupServiceCache.remove(gk);
                URL urlTemp = urlCopy.createCopy();
                urlTemp.addParameter(URLParamType.group.getName(), gk);
                registry.unsubscribeService(urlTemp, this);
            }
        }
    } else {
        LoggerUtil.info("command not change. url:" + serviceUrl.toSimpleString());
        // 指令没有变化,什么也不做
        return;
    }
 
    for (NotifyListener notifyListener : notifySet) {
        notifyListener.notify(registry.getUrl(), finalResult);
    }
 
    // 当指令从有改到无时,会触发取消订阅所有的group,需要重新订阅本组的service
    if ("".equals(commandString)) {
        LoggerUtil.info("reSub service" + refUrl.toSimpleString());
        registry.subscribeService(refUrl, this);
    }
}

3.discoverServiceWithCommand的相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public List<URL> discoverServiceWithCommand(URL serviceUrl, Map<String, Integer> weights, RpcCommand rpcCommand, String localIP) {
    if (rpcCommand == null || CollectionUtil.isEmpty(rpcCommand.getClientCommandList())) {
        return discoverOneGroup(serviceUrl);
    }
 
    List<URL> mergedResult = new LinkedList<URL>();
    String path = serviceUrl.getPath();//获取路径
 
    List<RpcCommand.ClientCommand> clientCommandList = rpcCommand.getClientCommandList();
    boolean hit = false;
    for (RpcCommand.ClientCommand command : clientCommandList) {
        mergedResult = new LinkedList<URL>();
        // 判断当前url是否符合过滤条件
        boolean match = RpcCommandUtil.match(command.getPattern(), path);
        if (match) {
            hit = true;
            if (!CollectionUtil.isEmpty(command.getMergeGroups())) {
                // 计算出所有要合并的分组及权重
                try {
                    buildWeightsMap(weights, command);
                } catch (MotanFrameworkException e) {
                    LoggerUtil.warn("build weights map fail!" + e.getMessage());
                    continue;
                }
                // 根据计算结果,分别发现各个group的service,合并结果
                mergedResult.addAll(mergeResult(serviceUrl, weights));
            } else {
                mergedResult.addAll(discoverOneGroup(serviceUrl));
            }
 
            LoggerUtil.info("mergedResult: size-" + mergedResult.size() + " --- " + mergedResult.toString());
 
            if (!CollectionUtil.isEmpty(command.getRouteRules())) {
                LoggerUtil.info("router: " + command.getRouteRules().toString());
 
                for (String routeRule : command.getRouteRules()) {
                    String[] fromTo = routeRule.replaceAll("\\s+", "").split("to");
 
                    if (fromTo.length != 2) {
                        routeRuleConfigError();
                        continue;
                    }
                    String from = fromTo[0];
                    String to = fromTo[1];
                    if (from.length() < 1 || to.length() < 1 || !IP_PATTERN.matcher(from).find() || !IP_PATTERN.matcher(to).find()) {
                        routeRuleConfigError();
                        continue;
                    }
                    boolean oppositeFrom = from.startsWith("!");
                    boolean oppositeTo = to.startsWith("!");
                    if (oppositeFrom) {
                        from = from.substring(1);
                    }
                    if (oppositeTo) {
                        to = to.substring(1);
                    }
                    int idx = from.indexOf('*');
                    boolean matchFrom;
                    if (idx != -1) {
                        matchFrom = localIP.startsWith(from.substring(0, idx));
                    } else {
                        matchFrom = localIP.equals(from);
                    }
 
                    // 开头有!,取反
                    if (oppositeFrom) {
                        matchFrom = !matchFrom;
                    }
                    LoggerUtil.info("matchFrom: " + matchFrom + ", localip:" + localIP + ", from:" + from);
                    if (matchFrom) {
                        boolean matchTo;
                        Iterator<URL> iterator = mergedResult.iterator();
                        while (iterator.hasNext()) {
                            URL url = iterator.next();
                            if (url.getProtocol().equalsIgnoreCase("rule")) {
                                continue;
                            }
                            idx = to.indexOf('*');
                            if (idx != -1) {
                                matchTo = url.getHost().startsWith(to.substring(0, idx));
                            } else {
                                matchTo = url.getHost().equals(to);
                            }
                            if (oppositeTo) {
                                matchTo = !matchTo;
                            }
                            if (!matchTo) {
                                iterator.remove();
                                LoggerUtil.info("router To not match. url remove : " + url.toSimpleString());
                            }
                        }
                    }
                }
            }
            // 只取第一个匹配的 TODO 考虑是否能满足绝大多数场景需求
            break;
        }
    }
 
    List<URL> finalResult = new ArrayList<URL>();
    if (!hit) {
        finalResult = discoverOneGroup(serviceUrl);
    } else {
        finalResult.addAll(mergedResult);
    }
    return finalResult;
}

  

posted @   【刘光亮】  阅读(730)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
点击右上角即可分享
微信分享提示