diff --git a/tunnel-server/pom.xml b/tunnel-server/pom.xml index f32494492..259a24683 100644 --- a/tunnel-server/pom.xml +++ b/tunnel-server/pom.xml @@ -55,6 +55,11 @@ spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-data-redis + + org.springframework.boot spring-boot-starter-security diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/RedisTunnelClusterStore.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/RedisTunnelClusterStore.java new file mode 100644 index 000000000..449ea84cd --- /dev/null +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/RedisTunnelClusterStore.java @@ -0,0 +1,95 @@ +package com.alibaba.arthas.tunnel.server; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +/** + * + * @author hengyunabc 2020-10-27 + * + */ +public class RedisTunnelClusterStore implements TunnelClusterStore { + + private String prefix = "arthas-tunnel-agent-"; + + private StringRedisTemplate redisTemplate; + + @Override + public String findHost(String agentId) { + ValueOperations opsForValue = redisTemplate.opsForValue(); + + return opsForValue.get(prefix + agentId); + } + + @Override + public void removeAgent(String agentId) { + ValueOperations opsForValue = redisTemplate.opsForValue(); + opsForValue.getOperations().delete(prefix + agentId); + } + + @Override + public void addHost(String agentId, String host, long timeout, TimeUnit timeUnit) { + ValueOperations opsForValue = redisTemplate.opsForValue(); + + opsForValue.set(prefix + agentId, host, timeout, timeUnit); + } + + public StringRedisTemplate getRedisTemplate() { + return redisTemplate; + } + + public void setRedisTemplate(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public Collection allAgentIds() { + ValueOperations opsForValue = redisTemplate.opsForValue(); + + Set result = new HashSet(); + + int length = prefix.length(); + for (String value : opsForValue.getOperations().keys(prefix + "*")) { + result.add(value.substring(length)); + + } + return result; + } + + @Override + public Collection> agentInfo(String appName) { + ValueOperations opsForValue = redisTemplate.opsForValue(); + + Set keys = new HashSet(); + + String prefixWithAppName = prefix + appName + "|"; + + for (String value : opsForValue.getOperations().keys(prefixWithAppName + "*")) { + keys.add(value); + + } + + List values = opsForValue.getOperations().opsForValue().multiGet(keys); + + Collection> result = new HashSet<>(); + + Iterator iterator = values.iterator(); + + for (String key : keys) { + String host = iterator.next(); + String agentId = key.substring(prefix.length()); + result.add(Pair.of(agentId, host)); + } + + return result; + } + +} diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelClusterStore.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelClusterStore.java new file mode 100644 index 000000000..21f959f42 --- /dev/null +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelClusterStore.java @@ -0,0 +1,24 @@ +package com.alibaba.arthas.tunnel.server; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.tuple.Pair; + +/** + * 保存agentId连接到哪个具体的 tunnel server,集群部署时使用 + * + * @author hengyunabc 2020-10-27 + * + */ +public interface TunnelClusterStore { + public void addHost(String agentId, String host, long expire, TimeUnit timeUnit); + + public String findHost(String agentId); + + public void removeAgent(String agentId); + + public Collection allAgentIds(); + + public Collection> agentInfo(String appName); +} diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java index bff3bedab..88437259b 100644 --- a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java @@ -1,6 +1,7 @@ package com.alibaba.arthas.tunnel.server; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -10,6 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.alibaba.arthas.tunnel.common.SimpleHttpResponse; +import com.alibaba.arthas.tunnel.server.utils.InetAddressUtil; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; @@ -31,10 +33,12 @@ import io.netty.util.concurrent.Promise; */ public class TunnelServer { private final static Logger logger = LoggerFactory.getLogger(TunnelServer.class); + public static final String DEFAULT_WEBSOCKET_PATH = "/ws"; private boolean ssl; private String host; private int port; + private String path = DEFAULT_WEBSOCKET_PATH; private Map agentInfoMap = new ConcurrentHashMap(); @@ -50,6 +54,16 @@ public class TunnelServer { private Channel channel; + /** + * 在集群部署时,保存agentId和host关系 + */ + private TunnelClusterStore tunnelClusterStore; + + /** + * 集群部署时外部连接的host + */ + private String clientConnectHost; + public void start() throws Exception { // Configure SSL. final SslContext sslCtx; @@ -78,6 +92,17 @@ public class TunnelServer { agentInfoMap.entrySet().removeIf(e -> !e.getValue().getChannelHandlerContext().channel().isActive()); clientConnectionInfoMap.entrySet() .removeIf(e -> !e.getValue().getChannelHandlerContext().channel().isActive()); + + // 更新集群key信息 + if (tunnelClusterStore != null && clientConnectHost != null) { + try { + for (Entry entry : agentInfoMap.entrySet()) { + tunnelClusterStore.addHost(entry.getKey(), clientConnectHost, 60 * 60, TimeUnit.SECONDS); + } + } catch (Throwable t) { + logger.error("update tunnel info error", t); + } + } } }, 60, 60, TimeUnit.SECONDS); @@ -97,10 +122,17 @@ public class TunnelServer { public void addAgent(String id, AgentInfo agentInfo) { agentInfoMap.put(id, agentInfo); + if (this.tunnelClusterStore != null) { + this.tunnelClusterStore.addHost(id, clientConnectHost, 60 * 60, TimeUnit.SECONDS); + } } public AgentInfo removeAgent(String id) { - return agentInfoMap.remove(id); + AgentInfo agentInfo = agentInfoMap.remove(id); + if (this.tunnelClusterStore != null) { + this.tunnelClusterStore.removeAgent(id); + } + return agentInfo; } public Optional findClientConnection(String id) { @@ -175,4 +207,28 @@ public class TunnelServer { public void setClientConnectionInfoMap(Map clientConnectionInfoMap) { this.clientConnectionInfoMap = clientConnectionInfoMap; } + + public TunnelClusterStore getTunnelClusterStore() { + return tunnelClusterStore; + } + + public void setTunnelClusterStore(TunnelClusterStore tunnelClusterStore) { + this.tunnelClusterStore = tunnelClusterStore; + } + + public String getClientConnectHost() { + return clientConnectHost; + } + + public void setClientConnectHost(String clientConnectHost) { + this.clientConnectHost = clientConnectHost; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } } diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java index de20c094e..8ea047cf2 100644 --- a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java +++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java @@ -18,8 +18,6 @@ import io.netty.handler.ssl.SslContext; */ public class TunnelSocketServerInitializer extends ChannelInitializer { - private static final String WEBSOCKET_PATH = "/ws"; - private final SslContext sslCtx; private TunnelServer tunnelServer; @@ -38,7 +36,7 @@ public class TunnelSocketServerInitializer extends ChannelInitializer + * 在超过一块网卡时会有问题,因为这里每次都只是取了第一块网卡绑定的IP地址 + */ + public static String getInetAddress() { + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + InetAddress address = null; + while (interfaces.hasMoreElements()) { + NetworkInterface ni = interfaces.nextElement(); + Enumeration addresses = ni.getInetAddresses(); + while (addresses.hasMoreElements()) { + address = addresses.nextElement(); + if (isValidAddress(address)) { + return address.getHostAddress(); + } + } + } + logger.warn("Can not get the server IP address"); + return null; + } catch (Throwable t) { + logger.error("Can not get the server IP address", t); + return null; + } + } + + public static boolean isValidAddress(InetAddress address) { + return address != null && !address.isLoopbackAddress() // filter 127.x.x.x + && !address.isAnyLocalAddress() // filter 0.0.0.0 + && !address.isLinkLocalAddress() // filter 169.254.0.0/16 + && !address.getHostAddress().contains(":");// filter IPv6 + } +} diff --git a/tunnel-server/src/main/resources/static/web-console.js b/tunnel-server/src/main/resources/static/web-console.js index d1388c034..fb8401c58 100644 --- a/tunnel-server/src/main/resources/static/web-console.js +++ b/tunnel-server/src/main/resources/static/web-console.js @@ -77,10 +77,10 @@ function getTerminalSize () { } /** init websocket **/ -function initWs (ip, port, agentId) { +function initWs (ip, port, path, agentId) { var protocol= location.protocol === 'https:' ? 'wss://' : 'ws://'; - var path = protocol + ip + ':' + port + '/ws?method=connectArthas&id=' + agentId; - ws = new WebSocket(path); + var uri = protocol + ip + ':' + port + '/' + path + '?method=connectArthas&id=' + agentId; + ws = new WebSocket(uri); } /** init xterm **/ @@ -114,8 +114,13 @@ function startConnect (silent) { alert('Already connected'); return; } + + var path = getUrlParam('path'); + if (path == null) { + path = "ws"; + } // init webSocket - initWs(ip, port, agentId); + initWs(ip, port, path, agentId); ws.onerror = function () { ws.close(); ws = null;