最近在研究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
启动后可以看到这种方式配置还算是成功的