聊聊如何基於eureka後設資料擴充套件namespace功能

linyb極客之路發表於2023-02-21

前言

最近朋友部門接手供方微服務專案過來運維,那套微服務的技術棧是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

相關文章