老规矩,5~10分钟,绝对让你有所收获!话接上篇,咱们了解了Springboot启动时是怎么触发Nacos自动注册事件的,这一篇着重说一下,触发自动注册事件后Nacos客户端是怎么发起注册请求到服务端的。
1、前置知识点
1.1、Nacos自动注册请求是grpc还是http
Nacos 2.x 版本默认使用 gRPC 协议来进行服务实例的自动注册和心跳检测。这是因为 gRPC 提供了高效的二进制传输格式和流式通信能力,适合于高性能的服务发现场景。 如果你的应用使用的是 Nacos 2.x,并且配置了默认的服务发现功能,那么服务实例会通过 gRPC 进行注册。如果需要使用 HTTP 协议来替代 gRPC,可以通过修改应用中的相关配置来实现,比如设置 spring.cloud.nacos.discovery.ephemeral=false 将实例设置为持久化的,这样就可以通过 HTTP 协议来进行服务注册和心跳检测。 需要注意的是,持久化实例不使用 gRPC 并可能影响到服务发现的性能和效率。如果你的应用环境支持 gRPC,推荐保留默认的 gRPC 配置以获得更好的性能。
1.2、gRPC通信说明
gRPC 默认使用 HTTP/2 协议,这意味着一旦建立了连接,客户端和服务端之间就会维持一个持久的连接。gRPC 客户端与服务端的通信机制如下:
-
建立连接:
当 gRPC 客户端首次连接到服务端时,它会建立一个持久的 TCP 连接。这个连接会一直保持打开状态,直到显式关闭或者由于网络问题而断开。
-
请求-响应模型:
gRPC 支持四种基本的请求-响应模型: 1、简单 RPC: 客户端发送一个请求,等待服务端响应。 2、服务端流 RPC: 客户端发送一个请求,服务端可以返回一系列消息。 3、客户端流 RPC: 客户端可以发送一系列消息,服务端返回一个响应。 4、双向流 RPC: 客户端和服务端都可以发送一系列消息。
-
心跳与保活:
gRPC 客户端和服务端之间的连接通常会有心跳或保活机制来检测连接的有效性。这些机制可以在没有活动请求的情况下定期发送小的数据包以维持连接的状态。
-
连接管理:
gRPC 客户端通常会使用连接池来管理与服务端的连接。连接池可以帮助客户端复用已有的连接,减少建立新连接的开销。
-
连接生命周期:
连接的生命周期受到多种因素的影响,包括但不限于客户端和服务端的配置、网络状况以及应用程序的需求。客户端和服务端可以配置连接的超时时间、空闲时间等,以控制连接的生命周期。
1.3、Nacos对应配置文件说明
@ConfigurationProperties 是 Spring Boot 提供的一个注解,用于将配置文件中的属性绑定到 Java 对象中。当你在一个类上使用 @ConfigurationProperties 注解时,它告诉 Spring Boot 自动将配置文件中匹配前缀的属性绑定到该类的字段上。 @ConfigurationProperties 的作用 1、自动绑定配置文件属性: 当你使用 @ConfigurationProperties 注解时,Spring Boot 会自动读取配置文件(如 application.yml 或 application.properties)中与指定前缀相匹配的属性,并将这些属性的值绑定到相应的 Java 对象的字段上。 2、配置前缀: 通过 prefix 属性指定配置文件中属性的前缀,Spring Boot 会根据这个前缀来查找配置文件中的属性。 3、自动装配: 使用 @ConfigurationProperties 注解的类通常也会使用 @Component 或 @Configuration 注解,这样 Spring Boot 就会在启动时自动检测并装配这些类。
1.3.1、疑问,如下配置为什么也会生效,并没有对应上图的前缀
server:
port: 8080
spring:
application:
name: nacos-service
cloud:
nacos:
#在discovery的上层
username: nacos
password: nacos
server-addr: 127.0.0.1:8848
discovery:
namespace: public
config:
namespace: public
file-extension: yml
1.3.2、源码分析
@PostConstruct 是一个注解,用于标记在 Spring Bean 初始化完成后执行的方法。它是 Java EE 规范的一部分,也被 Spring 框架支持。当一个带有 @PostConstruct 注解的方法被调用时,表明该方法应该在依赖注入完成后立即执行。
PS:后续用到的一些信息基本都是从NacosDiscoveryProperties这里拿的
2、Nacos客户端发起gRPC注册请求源码分析
上篇我们看到这里,明白了是什么时候出发的此方法,接着我们看这个方法都做了哪些事情
管理端点上下文(Management Endpoints Context)是指在 Spring Boot 应用中专门用于管理和监控应用的上下文。Spring Boot 提供了一组内置的管理端点(Endpoints),这些端点可以提供各种信息和服务,例如健康检查、应用信息、审计事件等。 管理端点上下文的作用 1、监控和管理: 管理端点上下文主要用于监控和管理 Spring Boot 应用。它提供了各种端点,允许外部系统查询应用状态、执行健康检查、获取应用信息等。 2、独立的上下文: 管理端点上下文通常运行在一个独立的上下文中,这意味着它有自己的配置和端口。这有助于隔离管理流量和应用流量,提高安全性。 3、配置分离: 管理端点上下文可以有自己的配置,例如安全配置、端口配置等。这使得可以单独配置管理端点的访问控制,以增强安全性。
下面代码比较重要直接源码分析
//com.alibaba.cloud.nacos.registry.NacosServiceRegistry
@Override
public void register(Registration registration) {
//registration.getServiceId()对应NacosRegistration.getServiceId()方法最终获取的还是 //nacosDiscoveryProperties.getService(),而nacosDiscoveryProperties.getService()
//如果没有配置的话获取的是spring.application.name,因此一般不会为空
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
//创建命名空间,为空的话会利用反射根据传入的NacosDiscoveryProperties通过
//NacosNamingService.init()构建对应的NacosNamingService实例
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
//构建Instance实例
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);
}
}
}
2.1、创建NamingService源码分析
看到这里已经了解NamingService具体创建过程;但是我们有必要了解下clientProxy的创建过程,以便于我们后面更方便的了解请求发起的逻辑代码;
2.2、gRPC代理以及与服务端建立连接源码说明
看到这里就我们已经知道了rpcClient建立连接默认的一些参数是怎么来的,下面我们接着看gRPC客户端是怎么和服务端建立连接的,在建立连接的时候都做了那些事情?
2.3、gRpc链接建立源码分析
//com.alibaba.nacos.common.remote.client.RpcClient.start()方法
public final void start() throws NacosException {
//原子性转换状态从INITIALIZED到STARTING
boolean success = rpcClientStatus.compareAndSet(RpcClientStatus.INITIALIZED, RpcClientStatus.STARTING);
if (!success) {
return;
}
//创建2个核心线程数的ScheduledThreadPoolExecutor线程,用来执行下面特定的逻辑
//注意线程设置为守护线程。守护线程会在所有非守护线程结束后自动退出。
clientEventExecutor = new ScheduledThreadPoolExecutor(2, r -> {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.remote.worker");
t.setDaemon(true);
return t;
});
//线程一:连接事件处理任务,用户可以针对ConnectionEventListener自定义建立连接和断开连接的一些扩展行为
//注意:任务会无限循环,直到线程池被终止或关闭
clientEventExecutor.submit(() -> {
while (!clientEventExecutor.isTerminated() && !clientEventExecutor.isShutdown()) {
ConnectionEvent take;
try {
//获取ConnectionEvent事件
take = eventLinkedBlockingQueue.take();
if (take.isConnected()) {
//遍历所有的ConnectionEventListener处理对应的事件方法onConnected()
notifyConnected();
} else if (take.isDisConnected()) {
//遍历所有的ConnectionEventListener处理对应的事件方法onDisConnect()
notifyDisConnected();
}
} catch (Throwable e) {
// Do nothing
}
}
});
//线程二:重连和健康检查任务,进行服务器的重连和健康检查
//注意:任务会无限循环,直到线程池被终止或关闭
clientEventExecutor.submit(() -> {
while (true) {
try {
if (isShutdown()) {
break;
}
//从reconnectionSignal队列里面获取需要重新建立连接的的链接信息,进行重新链接操作
//reconnectionSignal 变量很可能是一个 BlockingQueue 的实例,用于存放 ReconnectContext 类型的对
//象。这个队列通常会被用来管理需要重新连接的上下文信息。
//当发生以下几种情况时(不局限于以下几种情况),数据会被放入reconnectionSignal 队列中:
//连接断开:
//当客户端检测到与 Nacos 服务端的连接断开时,会将重新连接的信息放入队列中。
//连接超时:
//如果客户端在一定时间内没有收到服务端的响应,可能会触发重新连接机制,并将相应的信息放入队列。
//健康检查失败:
//当客户端定期执行健康检查时,如果发现当前连接的服务器不可用,可能会触发重新连接,并将相关信息放入队列。
//服务器列表更新:
//当客户端接收到新的服务器列表时,可能会根据新列表中的信息重新连接到可用的服务器。
//手动触发重新连接:
//应用程序可能会手动触发重新连接,比如在某些特定事件发生时。
ReconnectContext reconnectContext = reconnectionSignal
.poll(rpcClientConfig.connectionKeepAlive(), TimeUnit.MILLISECONDS);
if (reconnectContext == null) {
// 如果重新链接队列为空,检查当前时间距离上次活跃时间是否超过了配置的连接保持活动时间,如果超过了进行健康检
//查,通过则更新最后活跃时间,不通过则更新状态为不健康
if (System.currentTimeMillis() - lastActiveTimeStamp >= rpcClientConfig.connectionKeepAlive()) {
boolean isHealthy = healthCheck();
if (!isHealthy) {
if (currentConnection == null) {
continue;
}
LoggerUtils.printIfInfoEnabled(LOGGER,
"[{}] Server healthy check fail, currentConnection = {}",
rpcClientConfig.name(), currentConnection.getConnectionId());
RpcClientStatus rpcClientStatus = RpcClient.this.rpcClientStatus.get();
if (RpcClientStatus.SHUTDOWN.equals(rpcClientStatus)) {
break;
}
boolean statusFLowSuccess = RpcClient.this.rpcClientStatus
.compareAndSet(rpcClientStatus, RpcClientStatus.UNHEALTHY);
if (statusFLowSuccess) {
reconnectContext = new ReconnectContext(null, false);
} else {
continue;
}
} else {
lastActiveTimeStamp = System.currentTimeMillis();
continue;
}
} else {
continue;
}
}
//如果reconnectContext不为空,则判断服务列表里面是否还存在对应连接的服务信息,存在的话进行重新连接
//理论上会一直重连,只不过每次重连会存在Thread.sleep(Math.min(retryTurns + 1, 50) * 100L);对应的
//时间间隔
if (reconnectContext.serverInfo != null) {
// clear recommend server if server is not in server list.
boolean serverExist = false;
for (String server : getServerListFactory().getServerList()) {
ServerInfo serverInfo = resolveServerInfo(server);
if (serverInfo.getServerIp().equals(reconnectContext.serverInfo.getServerIp())) {
serverExist = true;
reconnectContext.serverInfo.serverPort = serverInfo.serverPort;
break;
}
}
if (!serverExist) {
LoggerUtils.printIfInfoEnabled(LOGGER,
"[{}] Recommend server is not in server list, ignore recommend server {}",
rpcClientConfig.name(), reconnectContext.serverInfo.getAddress());
reconnectContext.serverInfo = null;
}
}
//跟踪此方法可以看具体的重连逻辑,这里不做深入探究
reconnect(reconnectContext.serverInfo, reconnectContext.onRequestFail);
} catch (Throwable throwable) {
// Do nothing
}
}
});
//这里开始是真正的和服务器发起连接建立
// connect to server, try to connect to server sync retryTimes times, async starting if failed.
Connection connectToServer = null;
rpcClientStatus.set(RpcClientStatus.STARTING);
//这个是连接建立失败次数,默认应该是3次,上面有说到
int startUpRetryTimes = rpcClientConfig.retryTimes();
while (startUpRetryTimes > 0 && connectToServer == null) {
try {
startUpRetryTimes--;
ServerInfo serverInfo = nextRpcServer();
LoggerUtils.printIfInfoEnabled(LOGGER, "[{}] Try to connect to server on start up, server: {}",
rpcClientConfig.name(), serverInfo);
//此方法里面就是正常的gRPC连接创建过程, 连接失败则添加到reconnectionSignal重连队列里面
connectToServer = connectToServer(serverInfo);
} catch (Throwable e) {
LoggerUtils.printIfWarnEnabled(LOGGER,
"[{}] Fail to connect to server on start up, error message = {}, start up retry times left: {}",
rpcClientConfig.name(), e.getMessage(), startUpRetryTimes, e);
}
}
if (connectToServer != null) {
//连接建立成功:更新 rpcClientStatus 为 RUNNING 状态。向 eventLinkedBlockingQueue 中添加 //ConnectionEvent,表示连接成功
LoggerUtils
.printIfInfoEnabled(LOGGER, "[{}] Success to connect to server [{}] on start up, connectionId = {}",
rpcClientConfig.name(), connectToServer.serverInfo.getAddress(),
connectToServer.getConnectionId());
this.currentConnection = connectToServer;
rpcClientStatus.set(RpcClientStatus.RUNNING);
eventLinkedBlockingQueue.offer(new ConnectionEvent(ConnectionEvent.CONNECTED));
} else {
//失败则添加到reconnectionSignal重连队列里面
switchServerAsync();
}
//注册请求处理器,通常用于来处理服务端发送到客户端的请求
registerServerRequestHandler(new ConnectResetRequestHandler());
// register client detection request.
registerServerRequestHandler(request -> {
if (request instanceof ClientDetectionRequest) {
return new ClientDetectionResponse();
}
return null;
});
}
到这里我们已经了解了gRPC的具体创建过程,下面接着分析gRPC与服务端建立连接后,客户端发起注册请求的过程
3、发起注册请求源码分析
构建Instance实例方法比较简单,自己点进去看下就行
到这里实际上Nacos客户端发起自动注册的逻辑已经结束了,那么服务端是怎么处理接收到的请求的呢?
欲知后事如何,且听下回分解!恭喜你,又学到了一点知识,又进步了一点点...
添加公众号了解更多,定期分享、绝对实用,绝对对你有帮助!