一、问题简述

  • 172.17.0.2:为docker容器内部IP地址。
  • x00001.prod.shunyi.beijing为服务器名,即HostName。
  • 10.20.30.40:为服务器IP地址,使我们真正需要的地址。
  • ${pers.hanchao.ip}:为服务器IP地址的占位符表示方式,用于传递给docker容器。

1.1.eureka注册成功之后的正常表现

  • 在eureka注册中心,显示注册成功的服务的实例状态,点击实例名称,跳转至status-page-url页面,显示服务实例的状态。形如:
// 20190615100528
// http://10.20.30.40:8888/info

{
  
}
  • 在Spring Boot Admin运行日志中,显示了正常的health-check-url地址:形如:http://10.20.30.40:10002/health。
  • 在Spring Boot Admin应用管理页面,注册成功的服务整体状态为"UP",即:上线状态。

1.2.当前异常服务表现

  • A类服务
  • 在eureka注册中心一切正常,即status-page-url正常。
  • 在Spring Boot Admin运行日志中,显示health-check-url正常。
  • 在Spring Boot Admin应用管理页面,服务整体状态总是显示"DOWN"。
  • B类服务
  • C类服务

二、相关知识

想要搞清楚为什么部分微服务的eureka注册结果异常,先要弄清楚status-page-url、health-check-url以及Spring Boot Admin的服务整体状态的逻辑。

下面,依次对其进行描述。

2.1.Spring Boot Admin的服务整体状态

Spring Boot Admin的服务整体状态不仅仅是服务本身的状态,还包括此服务依赖的服务的状态。

服务整体状态可能是由以下状态综合决定的:

  • 服务本身的状态
  • eureka-client的状态
  • eureka-server的状态
  • 硬盘空间的状态
  • 依赖的数据库的状态
  • 依赖的ElasticSearch的状态
  • 依赖的Redis的状态
  • 。。。

服务整体状态具体依赖于哪些状态要看pom.xml中都依赖的哪些服务。

2.2.status-page-url和health-check-url

整体说明

  • eureka注册中心通过status-page-url去查询服务状态。
  • Spring Boot Admin通过health-check-url去检查服务状态。
  • status-page-url和health-check-url可以显式配置,即eureka.instance.status-page-urleureka.instance.health-check-url
  • 一般情况下,无需显式配置,eureka-client会根据规则自动拼接形成status-page-url和health-check-url。
  • 以上所述的三类项目,都没有显式配置。

版本说明

  • 不同版本的eureka-client拥有不同的status-page-url和health-check-url自动生成规则(其实这是马后炮,是解决问题过程中发现的)。
  • A类服务和B类服务依赖的版本是1.5.6.RELEASE;C类服务依赖的是1.5.8.RELEASE。
  • 下面对两种版本status-page-url和health-check-url的自动生成规则进行简述。
  • 后面章节对1.5.6.RELEASE生成规则的源码解读,关于1.5.8RELEASE的生成规则源码解读可以参考进行。

1.5.6.RELEASE版本status-page-url和health-check-url的自动生成规则

  • 获取配置文件prefer-ip-address,即:是否有限选择ip-address。
  • 获取配置文件ip-address配置的地址IpAddressConfiged。
  • 通过Socket相关接口获取本机(或容器)的IpAddressLocal和HostNameLocal。
  • 将服务真正使用的IpAddressUesd设置为IpAddressLocal,服务使用的HostNameUsed设置为HostNameLocal。
  • 如果IpAddressConfiged不为空,则覆盖IpAddressUesd为IpAddressConfiged。
  • 在自动生成status-page-url和health-check-url,依据如下规则:
  • status-page-url:https://{prefer-ip-address?IpAddressUesd:HostNameUsed}/info
  • health-check-url:https://{prefer-ip-address?IpAddressUesd:HostNameUsed}/health
  • 目前实践的eureka配置中,prefer-ip-address=true,ip-address={pers.hanchao.ip},所以,自动生成规则转换为:
  • status-page-url:https://{pers.hanchao.ip}/info
  • health-check-url:https://{pers.hanchao.ip}/health

