All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.redkalex.cluster.nacos.NacosClusterAgent Maven / Gradle / Ivy

There is a newer version: 2.7.7
Show newest version
/*
 */
package org.redkalex.cluster.nacos;

import java.io.Serializable;
import java.net.*;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.redkale.annotation.ResourceChanged;
import org.redkale.boot.*;
import org.redkale.cluster.spi.ClusterAgent;
import org.redkale.convert.json.JsonConvert;
import org.redkale.inject.ResourceEvent;
import org.redkale.service.Service;
import org.redkale.util.*;

/**
 *
 *
 * 
* *
 *  <cluster type="nacos" apiurl="http://localhost:8500/nacos/v1" ttls="5" namespaceid="dev">
 *  </cluster>
 * 
* *
* * @author zhangjx */ public class NacosClusterAgent extends ClusterAgent { protected static final Map httpHeaders = (Map) Utility.ofMap("Content-Type", "application/json", "Accept", "application/json"); // JDK11里面的HttpClient protected HttpClient httpClient; // 不会以/结尾,且不以/nacos结尾,目前以/nacos/v1结尾 protected String apiUrl; // 定时检查的秒数 protected int ttls = 5; // 命名空间 protected String namespaceid; protected ScheduledThreadPoolExecutor scheduler; protected ScheduledFuture taskFuture; // 可能被HttpMessageClient用到的服务 key: serviceName protected final ConcurrentHashMap> httpAddressMap = new ConcurrentHashMap<>(); // 可能被sncp用到的服务 key: serviceName protected final ConcurrentHashMap> sncpAddressMap = new ConcurrentHashMap<>(); @Override public void init(AnyValue config) { super.init(config); this.apiUrl = config.getValue("apiurl"); if (this.apiUrl.endsWith("/")) { this.apiUrl = this.apiUrl.substring(0, this.apiUrl.length() - 1); } this.ttls = config.getIntValue("ttls", 5); if (this.ttls < 3) { this.ttls = 5; } this.namespaceid = config.getValue("namespaceid"); this.httpClient = HttpClient.newHttpClient(); } @Override @ResourceChanged public void onResourceChange(ResourceEvent[] events) { StringBuilder sb = new StringBuilder(); int newTtls = this.ttls; for (ResourceEvent event : events) { if ("ttls".equals(event.name())) { newTtls = Integer.parseInt(event.newValue().toString()); if (newTtls < 5) { sb.append(NacosClusterAgent.class.getSimpleName()) .append(" cannot change '") .append(event.name()) .append("' to '") .append(event.coverNewValue()) .append("'\r\n"); } else { sb.append(NacosClusterAgent.class.getSimpleName()) .append(" change '") .append(event.name()) .append("' to '") .append(event.coverNewValue()) .append("'\r\n"); } } else { sb.append(NacosClusterAgent.class.getSimpleName()) .append(" skip change '") .append(event.name()) .append("' to '") .append(event.coverNewValue()) .append("'\r\n"); } } if (newTtls != this.ttls) { this.ttls = newTtls; start(); } if (sb.length() > 0) { logger.log(Level.INFO, sb.toString()); } } @Override public void destroy(AnyValue config) { if (scheduler != null) { scheduler.shutdownNow(); } } @Override // ServiceLoader时判断配置是否符合当前实现类 public boolean acceptsConf(AnyValue config) { if (config == null) { return false; } if (!"nacos".equalsIgnoreCase(config.getValue("type"))) { return false; } String url = config.getValue("apiurl"); return url != null && url.toLowerCase().contains("/nacos"); } @Override public void start() { if (this.scheduler == null) { AtomicInteger counter = new AtomicInteger(); this.scheduler = new ScheduledThreadPoolExecutor(4, (Runnable r) -> { final Thread t = new Thread( r, "Redkalex-" + NacosClusterAgent.class.getSimpleName() + "-Task-Thread-" + counter.incrementAndGet()); t.setDaemon(true); return t; }); } // delay为了错开请求 if (this.taskFuture != null) { this.taskFuture.cancel(true); } this.taskFuture = this.scheduler.scheduleAtFixedRate( () -> { beatApplicationHealth(); localEntrys.values().stream().filter(e -> !e.canceled).forEach(entry -> { beatLocalHealth(entry); }); reloadSncpAddressHealth(); reloadHttpAddressHealth(); remoteEntrys.values().stream() .filter(entry -> "SNCP".equalsIgnoreCase(entry.protocol)) .forEach(entry -> { updateSncpAddress(entry); }); }, 1000, Math.max(2000, ttls * 1000), TimeUnit.MILLISECONDS); } protected void reloadSncpAddressHealth() { try { String content = Utility.remoteHttpContent( httpClient, "GET", this.apiUrl + "/ns/service/list?pageNo=1&pageSize=99999&namespaceId=" + urlEncode(namespaceid), StandardCharsets.UTF_8, httpHeaders); final ServiceList list = JsonConvert.root().convertFrom(ServiceList.class, content); Set sncpkeys = new HashSet<>(); if (list != null && list.doms != null) { for (String key : list.doms) { if (key.startsWith("sncp:")) { sncpkeys.add(key); } } } sncpkeys.forEach(serviceName -> { try { this.sncpAddressMap.put( serviceName, queryAddress(serviceName).get(Math.max(2, ttls / 2), TimeUnit.SECONDS)); } catch (Exception e) { logger.log(Level.SEVERE, "reloadSncpAddressHealth check " + serviceName + " error", e); } }); } catch (Exception ex) { logger.log(Level.SEVERE, "reloadSncpAddressHealth check error", ex); } } protected void reloadHttpAddressHealth() { try { this.httpAddressMap.keySet().stream().forEach(serviceName -> { try { this.httpAddressMap.put( serviceName, queryAddress(serviceName).get(Math.max(2, ttls / 2), TimeUnit.SECONDS)); } catch (Exception e) { logger.log(Level.SEVERE, "reloadHttpAddressHealth check " + serviceName + " error", e); } }); } catch (Exception ex) { logger.log(Level.SEVERE, "reloadHttpAddressHealth check error", ex); } } @Override // 获取SNCP远程服务的可用ip列表 public CompletableFuture> querySncpAddress(String protocol, String module, String resname) { final String serviceName = generateSncpServiceName(protocol, module, resname); Set rs = sncpAddressMap.get(serviceName); if (rs != null) { return CompletableFuture.completedFuture(rs); } return queryAddress(serviceName).thenApply(t -> { sncpAddressMap.put(serviceName, t); return t; }); } @Override // 获取HTTP远程服务的可用ip列表 public CompletableFuture> queryHttpAddress(String protocol, String module, String resname) { final String serviceName = generateHttpServiceName(protocol, module, resname); Set rs = httpAddressMap.get(serviceName); if (rs != null) { return CompletableFuture.completedFuture(rs); } return queryAddress(serviceName).thenApply(t -> { httpAddressMap.put(serviceName, t); return t; }); } @Override protected CompletableFuture> queryAddress(final ClusterEntry entry) { return queryAddress(entry.serviceName); } private CompletableFuture> queryAddress(final String serviceName) { // return (httpClient != null) ? queryAddress11(serviceName) : queryAddress8(serviceName); return queryAddress11(serviceName); } // JDK11+版本以上的纯异步方法 private CompletableFuture> queryAddress11(final String serviceName) { final HttpClient client = httpClient; String url = this.apiUrl + "/ns/instance/list?serviceName=" + urlEncode(serviceName) + "&namespaceId=" + urlEncode(namespaceid); HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(url)) .expectContinue(true) .timeout(Duration.ofMillis(6000)); httpHeaders.forEach((n, v) -> { if (v instanceof Collection) { for (Object val : (Collection) v) { builder.header(n, val.toString()); } } else if (v != null) { builder.header(n, v.toString()); } }); return client.sendAsync(builder.GET().build(), BodyHandlers.ofString(StandardCharsets.UTF_8)) .thenApply(resp -> { final ServiceEntry entry = JsonConvert.root().convertFrom(ServiceEntry.class, resp.body()); final Set set = new HashSet<>(); if (entry != null && entry.hosts != null) { for (ServiceInstance instance : entry.hosts) { set.add(instance.createSocketAddress()); } } return set; }); } protected boolean isApplicationHealth() { String serviceName = generateApplicationServiceName(); String serviceType = generateApplicationServiceType(); String host = generateApplicationHost(); int port = generateApplicationPort(); String querys = "ip=" + host + "&port=" + port + "&serviceName=" + urlEncode(serviceName) + "&groupName=" + serviceType + "&namespaceId=" + urlEncode(namespaceid) + "&healthyOnly=true"; try { String rs = Utility.remoteHttpContent( httpClient, "GET", this.apiUrl + "/ns/instance?" + querys, StandardCharsets.UTF_8, httpHeaders); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "isApplicationHealth: " + querys + " --> " + rs); } // caused: no service DEFAULT_GROUP@@application.platf.node.20100 found!; if (!rs.startsWith("{")) { return false; } InstanceDetail detail = JsonConvert.root().convertFrom(InstanceDetail.class, rs); return detail != null && detail.healthy; } catch (RetcodeException e) { if (e.getRetcode() != 404) { logger.log(Level.SEVERE, serviceName + " check error", e); } return false; } catch (Exception ex) { logger.log(Level.SEVERE, serviceName + " check error", ex); return false; } } protected void beatHealth(String serviceName, String serviceType, String host, int port) { String beat = "{\"ip\":\"" + host + "\",\"metadata\":{},\"port\":" + port + ",\"scheduled\":true,\"groupName\":\"" + serviceType + "\",\"namespaceId\":\"" + namespaceid + "\",\"serviceName\":\"" + serviceName + "\"}"; try { String rs = Utility.remoteHttpContent( httpClient, "PUT", this.apiUrl + "/ns/instance/beat?serviceName=" + urlEncode(serviceName) + "&groupName=" + serviceType + "&namespaceId=" + urlEncode(namespaceid) + "&beat=" + urlEncode(beat), StandardCharsets.UTF_8, httpHeaders); // if (finest) logger.log(Level.FINEST, "checkLocalHealth: " + beat + " --> " + rs); if (!rs.startsWith("{")) { logger.log(Level.SEVERE, serviceName + " check error: " + rs); } } catch (Exception ex) { logger.log(Level.SEVERE, serviceName + " check error", ex); } } protected void register(String serviceName, String serviceType, String host, int port) { // https://nacos.io/zh-cn/docs/open-api.html#2.1 String querys = "ip=" + host + "&port=" + port + "&serviceName=" + urlEncode(serviceName) + "&groupName=" + serviceType + "&namespaceId=" + urlEncode(namespaceid) + "&healthy=true&enabled=true&ephemeral=false"; try { String rs = Utility.remoteHttpContent( httpClient, "POST", this.apiUrl + "/ns/instance?" + querys, StandardCharsets.UTF_8, httpHeaders); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "register: " + querys + " --> " + rs); } if (!"ok".equalsIgnoreCase(rs)) { logger.log(Level.SEVERE, serviceName + " register error: " + rs); } } catch (Exception ex) { logger.log(Level.SEVERE, serviceName + " register error", ex); } } protected void deregister( String serviceName, String serviceType, String host, int port, ClusterEntry currEntry, boolean realCanceled) { // https://nacos.io/zh-cn/docs/open-api.html#2.2 String querys = "ip=" + host + "&port=" + port + "&serviceName=" + urlEncode(serviceName) + "&groupName=" + serviceType + "&namespaceId=" + urlEncode(namespaceid) + "&ephemeral=false"; try { String rs = Utility.remoteHttpContent( httpClient, "DELETE", this.apiUrl + "/ns/instance?" + querys, StandardCharsets.UTF_8, httpHeaders); if (realCanceled && currEntry != null) { currEntry.canceled = true; } if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "deregister: " + querys + " --> " + rs); } if (!"ok".equalsIgnoreCase(rs)) { logger.log(Level.SEVERE, serviceName + " deregister error: " + rs); } } catch (RetcodeException e) { if (e.getRetcode() != 404) { logger.log(Level.SEVERE, serviceName + " deregister error", e); } } catch (Exception ex) { logger.log(Level.SEVERE, serviceName + " deregister error", ex); } } protected void beatApplicationHealth() { beatHealth( generateApplicationServiceName(), generateApplicationServiceType(), generateApplicationHost(), generateApplicationPort()); } protected void beatLocalHealth(final ClusterEntry cluster) { beatHealth( cluster.serviceName, cluster.resourceType, cluster.address.getHostString(), cluster.address.getPort()); } @Override public void register(Application application) { if (isApplicationHealth()) { throw new RedkaleException("application.nodeid=" + nodeid + " exists in cluster"); } deregister(application); register( generateApplicationServiceName(), generateApplicationServiceType(), generateApplicationHost(), generateApplicationPort()); } @Override public void deregister(Application application) { deregister( generateApplicationServiceName(), generateApplicationServiceType(), generateApplicationHost(), generateApplicationPort(), null, false); } @Override protected ClusterEntry register(NodeServer ns, String protocol, Service service) { deregister(ns, protocol, service, false); ClusterEntry cluster = new ClusterEntry(ns, protocol, service); register(cluster.serviceName, cluster.resourceType, cluster.address.getHostString(), cluster.address.getPort()); return cluster; } @Override protected void deregister(NodeServer ns, String protocol, Service service) { deregister(ns, protocol, service, true); } protected void deregister(NodeServer ns, String protocol, Service service, boolean realCanceled) { String serviceid = generateServiceId(ns, protocol, service); ClusterEntry currEntry = localEntrys.values().stream() .filter(x -> serviceid.equals(x.serviceId)) .findAny() .orElse(null); if (currEntry == null) { currEntry = remoteEntrys.values().stream() .filter(x -> serviceid.equals(x.serviceId)) .findAny() .orElse(null); } ClusterEntry cluster = currEntry == null ? new ClusterEntry(ns, protocol, service) : currEntry; deregister( cluster.serviceName, cluster.resourceType, cluster.address.getHostString(), cluster.address.getPort(), currEntry, realCanceled); } public static final class ServiceList { public int count; public List doms; } public static final class ServiceEntry { public List hosts; } public static final class ServiceInstance { public String ip; public int port; public InetSocketAddress createSocketAddress() { return new InetSocketAddress(ip, port); } } public static final class InstanceDetail { public boolean healthy; } public static final class NameSpaceList { public List data; } public static final class NameSpaceDetail { public String namespace; public String namespaceShowName; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy