Nacos源码—8.Nacos升级gRPC分析五

Nacos服务健康检查与下线注销源码分析

大纲

7.服务端对服务实例进行健康检查

8.服务下线如何注销注册表和客户端等信息

9.事件驱动架构源码分析

7.服务端对服务实例进行健康检查

(1)服务端对服务实例进行健康检查的设计逻辑

(2)服务端对服务实例进行健康检查的源码

(3)服务端检查服务实例不健康后的注销处理

(1)服务端对服务实例进行健康检查的设计逻辑

一.首先会获取所有客户端的Connection连接对象

Connection连接对象里有个属性叫lastActiveTime,表示的是最后存活时间。

二.然后判断当前时间-最后存活时间是否大于20s

如果大于,则把该Connection连接对象的connectionId放入到一个集合里。这个集合是一个名为outDatedConnections的待移除集合Set,此时该Connection连接对象并不会马上删除。

三.当判断完全部的Connection连接对象后会遍历outDatedConnections集合

向遍历到的Connection连接对象发起一次请求,确认是否真的下线。如果响应成功,则往successConnections集合中添加connectionId,并且刷新Connection连接对象的lastActiveTime属性。这个机制有一个专业的名称叫做:探活机制。

四.遍历待移除集合进行注销并且在注销之前先判断一下是否探活成功

也就是connectionId存在于待移除集合outDatedConnections中,但是不存在于探活成功集合successConnections中,那么这个connectionId对应的客户端就会被注销掉。

(2)服务端对服务实例进行健康检查的源码

对服务实例进行健康检查的源码入口是ConnectionManager的start()方法。

@Service
public class ConnectionManager extends Subscriber<ConnectionLimitRuleChangeEvent> {
    Map<String, Connection> connections = new ConcurrentHashMap<>();
    ...

    //Start Task:Expel the connection which active Time expire.
    @PostConstruct
    public void start() {
        //Start UnHealthy Connection Expel Task.
        RpcScheduledExecutor.COMMON_SERVER_EXECUTOR.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                ...
                //一.首先获取所有的连接
                Set<Map.Entry<String, Connection>> entries = connections.entrySet();
                ...
                //二.然后判断客户端是否超过20s没有发来心跳信息了,如果是则会将clientId加入outDatedConnections集合中
                Set<String> outDatedConnections = new HashSet<>();
                long now = System.currentTimeMillis();


                for (Map.Entry<String, Connection> entry : entries) {
                    Connection client = entry.getValue();
                    String clientIp = client.getMetaInfo().getClientIp();
                    AtomicInteger integer = expelForIp.get(clientIp);
                    if (integer != null && integer.intValue() > 0) {
                        integer.decrementAndGet();
                        expelClient.add(client.getMetaInfo().getConnectionId());
                        expelCount--;
                    } else if (now - client.getMetaInfo().getLastActiveTime() >= KEEP_ALIVE_TIME) {//判断心跳时间
                        //添加到待移除列表
                        outDatedConnections.add(client.getMetaInfo().getConnectionId());
                    }
                }
                ...
                //client active detection.
                //三.初次检测完超过20s的Connection连接对象后,并不会立马进行删除,而是进行探活,服务端主动请求客户端,来确认是否真的下线
                Loggers.REMOTE_DIGEST.info("Out dated connection ,size={}", outDatedConnections.size());
                if (CollectionUtils.isNotEmpty(outDatedConnections)) {
                    Set<String> successConnections = new HashSet<>();
                    final CountDownLatch latch = new CountDownLatch(outDatedConnections.size());
                    //遍历超过20s没有心跳的客户端clientId
                    for (String outDateConnectionId : outDatedConnections) {
                        try {
                            Connection connection = getConnection(outDateConnectionId);
                            if (connection != null) {
                                ClientDetectionRequest clientDetectionRequest = new ClientDetectionRequest();
                                //调用GrpcConnection.asyncRequest()方法异步发送请求
                                connection.asyncRequest(clientDetectionRequest, new RequestCallBack() {
                                    @Override
                                    public Executor getExecutor() {
                                        return null;
                                    }


                                    @Override
                                    public long getTimeout() {
                                        return 1000L;
                                    }


                                    @Override
                                    public void onResponse(Response response) {
                                        latch.countDown();
                                        if (response != null && response.isSuccess()) {
                                            //响应成功刷新心跳时间
                                            connection.freshActiveTime();
                                            //并且加入到探活成功的集合列表中
                                            successConnections.add(outDateConnectionId);
                                        }
                                    }

                                    @Override
                                    public void onException(Throwable e) {
                                        latch.countDown();
                                    }
                                });
                                Loggers.REMOTE_DIGEST.info("[{}]send connection active request ", outDateConnectionId);
                            } else {
                                latch.countDown();
                            }                            
                        } catch (ConnectionAlreadyClosedException e) {
                            latch.countDown();
                        } catch (Exception e) {
                            Loggers.REMOTE_DIGEST.error("[{}]Error occurs when check client active detection ,error={}", outDateConnectionId, e);
                            latch.countDown();
                        }
                    }


                    latch.await(3000L, TimeUnit.MILLISECONDS);
                    Loggers.REMOTE_DIGEST.info("Out dated connection check successCount={}", successConnections.size());
                    //经过探活还是不成功的Connection连接对象,就准备进行移除了
                    //遍历20s没有心跳的客户端,准备移除客户端信息
                    for (String outDateConnectionId : outDatedConnections) {
                        //判断探活是否成功,如果成功了则不需要移除
                        if (!successConnections.contains(outDateConnectionId)) {
                            Loggers.REMOTE_DIGEST.info("[{}]Unregister Out dated connection....", outDateConnectionId);
                            //执行客户端注销逻辑
                            unregister(outDateConnectionId);
                        }
                    }
                }
                ...
            }
        }, 1000L, 3000L, TimeUnit.MILLISECONDS);
    }
    ...
}

(3)服务端检查服务实例不健康后的注销处理

进行注销处理的方法是ConnectionManager的unregister()方法。该方法主要会移除Connection连接对象 + 清除一些数据,以及发布一个ClientDisconnectEvent客户端注销事件。

@Service
public class ConnectionManager extends Subscriber<ConnectionLimitRuleChangeEvent> {
    private Map<String, AtomicInteger> connectionForClientIp = new ConcurrentHashMap<String, AtomicInteger>(16);
    Map<String, Connection> connections = new ConcurrentHashMap<>();

    @Autowired
    private ClientConnectionEventListenerRegistry clientConnectionEventListenerRegistry;
    ...

    //unregister a connection .
    public synchronized void unregister(String connectionId) {
        //移除客户端信息
        Connection remove = this.connections.remove(connectionId);
        if (remove != null) {
            String clientIp = remove.getMetaInfo().clientIp;
            AtomicInteger atomicInteger = connectionForClientIp.get(clientIp);
            if (atomicInteger != null) {
                int count = atomicInteger.decrementAndGet();
                if (count <= 0) {
                    connectionForClientIp.remove(clientIp);
                }
            }
            remove.close();
            Loggers.REMOTE_DIGEST.info("[{}]Connection unregistered successfully. ", connectionId);
            //通知客户端注销连接
            clientConnectionEventListenerRegistry.notifyClientDisConnected(remove);
        }
    }
    ...
}

@Service
public class ClientConnectionEventListenerRegistry {
    ...
    //notify where a new client disconnected.
    public void notifyClientDisConnected(final Connection connection) {
        for (ClientConnectionEventListener clientConnectionEventListener : clientConnectionEventListeners) {
            try {
                //调用ConnectionBasedClientManager.clientDisConnected()方法
                clientConnectionEventListener.clientDisConnected(connection);
            } catch (Throwable throwable) {
                Loggers.REMOTE.info("[NotifyClientDisConnected] failed for listener {}", clientConnectionEventListener.getName(), throwable);
            }
        }
    }
    ...
}

@Component("connectionBasedClientManager")
public class ConnectionBasedClientManager extends ClientConnectionEventListener implements ClientManager {
    private final ConcurrentMap<String, ConnectionBasedClient> clients = new ConcurrentHashMap<>();
    ...

    @Override
    public boolean clientDisconnected(String clientId) {
        Loggers.SRV_LOG.info("Client connection {} disconnect, remove instances and subscribers", clientId);
        ConnectionBasedClient client = clients.remove(clientId);
        if (null == client) {
            return true;
        }
        client.release();
        //最后发布客户端注销事件
        NotifyCenter.publishEvent(new ClientEvent.ClientDisconnectEvent(client));
        return true;
    }
    ...
}

ClientDisconnectEvent客户端注销事件会被两个监听响应:一是ClientServiceIndexesManager的onEvent()方法用来移除注册表 + 订阅表信息,二是DistroClientDataProcessor的onEvent()方法用来同步服务实例被注销后的数据。

@Component
public class ClientServiceIndexesManager extends SmartSubscriber {
    //注册表(服务提供者),一个Service服务对象,对应多个服务实例的clientId
    private final ConcurrentMap<Service, Set<String>> publisherIndexes = new ConcurrentHashMap<>();

    //订阅者列表(服务消费者),一个Service服务对象,对应多个订阅者的clientId
    private final ConcurrentMap<Service, Set<String>> subscriberIndexes = new ConcurrentHashMap<>();
    ...

    @Override
    public void onEvent(Event event) {
        if (event instanceof ClientEvent.ClientDisconnectEvent) {
            handleClientDisconnect((ClientEvent.ClientDisconnectEvent) event);
        } else if (event instanceof ClientOperationEvent) {
            handleClientOperation((ClientOperationEvent) event);
        }
    }

    private void handleClientDisconnect(ClientEvent.ClientDisconnectEvent event) {
        Client client = event.getClient();
        for (Service each : client.getAllSubscribeService()) {
            //移除订阅者列表的元素
            removeSubscriberIndexes(each, client.getClientId());
        }
        for (Service each : client.getAllPublishedService()) {
            //移除注册表的元素
            removePublisherIndexes(each, client.getClientId());
        }
    }

    private void removePublisherIndexes(Service service, String clientId) {
        if (!publisherIndexes.containsKey(service)) {
            return;
        }
        publisherIndexes.get(service).remove(clientId);
        NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true));
    }

    private void removeSubscriberIndexes(Service service, String clientId) {
        if (!subscriberIndexes.containsKey(service)) {
            return;
        }
        subscriberIndexes.get(service).remove(clientId);
        if (subscriberIndexes.get(service).isEmpty()) {
            subscriberIndexes.remove(service);
        }
    }
    ...
}

public class DistroClientDataProcessor extends SmartSubscriber implements DistroDataStorage, DistroDataProcessor {
    ...
    @Override
    public void onEvent(Event event) {
        ...
        if (event instanceof ClientEvent.ClientVerifyFailedEvent) {
            syncToVerifyFailedServer((ClientEvent.ClientVerifyFailedEvent) event);
        } else {
            syncToAllServer((ClientEvent) event);
        }
    }

    private void syncToAllServer(ClientEvent event) {
        Client client = event.getClient();
        //Only ephemeral data sync by Distro, persist client should sync by raft.
        //临时实例使用Distro协议,持久化实例使用Raft协议
        //ClientManager.isResponsibleClient()方法,判断只有该client的责任节点才能进行集群数据同步
        if (null == client || !client.isEphemeral() || !clientManager.isResponsibleClient(client)) {
            return;
        }
        if (event instanceof ClientEvent.ClientDisconnectEvent) {
            //如果event是客户端注销实例时需要进行集群节点同步的事件
            DistroKey distroKey = new DistroKey(client.getClientId(), TYPE);
            distroProtocol.sync(distroKey, DataOperation.DELETE);
        } else if (event instanceof ClientEvent.ClientChangedEvent) {
            //如果event是客户端注册实例时需要进行集群节点同步的事件
            DistroKey distroKey = new DistroKey(client.getClientId(), TYPE);
            distroProtocol.sync(distroKey, DataOperation.CHANGE);
        }
    }
    ...
}

