springCloud中如何通过服务名找到对应的ip
原文很详细,建议两篇一起看,以下内容整合出核心部分
spring-cloud-openfeign 源码解析
Spring Cloud Ribbon源码深度解析(没有源码阅读经验也能看懂)
面试官方答案:
在Spring Cloud Alibaba环境中,Feign调用时通过服务名到Nacos中寻址的过程可以分为以下几个步骤:
-
服务启动与注册: 当一个服务应用(包含Feign客户端)启动时,它会根据配置的spring.cloud.nacos.discovery.*属性自动向Nacos服务注册中心注册。这通常包括服务名(spring.application.name)、Nacos服务器地址(spring.cloud.nacos.discovery.server-addr)以及可能的命名空间ID和服务分组等信息。一旦注册成功,Nacos中就会记录该服务的实例信息,包括IP地址、端口号及健康状态等。
-
Feign客户端配置: 在使用Feign进行服务间调用前,需要在Feign客户端接口上添加相应的注解(如@FeignClient(“serviceName”)),其中”serviceName”即是要调用的服务名。这个服务名必须与目标服务在Nacos中注册的服务名匹配。
-
服务发现: 当Feign发起一次远程调用时,它依赖于Spring Cloud的负载均衡器(默认是Ribbon)。在初始化或每次请求时,Feign客户端会通过Spring Cloud的DiscoveryClient接口查询Nacos服务注册中心,以获取目标服务的所有可用实例列表。这个过程依据服务名进行查找,同时也考虑命名空间和服务分组的配置,确保找到的是正确的服务实例集合。
-
负载均衡与路由: 获取到服务实例列表后,负载均衡器(Ribbon)会根据内置的策略(如轮询、随机等)选择一个实例。选定实例后,Feign构建HTTP请求并直接与该实例通信。这个过程实现了服务路由,即根据服务名寻址到具体的实例地址并完成调用。
-
健康检查与重试: 在实际调用过程中,如果选定的实例不可用,Feign和Ribbon会根据配置的重试策略尝试其他实例,确保服务调用的健壮性。同时,Nacos也会持续监控服务实例的健康状态,及时剔除不健康的实例。
源码解析:
重点在第三步和第四步,以下是详细解读:
当ZoneAwareLoadBalancer在初始化时,会调用父类DynamicServerListLoadBalancer的构造方法,代码如下:
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList<T> serverList, ServerListFilter<T> filter, ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping);
this.isSecure = false;
this.useTunnel = false;
this.serverListUpdateInProgress = new AtomicBoolean(false);
this.updateAction = new NamelessClass_1();
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter)filter).setLoadBalancerStats(this.getLoadBalancerStats());
}
this.restOfInit(clientConfig);//关键
}
void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
this.setEnablePrimingConnections(false);
//开启动态更新Server
enableAndInitLearnNewServersFeature();
//更新Server列表
updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections()
.primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}
enableAndInitLearnNewServersFeature
这是个定时任务,每隔30s执行一次
public synchronized void start(final ServerListUpdater.UpdateAction updateAction) {
if (this.isActive.compareAndSet(false, true)) {
Runnable wrapperRunnable = new Runnable() {
public void run() {
if (!PollingServerListUpdater.this.isActive.get()) {
if (PollingServerListUpdater.this.scheduledFuture != null) {
PollingServerListUpdater.this.scheduledFuture.cancel(true);
}
} else {
try {
updateAction.doUpdate();
PollingServerListUpdater.this.lastUpdated = System.currentTimeMillis();
} catch (Exception var2) {
PollingServerListUpdater.logger.warn("Failed one update cycle", var2);
}
}
}
};
this.scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(wrapperRunnable, this.initialDelayMs, this.refreshIntervalMs, TimeUnit.MILLISECONDS);
} else {
logger.info("Already active, no-op");
}
}
updateListOfServers
全量更新一次服务列表。
public void enableAndInitLearnNewServersFeature() {
LOGGER.info("Using serverListUpdater {}", this.serverListUpdater.getClass().getSimpleName());
this.serverListUpdater.start(this.updateAction);
}
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
调用nacosServerList,在这里就会获取到服务名所对应的host
updateAllServerList
更新所有Server到本地缓存中。
protected void updateAllServerList(List<T> ls) {
if (this.serverListUpdateInProgress.compareAndSet(false, true)) {
try {
Iterator var2 = ls.iterator();
while(var2.hasNext()) {
T s = (Server)var2.next();
s.setAlive(true);
}
this.setServersList(ls);
// 进行心跳健康检测。
super.forceQuickPing();
} finally {
this.serverListUpdateInProgress.set(false);
}
}
}
完整链路:
-
通过注解 @EnableFeignClients 来集成feign客户端。这个注解开启了FeignClient的解析过程。这个注解的声明如下,它用到了一个@Import注解,Import是用来导入一个配置类的,FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar,它是一个动态注入bean的接口,Spring Boot启动的时候,会去调用这个类中的registerBeanDefinitions来实现动态Bean的装载,将其添加到spring的ioc容器中。
-
FeignClient标注的这个接口,会通过FeignClientFactoryBean.getObject()这个方法获得一个代理对象,也就是说OpenFeign最终返回的是一个 ReflectiveFeign.FeignInvocationHandler 的对象。那么当客户端发起请求时,会进入到 FeignInvocationHandler.invoke 方法中,它是一个动态代理的实现。
FeignClientFactoryBean.getObject
|
Targeter.target()
|
ReflectiveFeign.newInstance() //有一行代码是
|
Map<String, InvocationHandlerFactory.MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
targetToHandlersByName.apply(target) :根据Contract协议规则,会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler 然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。
- 而接着,在FeignInvocationHandler.invoke方法中,会调用 this.dispatch.get(method)).invoke(args) 。this.dispatch.get(method) 会返回一个SynchronousMethodHandler,进行拦截处理。这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版。
- 执行 executeAndDecode() 方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client获取response,来获取响应信息。
然后执行executeWithLoadBalancer,进入submit(),调用selectServer的方法获取server
lb.chooseServer() - > BaseLoadBalancer.chooseServer() 获取server,其中包含ip
默认情况下,rule的实现为ZoneAvoidanceRule,ZoneAvoidanceRule继承自PredicateBasedRule,它是在RibbonClientConfiguration这个配置类中定义的,代码如下:
这里的allServerList就是上述每隔30s就更新一次的那个参数。
然后执行回调方法call(),call方法中执行execute()方法。
利用 JDK 提供的 HttpURLConnection 发起远程的 HTTP通讯。