目錄
微服務:整合 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 會有不一樣的結果
測試結果完美!