(4)总结

在Nacos 1.4.1版本中的服务健康检查:是15s没心跳则把健康状态修改为不健康,30s没心跳则把实例对象移除。

在Nacos 2.1.0版本中的服务健康检查:是20s没心跳则把客户端放入一个过期集合,此时并不移除客户端连接。由于引入了gRPC长连接,所以可以新增探活机制检查过期集合中的连接。服务端发送探活请求给客户端时的代价并不大,可确保客户端下线。

8.服务下线如何注销注册表和客户端等信息

(1)客户端发出服务下线请求的源码

(2)服务端处理服务下线请求的源码

(1)客户端发出服务下线请求的源码

Nacos客户端发起服务下线的入口在AbstractAutoServiceRegistration这个类之中,而AbstractAutoServiceRegistration是nacos-discovery中的类。

由于AbstractAutoServiceRegistration的destroy()方法被@PreDestroy修饰,所以当容器关闭时,会调用AbstractAutoServiceRegistration的destroy()方法。该方法最后会触发调用NacosNamingService的deregisterInstance()方法,然后调用NamingClientProxyDelegate的deregisterService()方法,接着调用NamingGrpcClientProxy的deregisterService()方法。和客户端发起服务注册一样,首先会创建请求参数对象,然后通过NamingGrpcClientProxy的requestToServer()方法发起请求,也就是调用RpcClient的request()方法发起gRPC请求进行服务下线。

public abstract class AbstractAutoServiceRegistration<R extends Registration> 
    implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {
    ...

    @PreDestroy
    public void destroy() {
        stop();
    }

    public void stop() {
        if (this.getRunning().compareAndSet(true, false) && isEnabled()) {
            deregister();
            if (shouldRegisterManagement()) {
                deregisterManagement();
            }
            this.serviceRegistry.close();
        }
    }

    protected void deregister() {
        this.serviceRegistry.deregister(getRegistration());
    }
    ...
}

public class NacosServiceRegistry implements ServiceRegistry<Registration> {
    ...
    @Override
    public void deregister(Registration registration) {
        log.info("De-registering from Nacos Server now...");
        if (StringUtils.isEmpty(registration.getServiceId())) {
            log.warn("No dom to de-register for nacos client...");
            return;
        }
        NamingService namingService = namingService();
        String serviceId = registration.getServiceId();
        String group = nacosDiscoveryProperties.getGroup();
        try {
            //调用NacosNamingService.deregisterInstance()方法
            namingService.deregisterInstance(serviceId, group, registration.getHost(), registration.getPort(), nacosDiscoveryProperties.getClusterName());
        } catch (Exception e) {
            log.error("ERR_NACOS_DEREGISTER, de-register failed...{},", registration.toString(), e);
        }
        log.info("De-registration finished.");
    }
    ...
}

@SuppressWarnings("PMD.ServiceOrDaoClassShouldEndWithImplRule")
public class NacosNamingService implements NamingService {
    private NamingClientProxy clientProxy;
    ...
    
    public NacosNamingService(Properties properties) throws NacosException {
        init(properties);
    }

    private void init(Properties properties) throws NacosException {
        ...
        this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
    }

    @Override
    public void deregisterInstance(String serviceName, String groupName, String ip, int port, String clusterName) throws NacosException {
        Instance instance = new Instance();
        instance.setIp(ip);
        instance.setPort(port);
        instance.setClusterName(clusterName);
        deregisterInstance(serviceName, groupName, instance);
    }

    @Override
    public void deregisterInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        //调用NamingClientProxyDelegate.deregisterService()方法
        clientProxy.deregisterService(serviceName, groupName, instance);
    }
    ...
}

public class NamingClientProxyDelegate implements NamingClientProxy {
    private final NamingHttpClientProxy httpClientProxy;
    private final NamingGrpcClientProxy grpcClientProxy;
    ...

