前言
最近朋友部門接手供方微服務專案過來運維,那套微服務的技術棧是springcloud Netflix,和朋友部門的微服務技術棧剛好匹配。當時朋友部門的想法,既然都是同一套技術體系,那些基礎服務治理元件比如註冊中心之類,就共用同一套。然而在落地實施的過程中,發現供方提供的微服務專案服務有些serviceId和朋友部門他們已有服務serviceId名字竟然一模一樣。這樣就有問題了,eureka服務發現是透過serviceId識別
朋友部門的策略是將供方微服務專案serviceId改掉,比如原本serviceId是user,就改成xxx-user。然而調整後,發現供方微服務某些微服務會報錯,後面瞭解到供方僅提供應用層程式碼,有些核心程式碼庫他們是不提供的。朋友他們部門也考慮要不就替換自己已有服務的serviceId,後邊發現可行性也不大,因為朋友他們微服務也對其他部門輸出了一些能力,如果改動,就得通知相關方進行改動,這邊會涉及到一些溝通成本等非技術性因素。
後邊朋友就和我交流了一下方案。首先問題點的本質是serviceId一樣引起的嗎?乍看一下,好像是這樣。我們換個角度考慮這個問題,是不是也可以說是因為隔離性做得不夠好導致。於是我就跟朋友說,如果把eureka切換成nacos,對你部門的切換成本大不大。朋友說程式碼層雖然僅需切換jar和配置即可實現,但是因為版本原因,如果切換,他們也只能用到nacos 1版本的能力。其次他部門對註冊中心的效能要求也不高,但對註冊中心的穩定性要求比較高,如果切換,他們需要做很多測試方面的工作,僅僅只是因為這個隔離性,他更傾向部署多個eureka。
聊到後面朋友就在吐槽eureka,為啥不能像nacos或者k8s那樣搞個namespace來做隔離。基於朋友這個想法,我就跟他說,我幫你擴充套件一下,讓eureka也擁有仿nacos namespace的能力
實現思路
注: 本文以朋友他們公司的微服務版本springcloud Hoxton.SR3來講解
實現的核心邏輯:利用註冊中心都有的後設資料,即metaMap,以及配合註冊中心具備的服務發現能力進行擴充套件
核心實現邏輯
1、後設資料擴充套件
a、新建擴充套件配置類
@ConfigurationProperties(prefix = "eureka.instance.ext")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EurekaInstanceProperties {
private String namespace = Constant.META_INFO_DEAFULT_NAMESPACE;
private String group = Constant.META_INFO_DEAFULT_GROUP;
private boolean loadBalanceAllowCross;
}
b、後設資料擴充套件填充
public class EurekaInstanceSmartInitializingSingleton implements SmartInitializingSingleton, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void afterSingletonsInstantiated() {
EurekaInstanceProperties eurekaInstanceProperties = applicationContext.getBean(EurekaInstanceProperties.class);
EurekaInstanceConfigBean bean = applicationContext.getBean(EurekaInstanceConfigBean.class);
Map<String, String> metadataMap = bean.getMetadataMap();
metadataMap.put(Constant.META_INFO_KEY_NAMESPACE,eurekaInstanceProperties.getNamespace());
metadataMap.put(Constant.META_INFO_KEY_GROUP,eurekaInstanceProperties.getGroup());
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
2、eureka服務端皮膚改造
a、status.ftlh頁面邏輯改造
在專案eureka服務端的src/main/resource目錄下新建/templates/eureka資料夾,將eureka原本有的status.ftlh複製過來,因為我們要對這個status.ftlh進行改造。而status.ftlh原本位置是放在
在status.ftlh新增如下內容
b、微調EurekaController內容
EurekaController是eureka服務用來皮膚展示的控制器,可以在eureka的服務端的專案建一個EurekaController一模一樣的類,形如
注: 也可以自己自定義一個controller,反正這個controller就是用來頁面渲染用的
對如下方法進行微調
org.springframework.cloud.netflix.eureka.server.EurekaController#populateApps
微調內容如下
for (InstanceInfo info : app.getInstances()) {
String id = info.getId();
String url = info.getStatusPageUrl();
Map<String, String> metadata = info.getMetadata();
String group = StringUtils.isEmpty(metadata.get("group")) ? "default" : metadata.get("group");
String namespace = StringUtils.isEmpty(metadata.get("namespace")) ? "default" : metadata.get("namespace");
String metaInfo = url + "_" + namespace + "_" + group;
List<Pair<String, String>> list = instancesByStatus
.computeIfAbsent(status, k -> new ArrayList<>());
list.add(new Pair<>(id, metaInfo));
}
for (Map.Entry<InstanceInfo.InstanceStatus, List<Pair<String, String>>> entry : instancesByStatus
.entrySet()) {
List<Pair<String, String>> value = entry.getValue();
InstanceInfo.InstanceStatus status = entry.getKey();
LinkedHashMap<String, Object> instanceData = new LinkedHashMap<>();
for (Pair<String, String> p : value) {
LinkedHashMap<String, Object> instance = new LinkedHashMap<>();
instances.add(instance);
instance.put("id", p.first());
String metaInfo = p.second();
String[] metaInfoArr = metaInfo.split("_");
String url = metaInfoArr[0];
instance.put("url", url);
String namespace = metaInfoArr[1];
instance.put("namespace", namespace);
String group = metaInfoArr[2];
instance.put("group", group);
boolean isHref = url != null && url.startsWith("http");
instance.put("isHref", isHref);
}
model.put("apps", apps);
c、改造後的皮膚展示
注: 在eureka的客戶端需配形如下配置
3、服務發現改造
a、重寫com.netflix.loadbalancer.ServerList
參照eureka的服務發現配置類
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config,
Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
我們可以發現我們僅需改造DiscoveryEnabledNIWSServerList即可
@Slf4j
public class CustomDiscoveryEnabledNIWSServerList extends DiscoveryEnabledNIWSServerList {
private final Provider<EurekaClient> eurekaClientProvider;
private final EurekaInstanceProperties eurekaInstanceProperties;
public CustomDiscoveryEnabledNIWSServerList(IClientConfig clientConfig, Provider<EurekaClient> eurekaClientProvider,EurekaInstanceProperties eurekaInstanceProperties) {
this.eurekaClientProvider = eurekaClientProvider;
this.eurekaInstanceProperties = eurekaInstanceProperties;
initWithNiwsConfig(clientConfig);
}
@Override
public List<DiscoveryEnabledServer> getInitialListOfServers(){
List<DiscoveryEnabledServer> initialListOfServers = super.getInitialListOfServers();
return selectListOfServersByMetaInfo(initialListOfServers);
}
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
List<DiscoveryEnabledServer> updatedListOfServers = super.getUpdatedListOfServers();
return selectListOfServersByMetaInfo(updatedListOfServers);
}
private List<DiscoveryEnabledServer> selectListOfServersByMetaInfo(List<DiscoveryEnabledServer> discoveryEnabledServerList){
List<DiscoveryEnabledServer> discoveryEnabledServersByMetaInfo = new ArrayList<>();
if(!CollectionUtils.isEmpty(discoveryEnabledServerList)){
for (DiscoveryEnabledServer discoveryEnabledServer : discoveryEnabledServerList) {
Map<String, String> metadata = discoveryEnabledServer.getInstanceInfo().getMetadata();
String namespace = metadata.get(Constant.META_INFO_KEY_NAMESPACE);
String group = metadata.get(Constant.META_INFO_KEY_GROUP);
if(eurekaInstanceProperties.getNamespace().equals(namespace) &&
eurekaInstanceProperties.getGroup().equals(group)){
discoveryEnabledServersByMetaInfo.add(discoveryEnabledServer);
}
}
}
if(CollectionUtils.isEmpty(discoveryEnabledServersByMetaInfo) &&
eurekaInstanceProperties.isLoadBalanceAllowCross()){
log.warn("not found enabledServerList in namespace : 【{}】 and group : 【{}】. will select default enabledServerList by isLoadBalanceAllowCross is {}",eurekaInstanceProperties.getNamespace(),eurekaInstanceProperties.getGroup(),eurekaInstanceProperties.isLoadBalanceAllowCross());
return discoveryEnabledServerList;
}
return discoveryEnabledServersByMetaInfo;
}
}
b、配置我們重寫後的ServerList
@Configuration
public class EurekaClientAutoConfiguration extends EurekaRibbonClientConfiguration{
@Value("${ribbon.eureka.approximateZoneFromHostname:false}")
private boolean approximateZoneFromHostname = false;
@RibbonClientName
private String serviceId = "client";;
@Autowired
private PropertiesFactory propertiesFactory;
@Bean
@Primary
public ServerList<?> ribbonServerList(IClientConfig config,
Provider<EurekaClient> eurekaClientProvider, EurekaInstanceProperties properties) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
DiscoveryEnabledNIWSServerList discoveryServerList = new CustomDiscoveryEnabledNIWSServerList(
config, eurekaClientProvider,properties);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
}
c、修改ribbionclient的預設配置
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnRibbonAndEurekaEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = EurekaClientAutoConfiguration.class)
public class RibbonEurekaAutoConfiguration {
}
測試
示例服務:閘道器、消費者服務、提供者服務、eureka
相關的eureka配置內容如下
1、閘道器:
閘道器佔用埠:8000
eureka:
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
prefer-ip-address: ${PREFER_IP:true} #是否選擇IP註冊
# ip-address: ${IP_ADDRESS:localhost} #指定IP地址註冊
lease-renewal-interval-in-seconds: 5 #續約更新時間間隔(預設30秒),使得eureka及時剔除無效服務
lease-expiration-duration-in-seconds: 10 #續約到期時間(預設90秒)
hostname: ${HOSTNAME:${spring.application.name}}
ext:
namespace: dev
group: lybgeek
client:
service-url:
defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}
#縮短延遲向服務端註冊的時間、預設40s
initial-instance-info-replication-interval-seconds: 10
#提高Eureka-Client端拉取Server註冊資訊的頻率,預設30s
registry-fetch-interval-seconds: 5
2、消費者
消費者1: 佔用埠:6614
eureka:
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
prefer-ip-address: ${PREFER_IP:true} #是否選擇IP註冊
# ip-address: ${IP_ADDRESS:localhost} #指定IP地址註冊
lease-renewal-interval-in-seconds: 5 #續約更新時間間隔(預設30秒),使得eureka及時剔除無效服務
lease-expiration-duration-in-seconds: 10 #續約到期時間(預設90秒)
hostname: ${HOSTNAME:${spring.application.name}}
ext:
group: lybgeek
namespace: dev
client:
service-url:
defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}
#縮短延遲向服務端註冊的時間、預設40s
initial-instance-info-replication-interval-seconds: 10
#提高Eureka-Client端拉取Server註冊資訊的頻率,預設30s
registry-fetch-interval-seconds: 5
消費者2: 佔用埠:6613
eureka:
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
prefer-ip-address: ${PREFER_IP:true} #是否選擇IP註冊
# ip-address: ${IP_ADDRESS:localhost} #指定IP地址註冊
lease-renewal-interval-in-seconds: 5 #續約更新時間間隔(預設30秒),使得eureka及時剔除無效服務
lease-expiration-duration-in-seconds: 10 #續約到期時間(預設90秒)
hostname: ${HOSTNAME:${spring.application.name}}
ext:
group: lybgeek6613
namespace: dev
client:
service-url:
defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}
#縮短延遲向服務端註冊的時間、預設40s
initial-instance-info-replication-interval-seconds: 10
#提高Eureka-Client端拉取Server註冊資訊的頻率,預設30s
registry-fetch-interval-seconds: 5
控制層示例
@RestController
@RequestMapping("instance")
public class InstanceInfoController {
@InstancePort
private String port;
@InstanceName
private String instanceName;
@Autowired
private InstanceServiceFeign instanceServiceFeign;
@GetMapping("list")
public List<InstanceInfo> list(){
List<InstanceInfo> instanceInfos = new ArrayList<>();
InstanceInfo comsumeInstanceInfo = InstanceInfo.builder()
.port(port).name(instanceName).build();
instanceInfos.add(comsumeInstanceInfo);
InstanceInfo providerInstanceInfo = null;
try {
providerInstanceInfo = instanceServiceFeign.getInstanceInfo();
instanceInfos.add(providerInstanceInfo);
} catch (Exception e) {
e.printStackTrace();
}
return instanceInfos;
}
}
3、提供者
提供者1: 佔用埠:6605
eureka:
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
prefer-ip-address: ${PREFER_IP:true} #是否選擇IP註冊
# ip-address: ${IP_ADDRESS:localhost} #指定IP地址註冊
lease-renewal-interval-in-seconds: 5 #續約更新時間間隔(預設30秒),使得eureka及時剔除無效服務
lease-expiration-duration-in-seconds: 10 #續約到期時間(預設90秒)
hostname: ${HOSTNAME:${spring.application.name}}
ext:
namespace: dev
group: lybgeek
client:
service-url:
defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}
#縮短延遲向服務端註冊的時間、預設40s
initial-instance-info-replication-interval-seconds: 10
#提高Eureka-Client端拉取Server註冊資訊的頻率,預設30s
registry-fetch-interval-seconds: 5
提供者2: 佔用埠:6604
eureka:
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
prefer-ip-address: ${PREFER_IP:true} #是否選擇IP註冊
# ip-address: ${IP_ADDRESS:localhost} #指定IP地址註冊
lease-renewal-interval-in-seconds: 5 #續約更新時間間隔(預設30秒),使得eureka及時剔除無效服務
lease-expiration-duration-in-seconds: 10 #續約到期時間(預設90秒)
hostname: ${HOSTNAME:${spring.application.name}}
ext:
namespace: dev
group: lybgeek6613
client:
service-url:
defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}
#縮短延遲向服務端註冊的時間、預設40s
initial-instance-info-replication-interval-seconds: 10
#提高Eureka-Client端拉取Server註冊資訊的頻率,預設30s
registry-fetch-interval-seconds: 5
控制層示例
@RestController
@RequestMapping(InstanceServiceFeign.PATH)
public class InstanceServiceFeignImpl implements InstanceServiceFeign {
@InstancePort
private String port;
@InstanceName
private String instanceName;
@Override
public InstanceInfo getInstanceInfo() {
return InstanceInfo.builder()
.name(instanceName).port(port).build();
}
}
訪問eureka皮膚
透過閘道器訪問會發現不管訪問多少次,閘道器只能命中namespace為dev、group為lybgeek的服務,說明隔離效果生效
當我們的服務和其他服務不屬於同個namespace或者group時,可以透過配置load-balance-allow-cross: true,實現跨namespace和group訪問。配置形如下
eureka:
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
prefer-ip-address: ${PREFER_IP:true} #是否選擇IP註冊
# ip-address: ${IP_ADDRESS:localhost} #指定IP地址註冊
lease-renewal-interval-in-seconds: 5 #續約更新時間間隔(預設30秒),使得eureka及時剔除無效服務
lease-expiration-duration-in-seconds: 10 #續約到期時間(預設90秒)
hostname: ${HOSTNAME:${spring.application.name}}
ext:
namespace: dev
group: lybgeek123
load-balance-allow-cross: true
我們再透過閘道器訪問一下
觀察控制檯,會發現出現警告
總結
本文主要是仿造nacos的一些思路,對eureka進行擴充套件。其實註冊中心的功能大同小異,尤其整合springcloud後,基本上都有固定套路了。本文只是實現eureka 服務發現隔離的一種方式,也可以透過eureka本身的zone和region,透過自定義負載均衡策略來實現。最後eureka instance其實也有namespace的屬性,只是在springcloud整合,被忽略了