一、问题简述
- 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类服务
- 在eureka注册中心,status-page-url异常,如:http://x00001.prod.shunyi.beijing:8888/info。
- 在Spring Boot Admin运行日志中,显示health-check-url异常,如:http://x00001.prod.shunyi.beijing:8888/health。
- 在Spring Boot Admin应用管理页面,服务整体状态总是显示"OFFLINE"。
- C类服务
- 在eureka注册中心,status-page-url异常,如:http://172.17.0.2:8888/info。
- 在Spring Boot Admin运行日志中,显示health-check-url异常,如:http://172.17.0.2:8888/health。
- 在Spring Boot Admin应用管理页面,服务整体状态总是显示"OFFLINE"。
二、相关知识
想要搞清楚为什么部分微服务的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-url
和eureka.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.修改说明
- 修改application.properties,补全服务注册相关配置(建议参考章节四:推荐配置)。
# 补充以下配置
eureka.instance.ip-address=${pers.hanchao.ip}
eureka.instance.prefer-ip-address=true
management.security.enabled=false
- 修改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;
}