    public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfoHolder, Properties properties, InstancesChangeNotifier changeNotifier) throws NacosException {
        ...
        this.httpClientProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, properties, serviceInfoHolder);
        this.grpcClientProxy = new NamingGrpcClientProxy(namespace, securityProxy, serverListManager, properties, serviceInfoHolder);
    }

    @Override
    public void deregisterService(String serviceName, String groupName, Instance instance) throws NacosException {
        getExecuteClientProxy(instance).deregisterService(serviceName, groupName, instance);
    }

    private NamingClientProxy getExecuteClientProxy(Instance instance) {
        return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
    }
    ...
}

public class NamingGrpcClientProxy extends AbstractNamingClientProxy {
    private final NamingGrpcRedoService redoService;
    ...

    @Override
    public void deregisterService(String serviceName, String groupName, Instance instance) throws NacosException {
        NAMING_LOGGER.info("[DEREGISTER-SERVICE] {} deregistering service {} with instance: {}", namespaceId, serviceName, instance);
        redoService.instanceDeregister(serviceName, groupName);
        doDeregisterService(serviceName, groupName, instance);
    }

    //Execute deregister operation.
    public void doDeregisterService(String serviceName, String groupName, Instance instance) throws NacosException {
        //创建请求参数对象  
        InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName, NamingRemoteConstants.DE_REGISTER_INSTANCE, instance);
        //向服务端发起请求
        requestToServer(request, Response.class);
        redoService.removeInstanceForRedo(serviceName, groupName);
    }

    private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass) throws NacosException {
        try {
            request.putAllHeader(getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
            //实际会调用RpcClient.request()方法发起gRPC请求
            Response response = requestTimeout < 0 ? rpcClient.request(request) : rpcClient.request(request, requestTimeout);
            if (ResponseCode.SUCCESS.getCode() != response.getResultCode()) {
                throw new NacosException(response.getErrorCode(), response.getMessage());
            }
            if (responseClass.isAssignableFrom(response.getClass())) {
                return (T) response;
            }
            NAMING_LOGGER.error("Server return unexpected response '{}', expected response should be '{}'", response.getClass().getName(), responseClass.getName());
        } catch (Exception e) {
            throw new NacosException(NacosException.SERVER_ERROR, "Request nacos server failed: ", e);
        }
        throw new NacosException(NacosException.SERVER_ERROR, "Server return invalid response");
    }
    ...
}

(2)服务端处理服务下线请求的源码

服务注册时请求参数InstanceRequest的类型是REGISTER_INSTANCE,服务下线时请求参数InstanceRequest的类型是DE_REGISTER_INSTANCE。

处理服务注册和服务下线的入口是InstanceRequestHandler的handle()方法,这个方法会触发调用InstanceRequestHandler的deregisterInstance()方法,也就是调用EphemeralClientOperationServiceImpl的deregisterInstance()方法。

在EphemeralClientOperationServiceImpl的deregisterInstance()方法中,会在移除Client对象中的instance信息时,发布ClientChangedEvent事件,然后接着发布客户端注销服务实例的事件ClientDeregisterServiceEvent。

其中ClientChangedEvent事件是用来同步数据给集群节点的,ClientDeregisterServiceEvent事件是用来移除注册表 + 订阅表的服务实例。移除注册表 + 订阅表的服务实例时,还会发布ServiceChangeEvent事件,ServiceChangeEvent事件是用来通知订阅了该服务的Nacos客户端的。同理,服务注册时其实也会发布类似的三个事件。

@Component
public class InstanceRequestHandler extends RequestHandler<InstanceRequest, InstanceResponse> {
    private final EphemeralClientOperationServiceImpl clientOperationService;

    public InstanceRequestHandler(EphemeralClientOperationServiceImpl clientOperationService) {
        this.clientOperationService = clientOperationService;
    }

