版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Mr_Errol/article/details/85089129
上面已经大概了解了一下Nacos服务注册的逻辑,接下来来看看服务是如何被发现已经消费的。
一、服务发现前
目前在Spring Cloud,基本都是使用Feign去调用服务,Feign其实也是Ribbon的一个封装,主要功能,是将我们通常http请求服务这个过程帮我们封装起来,使我们使用时更加的简便,通过一个注解就能实现对服务的调用,对于ribbon的源码解析,参考这篇文章:
https://blog.csdn.net/forezp/article/details/74820899
文章讲的非常清楚了,这里大概总结一下,ribbon最最底层也是实现spring cloud common包下的
org.springframework.cloud.alibaba.nacos.ribbon.NacosServerList
接口,主要是ServiceInstanceChooser 下的继承接口:
org.springframework.cloud.client.loadbalancer.LoadBalancerClient
这是Ribbon实现负载均衡的父类接口,接下来一系列的接口实现最终会落到如何获取serverList这个问题是,答案在这个接口:
com.netflix.loadbalancer.ServerList
接下来,就是服务发现组件的事情了,比如,eureka对于这个接口的实现就是
DiscoveryEnabledNIWSServerList
而,Nacos的实现就是:org.springframework.cloud.alibaba.nacos.ribbon.NacosServerList,这也是我们的重点。
二、服务发现
上面主要是讲述了一下,服务发现的身世,Springcloud是如何走到获取服务列表这一步的,期间经过了ribbon的负载均衡,最后落到了Nacos的实现类NacosServerList中。来看代码
public class NacosServerList extends AbstractServerList<NacosServer> {
@Autowired
private NacosDiscoveryProperties discoveryProperties;
private String serviceId;
public NacosServerList() {
}
public NacosServerList(String serviceId) {
this.serviceId = serviceId;
}
@Override
public List<NacosServer> getInitialListOfServers() {
return getServers();
}
@Override
public List<NacosServer> getUpdatedListOfServers() {
return getServers();
}
private List<NacosServer> getServers() {
try {
List<Instance> instances = discoveryProperties.namingServiceInstance()
.getAllInstances(serviceId);
return instancesToServerList(instances);
}
catch (Exception e) {
throw new IllegalStateException(
"Can not get service instances from nacos, serviceId=" + serviceId,
e);
}
}
private List<NacosServer> instancesToServerList(List<Instance> instances) {
List<NacosServer> result = new ArrayList<>(instances.size());
for (Instance instance : instances) {
if (instance.isHealthy()) {
result.add(new NacosServer(instance));
}
}
return result;
}
public String getServiceId() {
return serviceId;
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
this.serviceId = iClientConfig.getClientName();
}
}
从ribbon的代码一路跟踪过来,最后落到了getServers()方法中,继续跟踪,com.alibaba.nacos.client.naming.NacosNamingService#getAllInstances(java.lang.String, java.util.List<java.lang.String>)
com.alibaba.nacos.client.naming.core.HostReactor#getServiceInfo(java.lang.String, java.lang.String, java.lang.String, boolean)
这里是具体逻辑的实现,在这个类中HostReactor,HostReactor这个类中维护了一个serviceInfoMap,顾名思义,维护了serverList的信息,key值是serverName,value是ServiceInfo;类中还有一个定时任务ScheduledExecutorService,后面会使用到。getServiceInfo()方法主要逻辑是:
1、先从已经存在的serviceInfoMap中通过serverName获取一个ServiceInfo,如果已经有了,需要再判断,另一个updatingMap是否存在这个key,如果存在,在wait 5秒,这个时间是写死的。在返回ServiceInfo前,调用scheduleUpdateIfAbsent()方法更新。
2、如果上面第一步serviceInfoMap不存在,则将传来的参数(erviceName, clusters, env)构建一个ServiceInfo,同时维护到serviceInfoMap和updatingMap中,同时根据allIPs这个参数的不同(我断点时为false)调用不同的接口去Nacos服务端拉取数据,通过方法updateService4AllIPNow 和 updateServiceNow,最后与上一步一样,调用scheduleUpdateIfAbsent方法。
3、scheduleUpdateIfAbsent方法,维护另一个map --futureMap,看代码:
public void scheduleUpdateIfAbsent(String serviceName, String clusters, String env, boolean allIPs) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters, env, allIPs)) != null) {
return;
}
synchronized (futureMap) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters, env, allIPs)) != null) {
return;
}
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters, env, allIPs));
futureMap.put(ServiceInfo.getKey(serviceName, clusters, env, allIPs), future);
}
}
最重要的是,将一个UpdateTask 添加到了定时任务当中启动了,这个UpdateTask.run() 的代码:
@Override
public void run() {
try {
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters, env, allIPs));
if (serviceObj == null) {
if (allIPs) {
updateService4AllIPNow(serviceName, clusters, env);
} else {
updateServiceNow(serviceName, clusters, env);
executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
}
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
if (allIPs) {
updateService4AllIPNow(serviceName, clusters, env);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters, env, true));
} else {
updateServiceNow(serviceName, clusters, env);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters, env));
}
} else {
// if serviceName already updated by push, we should not override it
// since the push data may be different from pull through force push
refreshOnly(serviceName, clusters, env, allIPs);
}
executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
lastRefTime = serviceObj.getLastRefTime();
} catch (Throwable e) {
LogUtils.LOG.warn("NA", "failed to update serviceName: " + serviceName, e);
}
}
非常的清晰明朗,如果serviceInfoMap中不存在需要的serverName,则,通过方法updateService4AllIPNow 和 updateServiceNow调用接口去获取,所以,serviceInfoMap并不是只在调用服务时才去获取更新,而是通过定时任务,通过心跳式的方式,不停的异步更新,10秒更新一次。
4、最后获取到ServiceInfo,将ServiceInfo.hosts返回为一个集合,因为同一个服务,可能有多个实例,host可能不一样。
public List<Instance> getAllInstances(String serviceName, List<String> clusters) throws NacosException {
ServiceInfo serviceInfo = hostReactor.getServiceInfo(serviceName, StringUtils.join(clusters, ","), StringUtils.EMPTY, false);
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
return list;
}
以上就是Nacos 客户端服务发现的逻辑。
下面来看服务端如何处理请求的。
三、服务端处理服务发现请求
上面说到的两个接口分别是:
/nacos/v1/ns/api/srvAllIP 断点调用,我们就只看这一个接口。
/nacos/v1/ns/api/srvIPXT
@RequestMapping("/srvIPXT")
@ResponseBody
public JSONObject srvIPXT(HttpServletRequest request) throws Exception {
JSONObject result = new JSONObject();
if (DistroMapper.getLocalhostIP().equals(UtilsAndCommons.LOCAL_HOST_IP)) {
throw new Exception("invalid localhost ip: " + DistroMapper.getLocalhostIP());
}
String dom = BaseServlet.required(request, "dom");
VirtualClusterDomain domObj = (VirtualClusterDomain) domainsManager.getDomain(dom);
String agent = request.getHeader("Client-Version");
String clusters = BaseServlet.optional(request, "clusters", StringUtils.EMPTY);
String clientIP = BaseServlet.optional(request, "clientIP", StringUtils.EMPTY);
Integer udpPort = Integer.parseInt(BaseServlet.optional(request, "udpPort", "0"));
String env = BaseServlet.optional(request, "env", StringUtils.EMPTY);
String error = BaseServlet.optional(request, "unconsistentDom", StringUtils.EMPTY);
boolean isCheck = Boolean.parseBoolean(BaseServlet.optional(request, "isCheck", "false"));
String app = BaseServlet.optional(request, "app", StringUtils.EMPTY);
String tenant = BaseServlet.optional(request, "tid", StringUtils.EMPTY);
boolean healthyOnly = Boolean.parseBoolean(BaseServlet.optional(request, "healthOnly", "false"));
if (!StringUtils.isEmpty(error)) {
Loggers.ROLE_LOG.info("ENV-NOT-CONSISTENT", error);
}
if (domObj == null) {
throw new NacosException(NacosException.NOT_FOUND, "dom not found: " + dom);
}
checkIfDisabled(domObj);
long cacheMillis = Switch.getCacheMillis(dom);
// now try to enable the push
try {
if (udpPort > 0 && PushService.canEnablePush(agent)) {
PushService.addClient(dom,
clusters,
agent,
new InetSocketAddress(clientIP, udpPort),
pushDataSource,
tenant,
app);
cacheMillis = Switch.getPushCacheMillis(dom);
}
} catch (Exception e) {
Loggers.SRV_LOG.error("VIPSRV-API", "failed to added push client", e);
cacheMillis = Switch.getCacheMillis(dom);
}
List<IpAddress> srvedIPs;
srvedIPs = domObj.srvIPs(clientIP, Arrays.asList(StringUtils.split(clusters, ",")));
if (CollectionUtils.isEmpty(srvedIPs)) {
String msg = "no ip to serve for dom: " + dom;
Loggers.SRV_LOG.debug(msg);
}
Map<Boolean, List<IpAddress>> ipMap = new HashMap<>(2);
ipMap.put(Boolean.TRUE, new ArrayList<IpAddress>());
ipMap.put(Boolean.FALSE, new ArrayList<IpAddress>());
for (IpAddress ip : srvedIPs) {
ipMap.get(ip.isValid()).add(ip);
}
if (isCheck) {
result.put("reachProtectThreshold", false);
}
double threshold = domObj.getProtectThreshold();
if ((float) ipMap.get(Boolean.TRUE).size() / srvedIPs.size() <= threshold) {
Loggers.SRV_LOG.warn("protect threshold reached, return all ips, " +
"dom: " + dom);
if (isCheck) {
result.put("reachProtectThreshold", true);
}
ipMap.get(Boolean.TRUE).addAll(ipMap.get(Boolean.FALSE));
ipMap.get(Boolean.FALSE).clear();
}
if (isCheck) {
result.put("protectThreshold", domObj.getProtectThreshold());
result.put("reachLocalSiteCallThreshold", false);
return new JSONObject();
}
JSONArray hosts = new JSONArray();
for (Map.Entry<Boolean, List<IpAddress>> entry : ipMap.entrySet()) {
List<IpAddress> ips = entry.getValue();
if (healthyOnly && !entry.getKey()) {
continue;
}
for (IpAddress ip : ips) {
JSONObject ipObj = new JSONObject();
ipObj.put("ip", ip.getIp());
ipObj.put("port", ip.getPort());
ipObj.put("valid", entry.getKey());
ipObj.put("marked", ip.isMarked());
ipObj.put("instanceId", ip.getInstanceId());
ipObj.put("metadata", ip.getMetadata());
ipObj.put("enabled", ip.isEnabled());
ipObj.put("weight", ip.getWeight());
ipObj.put("clusterName", ip.getClusterName());
ipObj.put("serviceName", ip.getServiceName());
hosts.add(ipObj);
}
}
result.put("hosts", hosts);
result.put("dom", dom);
result.put("cacheMillis", cacheMillis);
result.put("lastRefTime", System.currentTimeMillis());
result.put("checksum", domObj.getChecksum() + System.currentTimeMillis());
result.put("useSpecifiedURL", false);
result.put("clusters", clusters);
result.put("env", env);
result.put("metadata", domObj.getMetadata());
return result;
}
代码逻辑也非常直白,就是根据请求的参数dom,去DomainManager管理的domMap中获取对应的Domain信息,如果为空则抛异常,否则,将Domain信息已一个JSONObject返回,其中服务对应的IP信息以数组的形式返回(单个服务多个实例)。
————————————————
版权声明:本文为CSDN博主「Errol的杂货铺」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Mr_Errol/article/details/85089129