1. 程式人生 > >spring cloud EurekaClient 多網絡卡 ip 配置 和 原始碼分析

spring cloud EurekaClient 多網絡卡 ip 配置 和 原始碼分析

1、前言

對於spring cloud,各個服務例項需要註冊到Eureka註冊中心。
一般會配置ip註冊,即eureka.instance.prefer-ip-address=true。
但是,如果服務例項所在的環境存在多個網絡卡,經常會出現註冊過去的ip不是我們想要的ip。

2、配置解決說明

針對上面的情況,我們一般有幾種不同的解決思路。

2.1、方法一:直接配置eureka.instance.ip-address

  • 如:eureka.instance.ip-address=192.168.1.7

直接配置一個完整的ip,一般適用於環境單一場景,對於複雜場景缺少有利支援。

具體實現可以參考org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean
和org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean#getHostName,
這裡不再描述。如果不清楚,可以先看下後面的原始碼邏輯分析,再回頭來看下,思路類似。

2.2、方法二:增加inetutils相關配置

配置對應org.springframework.cloud.commons.util.InetUtilsProperties,其中包含:

配置 說明
spring.cloud.inetutils.default-hostname 預設主機名,只有解析出錯才會用到
spring.cloud.inetutils.default-ip-address 預設ip地址,只有解析出錯才會用到
spring.cloud.inetutils.ignored-interfaces 配置忽略的網絡卡地址,多個用,分割
spring.cloud.inetutils.preferred-networks 正則匹配的ip地址或者ip字首,多個用,分割,是並的關係
spring.cloud.inetutils.timeout-seconds 計算主機ip資訊的超時時間,預設1秒鐘
spring.cloud.inetutils.use-only-site-local-interfaces 只使用內網ip

  • 舉例說明:
  • 只使用以192.168.開頭的ip,注意多個項是並的關係,需要都滿足。
spring.cloud.inetutils.preferred-networks=^192\.168\.[\d]+\.[\d]+$
  • 使用/etc/hosts中主機名稱對映的ip,這一種在docker swarm環境中比較好用。
# 隨便配置一個不可能存在的ip,會走到InetAddress.getLocalHost()邏輯。
spring.cloud.inetutils.preferred-networks=none
  • 排除網絡卡en0和en1
#ignored-interfaces配置的是正則表示式
spring.cloud.inetutils.ignored-interfaces=en0,en1
  • 只使用內網地址
# 遵循 RFC 1918
# 10/8 字首
# 172.16/12 字首
# 192.168/16 字首
spring.cloud.inetutils.use-only-site-local-interfaces=true

一般來說這幾種就夠用了。

3、原始碼分析

主要分析下為什麼這樣配置可以生效。會從最開始的自動配置入手往下看。如果要看ip相部分關,直接跳到指定目錄即可。

這裡使用的版本是spring-boot 1.5.13.RELEASE, spring cloud Dalston.SR5, 版本不同,會有一些差異。

3.1 服務端註冊說明

eureka server常用api說明見:https://blog.csdn.net/qq_30062125/article/details/83829357

服務端程式碼從@EnableEurekaServer入口,api使用了Jersey實現,,其中使用到了子資源載入器。具體可以參考:https://blog.csdn.net/qq_30062125/article/details/83758334

我們主要關心應用例項註冊時候傳遞的hostname,如: <hostName>192.168.1.7</hostName>

3.2 客戶端發起註冊說明

spring boot eureka client 客戶端邏輯可以參考 https://blog.csdn.net/qq_30062125/article/details/83833006

我們這裡關心的是org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean,這是服務例項的資訊。

