前言
最近敌人部门接手供方微服务项目过去运维,那套微服务的技术栈是 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 集成,被忽略了
demo 链接
https://github.com/lyb-geek/springboot-cloud-metadata-ext