微服务:整合 Spring Cloud Eureka - 高级属性Region、Zone
目录
微服务:整合 Spring Cloud Eureka - 注册中心 Eureka Server
微服务:整合 Spring Cloud Eureka - 服务注册 Eureka Client
微服务:整合 Spring Cloud Eureka - 服务发现 DiscoveryClient
微服务:整合 Spring Cloud Eureka - 服务消费以及Ribbon简单使用
微服务:整合 Spring Cloud Eureka - 高可用集群
微服务:整合 Spring Cloud Eureka - .NET Core Mvc Api (C#)
微服务:整合 Spring Cloud Eureka - 服务治理机制
微服务:整合 Spring Cloud Eureka - 服务事件监听
微服务:整合 Spring Cloud Eureka - 高级属性Region、Zone
微服务:整合 Spring Cloud Eureka - Rest接口文档
微服务:整合 Spring Cloud Eureka - Security 安全保护
一、简介
当用户地理分布范围很广的时候,比如公司在北京、上海、广州等都有分公司的时候,一般都会有多个机房。那么对于用户而言,当然是希望调用本地分公司的机房中的微服务应用。比如:上海用户A,调用OAuth2服务,用户A当然希望调用上海机房里面的微服务应用。如果上海用户A调用北京机房的OAuth2服务,就增加的延时时间。所以我们希望一个机房内的服务优先调用同一个机房内的服务,当同一个机房的服务不可用的时候,再去调用其它机房的服务,以达到减少延时的作用。
为此,Eureka提供了Region、Zone参数设置,就是用来解决这个问题。
二、概念
eureka提供了region和zone两个概念来进行分区,这两个概念均来自于亚马逊的AWS:
(1)region:可以简单理解为地理上的分区,比如上海地区,或者广州地区,再或者北京等等,没有具体大小的限制。根据项目具体的情况,可以自行合理划分region。
(2)zone:可以简单理解为region内的具体机房,比如说region划分为北京,然后北京有两个机房,就可以在此region之下划分出zone1,zone2两个zone。
三、源码解析
1、关于Region、Zone的处理类:com.netflix.discovery.endpoint.EndpointUtils.java
1 package com.netflix.discovery.endpoint; 2 3 import com.netflix.appinfo.InstanceInfo; 4 import com.netflix.discovery.EurekaClientConfig; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 8 import java.util.ArrayList; 9 import java.util.HashMap; 10 import java.util.LinkedHashMap; 11 import java.util.List; 12 import java.util.Map; 13 import java.util.Set; 14 import java.util.TreeMap; 15 import java.util.TreeSet; 16 17 /** 18 * This class contains some of the utility functions previously found in DiscoveryClient, but should be elsewhere. 19 * It *does not yet* clean up the moved code. 20 */ 21 public class EndpointUtils { 22 private static final Logger logger = LoggerFactory.getLogger(EndpointUtils.class); 23 24 public static final String DEFAULT_REGION = "default"; 25 public static final String DEFAULT_ZONE = "default"; 26 27 public enum DiscoveryUrlType { 28 CNAME, A 29 } 30 31 public static interface ServiceUrlRandomizer { 32 void randomize(List<String> urlList); 33 } 34 35 public static class InstanceInfoBasedUrlRandomizer implements ServiceUrlRandomizer { 36 private final InstanceInfo instanceInfo; 37 38 public InstanceInfoBasedUrlRandomizer(InstanceInfo instanceInfo) { 39 this.instanceInfo = instanceInfo; 40 } 41 42 @Override 43 public void randomize(List<String> urlList) { 44 int listSize = 0; 45 if (urlList != null) { 46 listSize = urlList.size(); 47 } 48 if ((instanceInfo == null) || (listSize == 0)) { 49 return; 50 } 51 // Find the hashcode of the instance hostname and use it to find an entry 52 // and then arrange the rest of the entries after this entry. 53 int instanceHashcode = instanceInfo.getHostName().hashCode(); 54 if (instanceHashcode < 0) { 55 instanceHashcode = instanceHashcode * -1; 56 } 57 int backupInstance = instanceHashcode % listSize; 58 for (int i = 0; i < backupInstance; i++) { 59 String zone = urlList.remove(0); 60 urlList.add(zone); 61 } 62 } 63 } 64 65 /** 66 * Get the list of all eureka service urls for the eureka client to talk to. 67 * 68 * @param clientConfig the clientConfig to use 69 * @param zone the zone in which the client resides 70 * @param randomizer a randomizer to randomized returned urls, if loading from dns 71 * 72 * @return The list of all eureka service urls for the eureka client to talk to. 73 */ 74 public static List<String> getDiscoveryServiceUrls(EurekaClientConfig clientConfig, String zone, ServiceUrlRandomizer randomizer) { 75 boolean shouldUseDns = clientConfig.shouldUseDnsForFetchingServiceUrls(); 76 if (shouldUseDns) { 77 return getServiceUrlsFromDNS(clientConfig, zone, clientConfig.shouldPreferSameZoneEureka(), randomizer); 78 } 79 return getServiceUrlsFromConfig(clientConfig, zone, clientConfig.shouldPreferSameZoneEureka()); 80 } 81 82 /** 83 * Get the list of all eureka service urls from DNS for the eureka client to 84 * talk to. The client picks up the service url from its zone and then fails over to 85 * other zones randomly. If there are multiple servers in the same zone, the client once 86 * again picks one randomly. This way the traffic will be distributed in the case of failures. 87 * 88 * @param clientConfig the clientConfig to use 89 * @param instanceZone The zone in which the client resides. 90 * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise. 91 * @param randomizer a randomizer to randomized returned urls 92 * 93 * @return The list of all eureka service urls for the eureka client to talk to. 94 */ 95 public static List<String> getServiceUrlsFromDNS(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone, ServiceUrlRandomizer randomizer) { 96 String region = getRegion(clientConfig); 97 // Get zone-specific DNS names for the given region so that we can get a 98 // list of available zones 99 Map<String, List<String>> zoneDnsNamesMap = getZoneBasedDiscoveryUrlsFromRegion(clientConfig, region); 100 Set<String> availableZones = zoneDnsNamesMap.keySet(); 101 List<String> zones = new ArrayList<String>(availableZones); 102 if (zones.isEmpty()) { 103 throw new RuntimeException("No available zones configured for the instanceZone " + instanceZone); 104 } 105 int zoneIndex = 0; 106 boolean zoneFound = false; 107 for (String zone : zones) { 108 logger.debug("Checking if the instance zone {} is the same as the zone from DNS {}", instanceZone, zone); 109 if (preferSameZone) { 110 if (instanceZone.equalsIgnoreCase(zone)) { 111 zoneFound = true; 112 } 113 } else { 114 if (!instanceZone.equalsIgnoreCase(zone)) { 115 zoneFound = true; 116 } 117 } 118 if (zoneFound) { 119 logger.debug("The zone index from the list {} that matches the instance zone {} is {}", 120 zones, instanceZone, zoneIndex); 121 break; 122 } 123 zoneIndex++; 124 } 125 if (zoneIndex >= zones.size()) { 126 if (logger.isWarnEnabled()) { 127 logger.warn("No match for the zone {} in the list of available zones {}", 128 instanceZone, zones.toArray()); 129 } 130 } else { 131 // Rearrange the zones with the instance zone first 132 for (int i = 0; i < zoneIndex; i++) { 133 String zone = zones.remove(0); 134 zones.add(zone); 135 } 136 } 137 138 // Now get the eureka urls for all the zones in the order and return it 139 List<String> serviceUrls = new ArrayList<String>(); 140 for (String zone : zones) { 141 for (String zoneCname : zoneDnsNamesMap.get(zone)) { 142 List<String> ec2Urls = new ArrayList<String>(getEC2DiscoveryUrlsFromZone(zoneCname, DiscoveryUrlType.CNAME)); 143 // Rearrange the list to distribute the load in case of multiple servers 144 if (ec2Urls.size() > 1) { 145 randomizer.randomize(ec2Urls); 146 } 147 for (String ec2Url : ec2Urls) { 148 StringBuilder sb = new StringBuilder() 149 .append("http://") 150 .append(ec2Url) 151 .append(":") 152 .append(clientConfig.getEurekaServerPort()); 153 if (clientConfig.getEurekaServerURLContext() != null) { 154 if (!clientConfig.getEurekaServerURLContext().startsWith("/")) { 155 sb.append("/"); 156 } 157 sb.append(clientConfig.getEurekaServerURLContext()); 158 if (!clientConfig.getEurekaServerURLContext().endsWith("/")) { 159 sb.append("/"); 160 } 161 } else { 162 sb.append("/"); 163 } 164 String serviceUrl = sb.toString(); 165 logger.debug("The EC2 url is {}", serviceUrl); 166 serviceUrls.add(serviceUrl); 167 } 168 } 169 } 170 // Rearrange the fail over server list to distribute the load 171 String primaryServiceUrl = serviceUrls.remove(0); 172 randomizer.randomize(serviceUrls); 173 serviceUrls.add(0, primaryServiceUrl); 174 175 if (logger.isDebugEnabled()) { 176 logger.debug("This client will talk to the following serviceUrls in order : {} ", 177 (Object) serviceUrls.toArray()); 178 } 179 return serviceUrls; 180 } 181 182 /** 183 * Get the list of all eureka service urls from properties file for the eureka client to talk to. 184 * 185 * @param clientConfig the clientConfig to use 186 * @param instanceZone The zone in which the client resides 187 * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise 188 * @return The list of all eureka service urls for the eureka client to talk to 189 */ 190 public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { 191 List<String> orderedUrls = new ArrayList<String>(); 192 String region = getRegion(clientConfig); 193 String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); 194 if (availZones == null || availZones.length == 0) { 195 availZones = new String[1]; 196 availZones[0] = DEFAULT_ZONE; 197 } 198 logger.debug("The availability zone for the given region {} are {}", region, availZones); 199 int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); 200 201 List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]); 202 if (serviceUrls != null) { 203 orderedUrls.addAll(serviceUrls); 204 } 205 int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1); 206 while (currentOffset != myZoneOffset) { 207 serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]); 208 if (serviceUrls != null) { 209 orderedUrls.addAll(serviceUrls); 210 } 211 if (currentOffset == (availZones.length - 1)) { 212 currentOffset = 0; 213 } else { 214 currentOffset++; 215 } 216 } 217 218 if (orderedUrls.size() < 1) { 219 throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"); 220 } 221 return orderedUrls; 222 } 223 224 /** 225 * Get the list of all eureka service urls from properties file for the eureka client to talk to. 226 * 227 * @param clientConfig the clientConfig to use 228 * @param instanceZone The zone in which the client resides 229 * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise 230 * @return an (ordered) map of zone -> list of urls mappings, with the preferred zone first in iteration order 231 */ 232 public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { 233 Map<String, List<String>> orderedUrls = new LinkedHashMap<>(); 234 String region = getRegion(clientConfig); 235 String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); 236 if (availZones == null || availZones.length == 0) { 237 availZones = new String[1]; 238 availZones[0] = DEFAULT_ZONE; 239 } 240 logger.debug("The availability zone for the given region {} are {}", region, availZones); 241 int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); 242 243 String zone = availZones[myZoneOffset]; 244 List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); 245 if (serviceUrls != null) { 246 orderedUrls.put(zone, serviceUrls); 247 } 248 int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1); 249 while (currentOffset != myZoneOffset) { 250 zone = availZones[currentOffset]; 251 serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); 252 if (serviceUrls != null) { 253 orderedUrls.put(zone, serviceUrls); 254 } 255 if (currentOffset == (availZones.length - 1)) { 256 currentOffset = 0; 257 } else { 258 currentOffset++; 259 } 260 } 261 262 if (orderedUrls.size() < 1) { 263 throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"); 264 } 265 return orderedUrls; 266 } 267 268 /** 269 * Get the list of EC2 URLs given the zone name. 270 * 271 * @param dnsName The dns name of the zone-specific CNAME 272 * @param type CNAME or EIP that needs to be retrieved 273 * @return The list of EC2 URLs associated with the dns name 274 */ 275 public static Set<String> getEC2DiscoveryUrlsFromZone(String dnsName, DiscoveryUrlType type) { 276 Set<String> eipsForZone = null; 277 try { 278 dnsName = "txt." + dnsName; 279 logger.debug("The zone url to be looked up is {} :", dnsName); 280 Set<String> ec2UrlsForZone = DnsResolver.getCNamesFromTxtRecord(dnsName); 281 for (String ec2Url : ec2UrlsForZone) { 282 logger.debug("The eureka url for the dns name {} is {}", dnsName, ec2Url); 283 ec2UrlsForZone.add(ec2Url); 284 } 285 if (DiscoveryUrlType.CNAME.equals(type)) { 286 return ec2UrlsForZone; 287 } 288 eipsForZone = new TreeSet<String>(); 289 for (String cname : ec2UrlsForZone) { 290 String[] tokens = cname.split("\\."); 291 String ec2HostName = tokens[0]; 292 String[] ips = ec2HostName.split("-"); 293 StringBuilder eipBuffer = new StringBuilder(); 294 for (int ipCtr = 1; ipCtr < 5; ipCtr++) { 295 eipBuffer.append(ips[ipCtr]); 296 if (ipCtr < 4) { 297 eipBuffer.append("."); 298 } 299 } 300 eipsForZone.add(eipBuffer.toString()); 301 } 302 logger.debug("The EIPS for {} is {} :", dnsName, eipsForZone); 303 } catch (Throwable e) { 304 throw new RuntimeException("Cannot get cnames bound to the region:" + dnsName, e); 305 } 306 return eipsForZone; 307 } 308 309 /** 310 * Get the zone based CNAMES that are bound to a region. 311 * 312 * @param region 313 * - The region for which the zone names need to be retrieved 314 * @return - The list of CNAMES from which the zone-related information can 315 * be retrieved 316 */ 317 public static Map<String, List<String>> getZoneBasedDiscoveryUrlsFromRegion(EurekaClientConfig clientConfig, String region) { 318 String discoveryDnsName = null; 319 try { 320 discoveryDnsName = "txt." + region + "." + clientConfig.getEurekaServerDNSName(); 321 322 logger.debug("The region url to be looked up is {} :", discoveryDnsName); 323 Set<String> zoneCnamesForRegion = new TreeSet<String>(DnsResolver.getCNamesFromTxtRecord(discoveryDnsName)); 324 Map<String, List<String>> zoneCnameMapForRegion = new TreeMap<String, List<String>>(); 325 for (String zoneCname : zoneCnamesForRegion) { 326 String zone = null; 327 if (isEC2Url(zoneCname)) { 328 throw new RuntimeException( 329 "Cannot find the right DNS entry for " 330 + discoveryDnsName 331 + ". " 332 + "Expected mapping of the format <aws_zone>.<domain_name>"); 333 } else { 334 String[] cnameTokens = zoneCname.split("\\."); 335 zone = cnameTokens[0]; 336 logger.debug("The zoneName mapped to region {} is {}", region, zone); 337 } 338 List<String> zoneCnamesSet = zoneCnameMapForRegion.get(zone); 339 if (zoneCnamesSet == null) { 340 zoneCnamesSet = new ArrayList<String>(); 341 zoneCnameMapForRegion.put(zone, zoneCnamesSet); 342 } 343 zoneCnamesSet.add(zoneCname); 344 } 345 return zoneCnameMapForRegion; 346 } catch (Throwable e) { 347 throw new RuntimeException("Cannot get cnames bound to the region:" + discoveryDnsName, e); 348 } 349 } 350 351 /** 352 * Get the region that this particular instance is in. 353 * 354 * @return - The region in which the particular instance belongs to. 355 */ 356 public static String getRegion(EurekaClientConfig clientConfig) { 357 String region = clientConfig.getRegion(); 358 if (region == null) { 359 region = DEFAULT_REGION; 360 } 361 region = region.trim().toLowerCase(); 362 return region; 363 } 364 365 // FIXME this is no valid for vpc 366 private static boolean isEC2Url(String zoneCname) { 367 return zoneCname.startsWith("ec2"); 368 } 369 370 /** 371 * Gets the zone to pick up for this instance. 372 */ 373 private static int getZoneOffset(String myZone, boolean preferSameZone, String[] availZones) { 374 for (int i = 0; i < availZones.length; i++) { 375 if (myZone != null && (availZones[i].equalsIgnoreCase(myZone.trim()) == preferSameZone)) { 376 return i; 377 } 378 } 379 logger.warn("DISCOVERY: Could not pick a zone based on preferred zone settings. My zone - {}," + 380 " preferSameZone - {}. Defaulting to {}", myZone, preferSameZone, availZones[0]); 381 382 return 0; 383 } 384 }
2、重点解读:getServiceUrlsFromConfig
/** * Get the list of all eureka service urls from properties file for the eureka client to talk to. * * @param clientConfig the clientConfig to use * @param instanceZone The zone in which the client resides * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise * @return The list of all eureka service urls for the eureka client to talk to */ public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { List<String> orderedUrls = new ArrayList<String>(); String region = getRegion(clientConfig); String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); if (availZones == null || availZones.length == 0) { availZones = new String[1]; availZones[0] = DEFAULT_ZONE; } logger.debug("The availability zone for the given region {} are {}", region, availZones); int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]); if (serviceUrls != null) { orderedUrls.addAll(serviceUrls); } int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1); while (currentOffset != myZoneOffset) { serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]); if (serviceUrls != null) { orderedUrls.addAll(serviceUrls); } if (currentOffset == (availZones.length - 1)) { currentOffset = 0; } else { currentOffset++; } } if (orderedUrls.size() < 1) { throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"); } return orderedUrls; }
1、通过String region = getRegion(clientConfig),我们可以知道一个微服务应用只能设置一个Region。
2、通过String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()),我们可以知道一个Region下可以配置多个zone。
3、通过 List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);,我们可以知道在一个zone下可以配置多个serviceUrl。
4、当我们设置了Region=shanghai,系统会优先加载Region=shanghai下的Zones。
5、如果在Region=shanghai下没有可用的zone,系统会默认加载 DEFAULT_ZONE。
四、Ribbon调用
当我们在微服务应用中使用Ribbon来实现服务调用时,对于Zone的设置可以在负载均衡是实现区域亲和特性,也就是说,Ribbon的默认策略会优先访问同一个客户端处于一个Zone中的服务端实例。只有当同一个Zone中没有可用的服务端实例的时候才会访问其他Zone中的实例。所以通过Zone属性的定义,配合实际部署的物理结构,我们可以有效的设计出针对区域性的故障的容错集群。
五、代码配置
注册中心-1 : application.yml
server: port: 8201 spring: application: name: demo-service-consumer eureka: instance: lease-renewal-interval-in-seconds: 3 lease-expiration-duration-in-seconds: 9 hostname: peer1 metadata-map: zone: zone-1 client: register-with-eureka: true fetch-registry: true instance-info-replication-interval-seconds: 9 registry-fetch-interval-seconds: 3 serviceUrl: defaultZone: http://peer1:8001/register/eureka/
注册中心-2 : application.yml
server: port: 9001 servlet: context-path: /register spring: application: name: demo-register eureka: instance: hostname: peer2 client: register-with-eureka: true fetch-registry: true instance-info-replication-interval-seconds: 30 serviceUrl: defaultZone: http://peer1:8001/register/eureka/
demo-service-provider-1 : application.yml
server: port: 8102 spring: application: name: demo-service-provider eureka: instance: lease-renewal-interval-in-seconds: 3 lease-expiration-duration-in-seconds: 9 hostname: peer1 metadata-map: zone: zone-1 client: register-with-eureka: true fetch-registry: true instance-info-replication-interval-seconds: 9 registry-fetch-interval-seconds: 3 serviceUrl: defaultZone: http://peer1:8001/register/eureka/
demo-service-provider-2
: application.yml
server: port: 9102 spring: application: name: demo-service-provider eureka: instance: lease-renewal-interval-in-seconds: 3 lease-expiration-duration-in-seconds: 9 hostname: peer2 metadata-map: zone: zone-2 client: register-with-eureka: true fetch-registry: true instance-info-replication-interval-seconds: 9 registry-fetch-interval-seconds: 3 serviceUrl: defaultZone: http://peer2:9001/register/eureka/
demo-service-consumer-1 : application.yml
server: port: 8201 spring: application: name: demo-service-consumer eureka: instance: lease-renewal-interval-in-seconds: 3 lease-expiration-duration-in-seconds: 9 hostname: peer1 metadata-map: zone: zone-1 client: register-with-eureka: true fetch-registry: true instance-info-replication-interval-seconds: 9 registry-fetch-interval-seconds: 3 serviceUrl: defaultZone: http://peer1:8001/register/eureka/
六、运行测试
启动: 注册中心-1, 注册中心-2,demo-service-provider-1,demo-service-provider-2,demo-service-consumer-1
打开注册中心:http://localhost:8001/register/,http://localhost:9001/register/
打开服务消费者:http://localhost:8201/hello/java
停掉demo-service-provider-1微服务的实例,再次打开服务消费者:http://localhost:8201/hello/java 会有不一样的结果
测试结果完美!