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; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?