Eureka结合网关实现多集群分片
问题描述
目前EurekaClient客户端只能往一个EurekaServer集群进行注册发现。而一个EurekaServer集群受制于硬件配置的高低,所能承受的最大服务注册实例数一般在4000至8000个不等。当一群服务的实例数超过了一个EurekaServer集群所能承受的最大实例数时,EurekaServer集群就不能正常运行,会导致大量请求超时,影响所有服务的注册和发现。
解决思路
为了解决这个问题,可以引入一个注册网关,将这一群服务按照某种规则分别注册到不同的EurekaServer集群中,再由注册网关通过调用Eureka API获取所有集群中的注册列表,将这些注册列表全部返回给EurekaClient客户端,这样每一个客户端都能拿到所有EurekaServer集群里的服务实例的注册列表。
具体实现
逻辑图
实现步骤说明
1、 通过修改Eureka Client源码,在请求的注册中心URL里添加项目名;
2、 注册网关通过识别请求中的项目名,将注册的请求转发到对应的集群中;
3、 注册网关通过定时任务每3秒调用Eureka API,从所有的集群中获取全量的注册列表和增量的注册列表分别存入本地缓存;
4、 当客户端发送获取全量注册列表或者是获取增量注册列表请求时,注册网关将本地缓存中存放的所有集群的未解压的全量注册列表或增量注册列表信息封装处理后返回给客户端;
5、 客户端接收到从注册网关返回的全量或增量注册列表后,经过解压合并处理转换成Applications对象存入本地注册列表缓存。
Eureka Client源码调整
将从GitHub上下载的Eureka源码spring-cloud-netflix-2.2.0.RELEASE打开,修改里面的子模块spring-cloud-netflix-eureka-client。
找到类org.springframework.cloud.netflix.eureka.EurekaClientConfigBean.java,修改getEurekaServerServiceUrls方法,将配置文件中的project.name参数加入到eurekaServerUrl中,修改的示例如下:
@Override
public List<String> getEurekaServerServiceUrls(String myZone) {
String serviceUrls = this.serviceUrl.get(myZone);
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = this.serviceUrl.get(DEFAULT_ZONE);
}
if (!StringUtils.isEmpty(serviceUrls)) {
final String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
List<String> eurekaServiceUrls = new ArrayList<>(serviceUrlsSplit.length);
for (String eurekaServiceUrl : serviceUrlsSplit) {
if (!endsWithSlash(eurekaServiceUrl)) {
eurekaServiceUrl += "/";
}
// ### 根据配置判断是否是需要连接注册网关 TOP ###
String isEurekaServer = this.environment.getProperty("is.use.eureka.gateway", "true");
if ("true".equalsIgnoreCase(isEurekaServer.toLowerCase())) {
/*
* 微服务客户端连注册中心需要经过注册网关转发
* 连注册网关的URL=http://gatewayIp:port/{project.name}/eureka/
*/
String projectName = this.environment.getProperty("project.name", "default");
String str = "/" + projectName + "/eureka/";
eurekaServiceUrl = eurekaServiceUrl.trim().replace("/eureka/", str);
System.out.println("EurekaGatewayUrl=" + eurekaServiceUrl);
} else {
System.out.println("EurekaServiceUrl=" + eurekaServiceUrl);
}
// ### END ###
eurekaServiceUrls.add(eurekaServiceUrl);
}
return eurekaServiceUrls;
}
return new ArrayList<>();
}
注册网关添加路由配置
使用Spring Cloud Gateway做为注册网关,在注册网关中添加对客户端请求的路由设置,将项目APP-1发来的注册发现请求转发到eurekaServer1,将APP-2发来的注册发现请求转发到eurekaServer2,配置示例如下:
spring.cloud.gateway.routes.0.id = app-1
spring.cloud.gateway.routes.0.uri = lb://app-1
spring.cloud.gateway.routes.0.predicates.0 = Path=/**/APP-1/eureka/**
spring.cloud.gateway.routes.0.filters.0 = StripPrefix=3
spring.cloud.gateway.routes.0.filters.1 = EurekaResponse
spring.cloud.gateway.routes.1.id = app-2
spring.cloud.gateway.routes.1.uri = lb://app-2
spring.cloud.gateway.routes.1.predicates.0 = Path=/**/APP-2/eureka/**
spring.cloud.gateway.routes.1.filters.0 = StripPrefix=3
spring.cloud.gateway.routes.1.filters.1 = EurekaResponse
app-1.ribbon.listOfServers = http://eurekaServer1Ip:8001
app-2.ribbon.listOfServers = http://eurekaServer2Ip:8002
注册网关定时获取注册列表
注册网关通过定时任务,每3秒去拉取eurekaServer1和eurekaServer2两个集群的全量注册列表,存入本地缓存。
@Slf4j
@Component
@EnableScheduling
public class EurekaAppsTask implements SchedulingConfigurer {
@Autowired
private ConfigBean configBean;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addTriggerTask(() -> eurekaAppsInfo(),
triggerContext -> {
String cron = configBean.getEurekainstanceCron();
if (StringUtils.isEmpty(cron)) {
cron = "*/3 * * * * ?";
}
return new CronTrigger(cron).nextExecutionTime(triggerContext);
});
}
// 全量拉取分片注册中心服务列表
private void eurekaAppsInfo() {
List<String> allSubDomainId = configBean.getAllSubDomainId();
allSubDomainId.stream().filter(subDomainId -> !CollectionUtils.isEmpty(allSubDomainId))
.forEach(subDomainId -> {
String oneDomainPath = configBean.getOneDomainPath(subDomainId);
if (StringUtils.isEmpty(oneDomainPath)) return;
String url = "http://" + oneDomainPath + "/eureka/apps";
long startTime = System.currentTimeMillis();
RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Accept-Encoding", "gzip");
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(httpHeaders);
byte[] value = new byte[0];
try {
ResponseEntity<byte[]> exchange = restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
value = exchange.getBody();
ServerData.allInstanceServersMap.put(subDomainId, value);
} catch (Exception e) {
log.error(e.getMessage(),e);
}
log.warn("获取分片 {} ,耗时:{} ms", url, System.currentTimeMillis() - startTime);
});
}
}
注册网关通过定时任务,每3秒去拉取eurekaServer1和eurekaServer2两个集群的增量注册列表,存入本地缓存。
@Slf4j
@Component
@EnableScheduling
public class EurekaDeltaAppsTask implements SchedulingConfigurer {
@Autowired
private ConfigBean configBean;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addTriggerTask(() -> eurekaDeltaAppsInfo(),
triggerContext -> {
String cron = configBean.getEurekainstanceCron();
if (StringUtils.isEmpty(cron)) {
cron = "*/3 * * * * ?";
}
return new CronTrigger(cron).nextExecutionTime(triggerContext);
});
}
// 增量拉取分片注册中心服务列表
private void eurekaDeltaAppsInfo() {
List<String> allSubDomainId = configBea