https://www.cnblogs.com/relinson/p/eureka_ha_use_dns.html
最近在研究spring cloud eureka集群配置的時候碰到問題:多台eureka server如果需要互相注冊,需要在配置文件中將其他服務器地址配置寫死.同樣客戶端啟用服務發現功能(eureka client)也需要配置服務端地址列表(其實eureka server與其他eureka server通信也是用的是eureka client組件).按照官方案例提供3台server,如果現在需要增加第四台,第五台...那么問題就來了,所有eureka client的serverUrls列表是否都得更新(修改配置文件)?
一句話總結如上問題就是:eureka集群有什么辦法能支持動態集群(集群數量可增減客戶端不需要改動任何內容)?
經過尋找發現spring cloud eureka client提供一個eureka.client.useDnsForFetchingServiceUrls選項,使用Dns獲取服務地址.
經過各種了解,明確了該配置項就是啟用dns來存儲eureka server列表的,可以實現動態eureka server集群的功能.但是問題又來了,相關屬性還有那些?dns又該如何配置呢?
相關屬性好找,有網友提供的例子,dns這塊沒有比較明確的說明,為了弄明白這塊自己嘗試着看了看源碼,結果找到了DNS里面關於具體如何使用DNS結果的相關代碼.代碼如下:

getDiscoveryServiceUrls的作用是獲取所有eureka service urls,該方法首先判斷是否需要從DNS獲取服務列表,圖中紅框部分就是從DNS獲取服務列表,繼續往下分析:
在分析之前,插播一個知識點:eureka集群與region和zone這幾個概念的關系.http://www.vccoo.com/v/bqq4vj
繼續看代碼:
1 /**
2 * Get the list of all eureka service urls from DNS for the eureka client to
3 * talk to. The client picks up the service url from its zone and then fails over to
4 * other zones randomly. If there are multiple servers in the same zone, the client once
5 * again picks one randomly. This way the traffic will be distributed in the case of failures.
6 *
7 * @param clientConfig the clientConfig to use
8 * @param instanceZone The zone in which the client resides.
9 * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise.
10 * @param randomizer a randomizer to randomized returned urls
11 *
12 * @return The list of all eureka service urls for the eureka client to talk to.
13 */
14 public static List<String> getServiceUrlsFromDNS(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone, ServiceUrlRandomizer randomizer) {
15 String region = getRegion(clientConfig);
16 // Get zone-specific DNS names for the given region so that we can get a
17 // list of available zones
18 Map<String, List<String>> zoneDnsNamesMap = getZoneBasedDiscoveryUrlsFromRegion(clientConfig, region);
19 Set<String> availableZones = zoneDnsNamesMap.keySet();
20 List<String> zones = new ArrayList<String>(availableZones);
21 if (zones.isEmpty()) {
22 throw new RuntimeException("No available zones configured for the instanceZone " + instanceZone);
23 }
24 int zoneIndex = 0;
25 boolean zoneFound = false;
26 for (String zone : zones) {
27 logger.debug("Checking if the instance zone {} is the same as the zone from DNS {}", instanceZone, zone);
28 if (preferSameZone) {
29 if (instanceZone.equalsIgnoreCase(zone)) {
30 zoneFound = true;
31 }
32 } else {
33 if (!instanceZone.equalsIgnoreCase(zone)) {
34 zoneFound = true;
35 }
36 }
37 if (zoneFound) {
38 Object[] args = {zones, instanceZone, zoneIndex};
39 logger.debug("The zone index from the list {} that matches the instance zone {} is {}", args);
40 break;
41 }
42 zoneIndex++;
43 }
44 if (zoneIndex >= zones.size()) {
45 logger.warn("No match for the zone {} in the list of available zones {}",
46 instanceZone, Arrays.toString(zones.toArray()));
47 } else {
48 // Rearrange the zones with the instance zone first
49 for (int i = 0; i < zoneIndex; i++) {
50 String zone = zones.remove(0);
51 zones.add(zone);
52 }
53 }
54
55 // Now get the eureka urls for all the zones in the order and return it
56 List<String> serviceUrls = new ArrayList<String>();
57 for (String zone : zones) {
58 for (String zoneCname : zoneDnsNamesMap.get(zone)) {
59 List<String> ec2Urls = new ArrayList<String>(getEC2DiscoveryUrlsFromZone(zoneCname, DiscoveryUrlType.CNAME));
60 // Rearrange the list to distribute the load in case of
61 // multiple servers
62 if (ec2Urls.size() > 1) {
63 randomizer.randomize(ec2Urls);
64 }
65 for (String ec2Url : ec2Urls) {
66 String serviceUrl = "http://" + ec2Url + ":"
67 + clientConfig.getEurekaServerPort()
68 + "/" + clientConfig.getEurekaServerURLContext()
69 + "/";
70 logger.debug("The EC2 url is {}", serviceUrl);
71 serviceUrls.add(serviceUrl);
72 }
73 }
74 }
75 // Rearrange the fail over server list to distribute the load
76 String primaryServiceUrl = serviceUrls.remove(0);
77 randomizer.randomize(serviceUrls);
78 serviceUrls.add(0, primaryServiceUrl);
79
80 logger.debug("This client will talk to the following serviceUrls in order : {} ",
81 Arrays.toString(serviceUrls.toArray()));
82 return serviceUrls;
83 }
從代碼中可以看到,首先獲取當前eureka-client所在的region,然后根據region獲取所有zone以及對應的域名信息,然后循環所有zone域名信息獲取eureka-server地址,拼接成完整的serviceUrl並加入serviceUrls列表中.
拼接的serviceUrl格式為:"http://" + ec2Url + ":" + clientConfig.getEurekaServerPort() + "/" + clientConfig.getEurekaServerURLContext() + "/";
另外在代碼中有標出兩處重點,紅色背景的是根據region獲取zone的具體方法,藍色背景的是根據zone具體地址獲取eureka地址列表的方法.重點就在這兩個方法中.
首先看第一個,獲取zone的邏輯:
1 /**
2 * Get the zone based CNAMES that are bound to a region.
3 *
4 * @param region
5 * - The region for which the zone names need to be retrieved
6 * @return - The list of CNAMES from which the zone-related information can
7 * be retrieved
8 */
9 public static Map<String, List<String>> getZoneBasedDiscoveryUrlsFromRegion(EurekaClientConfig clientConfig, String region) {
10 String discoveryDnsName = null;
11 try {
12 discoveryDnsName = "txt." + region + "." + clientConfig.getEurekaServerDNSName();
13
14 logger.debug("The region url to be looked up is {} :", discoveryDnsName);
15 Set<String> zoneCnamesForRegion = new TreeSet<String>(DnsResolver.getCNamesFromTxtRecord(discoveryDnsName));
16 Map<String, List<String>> zoneCnameMapForRegion = new TreeMap<String, List<String>>();
17 for (String zoneCname : zoneCnamesForRegion) {
18 String zone = null;
19 if (isEC2Url(zoneCname)) {
20 throw new RuntimeException(
21 "Cannot find the right DNS entry for "
22 + discoveryDnsName
23 + ". "
24 + "Expected mapping of the format <aws_zone>.<domain_name>");
25 } else {
26 String[] cnameTokens = zoneCname.split("\\.");
27 zone = cnameTokens[0];
28 logger.debug("The zoneName mapped to region {} is {}", region, zone);
29 }
30 List<String> zoneCnamesSet = zoneCnameMapForRegion.get(zone);
31 if (zoneCnamesSet == null) {
32 zoneCnamesSet = new ArrayList<String>();
33 zoneCnameMapForRegion.put(zone, zoneCnamesSet);
34 }
35 zoneCnamesSet.add(zoneCname);
36 }
37 return zoneCnameMapForRegion;
38 } catch (Throwable e) {
39 throw new RuntimeException("Cannot get cnames bound to the region:" + discoveryDnsName, e);
40 }
41 }
12行是請求dns中的地址格式:"txt." + region + "." + clientConfig.getEurekaServerDNSName(),例如:txt.region1.baidu.com,txt.region1.163.com,txt.region2.163.com等
17,27,35行是對返回結果的解析邏輯,可以看出返回值應當是多條記錄並且以空格分開(在15行方法內),每條記錄都應當是一個域名並且第一個.之前的部分作為zone名稱,最終按照zone組織成zone:List<區域地址>的結果返回.
再看第二個,獲取eureka server地址邏輯:
1 /**
2 * Get the list of EC2 URLs given the zone name.
3 *
4 * @param dnsName The dns name of the zone-specific CNAME
5 * @param type CNAME or EIP that needs to be retrieved
6 * @return The list of EC2 URLs associated with the dns name
7 */
8 public static Set<String> getEC2DiscoveryUrlsFromZone(String dnsName, DiscoveryUrlType type) {
9 Set<String> eipsForZone = null;
10 try {
11 dnsName = "txt." + dnsName;
12 logger.debug("The zone url to be looked up is {} :", dnsName);
13 Set<String> ec2UrlsForZone = DnsResolver.getCNamesFromTxtRecord(dnsName);
14 for (String ec2Url : ec2UrlsForZone) {
15 logger.debug("The eureka url for the dns name {} is {}", dnsName, ec2Url);
16 ec2UrlsForZone.add(ec2Url);
17 }
18 if (DiscoveryUrlType.CNAME.equals(type)) {
19 return ec2UrlsForZone;
20 }
21 eipsForZone = new TreeSet<String>();
22 for (String cname : ec2UrlsForZone) {
23 String[] tokens = cname.split("\\.");
24 String ec2HostName = tokens[0];
25 String[] ips = ec2HostName.split("-");
26 StringBuilder eipBuffer = new StringBuilder();
27 for (int ipCtr = 1; ipCtr < 5; ipCtr++) {
28 eipBuffer.append(ips[ipCtr]);
29 if (ipCtr < 4) {
30 eipBuffer.append(".");
31 }
32 }
33 eipsForZone.add(eipBuffer.toString());
34 }
35 logger.debug("The EIPS for {} is {} :", dnsName, eipsForZone);
36 } catch (Throwable e) {
37 throw new RuntimeException("Cannot get cnames bound to the region:" + dnsName, e);
38 }
39 return eipsForZone;
40 }
11行代碼明確了請求格式為:"txt." + dnsName,例如:txt.zone1.163.com,txt.zone2.baidu.com等
而返回結果則比較復雜,我們只關心cnametype的,從getServiceUrlsFromDNS方法中可以得知,只需要返回服務器地址或域名就可以了(返回多組同樣用空格隔開).
看到這里對dns獲取eureka地址的過程已經明白了.然后就是配置了
eureka:
client:
eureka-server-d-n-s-name: relinson.com
use-dns-for-fetching-service-urls: true
region: region1
eureka-server-u-r-l-context: eureka
eureka-server-port: 9999
prefer-same-zone-eureka: true
啟動后可以看到這種方式配置還算是成功的

