最近朋友部门接手供方微服务项目过来运维,那套微服务的技术栈是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,以及配合注册中心具备的服务发现能力进行扩展
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; } }
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的客户端需配形如下配置
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集成,被忽略了
https://github.com/lyb-geek/springboot-cloud-metadata-ext