微服务:整合 Spring cloud Eureka - 高级属性Region、Zone

一、简介

  当用户地理分布范围很广的时候,比如公司在北京、上海、广州等都有分公司的时候,一般都会有多个机房。那么对于用户而言,当然是希望调用本地分公司的机房中的微服务应用。比如:上海用户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 }
View Code

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 会有不一样的结果

 测试结果完美!

猜你喜欢

转载自www.cnblogs.com/yansg/p/12541166.html
今日推荐