    @Override
    @Secured(action = ActionTypes.WRITE)
    public InstanceResponse handle(InstanceRequest request, RequestMeta meta) throws NacosException {
        //根据请求信息创建一个Service对象,里面包含了:命名空间、分组名、服务名
        Service service = Service.newService(request.getNamespace(), request.getGroupName(), request.getServiceName(), true);
        switch (request.getType()) {
            case NamingRemoteConstants.REGISTER_INSTANCE:
                //注册实例
                return registerInstance(service, request, meta);
            case NamingRemoteConstants.DE_REGISTER_INSTANCE:
                //注销实例
                return deregisterInstance(service, request, meta);
            default:
                throw new NacosException(NacosException.INVALID_PARAM, String.format("Unsupported request type %s", request.getType()));
        }
    }

    private InstanceResponse registerInstance(Service service, InstanceRequest request, RequestMeta meta) {
        //调用EphemeralClientOperationServiceImpl的注册方法registerInstance(),这里需要注意如下参数;
        //参数service:根据请求信息创建的一个Service对象,里面有命名空间、分组名、服务名
        //参数request.getInstance():这个参数就对应了客户端的实例对象,里面包含IP、端口等信息
        //参数meta.getConnectionId():这个参数很关键,它是连接ID
        clientOperationService.registerInstance(service, request.getInstance(), meta.getConnectionId());
        return new InstanceResponse(NamingRemoteConstants.REGISTER_INSTANCE);
    }

    private InstanceResponse deregisterInstance(Service service, InstanceRequest request, RequestMeta meta) {
        //调用EphemeralClientOperationServiceImpl的注销方法deregisterInstance()
        clientOperationService.deregisterInstance(service, request.getInstance(), meta.getConnectionId());
        return new InstanceResponse(NamingRemoteConstants.DE_REGISTER_INSTANCE);
    }
}

@Component("ephemeralClientOperationService")
public class EphemeralClientOperationServiceImpl implements ClientOperationService {
    private final ClientManager clientManager;
    ...

    @Override
    public void deregisterInstance(Service service, Instance instance, String clientId) {
        if (!ServiceManager.getInstance().containSingleton(service)) {
            Loggers.SRV_LOG.warn("remove instance from non-exist service: {}", service);
            return;
        }
        //从ServiceManager中根据由请求信息创建的Service对象获取一个已注册的Service对象
        Service singleton = ServiceManager.getInstance().getSingleton(service);
        //从ClientManagerDelegate中根据请求参数中的connectionId获取一个Client对象,即IpPortBasedClient对象
        Client client = clientManager.getClient(clientId);
        if (!clientIsLegal(client, clientId)) {
            return;
        }
        //调用AbstractClient.removeServiceInstance()方法
        //移除Client对象中的instance信息并发布ClientChangedEvent事件来同步集群节点
        InstancePublishInfo removedInstance = client.removeServiceInstance(singleton);
        client.setLastUpdatedTime();
        if (null != removedInstance) {
            //发布客户端注销服务实例的事件
            NotifyCenter.publishEvent(new ClientOperationEvent.ClientDeregisterServiceEvent(singleton, clientId));
            NotifyCenter.publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, removedInstance.getMetadataId(), true));
        }
    }
    ...
}

public abstract class AbstractClient implements Client {
    //publishers其实就是记录该客户端提供的服务和服务实例,一个客户端可提供多个服务
    //存储客户端发送过来的请求中的Instance信息,当然这些信息已封装为InstancePublishInfo对象
    //key为已注册的Service,value是根据请求中的instance实例信息封装的InstancePublishInfo对象
    protected final ConcurrentHashMap<Service, InstancePublishInfo> publishers = new ConcurrentHashMap<>(16, 0.75f, 1);
    ...

    @Override
    public InstancePublishInfo removeServiceInstance(Service service) {
        InstancePublishInfo result = publishers.remove(service);
        if (null != result) {
            MetricsMonitor.decrementInstanceCount();
            //发布客户端改变事件,用于处理集群间的数据同步
            NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
        }
        Loggers.SRV_LOG.info("Client remove for service {}, {}", service, getClientId());
        return result;
    }
    ...
}

