前言

最近敌人部门接手供方微服务项目过去运维,那套微服务的技术栈是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@NoArgsConstructorpublic 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即可

@Slf4jpublic 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
@Configurationpublic 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