——————————————————————————————————————————————————————
eureka集群的兩種配置方式:配置文件方式與DNS方式
eureka client獲取serviceUrls(eureka server地址)列表的過程:
- 1. 根據use-dns-for-fetching-service-urls屬性判斷是從dns還是從config獲取region和zone以及serviceUrl相關信息
- 2. 獲取過程首先從配置中獲取應用所在region,通過region屬性設置
- 3. 根據region獲取所有zone信息,dns與config獲取方式不一樣
- 3.1. dns會請求dns服務器獲取指定region的所有zone列表,如果沒有查詢到則拋出異常.使用該方式需要設定 eureka-server-d-n-s-name,eureka-server-u-r-l-context,eureka-server-port屬性,具體配置可參考另一篇文章《eureka集群基於DNS配置方式》
- 3.2. config則從配置文件的availabilityZones列表獲取,如果沒有配置,則返回defaultZone.使用該方式需要配置availability-zones(Map<region:zone>),service-url(Map<zone:serviceUrls>)
- 4. 獲取zone列表成功后需要對列表進行排序,排序根據prefer-same-zone-eureka而不同,該屬性意為是否將相同名稱的zone作為注冊首選
- 4.0. 這里有一個客戶端zone的概念,該zone為availabilityZones的第一個zone,如果沒有設置則為defaultZone
- 4.1. 如果設置true,則查詢zone列表,找到與客戶端zone同名的元素,並將其之前元素轉移到列表最后
- 4.2. 如果設置false,則查找zone列表,找到第一個與客戶端zone不同名的zone,並將其之前的元素轉移到列表最后
- 5. 然后循環獲取所有zone對應的serviceUrls並按順序追加到一個列表中
- 5.1. 如果是配置方式,獲取某個zone的serviceUrls為空則返回defaultZone的serviceUrl,而DNS方式不會進行任何處理
通過以上步驟可以獲取到region對應的zone的serviceUrls.
這里出現了region,zone,serviceUrl這幾個概念,可以簡單理解為region包含多個zone,zone包含多個serviceUrl.但需要注意的是,zone可以出現在多個region中,serviceUrl可以出現在多個zone中,也就是說他們三個概念兩兩之間的關系是多對多而非一對多.
#基於DNS的配置
eureka:
client:
#DNS域名,獲取其他信息將以該域名為根域名
eureka-server-d-n-s-name: relinson.com
#開啟DNS方式獲取serviceUrl,默認為false
use-dns-for-fetching-service-urls: true
#當前應用所在區域,默認為us-east-1
region: region1
#eureka服務根目錄
eureka-server-u-r-l-context: eureka
#服務所在端口
eureka-server-port: 9999
#獲取serviceUrl時候是否優先獲取相同zone的列表(如果獲取為空則獲取所在region第一個zone),如果為false則優先獲取不在相同zone的列表
prefer-same-zone-eureka: true
#是否獲取注冊信息到本地
fetch-registry: true
#是否將自己注冊到eureka
register-with-eureka: true
#基於CONFIG的配置
eureka:
client:
#開啟DNS方式獲取serviceUrl,默認為false
use-dns-for-fetching-service-urls: false
#當前應用所在區域,默認為us-east-1
region: region1
#獲取serviceUrl時候是否優先獲取相同zone的列表(如果獲取為空則獲取所在region第一個zone),如果為false則優先獲取不在相同zone的列表
#client所在zone為availabilityZones的第一個zone,如果未配置,則為defaultZone
prefer-same-zone-eureka: true
#是否獲取注冊信息到本地
fetch-registry: true
#是否將自己注冊到eureka
register-with-eureka: true
#與DNS獲取的方式相同,這里需要手工配置包含哪些region以及zone(Map類型),如果沒有給相關的region配置zone,則默認返回defaultZone
availability-zones:
region1: zone1-2,zone1-2,zone2-2
region2: zone2-2,zone2-2,zone2-3
#與DNS獲取數據方式類似,這里需要手工配置每個zone包含哪些URL,如果應用所在區域沒有zone,則默認返回defaultZone的數據
service-url:
zone1-1: http://xxx,http://xxx2
zone1-2: http://xxx,http://xxx2
zone2-1: http://xxx,http://xxx2
zone2-2: http://xxx,http://xxx2