内容概要:本文介绍了一个基于MATLAB实现的多目标粒子群优化算法(MOPSO)在无人机三维路径规划中的应用。该代码实现了完整的路径规划流程,包括模拟数据生成、障碍物随机生成、MOPSO优化求解、帕累托前沿分析、最优路径选择、代理模型训练以及丰富的可视化功能。系统支持用户通过GUI界面设置参数,如粒子数量、迭代次数、路径节点数等,并能一键运行完成路径规划与评估。代码采用模块化设计,包含详细的注释,同时提供了简洁版本,便于理解和二次开发。此外,系统还引入了代理模型(surrogate model)进行性能预测,并通过多种图表对结果进行全面评估。 适合人群:具备一定MATLAB编程基础的科研人员、自动化/控制/航空航天等相关专业的研究生或高年级本科生,以及从事无人机路径规划、智能优化算法研究的工程技术人员。 使用场景及目标:①用于教学演示多目标优化算法(如MOPSO)的基本原理与实现方法;②为无人机三维路径规划提供可复现的仿真平台;③支持对不同参数配置下的路径长度、飞行时间、能耗与安全风险之间的权衡进行分析;④可用于进一步扩展研究,如融合动态环境、多无人机协同等场景。 其他说明:该资源包含两份代码(详细注释版与简洁版),运行结果可通过图形界面直观展示,包括Pareto前沿、收敛曲线、风险热图、路径雷达图等,有助于深入理解优化过程与结果特性。建议使用者结合实际需求调整参数,并利用提供的模型导出功能将最优路径应用于真实系统。
<think>我们正在处理一个关于Nacos客户端日志配置的问题。用户报告的错误是: "Unable to locate appender ASYNC-REMOTE for logger config com.alibaba.nacos.shaded.io.grpc" 这个错误通常发生在Nacos客户端尝试使用名为"ASYNC-REMOTE"的appender记录日志时,但在当前的日志配置中找不到该appender。 ### 问题分析 1. **日志框架配置问题**:Nacos客户端内部使用了SLF4J作为日志门面,实际日志实现可能是Logback、Log4j等。错误信息表明,在日志配置中,没有为名为"com.alibaba.nacos.shaded.io.grpc"的logger配置一个叫做"ASYNC-REMOTE"的appender。 2. **Nacos客户端的日志配置**:Nacos客户端在启动时会尝试加载默认的日志配置,但用户可能自定义了日志配置,覆盖了Nacos的默认配置。 3. **依赖冲突**:如果项目中存在多个日志框架的依赖,可能会导致配置混乱。 ### 解决方案 #### 1. 检查日志配置文件 确保日志配置文件(如logback.xml、log4j2.xml等)中定义了名为"ASYNC-REMOTE"的appender,并且该appender被关联到logger"com.alibaba.nacos.shaded.io.grpc"。 例如,在Logback中,配置可能如下: ```xml <appender name="ASYNC-REMOTE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 配置略 --> </appender> <logger name="com.alibaba.nacos.shaded.io.grpc" level="INFO"> <appender-ref ref="ASYNC-REMOTE"/> </logger> ``` #### 2. 检查默认日志配置 如果用户没有提供自定义日志配置,Nacos客户端会使用默认配置。但某些情况下,默认配置可能被覆盖。可以尝试在项目中添加一个简单的日志配置文件(如logback.xml)并配置所需的appender。 #### 3. 排除日志依赖冲突 检查项目的依赖树,确保没有引入多个日志框架的冲突版本。可以使用Maven的依赖分析工具: ```bash mvn dependency:tree ``` 或Gradle的依赖分析: ```bash gradle dependencies ``` 然后排除不必要的日志依赖。 例如,在Maven中排除冲突的日志依赖: ```xml <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> ``` #### 4. 使用Nacos默认日志配置 如果问题仍然存在,可以尝试使用Nacos自带的默认日志配置。在classpath下添加Nacos的默认日志配置文件(例如,从Nacos客户端jar包中提取logback.xml文件并放入项目的resources目录下)。但请注意,这可能会覆盖你的自定义配置。 ### 补充说明 - 如果问题仅在特定环境(如生产环境)出现,请检查该环境下的日志配置是否正确部署。 - 确保日志配置文件的名称和位置正确,并且被正确加载。 ### 示例配置(Logback) 以下是一个简单的Logback配置示例,定义了一个控制台输出和一个文件输出的appender,并将文件输出appender命名为"ASYNC-REMOTE"(注意:实际中"ASYNC-REMOTE"可能是一个异步appender,这里仅作示例): ```xml <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <appender name="ASYNC-REMOTE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>nacos.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>nacos.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> <totalSizeCap>1GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="com.alibaba.nacos.shaded.io.grpc" level="INFO"> <appender-ref ref="ASYNC-REMOTE"/> </logger> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </configuration> ``` 请根据实际需求调整。 ### 相关问题 1. 如何为Nacos客户端配置日志级别? 2. 如何解决SLF4J绑定多个日志框架的问题? 3. Nacos客户端在Spring Cloud项目中日志配置的优先级是怎样的?</think>### Nacos客户端日志配置问题分析 您遇到的错误信息 `Nacos client ERROR Unable to locate appender ASYNC-REMOTE for logger config com.alibaba.nacos.shaded.io.grpc` 表明Nacos客户端在尝试使用名为"ASYNC-REMOTE"的日志追加器(appender)时失败。以下是系统性的解决方案: --- ### 问题原因分析 1. **日志配置缺失** Nacos客户端内部使用`com.alibaba.nacos.shaded.io.grpc`包下的GRPC组件,该组件默认配置了名为`ASYNC-REMOTE`的appender。当您的项目日志配置中缺少此appender定义时,会触发此错误[^1]。 2. **日志框架冲突** 项目可能存在多个日志框架(如Logback、Log4j2、SLF4J)的依赖冲突,导致配置加载异常。 3. **配置覆盖问题** 自定义日志配置文件(如`logback.xml`)未正确继承Nacos默认配置,或错误覆盖了关键配置项。 --- ### 解决方案 #### 方案1:添加缺失的appender配置(推荐) 在您的日志配置文件(如`logback.xml`)中明确定义`ASYNC-REMOTE` appender: ```xml <!-- logback.xml 示例 --> <configuration> <!-- 1. 定义ASYNC-REMOTE appender --> <appender name="ASYNC-REMOTE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/nacos-remote.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>logs/nacos-remote.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 2. 关联到Nacos GRPC日志器 --> <logger name="com.alibaba.nacos.shaded.io.grpc" level="INFO"> <appender-ref ref="ASYNC-REMOTE"/> <!-- 关键配置 --> </logger> </configuration> ``` #### 方案2:排除冲突日志依赖 检查并排除重复的日志框架依赖(Maven示例): ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <exclusions> <!-- 排除冲突的日志绑定 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> ``` #### 方案3:启用Nacos默认配置 在`application.properties`中强制使用Nacos内置日志配置: ```properties # 强制加载Nacos默认日志配置 nacos.logging.config=classpath:nacos-logback.xml ``` --- ### 验证步骤 1. **检查配置生效** 启动时添加JVM参数验证配置加载: ```bash -Dlogging.config=classpath:logback.xml -Dnacos.logging.default.config.enabled=true ``` 2. **查看日志输出** 检查`logs/nacos-remote.log`是否生成,并包含GRPC相关日志。 3. **依赖树分析** 使用命令检查冲突: ```bash mvn dependency:tree | grep -E 'log4j|logback|slf4j' ``` --- ### 技术原理 Nacos客户端通过`nacos-client-logging`模块初始化日志系统,其默认配置要求存在`ASYNC-REMOTE` appender用于GRPC通信日志[^2]。当自定义配置覆盖默认配置但未保留此appender时,会导致组件级日志记录失败。 --- ### 相关问题 1. 如何为Nacos客户端配置不同的日志级别? 2. Spring Boot项目中如何统一管理Nacos与其他组件的日志配置? 3. Nacos客户端日志配置不生效的常见排查方法有哪些? [^1]: Nacos官方文档 - 日志配置说明 [^2]: Nacos GitHub源码 - nacos-client-logging模块
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值