目录
一、Spring Factories机制
在外部包的META-INF/spring.factories文件中添加配置文件,Spring Boot项目会自动扫描这个配置文件
二、客户端(注册和心跳)
注入入口
spring.factories
启动时NacosServiceRegistryAutoConfiguration会注入三个Bean。在NacosRegistration、NacosRegistration、NacosAutoServiceRegistration中进行获取Properties配置信息和封装数据、初始化、进行服务的注册等操作。
public class NacosServiceRegistryAutoConfiguration {
@Bean
public NacosServiceRegistry nacosServiceRegistry(
NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosServiceRegistry(nacosDiscoveryProperties);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosRegistration nacosRegistration(
ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers,
NacosDiscoveryProperties nacosDiscoveryProperties,
ApplicationContext context) {
return new NacosRegistration(registrationCustomizers.getIfAvailable(),
nacosDiscoveryProperties, context);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}
}
客户端注册
启动时start()会调用NacosAutoServiceRegistration.register()方法判断是否开启注册和端口号是否正常
//NacosAutoServiceRegistration.class
@Override
protected void register() {
if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) {
log.debug("Registration disabled.");
return;
}
if (this.registration.getPort() < 0) {
this.registration.setPort(getPort().get());
}
super.register();
}
最终会执行到NacosServiceRegistry.register()方法,整理注册所需要的数据信息
//NacosServiceRegistry.class
@Override
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
//获取实例数据、命名空间等
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
Instance instance = getNacosInstanceFromRegistration(registration);
try {
//执行实例注册
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
catch (Exception e) {
if (nacosDiscoveryProperties.isFailFast()) {
log.error("nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
rethrowRuntimeException(e);
}
else {
log.warn("Failfast is false. {} register failed...{},", serviceId,
registration.toString(), e);
}
}
}
核心方法namingService.registerInstance(),会检测该实例是否为临时实例。并将获取到系统定义的实例等信息注册到注册中心
//NacosNamingService.class
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
// 格式为:groupId@@微服务名称
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
// 若当前实例为临时实例,则向Server发送心跳
if (instance.isEphemeral()) {
// 构建一个心跳信息实例
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
// 向Server端发送心跳(定时任务)
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
// 向Server发送注册请求
serverProxy.registerService(groupedServiceName, groupName, instance);
}
注册请求内容大概就是reqApi()方法封装了http协议发送请求,在callServer()方法中如果响应码不为0或200就认定为失败进行3次的重试
//NamingProxy.class
public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,
String method) throws NacosException {
params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
if (CollectionUtils.isEmpty(servers) && StringUtils.isBlank(nacosDomain)) {
throw new NacosException(NacosException.INVALID_PARAM, "no server available");
}
NacosException exception = new NacosException();
if (StringUtils.isNotBlank(nacosDomain)) {
// 默认尝试连接3次
for (int i = 0; i < maxRetry; i++) {
try {
return callServer(api, params, body, nacosDomain, method);
} catch (NacosException e) {
exception = e;
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
}
}
}
} else {
// 生成一个随机数,轮询
Random random = new Random(System.currentTimeMillis());
int index = random.nextInt(servers.size());
// 遍历Server,从中选择一个Server进行连接,轮询
for (int i = 0; i < servers.size(); i++) {
String server = servers.get(index);
try {
// todo 发送请求
return callServer(api, params, body, server, method);
} catch (NacosException e) {
exception = e;
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", server, e);
}
}
index = (index + 1) % servers.size();
}
}
...
throw new NacosException(exception.getErrCode(),
"failed to req API:" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
}
客户端心跳
先会检测是否为临时实例,构建BeatInfo
//NamingProxyService.class
// 若当前实例为临时实例,则向Server发送心跳
if (instance.isEphemeral()) {
// 构建一个心跳信息实例
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
// todo 向Server端发送心跳(定时任务)
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
addBeatInfo()开启定时任务,向Server端发送心跳请求
//BeatReactor.class
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
// 格式为:groupId@@微服务名称#ip#port
// 这个key就固定一个主机
String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
//fix #1733
// dom2Beat为一个map,key为主机 value为该主机发送心跳beatInfo
if ((existBeat = dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
dom2Beat.put(key, beatInfo);
// todo 开启一个定时任务
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}
可以看到元源码中构建了BeatTask(beatInfo)的任务对象,这也就是临时对象通过registerService()方法向注册中心上报自己还活跃(推模式)。
class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
@Override
public void run() {
if (beatInfo.isStopped()) {
return;
}
long nextTime = beatInfo.getPeriod();
try {
// 发送心跳
JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = result.get("clientBeatInterval").asLong();
boolean lightBeatEnabled = false;
if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.has(CommonParams.CODE)) {
code = result.get(CommonParams.CODE).asInt();
}
// 若在服务端没有发现这个Client,则Server端返回错误码为20404
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
Instance instance = new Instance();
instance.setPort(beatInfo.getPort());
instance.setIp(beatInfo.getIp());
instance.setWeight(beatInfo.getWeight());
instance.setMetadata(beatInfo.getMetadata());
instance.setClusterName(beatInfo.getCluster());
instance.setServiceName(beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
// 向Server端发送注册请求
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
} catch (Exception ignore) {
}
}
} catch (NacosException ex) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
}
// 设置一个新的定时任务
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
三、客户端(更新本地注册表)
注入入口
配置注入了NacosWatch的Bean对象来管控初始注册表
public class NacosDiscoveryClientConfiguration {
@Bean
public DiscoveryClient nacosDiscoveryClient(
NacosServiceDiscovery nacosServiceDiscovery) {
return new NacosDiscoveryClient(nacosServiceDiscovery);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled",
matchIfMissing = true)
public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager,
NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosWatch(nacosServiceManager, nacosDiscoveryProperties);
}
}
该Bean实现了SmartLifecycle,在环境准备好了之后,会调用start()方法
//NacosWatch.class
public class NacosWatch implements ApplicationEventPublisherAware, SmartLifecycle {
@Override
public void start() {
if (this.running.compareAndSet(false, true)) {
EventListener eventListener = listenerMap.computeIfAbsent(buildKey(),
event -> new EventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof NamingEvent) {
List<Instance> instances = ((NamingEvent) event)
.getInstances();
Optional<Instance> instanceOptional = selectCurrentInstance(
instances);
instanceOptional.ifPresent(currentInstance -> {
resetIfNeeded(currentInstance);
});
}
}
});
//构建服务对象
NamingService namingService = nacosServiceManager
.getNamingService(properties.getNacosProperties());
try {
//订阅
namingService.subscribe(properties.getService(), properties.getGroup(),
Arrays.asList(properties.getClusterName()), eventListener);
}
catch (Exception e) {
log.error("namingService subscribe failed, properties:{}", properties, e);
}
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
this::nacosServicesWatch, this.properties.getWatchDelay());
}
}
}
com.alibaba.nacos.client.naming.NacosNamingService#subscribe()
@Override
public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
throws NacosException {
hostReactor.subscribe(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","),
listener);
}
com.alibaba.nacos.client.naming.core.HostReactor#subscribe
public void subscribe(String serviceName, String clusters, EventListener eventListener) {
notifier.registerListener(serviceName, clusters, eventListener);
getServiceInfo(serviceName, clusters);
}
核心方法getServiceInfo()。
该方法包含两个核心内容:1.更新本地服务、2.开启定时任务
//HostReactor.class
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
// key的格式为:groupId@@微服务名称@@clusters名称
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
// 从当前Client本地注册表中获取当前服务
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
// 若本地注册表中没有该服务,则创建一个
if (null == serviceObj) {
// 创建一个空的服务(没有任何提供者实例instance的ServiceInfo)
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
// updatingMap 是一个临时缓存,主要使用这个map的key
// map的key不能重复的特性
// 只要这个服务名称在这个map中,说明这个服务正在更新中
updatingMap.put(serviceName, new Object());
// 更新本地注册表ServiceName的服务
updateServiceNow(serviceName, clusters);
// 更新完毕,从updatingMap中删除
updatingMap.remove(serviceName);
// 若当前注册表中已经有这个服务,那么查看一下临时map下
// 是否存在该服务,若存在,说明当前服务正在更新中,所以本次操作先等待一段时间,默认5s
} else if (updatingMap.containsKey(serviceName)) {
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER
.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
// 启动一个定时任务,定时更新本地注册表中的当前服务
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
更新本地注册表
本地注册表有个好处,就是当一个服务挂了之后,短时间内不会造成影响,因为有个本地注册列表,在服务不更新的情况下,服务还能够正常的运转,其原因如下:
Nacos的服务发现,一般是通过订阅的形式来获取服务数据。而通过订阅的方式,则是从本地的服务注册列表中获取(可以理解为缓存)。相反,如果不订阅,那么服务的信息将会从Nacos服务端获取,这时候就需要对应的服务是健康的。(宕机就不能使用了)
在代码设计上,通过Map来存放实例数据,key为实例名称,value为实例的相关信息数据(ServiceInfo对象)。
更新本地注册表主要看updateService()方法:
public void updateService(String serviceName, String clusters) throws NacosException {
// 本地注册表中获取当前服务
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
// 提交get请求,获取服务注册信息
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
if (StringUtils.isNotEmpty(result)) {
//发送get请求后,服务端响应的ServiceInfo更新到本地
processServiceJson(result);
}
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
更新执行方法processServiceJson()
//来自Server的数据是最新数据
public ServiceInfo processServiceJson(String json) {
// 转成ServiceInfo类
ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
// 从本地注册表中获取对应服务
ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
if (pushEmptyProtection && !serviceInfo.validate()) {
//empty or error push, just ignore
return oldService;
}
boolean changed = false;
// 当前注册表中存在该服务,想办法将来自server端的数据更新到本地注册表中
if (oldService != null) {
// 为了安全起见,这种情况几乎是不存在的
if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
+ serviceInfo.getLastRefTime());
}
// 来自server的serviceInfo替换到注册表中的当前服务
serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
// 遍历本地注册表中当前服务所有instance实例
Map<String, Instance> oldHostMap = new HashMap<String, Instance>(oldService.getHosts().size());
for (Instance host : oldService.getHosts()) {
// 将当前遍历instance主机的ip:port作为key,instance为value
// 写入到一个新的map中
oldHostMap.put(host.toInetAddr(), host);
}
// 遍历来自server的当前服务 所有instance实例
Map<String, Instance> newHostMap = new HashMap<String, Instance>(serviceInfo.getHosts().size());
for (Instance host : serviceInfo.getHosts()) {
// 将当前遍历instance主机的ip:port作为key,instance为value
// 写入到一个新的map中
newHostMap.put(host.toInetAddr(), host);
}
// 该set集合中存放的是,两个map(oldHostMap与newHostMap)中都有的ip:port,
// 但它们的instance不相同,此时会将来自于server的instance写入到这个set
Set<Instance> modHosts = new HashSet<Instance>();
// 只有newHostMap中存在的instance,即在server端新增的instance
Set<Instance> newHosts = new HashSet<Instance>();
// 只有oldHostMap中存在的instance,即在server端被删除的instance
Set<Instance> remvHosts = new HashSet<Instance>();
List<Map.Entry<String, Instance>> newServiceHosts = new ArrayList<Map.Entry<String, Instance>>(
newHostMap.entrySet());
// 遍历来自于server的主机
for (Map.Entry<String, Instance> entry : newServiceHosts) {
Instance host = entry.getValue();
// ip:port
String key = entry.getKey();
// 在注册表中存在该ip:port,但这两个instance又不同,则将这个instance写入到modHosts
if (oldHostMap.containsKey(key) && !StringUtils
.equals(host.toString(), oldHostMap.get(key).toString())) {
modHosts.add(host);
continue;
}
// 若注册表中不存在该ip:port,说明这个主机是新增的,则将其写入到newHosts
if (!oldHostMap.containsKey(key)) {
newHosts.add(host);
}
}
// 遍历来自于本地注册表的主机
for (Map.Entry<String, Instance> entry : oldHostMap.entrySet()) {
Instance host = entry.getValue();
String key = entry.getKey();
if (newHostMap.containsKey(key)) {
continue;
}
// 注册表中存在,但来自于server的serviceInfo中不存在,
// 说明这个instance被干掉了,将其写入到remvHosts
if (!newHostMap.containsKey(key)) {
remvHosts.add(host);
}
}
if (newHosts.size() > 0) {
changed = true;
NAMING_LOGGER.info("new ips(" + newHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(newHosts));
}
if (remvHosts.size() > 0) {
changed = true;
NAMING_LOGGER.info("removed ips(" + remvHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(remvHosts));
}
if (modHosts.size() > 0) {
changed = true;
// 变更心跳信息BeatInfo
updateBeatInfo(modHosts);
NAMING_LOGGER.info("modified ips(" + modHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(modHosts));
}
serviceInfo.setJsonFromServer(json);
// 只要发生了变更,就将这个发生变更的serviceInfo记录到一个缓存队列
if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
serviceInfo.getClusters(), serviceInfo.getHosts()));
DiskCache.write(serviceInfo, cacheDir);
}
// 本地注册表中没有这个服务,直接将来自Server的serviceInfo写入到本地注册表
} else {
changed = true;
NAMING_LOGGER.info("init new ips(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(serviceInfo.getHosts()));
// 将来自于server的serviceInfo写入到注册表
serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
// 将这个发生变更的serviceInfo记录到一个缓存队列
NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
serviceInfo.getClusters(), serviceInfo.getHosts()));
serviceInfo.setJsonFromServer(json);
DiskCache.write(serviceInfo, cacheDir);
}
MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
if (changed) {
NAMING_LOGGER.info("current ips:(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(serviceInfo.getHosts()));
}
return serviceInfo;
}
这里可以看到,我启动了两个不同端口的nacos-stock服务,注册中心服务端一并返回了名称相同的实例
开启定时更新任务
//HostReactor.class
// todo 启动一个定时任务,定时更新本地注册表中的当前服务
scheduleUpdateIfAbsent(serviceName, clusters);
public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
// futureMap是一个缓存map,其key为 groupId@@微服务名称@@clusters
// value是一个定时异步操作对象
// 这种结构称之为:双重检测锁,DCL,Double Check Lock
// 该结构是为了避免在并发情况下,多线程重复写入数据
// 该结构的特征:
// 1)有两个不为null的判断
// 2)有共享集合
// 3)有synchronized代码块
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
synchronized (futureMap) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
// 创建一个定时异步操作对象,并启动这个定时任务
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
// 将这个定时异步操作对象写入到缓存map
futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
}
}
四、客户端(获取服务提供者列表)
服务发现的流程:
以调用远程接口(OpenFeign)为例,当执行远程调用时,需要经过服务发现的过程。
服务发现先执行NacosServerList类中的getServers()方法,根据远程调用接口上@FeignClient中的属性作为serviceId传入NacosNamingService.selectInstances()方法中进行调用。
根据subscribe的值来决定服务是从本地注册列表中获取还是从Nacos服务端中获取。
以本地注册列表为例,通过调用HostReactor.getServiceInfo()来获取服务的信息(serviceInfo),Nacos本地注册列表由3个Map来共同维护。
//NacosNamingService.class
@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
//判断是否订阅
if (subscribe) {
// 已订阅就会,获取到要调用服务的serviceInfo和更新本地注册表
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
} else {
//未订阅直接向服务端发送请求
serviceInfo = hostReactor
.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
}
// 从serviceInfo的所有instance实例中检测健康状态过滤出所有可用的
return selectInstances(serviceInfo, healthy);
}