1.5.8.RELEASE版本status-page-url和health-check-url的自动生成规则

  • 获取配置文件prefer-ip-address。
  • 获取配置文件ip-address配置的地址IpAddressConfiged。
  • 通过Socket相关接口获取本机(或容器)的IpAddressLocal和HostNameLocal。
  • 将服务真正使用的IpAddressUesd设置为IpAddressLocal,服务使用的HostNameUsed设置为HostNameLocal。
  • 如果IpAddressConfiged不为空,则覆盖IpAddressUesd为IpAddressConfiged。
  • 在自动生成status-page-url和health-check-url,依据如下规则:
  • status-page-url:https://{prefer-ip-address?IpAddressUesd:HostNameUsed}/info
  • health-check-url:https://{prefer-ip-address?IpAddressUesd:HostNameUsed}/health
  • 目前实践的eureka配置中,prefer-ip-address=true,ip-address={pers.hanchao.ip},所以,自动生成规则转换为:
  • status-page-url:https://{IpAddressLocal}/info
  • health-check-url:https://{IpAddressLocal}/health
  • ip-address这个配置在此逻辑中无效

三、问题排查

3.1.A类微服务

3.1.1.原因简析

参考2.1.章节,经排查,A类微服务引用了redis的依赖,但是并没有使用。

所以redis的连接状态为不正常的,即为DOWN,所以导致服务整体状态为DOWN。

3.1.2.修改说明

去除无用的依赖

<!-- 去除以下依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2.B类微服务

3.2.1.原因简析

原因一:依赖了redis,但是并未使用。参考:3.1.1.章节

原因二:eureka配置不齐全

参考章节2.2.的1.5.6.RELEASE部分,由于缺少prefer-ip-address和ip-address配置,导致最终status-page-url和health-check-url如下:

  • status-page-url:https://{HostNameLocal}/info
  • health-check-url:https://{HostNameLocal}/health

3.2.2.修改说明

  1. 修改application.properties,补全服务注册相关配置(建议参考章节四:推荐配置)。
# 补充以下配置
eureka.instance.ip-address=${pers.hanchao.ip}
eureka.instance.prefer-ip-address=true
management.security.enabled=false
  1. 修改pom.xml,去除无用的依赖。
<!-- 去除以下依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.3.C类微服务

3.3.1.原因简析

经排查,eureka配置本身并无问题,为题是由于SpringBoot版本不同造成的。

参考章节2.2.的1.5.8.RELEASE部分,由于ip-address配置的无效,导致最终status-page-url和health-check-url如下:

  • status-page-url:https://{IpAddressLocal}/info
  • health-check-url:https://{IpAddressLocal}/health
  • ip-address这个配置在此逻辑中无效
  • {IpAddressLocal}在本机则为本机地址,在docker则为docker内部IP地址,形如:http://172.17.0.2:10002/health等。

3.3.2.问题解决

修改bootstrap.yml,显式配置status-page-url和health-check-url(建议参考章节四:推荐配置)。

eureka:
  instance:
    ...
    prefer-ip-address: true
		# 显式配置 status-page-url和health-check-url
    status-page-url: https://${pers.hanchao.ip}:${server.port:@@server.port@@}/info
    health-check-url: https://${pers.hanchao.ip}:${server.port:@@server.port@@}/health

四、eureka推荐配置参考

  • 注意驼峰命名与短横线命名方式的区别。
  • 以下配置仅供参考,如有不足之处自行优化。

4.1.properties

eureka.client.enabled=true
# 推荐域名地址
eureka.client.service-url.defaultZone={eureka服务注册中心的注册地址}
eureka.instance.instance-id=${pers.hanchao.ip}:${spring.application.name}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.ip-address=${pers.hanchao.ip}
eureka.instance.lease-expiration-duration-in-seconds=15
eureka.instance.lease-renewal-interval-in-seconds=5
# 显式配置 status-page-url和health-check-url
eureka.instance.status-page-url=https://${pers.hanchao.ip}:${server.port:@@server.port@@}/info
eureka.instance.health-check-url=https://${pers.hanchao.ip}:${server.port:@@server.port@@}/health
management.security.enabled=false

4.2.yaml

eureka:
    client:
        enabled: true
        service-url:
        		# 推荐域名地址
            defaultZone: {eureka服务注册中心的注册地址}
    instance:
        instance-id: ${pers.hanchao.ip}:${spring.application.name}:${server.port}
        prefer-ip-address: true
        ip-address: ${pers.hanchao.ip}
        lease-expiration-duration-in-seconds: 15
        lease-renewal-interval-in-seconds: 5
        # 显式配置 status-page-url和health-check-url
        status-page-url: https://${pers.hanchao.ip}:${server.port:@@server.port@@}/info
        health-check-url: https://${pers.hanchao.ip}:${server.port:@@server.port@@}/health