@Bean
	@ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT)
	public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils) {
		RelaxedPropertyResolver relaxedPropertyResolver = new RelaxedPropertyResolver(env, "eureka.instance.");
		String hostname = relaxedPropertyResolver.getProperty("hostname");
		boolean preferIpAddress = Boolean.parseBoolean(relaxedPropertyResolver.getProperty("preferIpAddress"));
		// ip解析主要在這一步
		EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);
		instance.setNonSecurePort(this.nonSecurePort);
		instance.setInstanceId(getDefaultInstanceId(this.env));
		instance.setPreferIpAddress(preferIpAddress);

		if (this.managementPort != this.nonSecurePort && this.managementPort != 0) {
			if (StringUtils.hasText(hostname)) {
				instance.setHostname(hostname);
			}
			String statusPageUrlPath = relaxedPropertyResolver.getProperty("statusPageUrlPath");
			String healthCheckUrlPath = relaxedPropertyResolver.getProperty("healthCheckUrlPath");
			if (StringUtils.hasText(statusPageUrlPath)) {
				instance.setStatusPageUrlPath(statusPageUrlPath);
			}
			if (StringUtils.hasText(healthCheckUrlPath)) {
				instance.setHealthCheckUrlPath(healthCheckUrlPath);
			}
			String scheme = instance.getSecurePortEnabled() ? "https" : "http";
			instance.setStatusPageUrl(scheme + "://" + instance.getHostname() + ":"
					+ this.managementPort + instance.getStatusPageUrlPath());
			instance.setHealthCheckUrl(scheme + "://" + instance.getHostname() + ":"
					+ this.managementPort + instance.getHealthCheckUrlPath());
		}
		return instance;
	}

由於EurekaInstanceConfigBean類上面配置了@ConfigurationProperties(“eureka.instance”),所以生成bean的過程中,在ConfigurationPropertiesBindingPostProcessor邏輯中,會注入配置檔案的配置引數。

這個bean會加工成InstanceInfo,儲存到ApplicationInfoManager中,ApplicationInfoManager會注入到CloudEurekaClient。最終,在com.netflix.discovery.DiscoveryClient#DiscoveryClient邏輯中賦值,用於後續邏輯處理。\

org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration.EurekaClientConfiguration#eurekaApplicationInfoManager 原始碼:

@Bean
		@ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)
		public ApplicationInfoManager eurekaApplicationInfoManager(
				EurekaInstanceConfig config) {
				// 先加工成InstanceInfo
			InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
			return new ApplicationInfoManager(config, instanceInfo);
		}

DiscoveryClient部分原始碼如下:

@Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
        ......
        
        this.applicationInfoManager = applicationInfoManager;
        // 前面放入的InstanceInfo
        InstanceInfo myInfo = applicationInfoManager.getInfo();

        clientConfig = config;
        staticClientConfig = clientConfig;
        transportConfig = config.getTransportConfig();
        // 賦值給instanceInfo,註冊邏輯使用的引數
        instanceInfo = myInfo;
        ......
    }

註冊邏輯

