前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊如何基于eureka元数据扩展namespace功能

聊聊如何基于eureka元数据扩展namespace功能

原创
作者头像
lyb-geek
发布2023-02-21 10:25:34
3400
发布2023-02-21 10:25:34
举报
文章被收录于专栏:Linyb极客之路Linyb极客之路

前言

最近朋友部门接手供方微服务项目过来运维,那套微服务的技术栈是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、新建扩展配置类

代码语言:java
复制
@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、元数据扩展填充

代码语言:java
复制
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就是用来页面渲染用的

对如下方法进行微调

代码语言:java
复制
org.springframework.cloud.netflix.eureka.server.EurekaController#populateApps

微调内容如下

代码语言:java
复制
	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的服务发现配置类

代码语言:java
复制
	@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即可

代码语言:java
复制
@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

代码语言:java
复制
@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的默认配置

代码语言:java
复制
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnRibbonAndEurekaEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = EurekaClientAutoConfiguration.class)
public class RibbonEurekaAutoConfiguration {

}

测试

示例服务:网关、消费者服务、提供者服务、eureka

相关的eureka配置内容如下

1、网关:

网关占用端口:8000

代码语言:yaml
复制
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

代码语言:yaml
复制
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

代码语言:yaml
复制
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

控制层示例

代码语言:java
复制
@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

代码语言:java
复制
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

代码语言:java
复制
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

控制层示例

代码语言:java
复制
@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访问。配置形如下

代码语言:yaml
复制
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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 实现思路
  • 核心实现逻辑
    • 1、元数据扩展
      • 2、eureka服务端面板改造
        • 3、服务发现改造
        • 测试
        • 总结
        • demo链接
        相关产品与服务
        微服务引擎 TSE
        微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档