微服務:整合 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 }
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 會有不一樣的結果

 

 

 

 測試結果完美!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM