Nacos2注册中心服务端启动流程

718 阅读5分钟

吃东西的小埋

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

Nacos作为服务注册中心,整体的一个工作示意图如下所示。

Nacos-注册中心示意图

Nacos的服务端是Springboot应用,启动后会实例化大量Controller来处理HTTP请求,而在2.x版本中,新增加了gRPC的通信方式,因此Nacos的服务端启动时,也会在服务端进行gRPC的监听,用于处理客户端的gRPC请求。

本文将对Nacos作为注册中心,服务端启动时开启gRPC监听的一个流程进行学习。

Nacos版本:2.0.4

正文

Nacos提供了GrpcClusterServer用于集群间注册中心的数据同步)和GrpcSdkServer用于与客户端通信)作为gRPC服务端,这两个类由@Service注解修饰,因此在Nacos启动时,该类的实例会作为bean注册到Spring容器中。类图如下所示。

GrpcServer类图

GrpcClusterServerGrpcSdkServer的父类BasePrcServer有一个由@PostConstruct注解修饰的start() 方法,该方法会在初始化bean时被调用。下面看一下BaseRpcServerstart() 方法的实现。

@PostConstruct
public void start() throws Exception {
    String serverName = getClass().getSimpleName();
    Loggers.REMOTE.info("Nacos {} Rpc server starting at port {}", serverName, getServicePort());

    // 在这里进行启动
    startServer();

    Loggers.REMOTE.info("Nacos {} Rpc server started at port {}", serverName, getServicePort());
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        Loggers.REMOTE.info("Nacos {} Rpc server stopping", serverName);
        try {
            BaseRpcServer.this.stopServer();
            Loggers.REMOTE.info("Nacos {} Rpc server stopped successfully...", serverName);
        } catch (Exception e) {
            Loggers.REMOTE.error("Nacos {} Rpc server stopped fail...", serverName, e);
        }
    }));

}

继续跟进BaseRpcServerstartServer() 方法,如下所示。

@Override
public void startServer() throws Exception {
    final MutableHandlerRegistry handlerRegistry = new MutableHandlerRegistry();

    ServerInterceptor serverInterceptor = new ServerInterceptor() {
        @Override
        public <T, S> ServerCall.Listener<T> interceptCall(ServerCall<T, S> call, Metadata headers,
                ServerCallHandler<T, S> next) {
            Context ctx = Context.current()
                    .withValue(CONTEXT_KEY_CONN_ID, call.getAttributes().get(TRANS_KEY_CONN_ID))
                    .withValue(CONTEXT_KEY_CONN_REMOTE_IP, call.getAttributes().get(TRANS_KEY_REMOTE_IP))
                    .withValue(CONTEXT_KEY_CONN_REMOTE_PORT, call.getAttributes().get(TRANS_KEY_REMOTE_PORT))
                    .withValue(CONTEXT_KEY_CONN_LOCAL_PORT, call.getAttributes().get(TRANS_KEY_LOCAL_PORT));
            if (REQUEST_BI_STREAM_SERVICE_NAME.equals(call.getMethodDescriptor().getServiceName())) {
                Channel internalChannel = getInternalChannel(call);
                ctx = ctx.withValue(CONTEXT_KEY_CHANNEL, internalChannel);
            }
            return Contexts.interceptCall(ctx, call, headers, next);
        }
    };

    // 初始化用于处理客户端gRPC请求的handler
    addServices(handlerRegistry, serverInterceptor);

    // 构建gRPC的服务端
    server = ServerBuilder.forPort(getServicePort()).executor(getRpcExecutor())
            .maxInboundMessageSize(getInboundMessageSize()).fallbackHandlerRegistry(handlerRegistry)
            .compressorRegistry(CompressorRegistry.getDefaultInstance())
            .decompressorRegistry(DecompressorRegistry.getDefaultInstance())
            .addTransportFilter(new ServerTransportFilter() {
                @Override
                public Attributes transportReady(Attributes transportAttrs) {
                    InetSocketAddress remoteAddress = (InetSocketAddress) transportAttrs
                            .get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
                    InetSocketAddress localAddress = (InetSocketAddress) transportAttrs
                            .get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR);
                    int remotePort = remoteAddress.getPort();
                    int localPort = localAddress.getPort();
                    String remoteIp = remoteAddress.getAddress().getHostAddress();
                    Attributes attrWrapper = transportAttrs.toBuilder()
                            .set(TRANS_KEY_CONN_ID, System.currentTimeMillis() + "_" + remoteIp + "_" + remotePort)
                            .set(TRANS_KEY_REMOTE_IP, remoteIp).set(TRANS_KEY_REMOTE_PORT, remotePort)
                            .set(TRANS_KEY_LOCAL_PORT, localPort).build();
                    String connectionId = attrWrapper.get(TRANS_KEY_CONN_ID);
                    Loggers.REMOTE_DIGEST.info("Connection transportReady,connectionId = {} ", connectionId);
                    return attrWrapper;

                }

                @Override
                public void transportTerminated(Attributes transportAttrs) {
                    String connectionId = null;
                    try {
                        connectionId = transportAttrs.get(TRANS_KEY_CONN_ID);
                    } catch (Exception e) {
                        // Ignore
                    }
                    if (StringUtils.isNotBlank(connectionId)) {
                        Loggers.REMOTE_DIGEST
                                .info("Connection transportTerminated,connectionId = {} ", connectionId);
                        connectionManager.unregister(connectionId);
                    }
                }
            }).build();

    // 启动gRPC的服务端
    server.start();
}

上述方法会先初始化用于处理客户端gRPC请求的handler,然后构建一个gRPC Server实际类型是io.grpc.internal.ServerImpl),最后调用ServerImplstart() 方法进行启动。继续跟进ServerImplstart() 方法。

@Override
public ServerImpl start() throws IOException {
    synchronized (lock) {
        
        // 状态检查方式重复启动和关闭后启动
        checkState(!started, "Already started");
        checkState(!shutdown, "Shutting down");

        ServerListenerImpl listener = new ServerListenerImpl();
        // 启动Netty服务端
        for (InternalServer ts : transportServers) {
            ts.start(listener);
            activeTransportServers++;
        }
        executor = Preconditions.checkNotNull(executorPool.getObject(), "executor");
        started = true;
        return this;
    }
}

实际上gRPC服务端底层是通过Netty的方式监听端口,上述方法就是将Netty服务端启动起来并监听对应端口。默认情况下,如果是初始化GrpcClusterServerbean时调用到ServerImplstart() 方法,Netty服务端会监听在9849端口上,如果是初始化GrpcSdkServerbean时调用到ServerImplstart() 方法,Netty服务端会监听在9848端口上,因此,Nacos服务端作为注册中心启动后,默认情况下会监听三个端口。

  • 8848。用于处理HTTP请求;
  • 9848。用于处理客户端gRPC请求;
  • 9849。用于集群间注册中心的数据同步。

至此,Nacos作为注册中心时,服务端的一个启动流程分析完毕。

总结

Nacos 2.x版本中,Nacos作为注册中心服务端,启动时会做如下事情。

  1. 启动Springboot内嵌的Tomcat容器并监听在8848端口。用于处理HTTP请求;
  2. 启动gRPC Server并监听在9848端口。用于处理客户端的gRPC请求;
  3. 启动gRPC Server并监听在9849端口。用于集群间注册中心的数据同步。

gRPC Server的启动,依赖于GrpcClusterServerGrpcSdkServer这两个由@Service注解修饰的类,也就是在Springboot加载bean的时候,会调用到GrpcClusterServerGrpcSdkServer的初始化逻辑,从而完成gRPC Server的启动。流程示意如下。

Nacos-注册中心启动流程图

最后思考一下,为什么作为注册中心时,Nacos需要启动gRPC Server并监听在9849端口用于集群间注册中心的数据同步,而作为配置中心是不需要的。

这是由于作为注册中心时,注册数据保存在内存中,所以为了数据一致性Nacos集群中的实例之间需要通过9849端口通信用于数据同步,而作为配置中心时,配置数据持久化到了数据库中,因此数据的一致性,可以由数据库来保证。


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

可爱表情