/**
     * Register with the eureka service by making the appropriate REST call.
     */
    boolean register() throws Throwable {
        logger.info(PREFIX + appPathIdentifier + ": registering service...");
        EurekaHttpResponse<Void> httpResponse;
        try {
            // instanceInfo就是上面賦值的例項資訊
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == 204;
    }

3.3 ip解析配置說明

從上面可以看出來註冊到eureka的應用例項資訊是通過EurekaInstanceConfigBean處理的。ip解析主要在 new EurekaInstanceConfigBean(inetUtils):

EurekaInstanceConfigBean構造方法

public EurekaInstanceConfigBean(InetUtils inetUtils) {
		this.inetUtils = inetUtils;
		// 主機資訊獲取,其中包含ip的解析
		this.hostInfo = this.inetUtils.findFirstNonLoopbackHostInfo();
		// 這個地方ip地址是從hostInfo中獲取的。
		this.ipAddress = this.hostInfo.getIpAddress();
		this.hostname = this.hostInfo.getHostname();
	}
@Override
	public String getHostName(boolean refresh) {
		if (refresh && !this.hostInfo.override) {
			this.ipAddress = this.hostInfo.getIpAddress();
			this.hostname = this.hostInfo.getHostname();
		}
		// 獲取host,如果配置了preferIpAddress,就用ip地址。
		return this.preferIpAddress ? this.ipAddress : this.hostname;
	}

最終通過org.springframework.cloud.commons.util.InetUtils#findFirstNonLoopbackAddress 實現

public InetAddress findFirstNonLoopbackAddress() {
		InetAddress result = null;
		try {
			int lowest = Integer.MAX_VALUE;
			for (Enumeration<NetworkInterface> nics = NetworkInterface
					.getNetworkInterfaces(); nics.hasMoreElements();) {
				NetworkInterface ifc = nics.nextElement();
				if (ifc.isUp()) {
					log.trace("Testing interface: " + ifc.getDisplayName());
					if (ifc.getIndex() < lowest || result == null) {
						lowest = ifc.getIndex();
					}
					else if (result != null) {
						continue;
					}

					// @formatter:off
					// 網絡卡忽略邏輯
					if (!ignoreInterface(ifc.getDisplayName())) {
						for (Enumeration<InetAddress> addrs = ifc
								.getInetAddresses(); addrs.hasMoreElements();) {
							InetAddress address = addrs.nextElement();
						    // ip忽略邏輯
							if (address instanceof Inet4Address
									&& !address.isLoopbackAddress()
									&& !ignoreAddress(address)) {
								log.trace("Found non-loopback interface: "
										+ ifc.getDisplayName());
								result = address;
							}
						}
					}
					// @formatter:on
				}
			}
		}
		catch (IOException ex) {
			log.error("Cannot get first non-loopback address", ex);
		}

		if (result != null) {
			return result;
		}

		try {
		    // 當規則匹配不到ip時候, 直接使用該邏輯獲取資訊。
			return InetAddress.getLocalHost();
		}
		catch (UnknownHostException e) {
			log.warn("Unable to retrieve localhost");
		}

		return null;
	}

3.3.1 忽略網絡卡

不匹配 InetUtilsProperties#ignoredInterfaces配置正則表示式的網絡卡忽略掉

/** for testing */ boolean ignoreInterface(String interfaceName) {
		for (String regex : this.properties.getIgnoredInterfaces()) {
			if (interfaceName.matches(regex)) {
				log.trace("Ignoring interface: " + interfaceName);
				return true;
			}
		}
		return false;
	}

3.3.2 忽略ip

是否必須內網ip,通過org.springframework.cloud.commons.util.InetUtilsProperties#useOnlySiteLocalInterfaces控制。
是否匹配ip規則,並的操作,必須匹配org.springframework.cloud.commons.util.InetUtilsProperties#preferredNetworks 配置的多條正則表示式,或者完全匹配開頭,一條不符合,則忽略掉。

/** for testing */ boolean ignoreAddress(InetAddress address) {

       // 是否必須內網ip
		if (this.properties.isUseOnlySiteLocalInterfaces() && !address.isSiteLocalAddress()) {
			log.trace("Ignoring address: " + address.getHostAddress());
			return true;
		}
	
	    // 是否匹配ip,一條規則不匹配,就不匹配
		for (String regex : this.properties.getPreferredNetworks()) {
		if (!address.getHostAddress().matches(regex) && !address.getHostAddress().startsWith(regex)) {
			log.trace("Ignoring address: " + address.getHostAddress());
				return true;
			}
		}
		return false;
	}

內網ip規則校驗

	public boolean isSiteLocalAddress() {
        // refer to RFC 1918
        // 10/8 prefix
        // 172.16/12 prefix
        // 192.168/16 prefix
        int address = holder().getAddress();
        return (((address >>> 24) & 0xFF) == 10)
            || ((((address >>> 24) & 0xFF) == 172)
                && (((address >>> 16) & 0xF0) == 16))
            || ((((address >>> 24) & 0xFF) == 192)
                && (((address >>> 16) & 0xFF) == 168));
    }

3.3.3 InetAddress.getLocalHost邏輯

當我們配置spring.cloud.inetutils.preferred-networks=none時,根據網絡卡是找不到匹配的ip的,就會走到InetAddress.getLocalHost()邏輯中。
邏輯步驟如下:

  1. 查詢本地主機名稱
  2. 如果主機名稱是localhost直接返回地址,其中ipv4是127.0.0.1,ipv6是::1
  3. 如果不是,需要走本地域名解析。域名解析優先會獲取本地hosts中配置的ip地址。當為主機名稱配置了hosts,就會讀取該配置的ip地址。
  4. 如果本地沒找到,繼續走域名解析。

到這裡,spring cloud eureka client如何設定ip我們基本上已經理清楚了。好了,結束了。