关注 微信公众号:【芋道源码】 有福利:
本文主要分享 EndPoint 与 解析器 。
目前有多种 Eureka-Server 访问地址的配置方式, 本文只分享 Eureka 1.x 的配置 ,不包含 Eureka 1.x 对 Eureka 2.x 的兼容配置:
eureka.serviceUrl.defaultZone=http://127.0.0.1:8080/v2
。 eureka.shouldUseDns=true
并且 eureka.eurekaServer.domainName=eureka.iocoder.cn
。 本文涉及类在 com.netflix.discovery.shared.resolver
包下,涉及到主体类的类图如下(打开大图 ):
com.netflix.discovery.shared.resolver.EurekaEndpoint
,Eureka 服务端点 接口 ,实现代码如下:
public interface EurekaEndpoint extends Comparable<Object>{ /** * @return 完整的服务 URL */ String getServiceUrl(); /** * @deprecated use {@link #getNetworkAddress()} */ @Deprecated String getHostName(); /** * @return 网络地址 */ String getNetworkAddress(); /** * @return 端口 */ int getPort(); /** * @return 是否安全( https ) */ boolean isSecure(); /** * @return 相对路径 */ String getRelativeUri(); }
com.netflix.discovery.shared.resolver.DefaultEndpoint
,默认 Eureka 服务端点 实现类 。实现代码如下:
public class DefaultEndpoint implements EurekaEndpoint{ /** * 网络地址 */ protected final String networkAddress; /** * 端口 */ protected final int port; /** * 是否安全( https ) */ protected final boolean isSecure; /** * 相对地址 */ protected final String relativeUri; /** * 完整的服务 URL */ protected final String serviceUrl; public DefaultEndpoint(String serviceUrl){ this.serviceUrl = serviceUrl; // 将 serviceUrl 分解成 几个属性 try { URL url = new URL(serviceUrl); this.networkAddress = url.getHost(); this.port = url.getPort(); this.isSecure = "https".equals(url.getProtocol()); this.relativeUri = url.getPath(); } catch (Exception e) { throw new IllegalArgumentException("Malformed serviceUrl: " + serviceUrl); } } public DefaultEndpoint(String networkAddress, int port, boolean isSecure, String relativeUri){ this.networkAddress = networkAddress; this.port = port; this.isSecure = isSecure; this.relativeUri = relativeUri; // 几个属性 拼接成 serviceUrl StringBuilder sb = new StringBuilder().append(isSecure ? "https" : "http").append("://").append(networkAddress); if (port >= 0) { sb.append(':').append(port); } if (relativeUri != null) { if (!relativeUri.startsWith("/")) { sb.append('/'); } sb.append(relativeUri); } this.serviceUrl = sb.toString(); } }
#equals(...)
和 #hashCode(...)
方法,标准实现方式,这里就不贴代码了。 #compareTo(...)
方法,基于 serviceUrl
属性做比较。 com.netflix.discovery.shared.resolver.aws.AwsEndpoint
,基于 region
、 zone
的 Eureka 服务端点 实现类 ( 请不要在意 AWS 开头 )。实现代码如下:
public class AwsEndpoint extends DefaultEndpoint{ /** * 区域 */ protected final String region; /** * 可用区 */ protected final String zone; }
#equals(...)
和 #hashCode(...)
方法,标准实现方式,这里就不贴代码了。 EndPoint 解析器使用 委托设计模式 实现。所以,上文图片中我们看到好多个解析器, 实际代码非常非常非常清晰 。
FROM 《委托模式》
委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承,它还使我们可以模拟mixin。
我们在上图的基础上, 增加委托的关系 ,如下图:
com.netflix.discovery.shared.resolver.ClusterResolver
,集群解析器 接口 。接口代码如下:
public interface ClusterResolver<T extends EurekaEndpoint>{ /** * @return 地区 */ String getRegion(); /** * @return EndPoint 集群( 数组 ) */ List<T> getClusterEndpoints(); }
com.netflix.discovery.shared.resolver.ClosableResolver
, 可关闭 的解析器 接口 ,继承自 ClusterResolver 接口 。接口代码如下:
public interface ClosableResolver<T extends EurekaEndpoint> extends ClusterResolver<T>{ /** * 关闭 */ void shutdown(); }
com.netflix.discovery.shared.resolver.aws.DnsTxtRecordClusterResolver
,基于 DNS TXT 记录类型的集群解析器。 类属性 代码如下:
public class DnsTxtRecordClusterResolver implements ClusterResolver<AwsEndpoint>{ /** * 地区 */ private final String region; /** * 集群根地址,例如 txt.default.eureka.iocoder.cn */ private final String rootClusterDNS; /** * 是否解析可用区( zone ) */ private final boolean extractZoneFromDNS; /** * 端口 */ private final int port; /** * 是否安全 */ private final boolean isSecure; /** * 相对地址 */ private final String relativeUri; }
DnsTxtRecordClusterResolver 通过集群根地址( rootClusterDNS
) 解析出 EndPoint 集群。需要在 DNS 配置 两层 解析记录:
TXT.${REGION}.${自定义二级域名}
。 TXT.${ZONE}.${自定义二级域名}
或者 ${ZONE}.${自定义二级域名}
。 举个例子:
rootClusterDNS
,集群根地址。例如: txt.default.eureka.iocoder.cn
,其· txt.default.eureka
为 DNS 解析记录的第一层的 主机记录 。
region
:地区。需要和 rootClusterDNS
的 ${REGION}
一致。 extractZoneFromDNS
:是否解析 DNS 解析记录的第二层级的 主机记录 的 ${ZONE}
可用区。 #getClusterEndpoints(...)
方法,实现代码如下:
1: @Override 2: public List<AwsEndpoint> getClusterEndpoints(){ 3: List<AwsEndpoint> eurekaEndpoints = resolve(region, rootClusterDNS, extractZoneFromDNS, port, isSecure, relativeUri); 4: if (logger.isDebugEnabled()) { 5: logger.debug("Resolved {} to {}", rootClusterDNS, eurekaEndpoints); 6: } 7: return eurekaEndpoints; 8: } 9: 10: private static List<AwsEndpoint> resolve(String region, String rootClusterDNS, boolean extractZone, int port, boolean isSecure, String relativeUri){ 11: try { 12: // 解析 第一层 DNS 记录 13: Set<String> zoneDomainNames = resolve(rootClusterDNS); 14: if (zoneDomainNames.isEmpty()) { 15: throw new ClusterResolverException("Cannot resolve Eureka cluster addresses; there are no data in TXT record for DN " + rootClusterDNS); 16: } 17: // 记录 第二层 DNS 记录 18: List<AwsEndpoint> endpoints = new ArrayList<>(); 19: for (String zoneDomain : zoneDomainNames) { 20: String zone = extractZone ? ResolverUtils.extractZoneFromHostName(zoneDomain) : null; // 21: Set<String> zoneAddresses = resolve(zoneDomain); 22: for (String address : zoneAddresses) { 23: endpoints.add(new AwsEndpoint(address, port, isSecure, relativeUri, region, zone)); 24: } 25: } 26: return endpoints; 27: } catch (NamingException e) { 28: throw new ClusterResolverException("Cannot resolve Eureka cluster addresses for root: " + rootClusterDNS, e); 29: } 30: }
第 12 至 16 行 :调用 #resolve(rootClusterDNS)
解析 第一层 DNS 记录。实现代码如下:
1: private static Set<String> resolve(String rootClusterDNS) throws NamingException{ 2: Set<String> result; 3: try { 4: result = DnsResolver.getCNamesFromTxtRecord(rootClusterDNS); 5: // TODO 芋艿:这块是bug,不需要这一段 6: if (!rootClusterDNS.startsWith("txt.")) { 7: result = DnsResolver.getCNamesFromTxtRecord("txt." + rootClusterDNS); 8: } 9: } catch (NamingException e) { 10: if (!rootClusterDNS.startsWith("txt.")) { 11: result = DnsResolver.getCNamesFromTxtRecord("txt." + rootClusterDNS); 12: } else { 13: throw e; 14: } 15: } 16: return result; 17: }
DnsResolver#getCNamesFromTxtRecord(...)
方法,解析 TXT 主机记录。点击 链接 查看带中文注释的 DnsResolver 的代码,比较解析,笔者就不啰嗦了。 rootClusterDNS
不以 txt.
开头时,即使第 4 行解析成功,也会报错,此时是个 Eureka 的 BUG 。因此,配置 DNS 解析记录时,主机记录暂时必须以 txt.
开头。 第 17 至 25 行 :循环第一层 DNS 记录的解析结果,进一步解析第二层 DNS 记录。
zone
)。 #resolve(rootClusterDNS)
解析 第二层 DNS 记录。 com.netflix.discovery.shared.resolver.aws.ConfigClusterResolver
,基于 配置文件 的集群解析器。 类属性 代码如下:
public class ConfigClusterResolver implements ClusterResolver<AwsEndpoint>{ private final EurekaClientConfig clientConfig; private final InstanceInfo myInstanceInfo; public ConfigClusterResolver(EurekaClientConfig clientConfig, InstanceInfo myInstanceInfo){ this.clientConfig = clientConfig; this.myInstanceInfo = myInstanceInfo; } }
#getClusterEndpoints(...)
方法,实现代码如下:
1: @Override 2: public List<AwsEndpoint> getClusterEndpoints(){ 3: // 使用 DNS 获取 EndPoint 4: if (clientConfig.shouldUseDnsForFetchingServiceUrls()) { 5: if (logger.isInfoEnabled()) { 6: logger.info("Resolving eureka endpoints via DNS: {}", getDNSName()); 7: } 8: return getClusterEndpointsFromDns(); 9: } else { 10: // 直接配置实际访问地址 11: logger.info("Resolving eureka endpoints via configuration"); 12: return getClusterEndpointsFromConfig(); 13: } 14: }
第 3 至 8 行 :基于 DNS 获取 EndPoint 集群,调用 #getClusterEndpointsFromDns()
方法,实现代码如下:
private List<AwsEndpoint> getClusterEndpointsFromDns(){ String discoveryDnsName = getDNSName(); // 获取 集群根地址 int port = Integer.parseInt(clientConfig.getEurekaServerPort()); // 端口 // cheap enough so just re-use DnsTxtRecordClusterResolver dnsResolver = new DnsTxtRecordClusterResolver( getRegion(), discoveryDnsName, true, // 解析 zone port, false, clientConfig.getEurekaServerURLContext() ); // 调用 DnsTxtRecordClusterResolver 解析 EndPoint List<AwsEndpoint> endpoints = dnsResolver.getClusterEndpoints(); if (endpoints.isEmpty()) { logger.error("Cannot resolve to any endpoints for the given dnsName: {}", discoveryDnsName); } return endpoints; } private String getDNSName(){ return "txt." + getRegion() + '.' + clientConfig.getEurekaServerDNSName(); }
eureka.shouldUseDns=true
,开启基于 DNS 获取 EndPoint 集群。 eureka.eurekaServer.domainName=${xxxxx}
,配置集群根地址。 eureka.eurekaServer.port
, eureka.eurekaServer.context
。 第 9 至 13 行 :直接 配置文件 填写实际 EndPoint 集群,调用 #getClusterEndpointsFromConfig()
方法,实现代码如下:
1: private List<AwsEndpoint> getClusterEndpointsFromConfig(){ 2: // 获得 可用区 3: String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); 4: // 获取 应用实例自己 的 可用区 5: String myZone = InstanceInfo.getZone(availZones, myInstanceInfo); 6: // 获得 可用区与 serviceUrls 的映射 7: Map<String, List<String>> serviceUrls = EndpointUtils.getServiceUrlsMapFromConfig(clientConfig, myZone, clientConfig.shouldPreferSameZoneEureka()); 8: // 拼装 EndPoint 集群结果 9: List<AwsEndpoint> endpoints = new ArrayList<>(); 10: for (String zone : serviceUrls.keySet()) { 11: for (String url : serviceUrls.get(zone)) { 12: try { 13: endpoints.add(new AwsEndpoint(url, getRegion(), zone)); 14: } catch (Exception ignore) { 15: logger.warn("Invalid eureka server URI: {}; removing from the server pool", url); 16: } 17: } 18: } 19: 20: // 打印日志,EndPoint 集群 21: if (logger.isDebugEnabled()) { 22: logger.debug("Config resolved to {}", endpoints); 23: } 24: // 打印日志,解析结果为空 25: if (endpoints.isEmpty()) { 26: logger.error("Cannot resolve to any endpoints from provided configuration: {}", serviceUrls); 27: } 28: 29: return endpoints; 30: }
eureka.${REGION}.availabilityZones
配置。 InstanceInfo#getZone(...)
方法,获得 应用实例自己所在的可用区 ( zone
)。非亚马逊 AWS 环境下,可用区数组的第一个元素就是 应用实例自己所在的可用区 。 第 7 行 :调用 EndpointUtils#getServiceUrlsMapFromConfig(...)
方法,获得可用区与 serviceUrls
的映射。实现代码如下:
// EndpointUtils.java 1: public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { 2: Map<String, List<String>> orderedUrls = new LinkedHashMap<>(); // key:zone;value:serviceUrls 3: // 获得 应用实例的 地区( region ) 4: String region = getRegion(clientConfig); 5: // 获得 应用实例的 可用区 6: String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); 7: if (availZones == null || availZones.length == 0) { 8: availZones = new String[1]; 9: availZones[0] = DEFAULT_ZONE; 10: } 11: logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones)); 12: // 获得 开始位置 13: int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); 14: // 将 开始位置 的 serviceUrls 添加到结果 15: String zone = availZones[myZoneOffset]; 16: List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); 17: if (serviceUrls != null) { 18: orderedUrls.put(zone, serviceUrls); 19: } 20: // 从开始位置顺序遍历剩余的 serviceUrls 添加到结果 21: int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1); 22: while (currentOffset != myZoneOffset) { 23: zone = availZones[currentOffset]; 24: serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); 25: if (serviceUrls != null) { 26: orderedUrls.put(zone, serviceUrls); 27: } 28: if (currentOffset == (availZones.length - 1)) { 29: currentOffset = 0; 30: } else { 31: currentOffset++; 32: } 33: } 34: 35: // 为空,报错 36: if (orderedUrls.size() < 1) { 37: throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"); 38: } 39: return orderedUrls; 40: }
第 13 行 :获得 开始位置 。实现代码如下:
private static int getZoneOffset(String myZone, boolean preferSameZone, String[] availZones){ for (int i = 0; i < availZones.length; i++) { if (myZone != null && (availZones[i].equalsIgnoreCase(myZone.trim()) == preferSameZone)) { return i; } } logger.warn("DISCOVERY: Could not pick a zone based on preferred zone settings. My zone - {}," + " preferSameZone- {}. Defaulting to " + availZones[0], myZone, preferSameZone); return 0; }
preferSameZone=true
,即 eureka.preferSameZone=true
( 默认值 : true
) 时, 开始位置 为可用区数组( availZones
)的 第一个 和应用实例所在的可用区( myZone
)【 相等 】元素的位置。 preferSameZone=false
,即 eureka.preferSameZone=false
( 默认值 : true
) 时, 开始位置 为可用区数组( availZones
)的 第一个 和应用实例所在的可用区( myZone
)【 不相等 】元素的位置。 第 20 至 33 行 :从开始位置 顺序 将剩余的可用区的 serviceUrls
添加到结果。 顺序 理解如下图:
第 9 至 18 行 :拼装 EndPoint 集群结果。
com.netflix.discovery.shared.resolver.aws.ZoneAffinityClusterResolver
,使用可用区亲和的集群解析器。 类属性 代码如下:
public class ZoneAffinityClusterResolver implements ClusterResolver<AwsEndpoint>{ private static final Logger logger = LoggerFactory.getLogger(ZoneAffinityClusterResolver.class); /** * 委托的解析器 * 目前代码里为 {@link ConfigClusterResolver} */ private final ClusterResolver<AwsEndpoint> delegate; /** * 应用实例的可用区 */ private final String myZone; /** * 是否可用区亲和 */ private final boolean zoneAffinity; public ZoneAffinityClusterResolver(ClusterResolver<AwsEndpoint> delegate, String myZone, boolean zoneAffinity){ this.delegate = delegate; this.myZone = myZone; this.zoneAffinity = zoneAffinity; } }
delegate
,委托的解析器。目前代码里使用的是 ConfigClusterResolver 。 zoneAffinity
,是否可用区亲和。
true
:EndPoint 可用区为 本地 的优先被放在前面。 false
:EndPoint 可用区 非本地 的优先被放在前面。 #getClusterEndpoints(...)
方法,实现代码如下:
1: @Override 2: public List<AwsEndpoint> getClusterEndpoints(){ 3: // 拆分成 本地的可用区和非本地的可用区的 EndPoint 集群 4: List<AwsEndpoint>[] parts = ResolverUtils.splitByZone(delegate.getClusterEndpoints(), myZone); 5: List<AwsEndpoint> myZoneEndpoints = parts[0]; 6: List<AwsEndpoint> remainingEndpoints = parts[1]; 7: // 随机打乱 EndPoint 集群并进行合并 8: List<AwsEndpoint> randomizedList = randomizeAndMerge(myZoneEndpoints, remainingEndpoints); 9: // 非可用区亲和,将非本地的可用区的 EndPoint 集群放在前面 10: if (!zoneAffinity) { 11: Collections.reverse(randomizedList); 12: } 13: 14: if (logger.isDebugEnabled()) { 15: logger.debug("Local zone={}; resolved to: {}", myZone, randomizedList); 16: } 17: 18: return randomizedList; 19: }
ClusterResolver#getClusterEndpoints()
方法,获得 EndPoint 集群。再调用 ResolverUtils#splitByZone(...)
方法,拆分成 本地 和 非本地 的可用区的 EndPoint 集群,点击 链接 查看实现。 第 8 行 :调用 #randomizeAndMerge(...)
方法, 分别 随机打乱 每个 EndPoint 集群,并进行 合并 数组,实现代码如下:
// ZoneAffinityClusterResolver.java private static List<AwsEndpoint> randomizeAndMerge(List<AwsEndpoint> myZoneEndpoints, List<AwsEndpoint> remainingEndpoints){ if (myZoneEndpoints.isEmpty()) { return ResolverUtils.randomize(remainingEndpoints); // 打乱 } if (remainingEndpoints.isEmpty()) { return ResolverUtils.randomize(myZoneEndpoints); // 打乱 } List<AwsEndpoint> mergedList = ResolverUtils.randomize(myZoneEndpoints); // 打乱 mergedList.addAll(ResolverUtils.randomize(remainingEndpoints)); // 打乱 return mergedList; } // ResolverUtils.java public static <T extends EurekaEndpoint> List<T> randomize(List<T> list){ // 数组大小为 0 或者 1 ,不进行打乱 List<T> randomList = new ArrayList<>(list); if (randomList.size() < 2) { return randomList; } // 以本地IP为随机种子,有如下好处: // 多个主机,实现对同一个 EndPoint 集群负载均衡的效果。 // 单个主机,同一个 EndPoint 集群按照固定顺序访问。Eureka-Server 不是强一致性的注册中心,Eureka-Client 对同一个 Eureka-Server 拉取注册信息,保证两者之间增量同步的一致性。 Random random = new Random(LOCAL_IPV4_ADDRESS.hashCode()); int last = randomList.size() - 1; for (int i = 0; i < last; i++) { int pos = random.nextInt(randomList.size() - i); if (pos != i) { Collections.swap(randomList, i, pos); } } return randomList; }
ResolverUtils#randomize(...)
使用以本机IP为随机种子 ,有如下好处:
第 10 至 12 行 :非可用区亲和,将非本地的可用区的 EndPoint 集群放在前面。
com.netflix.discovery.shared.resolver.AsyncResolver
, 异步执行 解析的集群解析器。AsyncResolver 属性较多,而且复杂的多,我们拆分到具体方法里分享。
AsyncResolver 内置定时任务, 定时 刷新 EndPoint 集群解析结果。
为什么要刷新?例如,Eureka-Server 的 serviceUrls
基于 DNS 配置。
/** * 是否已经调度定时任务 {@link #updateTask} */ private final AtomicBoolean scheduled = new AtomicBoolean(false); /** * 委托的解析器 * 目前代码为 {@link com.netflix.discovery.shared.resolver.aws.ZoneAffinityClusterResolver} */ private final ClusterResolver<T> delegate; /** * 定时服务 */ private final ScheduledExecutorService executorService; /** * 线程池执行器 */ private final ThreadPoolExecutor threadPoolExecutor; /** * 后台任务 * 定时解析 EndPoint 集群 */ private final TimedSupervisorTask backgroundTask; /** * 解析 EndPoint 集群结果 */ private final AtomicReference<List<T>> resultsRef; /** * 定时解析 EndPoint 集群的频率 */ private final int refreshIntervalMs; /** * 预热超时时间,单位:毫秒 */ private final int warmUpTimeoutMs; // Metric timestamp, tracking last time when data were effectively changed. private volatile long lastLoadTimestamp = -1; AsyncResolver(String name, ClusterResolver<T> delegate, List<T> initialValue, int executorThreadPoolSize, int refreshIntervalMs, int warmUpTimeoutMs) { this.name = name; this.delegate = delegate; this.refreshIntervalMs = refreshIntervalMs; this.warmUpTimeoutMs = warmUpTimeoutMs; // 初始化 定时服务 this.executorService = Executors.newScheduledThreadPool(1, // 线程大小=1 new ThreadFactoryBuilder() .setNameFormat("AsyncResolver-" + name + "-%d") .setDaemon(true) .build()); // 初始化 线程池执行器 this.threadPoolExecutor = new ThreadPoolExecutor( 1, // 线程大小=1 executorThreadPoolSize, 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), // use direct handoff new ThreadFactoryBuilder() .setNameFormat("AsyncResolver-" + name + "-executor-%d") .setDaemon(true) .build() ); // 初始化 后台任务 this.backgroundTask = new TimedSupervisorTask( this.getClass().getSimpleName(), executorService, threadPoolExecutor, refreshIntervalMs, TimeUnit.MILLISECONDS, 5, updateTask ); this.resultsRef = new AtomicReference<>(initialValue); Monitors.registerObject(name, this); }
backgroundTask
,后台任务,定时解析 EndPoint 集群。
updateTask
实现代码如下:
private final Runnable updateTask = new Runnable() { @Override public void run(){ try { List<T> newList = delegate.getClusterEndpoints(); // 调用 委托的解析器 解析 EndPoint 集群 if (newList != null) { resultsRef.getAndSet(newList); lastLoadTimestamp = System.currentTimeMillis(); } else { logger.warn("Delegate returned null list of cluster endpoints"); } logger.debug("Resolved to {}", newList); } catch (Exception e) { logger.warn("Failed to retrieve cluster endpoints from the delegate", e); } } };
delegate
,委托的解析器,目前代码为 ZoneAffinityClusterResolver。 后台任务的 发起 在 #getClusterEndpoints()
方法,在「3.6.2 解析 EndPoint 集群」详细解析。
调用 #getClusterEndpoints()
方法,解析 EndPoint 集群,实现代码如下:
1: @Override 2: public List<T> getClusterEndpoints(){ 3: long delay = refreshIntervalMs; 4: // 若未预热解析 EndPoint 集群结果,进行预热 5: if (warmedUp.compareAndSet(false, true)) { 6: if (!doWarmUp()) { 7: delay = 0; // 预热失败,取消定时任务的第一次延迟 8: } 9: } 10: // 若未调度定时任务,进行调度 11: if (scheduled.compareAndSet(false, true)) { 12: scheduleTask(delay); 13: } 14: // 返回 EndPoint 集群 15: return resultsRef.get(); 16: }
第 5 至 9 行 : 若未预热解析 EndPoint 集群结果 ,调用 #doWarmUp()
方法,进行预热。若预热失败,取消定时任务的第一次延迟。 #doWarmUp()
方法实现代码如下:
boolean doWarmUp(){ Future future = null; try { future = threadPoolExecutor.submit(updateTask); future.get(warmUpTimeoutMs, TimeUnit.MILLISECONDS); // block until done or timeout return true; } catch (Exception e) { logger.warn("Best effort warm up failed", e); } finally { if (future != null) { future.cancel(true); } } return false; }
updateTask
,解析 EndPoint 集群。 第 10 至 13 行 : 若未调度定时任务,进行调度 ,调用 #scheduleTask()
方法,实现代码如下:
void scheduleTask(long delay){ executorService.schedule(backgroundTask, delay, TimeUnit.MILLISECONDS); }
第 15 行 :返回 EndPoint 集群。 当第一次预热失败,会返回空,直到定时任务获得到结果 。
Eureka-Client 在初始化时,调用 DiscoveryClient#scheduleServerEndpointTask()
方法,初始化 AsyncResolver 解析器。实现代码如下:
private void scheduleServerEndpointTask(EurekaTransport eurekaTransport, AbstractDiscoveryClientOptionalArgs args){ // ... 省略无关代码 // 创建 EndPoint 解析器 eurekaTransport.bootstrapResolver = EurekaHttpClients.newBootstrapResolver( clientConfig, transportConfig, eurekaTransport.transportClientFactory, applicationInfoManager.getInfo(), applicationsSource ); // ... 省略无关代码 }
调用 EurekaHttpClients#newBootstrapResolver(...)
方法,创建 EndPoint 解析器,实现代码如下:
1: public static final String COMPOSITE_BOOTSTRAP_STRATEGY = "composite"; 2: 3: public static ClosableResolver<AwsEndpoint> newBootstrapResolver( 4: final EurekaClientConfig clientConfig, 5: final EurekaTransportConfig transportConfig, 6: final TransportClientFactory transportClientFactory, 7: final InstanceInfo myInstanceInfo, 8: final ApplicationsResolver.ApplicationsSource applicationsSource) 9:{ 10: if (COMPOSITE_BOOTSTRAP_STRATEGY.equals(transportConfig.getBootstrapResolverStrategy())) { 11: if (clientConfig.shouldFetchRegistry()) { 12: return compositeBootstrapResolver( 13: clientConfig, 14: transportConfig, 15: transportClientFactory, 16: myInstanceInfo, 17: applicationsSource 18: ); 19: } else { 20: logger.warn("Cannot create a composite bootstrap resolver if registry fetch is disabled." + 21: " Falling back to using a default bootstrap resolver."); 22: } 23: } 24: 25: // if all else fails, return the default 26: return defaultBootstrapResolver(clientConfig, myInstanceInfo); 27: } 28: 29: /** 30: * @return a bootstrap resolver that resolves eureka server endpoints based on either DNS or static config, 31: * depending on configuration for one or the other. This resolver will warm up at the start. 32: */ 33: static ClosableResolver<AwsEndpoint> defaultBootstrapResolver(final EurekaClientConfig clientConfig, 34: final InstanceInfo myInstanceInfo){ 35: // 获得 可用区集合 36: String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); 37: // 获得 应用实例的 可用区 38: String myZone = InstanceInfo.getZone(availZones, myInstanceInfo); 39: 40: // 创建 ZoneAffinityClusterResolver 41: ClusterResolver<AwsEndpoint> delegateResolver = new ZoneAffinityClusterResolver( 42: new ConfigClusterResolver(clientConfig, myInstanceInfo), 43: myZone, 44: true 45: ); 46: 47: // 第一次 EndPoint 解析 48: List<AwsEndpoint> initialValue = delegateResolver.getClusterEndpoints(); 49: 50: // 解析不到 Eureka-Server EndPoint ,快速失败 51: if (initialValue.isEmpty()) { 52: String msg = "Initial resolution of Eureka server endpoints failed. Check ConfigClusterResolver logs for more info"; 53: logger.error(msg); 54: failFastOnInitCheck(clientConfig, msg); 55: } 56: 57: // 创建 AsyncResolver 58: return new AsyncResolver<>( 59: EurekaClientNames.BOOTSTRAP, 60: delegateResolver, 61: initialValue, 62: 1, 63: clientConfig.getEurekaServiceUrlPollIntervalSeconds() * 1000 64: ); 65: }
* 第 10 至 23 行 :组合解析器,用于 Eureka 1.x 对 Eureka 2.x 的兼容配置,暂时不需要了解。TODO[0028]写入集群和读取集群 * 第 26 行 :调用 `#defaultBootstrapResolver()` 方法,创建默认的解析器 AsyncResolver 。 * 第 40 至 45 行 :创建 ZoneAffinityClusterResolver 。在 ZoneAffinityClusterResolver 构造方法的参数,我们看到创建 ConfigClusterResolver 作为 `delegate` 参数。 * 第 48 行 :调用 `ZoneAffinityClusterResolver#getClusterEndpoints()` 方法,**第一次 Eureka-Server EndPoint 集群解析**。 * 第 51 至 55 行 :解析不到 Eureka-Server EndPoint 集群时,可以通过配置( `eureka.experimental.clientTransportFailFastOnInit=true` ),使 Eureka-Client 初始化失败。`#failFastOnInitCheck(...)` 方法,实现代码如下:
// potential future feature, guarding with experimental flag for now private static void failFastOnInitCheck(EurekaClientConfig clientConfig, String msg){ if ("true".equals(clientConfig.getExperimental("clientTransportFailFastOnInit"))) { throw new RuntimeException(msg); } }
* x
第 58 至 64 行 :创建 AsyncResolver 。从代码上,我们可以看到, AsyncResolver.resultsRef
属性一开始已经用 initialValue
传递给 AsyncResolver 构造方法。实现代码如下:
public AsyncResolver(String name, ClusterResolver<T> delegate, List<T> initialValues, int executorThreadPoolSize, int refreshIntervalMs){ this( name, delegate, initialValues, executorThreadPoolSize, refreshIntervalMs, 0 ); // 设置已经预热 warmedUp.set(true); }
T T 一开始看解析器,没反应过来是委托设计模式,一脸懵逼+一脸懵逼+一脸懵逼。后面理顺了,发现超级奈斯( Nice ) 啊 !!!!
胖友,你学会了么?
胖友,分享我的公众号( 芋道源码 ) 给你的胖友可好?