最近在使用Spring Boot Admin的時候發現(以下簡稱SBA),使用eureka的注冊中心的時候sba可以正常擷取到服務的管理位址和健康檢測位址,而換了Nacos作為注冊中的時候,sba中的服務管理位址和健康檢測位址是錯誤的。如圖
一般來說正常的
服務通路位址是:http://IP:${server.port}/${server.servlet.context-path},
服務管理位址是:http://IP:${management.server.port}/${management.server.servlet.context-path}/${management.endpoints.web.base-path},其中這裡management.endpoints.web.base-path預設為/actuator,
健康檢測位址是:http://IP:${management.server.port}/${management.server.servlet.context-path}/${management.endpoints.web.base-path}/health
例如一個服務的如果設定
management.server.port = 30000
management.server.servlet.context-path = /sba
服務管理位址為:http://127.0.0.1:30000/sba/actuator,
健康檢測位址為:http://127.0.0.1:30000/sba/actuator/health,
如果management.endpoints.web.base-path = /act
服務管理位址為:http://127.0.0.1:30000/sba/act,
健康檢測位址為:http://127.0.0.1:30000/sba/act/health,
這是從應用程式的使用角度來說的,也就是通過上面的配置就可以在對應的配置位址通路到端點監控資料。
而通常來說我們是不配置management.server.servlet.context-path和management.endpoints.web.base-path的,是以對于管理位址來說,就是簡單的http://IP:${management.server.port}/actuator
哪麼問題來了,為何使用Nacos作為注冊中心的時候,sba取到的位址有問題呢?這樣我們就隻能從sba的角度來檢視了,我們首先來看一下sba的自動配置類AdminServerDiscoveryAutoConfiguration,de.codecentric.boot.admin.server.cloud.config.AdminServerDiscoveryAutoConfiguration
@Bean
@ConditionalOnMissingBean({ ServiceInstanceConverter.class })
@ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter")
public DefaultServiceInstanceConverter serviceInstanceConverter() {
return new DefaultServiceInstanceConverter();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({ ServiceInstanceConverter.class })
@ConditionalOnBean(EurekaClient.class)
public static class EurekaConverterConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter")
public EurekaServiceInstanceConverter serviceInstanceConverter() {
return new EurekaServiceInstanceConverter();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({ ServiceInstanceConverter.class })
@ConditionalOnBean(KubernetesDiscoveryClient.class)
public static class KubernetesConverterConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter")
public KubernetesServiceInstanceConverter serviceInstanceConverter() {
return new KubernetesServiceInstanceConverter();
}
}
可以看出來,這裡包含了執行個體的轉換器,這裡有一個de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter 我們首先來看一下,DefaultServiceInstanceConverter為其預設Converter,EurekaServiceInstanceConverter 和 KubernetesServiceInstanceConverter都繼承于DefaultServiceInstanceConverter,作為Eureka和Kubernetes實作,我們來分析一下DefaultServiceInstanceConverter裡是如何擷取到服務執行個體的位址的。
public class DefaultServiceInstanceConverter implements ServiceInstanceConverter {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceInstanceConverter.class);
private static final String KEY_MANAGEMENT_SCHEME = "management.scheme";
private static final String KEY_MANAGEMENT_ADDRESS = "management.address";
private static final String KEY_MANAGEMENT_PORT = "management.port";
private static final String KEY_MANAGEMENT_PATH = "management.context-path";
private static final String KEY_HEALTH_PATH = "health.path";
/**
* Default context-path to be appended to the url of the discovered service for the
* managment-url.
*/
private String managementContextPath = "/actuator";
/**
* Default path of the health-endpoint to be used for the health-url of the discovered
* service.
*/
private String healthEndpointPath = "health";
// 注冊服務執行個體的名稱,健康檢測位址,管理位址,服務位址,metadata資料
@Override
public Registration convert(ServiceInstance instance) {
LOGGER.debug("Converting service '{}' running at '{}' with metadata {}", instance.getServiceId(),
instance.getUri(), instance.getMetadata());
return Registration.create(instance.getServiceId(), getHealthUrl(instance).toString())
.managementUrl(getManagementUrl(instance).toString()).serviceUrl(getServiceUrl(instance).toString())
.metadata(getMetadata(instance)).build();
}
// 這裡就是拼接管理位址和健康檢測路徑,http://192.168.14.240:2429/actuator/health
protected URI getHealthUrl(ServiceInstance instance) {
return UriComponentsBuilder.fromUri(getManagementUrl(instance)).path("/").path(getHealthPath(instance)).build()
.toUri();
}
// 這裡取的是健康檢測位址,首先從服務執行個體的MetaData中的health.path取,取不到預設值是health
protected String getHealthPath(ServiceInstance instance) {
String healthPath = instance.getMetadata().get(KEY_HEALTH_PATH);
if (!isEmpty(healthPath)) {
return healthPath;
}
return this.healthEndpointPath;
}
protected URI getManagementUrl(ServiceInstance instance) {
URI serviceUrl = this.getServiceUrl(instance);
String managementScheme = this.getManagementScheme(instance);
String managementHost = this.getManagementHost(instance);
int managementPort = this.getManagementPort(instance);
UriComponentsBuilder builder;
if (serviceUrl.getHost().equals(managementHost) && serviceUrl.getScheme().equals(managementScheme)
&& serviceUrl.getPort() == managementPort) {
builder = UriComponentsBuilder.fromUri(serviceUrl);
}
else {
builder = UriComponentsBuilder.newInstance().scheme(managementScheme).host(managementHost);
if (managementPort != -1) {
builder.port(managementPort);
}
}
return builder.path("/").path(getManagementPath(instance)).build().toUri();
}
// 這裡取的是Scheme的内容,首先從服務執行個體的MetaData中的management.scheme取,取不到就是instance.getUri().getScheme()預設的http
private String getManagementScheme(ServiceInstance instance) {
String managementServerScheme = instance.getMetadata().get(KEY_MANAGEMENT_SCHEME);
if (!isEmpty(managementServerScheme)) {
return managementServerScheme;
}
return getServiceUrl(instance).getScheme();
}
// 這裡取的是管理主機位址,也是從MeatData中management.address取,取不到用的就是instance.getUri().getHost()
protected String getManagementHost(ServiceInstance instance) {
String managementServerHost = instance.getMetadata().get(KEY_MANAGEMENT_ADDRESS);
if (!isEmpty(managementServerHost)) {
return managementServerHost;
}
return getServiceUrl(instance).getHost();
}
// 這裡取的是管理端口号,也是從MeatData中management.port取,取不到用的就是instance.getUri().getPort(),執行個體端口
protected int getManagementPort(ServiceInstance instance) {
String managementPort = instance.getMetadata().get(KEY_MANAGEMENT_PORT);
if (!isEmpty(managementPort)) {
return Integer.parseInt(managementPort);
}
return getServiceUrl(instance).getPort();
}
// 這裡取的是管理路徑,也是從MeatData中management.context-path取,取不到用的就是使用預設值/actuator
protected String getManagementPath(ServiceInstance instance) {
String managementPath = instance.getMetadata().get(DefaultServiceInstanceConverter.KEY_MANAGEMENT_PATH);
if (!isEmpty(managementPath)) {
return managementPath;
}
return this.managementContextPath;
}
// 這裡取到的是 IP:端口号 htpp://instance.host:instance.port
protected URI getServiceUrl(ServiceInstance instance) {
return instance.getUri();
}
protected Map<String, String> getMetadata(ServiceInstance instance) {
return (instance.getMetadata() != null) ? instance.getMetadata() : emptyMap();
}
public void setManagementContextPath(String managementContextPath) {
this.managementContextPath = managementContextPath;
}
public String getManagementContextPath() {
return this.managementContextPath;
}
public void setHealthEndpointPath(String healthEndpointPath) {
this.healthEndpointPath = healthEndpointPath;
}
public String getHealthEndpointPath() {
return this.healthEndpointPath;
}
}
是以sba 會擷取 instance的name、managementUrl、healthUrl、serviceUrl、metadata資料,這些位址都是首先從metadata資料中擷取并進行組裝,主要的涉及到配置的重點參數包含如下三個,
KEY_MANAGEMENT_PORT = "management.port"; 這個參數不需要特殊配置,因為注冊中心使用的時候會自動配置進入metadata,重點在于下面的兩個參數上
KEY_MANAGEMENT_PATH = "management.context-path";
KEY_HEALTH_PATH = "health.path";
那麼隻要保持這些參數使用了正确的配置,那麼讓sba擷取到完美的管理位址、健康檢測位址就變得輕松容易了。
如果你使用的是eureka作為注冊中心,erueka中如何配置呢
eureka:
instance:
metadata-map:
management.context-path: ${management.endpoints.web.base-path:/actuator}
health.path: /health
如果你使用的是Nacos作為注冊中心,Nacos中如何配置呢
spring:
cloud:
nacos:
discovery:
metadata:
management.context-path: ${management.endpoints.web.base-path:/actuator}
health.path: /health
因為開始已經說了服務管理位址是:http://IP:${management.server.port}/${management.server.servlet.context-path}/${management.endpoints.web.base-path},其中這裡management.endpoints.web.base-path預設為/actuator, 是以management.context-path 就要跟随 management.endpoints.web.base-path走
這裡有一個特别特别重要的點,就是在sba時候,内部預設是沒有對 management.server.servlet.context-path 進行特殊的處理,是以使用sba的時候,切記,不要設定management.server.servlet.context-path,而且用處也不是很大隻要不配置就可以了,也不要配置成management.server.servlet.context-path=/ 這種形式。如果你說你特别想用,哪隻能去改寫自己實作一個繼承DefaultServiceInstanceConverter 并完成自動配置,以便完成sba對 management.server.servlet.context-path 的處理邏輯也是可以實作的,不過這裡就不做過多講解了,因為沒必要浪費這樣的精力去搞這件事。
到此我們隻需要考慮如何為sba實作正确的配置了。
下面我們反過來再說,為何配置檔案中metadata中不配置management.context-path的時候,Nacos為何無法支援呢,因為從上面的源碼可以看到取 getManagementPath 的時候,優先級是先從metadata中取得,如果沒有配置,那麼取的就是預設的 /actuator,但是Nacos中初始化的時候卻偏偏設定了management.context-path,nacos注冊中心可以說是對metadata填充的還是比較全面的了,我們從com.alibaba.cloud.nacos.registry.NacosRegistration這個類中可以看到,com.alibaba.cloud.nacos.registry.NacosRegistration.init() ,在init初始化方法中,
if (null != managementPort) {
metadata.put(MANAGEMENT_PORT, managementPort.toString());
String contextPath = env.getProperty("management.server.servlet.context-path");
String address = env.getProperty("management.server.address");
if (!StringUtils.isEmpty(contextPath)) {
metadata.put(MANAGEMENT_CONTEXT_PATH, contextPath);
}
if (!StringUtils.isEmpty(address)) {
metadata.put(MANAGEMENT_ADDRESS, address);
}
}
management.context-path 被 management.server.servlet.context-path 給覆寫掉了,這樣取到的位址就少了一層目錄
例如假設如下設定:
management.server.port = 30000
management.server.servlet.context-path = /sba
management.endpoints.web.base-path = /act
按設定來說 metadata中的management.context-path = ${management.endpoints.web.base-path:/actuator} 也就是 metadata中的 management.context-path = /act,sba生成的管理位址是 http://127.0.0.1:30000/sba/act
如果此時不設定management.endpoints.web.base-path,sba管理位址是 http://127.0.0.1:30000/sba/actuator
如果此時不設定management.server.servlet.context-path,sba管理位址是http://127.0.0.1:30000/actuator
但是如果此時設定了management.server.servlet.context-pathNacos會把metadata中的management.context-path覆寫
是以對于Nacos來說
例如假設隻設定了management.server.servlet.context-path = /sba
那麼從服務角度來說管理位址正确的應該是 http://127.0.0.1:30000/sba/actuator,
而從sba角度來說位址是http://127.0.0.1:30000/sba,是以sba生成位址就不正常了是以取不到管理位址及健康檢測位址
歸納起來總結為:
sba管理位址為:http://127.0.0.1:30000/${management.context-path}
由于nacos中metadata中的management.context-path = management.server.servlet.context-path 位址就變為了 http://127.0.0.1:30000/${management.server.servlet.context-path}
而服務的實際位址是 http://127.0.0.1:30000/${management.server.servlet.context-path}/${management.endpoints.web.base-path:/actuator}, 這就是nacos的問題所在。
上面隻是解釋了應用Nacos會出現sba取錯位址問題的原因,說了很多,可能你已經迷糊了,但隻要記住幾點,設定如下幾個參數的時候要注意。
1、management.server.servlet.context-path 盡量不要設定,設定了就會影響sba的使用除非你自己處理sba。這樣nacos就不會把metadata中的management.context-path覆寫,為第三步正常使用management.context-path奠定了條件
2、management.endpoints.web.base-path 盡量不要設定,預設即可 /actuator 也挺好的,但是設定了也不影響,隻是第三步要跟随設定
3、配置的時候一定要且僅僅設定metadata中的management.context-path 和 health.path兩項即可
metadata:
management.context-path: ${management.endpoints.web.base-path:/actuator}
health.path: /health
這樣就可以保證在Spring Boot Admin中即便使用不同注冊中心都可以取到和服務比對的管理位址和健康檢測位址啦!