management:
    security:
        enabled: false

五、1.5.6.RELEASE版本health-check-url自动生成规则的源码解读

1.获取配置文件prefer-ip-address

  • org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean:131
boolean preferIpAddress = Boolean.parseBoolean(eurekaPropertyResolver.getProperty("preferIpAddress"));

2.获取配置文件ip-address配置的地址IpAddressConfiged。

  • org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean:132
String ipAddress = eurekaPropertyResolver.getProperty("ipAddress");

3.通过Socket相关接口获取本机(或容器)的IpAddressLocal和HostNameLocal。

  • org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean:140
EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);
  • org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean#EurekaInstanceConfigBean(org.springframework.cloud.commons.util.InetUtils):286
public EurekaInstanceConfigBean(InetUtils inetUtils) {
		this.inetUtils = inetUtils;
    //通过Socket相关变成,获取host信息
		this.hostInfo = this.inetUtils.findFirstNonLoopbackHostInfo();
		this.ipAddress = this.hostInfo.getIpAddress();
		this.hostname = this.hostInfo.getHostname();
	}
  • org.springframework.cloud.commons.util.InetUtils#findFirstNonLoopbackHostInfo:69
public HostInfo findFirstNonLoopbackHostInfo() {
    //通过Socket相关变成,获取host信息
		InetAddress address = findFirstNonLoopbackAddress();
		if (address != null) {
			return convertAddress(address);
		}
		HostInfo hostInfo = new HostInfo();
    //将获取的hostName和ipAddress进行返还
		hostInfo.setHostname(this.properties.getDefaultHostname());
		hostInfo.setIpAddress(this.properties.getDefaultIpAddress());
		return hostInfo;
	}
  • org.springframework.cloud.commons.util.InetUtils#findFirstNonLoopbackAddress:79
/**
	* 通过NetworkInterface获取InetAddress
	**/
	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();
							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 {
			return InetAddress.getLocalHost();
		}
		catch (UnknownHostException e) {
			log.warn("Unable to retrieve localhost");
		}

		return null;
	}

4.将服务真正使用的IpAddressUesd设置为IpAddressLocal,服务使用的HostNameUsed设置为HostNameLocal。

  • org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean#EurekaInstanceConfigBean(org.springframework.cloud.commons.util.InetUtils):287
this.ipAddress = this.hostInfo.getIpAddress();
  • org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean#EurekaInstanceConfigBean(org.springframework.cloud.commons.util.InetUtils):288
this.hostname = this.hostInfo.getHostname();

5.如果IpAddressConfiged不为空,则覆盖IpAddressUesd为IpAddressConfiged。

  • org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean:146
if (StringUtils.hasText(ipAddress)) {
			instance.setIpAddress(ipAddress);
		}

6.在自动生成status-page-url和health-check-url,依据如下规则:

  • org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean:171
instance.setStatusPageUrl(metadata.getStatusPageUrl());
  • org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean:167
ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort,
				serverContextPath, managementContextPath, managementPort);
  • org.springframework.cloud.netflix.eureka.metadata.DefaultManagementMetadataProvider#get:26
String healthCheckUrl = getHealthCheckUrl(instance, serverPort, serverContextPath,
                managementContextPath, managementPort, false);
  • org.springframework.cloud.netflix.eureka.metadata.DefaultManagementMetadataProvider#getHealthCheckUrl:46
String healthCheckUrl = getUrl(instance, serverPort, serverContextPath, managementContextPath,
                managementPort, healthCheckUrlPath, isSecure);
  • org.springframework.cloud.netflix.eureka.metadata.DefaultManagementMetadataProvider#getUrl:69
//关键在于参数 instance.getHostname() 
return constructValidUrl(scheme, instance.getHostname(), managementPort, managementContextPath, urlPath);
  • org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean#getHostname:276
public String getHostname() {
		return getHostName(false);
	}

7. prefer-ip-address ? IpAddressUesd : HostNameUsed

  • org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean#getHostName
@Override
	public String getHostName(boolean refresh) {
		if (refresh && !this.hostInfo.override) {
			this.ipAddress = this.hostInfo.getIpAddress();
			this.hostname = this.hostInfo.getHostname();
		}
    //prefer-ip-address ? IpAddressUesd : HostNameUsed
		return this.preferIpAddress ? this.ipAddress : this.hostname;
	}