【微服务】Nacos服务端完成微服务注册以及健康检查流程
创始人
2024-04-05 05:40:03
0

目录

一、前言

1、Nacos项目结构

二、微服务注册以及健康检查流程

1、流程图

2、处理微服务注册请求

2.1、InstanceController的register()注册入口

2.2、InstanceOperatorServiceImpl的registerInstance()逻辑

3、ServiceManager组件

3.1、registerInstance()逻辑

3.2、createServiceIfAbsent()创建服务

3.3、putServiceAndInit(Service service)

3.4、Service.init()逻辑

3.5、定时任务调度

3.6、ClientBeatCheckTask的run()逻辑,服务健康检查剔除下线

3.7、addInstance()

3.8、DelegateConsistencyServiceImpl的put()

4、DistroConsistencyServiceImpl组件

4.1、put()逻辑

4.2、onPut()逻辑

4.3、DataStore组件的put()逻辑

4.4、DistroConsistencyServiceImpl内部类Notifier

4.5、Service的onChange()方法

4.6、updateIPs()

5、处理心跳请求

5.1、beat(HttpServletRequest request)逻辑

5.2、InstanceOperatorServiceImpl的handleBeat()

5.3、调用处理心跳任务

5.4、ClientBeatProcessor的run()逻辑


一、前言

    上一篇讲解了通过SpringBoot的自动装配将Nacos的APIClient集成到了我们自己的微服,使之成为一个更加完善的微服务系统,可以通过http请求将自己的服务信息心跳发送给Nacos服务注册中心做处理以及维护。

    目前许许多多的公司已经开始使用了Nacos作为微服务的注册配置中心,在面试的时候也喜欢拿Nacos与Eureka做比较,所以是有必要理解Nacos的原理的。

1、Nacos项目结构

client模块是提供对外的微服务集成的,集成原理就是SpringBoot的自动装配以及Spring的一些核心流程。在使用SpringCloud时我们需要自己去手动写注册配置中心;在Nacos它帮我们写了,并且也是基于SpringBoot项目有注册、配置启动类,相应的模块如上图所示。 

二、微服务注册以及健康检查流程

1、流程图

2、处理微服务注册请求

既然Nacos也是基于SpringBoot的,那么我们都是比较熟悉了吧。上一节有指出微服务自己发送的http请求的入参中都有各自的URL,分别是/nacos/v1/ns/instance/beat/nacos/v1/ns/instance。那么我们去服务注册中心找找看看。

InstanceController跟我们平常见的Controller是一样的,顾名思义,它是专门处理服务实例的控制器,我们就找找注册入口。

2.1、InstanceController的register()注册入口

​​​​    @CanDistro@PostMapping@Secured(action = ActionTypes.WRITE)public String register(HttpServletRequest request) throws Exception {// 命名空间final String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);// 服务名final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);// 检查命名格式NamingUtils.checkServiceNameFormat(serviceName);// 服务实例final Instance instance = HttpRequestInstanceBuilder.newBuilder().setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();// 注册服务getInstanceOperator().registerInstance(namespaceId, serviceName, instance);// 发布事件NotifyCenter.publishEvent(new NamingTraceEvent.RegisterInstanceTraceEvent(System.currentTimeMillis(), "",false, namespaceId, NamingUtils.getGroupName(serviceName), NamingUtils.getServiceName(serviceName),instance.toInetAddr()));return "ok";}

 主要逻辑:

  1. 通过WebUtils获取命名空间以及服务名,里面会做一些处理,如解码、不存在抛异常等
  2. 通过NamingUtils校验服务名是否合法
  3. 获取服务端操作实例,构建由 InstanceExtensionHandler 处理的新实例和链。
  4. 发布事件

点击getInstanceOperator().registerInstance(namespaceId, serviceName, instance);进来

它有两个实现类,上面说了使用的是服务端实例,进入下面处理逻辑 

2.2、InstanceOperatorServiceImpl的registerInstance()逻辑

​​    @Overridepublic void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {// 解析并设置实例com.alibaba.nacos.naming.core.Instance coreInstance = parseInstance(instance);// 委托ServiceManager注册服务serviceManager.registerInstance(namespaceId, serviceName, coreInstance);}

 解析并设置实例,InstanceOperatorServiceImpl委托ServiceManager注册服务。

3、ServiceManager组件

3.1、registerInstance()逻辑

​​    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {// 如果服务不存在,则创建一个服务createEmptyService(namespaceId, serviceName, instance.isEphemeral());// 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务Service service = getService(namespaceId, serviceName);// 校验服务不能为null,若空抛异常checkServiceIsNull(service, namespaceId, serviceName);// 将服务的实例信息添加到DataStore的dataMap缓存中addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);}

主要逻辑:

  1. 如果服务不存在,则创建一个服务
  2. 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务
  3. 校验服务不能为null,若空抛异常
  4. 将服务的实例信息添加到DataStoredataMap缓存中 

3.2、createServiceIfAbsent()创建服务

    public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)throws NacosException {// 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务Service service = getService(namespaceId, serviceName);// 服务空新建if (service == null) {Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);service = new Service();service.setName(serviceName);service.setNamespaceId(namespaceId);service.setGroupName(NamingUtils.getGroupName(serviceName));// now validate the service. if failed, exception will be thrownservice.setLastModifiedMillis(System.currentTimeMillis());service.recalculateChecksum();// 集群非空if (cluster != null) {// 将服务添加到集群cluster.setService(service);// 将集群对象设置到服务clusterMap字段service.getClusterMap().put(cluster.getName(), cluster);}// 判断服务是否有效。service.validate();// 存储服务信息和初始化,点进去putServiceAndInit(service);if (!local) {addOrReplaceService(service);}}}

主要逻辑:

  1. 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务

  2. 服务空新建:集群非空,则将服务添加到集群,将集群对象设置到服务clusterMap字段;判断服务是否有效;存储服务信息和初始化

3.3、putServiceAndInit(Service service)

主要逻辑:

  1. 将服务信息保存到本地缓存serviceMap
  2. 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务
  3. 根据命名空间、服务名以及ephemeral(决定使用哪个实现类)通过KeyBuilder 构建唯一key,然后调用DistroConsistencyServiceImpl的listen将key与listen(Service)绑定。

3.4、Service.init()逻辑

 该方法中调用HealthCheckReactor的scheduleCheck(BeatCheckTask task)方法开始一个定时任务,入参为ClientBeatCheckTask的对象BeatCheckTask类型。

3.5、定时任务调度

判断任务是否NacosHealthCheckTask类型,如果是则包装拦截链。从3.4就知道该任务不是NacosHealthCheckTask类型。GlobalExecutor是一个全局执行器的封装类,封装了好几个执行器,比较有意思,然后负责提交执行任务,固定延时5秒执行的定时任务做服务的剔除。

3.6、ClientBeatCheckTask的run()逻辑,服务健康检查剔除下线

    @Overridepublic void run() {try {// If upgrade to 2.0.X stop health check with v1if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {return;}if (!getDistroMapper().responsible(service.getName())) {return;}if (!getSwitchDomain().isHealthCheckEnabled()) {return;}List instances = service.allIPs(true);// first set health status of instances:设置服务健康状态for (Instance instance : instances) {// 如果系统当前时间与上次心跳时间差,大于设置的阈值,则设置服务实例为不健康状态if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {if (!instance.isMarked()) {if (instance.isHealthy()) {instance.setHealthy(false);Loggers.EVT_LOG.info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",instance.getIp(), instance.getPort(), instance.getClusterName(),service.getName(), UtilsAndCommons.LOCALHOST_SITE,instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());getPushService().serviceChanged(service);}}}}if (!getGlobalConfig().isExpireInstance()) {return;}// then remove obsolete instances:然后删除过时的实例:for (Instance instance : instances) {if (instance.isMarked()) {continue;}// 通过判断当前时间和实例最后一次心跳时间的间隔是否大于阈值(默认15s),决定是否进行服务剔除/下线;if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {// delete instance删除实例Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(),JacksonUtils.toJson(instance));// 异步删除实例deleteIp(instance);}}} catch (Exception e) {Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);}}

由3.4可见任务的逻辑在这里,这里就是服务健康检查。ClientBeatCheckTask也是Runnable的间接实现类,run()方法主要逻辑:

  1. service获取所有服务实例,并进行遍历
  2. 如果系统当前时间上次心跳时间差大于设置的阈值,则设置服务实例为不健康状态
  3. 通过判断当前时间和实例最后一次心跳时间的间隔是否大于阈值(默认15s),决定是否进行服务剔除/下线,若需剔除下线则异步删除实例。

3.7、addInstance()

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)throws NacosException {String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);Service service = getService(namespaceId, serviceName);synchronized (service) {List instanceList = addIpAddresses(service, ephemeral, ips);Instances instances = new Instances();instances.setInstanceList(instanceList);// 保存服务信息,key为namespaceId和serviceName的拼接consistencyService.put(key, instances);}
}

主要逻辑:

由于存在多个服务实例同时注册的场景,所以要加一个synchronized锁。

  1. 根据命名空间服务名、ephemeral(默认true),通过KeyBuilder拼接key
  2. 根据命名空间服务名从本地缓存serviceMap获取服务
  3. 比较并获得新的实例列表设置instances
  4. 保存服务信息,key为namespaceIdserviceName拼接

ConsistencyService是一个接口,有好几个实现类。由上图可见在ServiceManager中默认注入的实现类是DelegateConsistencyServiceImpl,所以我们会进入DelegateConsistencyServiceImpl处理。

3.8、DelegateConsistencyServiceImpl的put()

 3.7中步骤一拼接keyephemeral(默认true),源码里面会拼接ephemeral。故这里会获取ephemeralConsistencyService,即会调用其put()方法。但是EphemeralConsistencyService是一个接口(继承了ConsistencyService接口的put()方法),唯一实现类为DistroConsistencyServiceImpl,所以我们看看其put()方法逻辑。

4、DistroConsistencyServiceImpl组件

4.1、put()逻辑

 put()方法里面的逻辑基本上都封装了出去,调用onPut()持久化服务实例,调用distroProtocol.sync()方法同步Nacos集群间数据。

4.2、onPut()逻辑

主要逻辑:

  1. 若匹配到key,则保存到DataStore的dataMap字段中缓存起来
  2. 如果listener中没有这个key的话直接返回,key是在创建Service时添加进去的,见ServiceManager#putServiceAndInit()方法
  3. 通知NacoClient服务端服务实例信息发生变更,这里是先添加任务

4.3、DataStore组件的put()逻辑

这里没有多少逻辑,单纯放到一个map缓存起来。

4.4、DistroConsistencyServiceImpl内部类Notifier

1)addTask()逻辑

 Notifier是DistroConsistencyServiceImpl的内部类,实现了Runnable接口。

这里的主要逻辑是往阻塞队列添加新的任务,但是添加的任务究竟是怎么执行的呢?熟悉Runnable接口的都知道肯定会用到它或者它的实现类,我们不妨点击一下Notifier。

发现了惊喜,在DistroConsistencyServiceImpl中有一个@PostConstruct修饰的init()方法,即在DistroConsistencyServiceImpl类构造器执行之后会执行这个方法启动Notifier通知器。那么Runnable里面有个子类必须实现的run()方法,那么这里的逻辑是?

2)Notifier的run()方法

调用阻塞队列的take()方法取出任务,然后调用Notifier的handle()方法处理服务变更。

3)handle(Pair pair)逻辑

private void handle(Pair pair) {try {String datumKey = pair.getValue0();DataOperation action = pair.getValue1();// 任务开始受理,那么移除datumKeyservices.remove(datumKey);int count = 0;// 因为任务由listeners受理的,在ServiceManager的putServiceAndInit(Service service)// 方法中已经添加了该datumKey,如果没有则return不用受理if (!listeners.containsKey(datumKey)) {return;}for (RecordListener listener : listeners.get(datumKey)) {count++;try {// 通知数据发生更改if (action == DataOperation.CHANGE) {listener.onChange(datumKey, dataStore.get(datumKey).value);continue;}// 通知数据删除if (action == DataOperation.DELETE) {listener.onDelete(datumKey);continue;}} catch (Throwable e) {Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);}}if (Loggers.DISTRO.isDebugEnabled()) {Loggers.DISTRO.debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",datumKey, count, action.name());}} catch (Throwable e) {Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);}}

    任务开始受理,那么从缓存services移除datumKey。因为任务由listeners受理的,在ServiceManagerputServiceAndInit(Service service)方法中已经添加了该datumKey,如果没有则return不用受理。通知数据的变更/删除由listener处理的,那么我们看下其中的逻辑。

    ServiceManagerputServiceAndInit(Service service)的逻辑里面,将datumKeylistener绑定,可见listener为Service,所以我们看下Service的onChange()方法。

4.5、Service的onChange()方法

    @Overridepublic void onChange(String key, Instances value) throws Exception {Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);for (Instance instance : value.getInstanceList()) {if (instance == null) {// Reject this abnormal instance list:throw new RuntimeException("got null instance " + key);}if (instance.getWeight() > 10000.0D) {instance.setWeight(10000.0D);}if (instance.getWeight() < 0.01D && instance.getWeight() > 0.0D) {instance.setWeight(0.01D);}}// 更新服务IPupdateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));recalculateChecksum();}

遍历实例所在的服务列表,在一定范围内设置权重,主要调用updateIPs()更新服务IP

4.6、updateIPs()

    public void updateIPs(Collection instances, boolean ephemeral) {Map> ipMap = new HashMap<>(clusterMap.size());for (String clusterName : clusterMap.keySet()) {ipMap.put(clusterName, new ArrayList<>());}for (Instance instance : instances) {try {if (instance == null) {Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");continue;}// 如果实例所在集群名称为空,则设置集群名称为DEFAULTif (StringUtils.isEmpty(instance.getClusterName())) {instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);}// 如果服务实例所在的集群实例clusterMap没有缓存,则新建设置默认配置并缓存到clusterMapif (!clusterMap.containsKey(instance.getClusterName())) {Loggers.SRV_LOG.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",instance.getClusterName(), instance.toJson());Cluster cluster = new Cluster(instance.getClusterName(), this);cluster.init();getClusterMap().put(instance.getClusterName(), cluster);}// 获取集群下所有实例List clusterIPs = ipMap.get(instance.getClusterName());if (clusterIPs == null) {clusterIPs = new LinkedList<>();ipMap.put(instance.getClusterName(), clusterIPs);}//clusterIPs.add(instance);} catch (Exception e) {Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);}}for (Map.Entry> entry : ipMap.entrySet()) {// make every ip mineList entryIPs = entry.getValue();// 更新实例列表。clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);}setLastModifiedMillis(System.currentTimeMillis());// 调用PushService,通知客户端服务发生改变getPushService().serviceChanged(this);ApplicationUtils.getBean(DoubleWriteEventListener.class).doubleWriteToV2(this, ephemeral);StringBuilder stringBuilder = new StringBuilder();for (Instance instance : allIPs()) {stringBuilder.append(instance.toIpAddr()).append('_').append(instance.isHealthy()).append(',');}Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),stringBuilder.toString());}

主要逻辑:

  • 如果实例所在集群名称为空,则设置集群名称为DEFAULT
  • 如果服务实例所在的集群实例clusterMap没有缓存,则新建设置默认配置并缓存到clusterMap
  • 获取集群下所有实例
  • 更新实例列表,这里还有一部分逻辑
  • 调用PushService,通知客户端服务发生改变

5、处理心跳请求

5.1、beat(HttpServletRequest request)逻辑

    @CanDistro@PutMapping("/beat")@Secured(action = ActionTypes.WRITE)public ObjectNode beat(HttpServletRequest request) throws Exception {// 主要获取处理心跳入参,并校验ObjectNode result = JacksonUtils.createEmptyJsonNode();result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);RsInfo clientBeat = null;if (StringUtils.isNotBlank(beat)) {clientBeat = JacksonUtils.toObj(beat, RsInfo.class);}String clusterName = WebUtils.optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME);String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));if (clientBeat != null) {if (StringUtils.isNotBlank(clientBeat.getCluster())) {clusterName = clientBeat.getCluster();} else {// fix #2533clientBeat.setCluster(clusterName);}ip = clientBeat.getIp();port = clientBeat.getPort();}String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);NamingUtils.checkServiceNameFormat(serviceName);Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}, namespaceId: {}", clientBeat,serviceName, namespaceId);BeatInfoInstanceBuilder builder = BeatInfoInstanceBuilder.newBuilder();builder.setRequest(request);// 处理心跳int resultCode = getInstanceOperator().handleBeat(namespaceId, serviceName, ip, port, clusterName, clientBeat, builder);// 返回处理结果result.put(CommonParams.CODE, resultCode);result.put(SwitchEntry.CLIENT_BEAT_INTERVAL,getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, ip, port, clusterName));result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());return result;}

主要获取处理心跳入参,并校验。由于是服务端,所以使用InstanceOperatorServiceImpl处理心跳逻辑。

5.2、InstanceOperatorServiceImpl的handleBeat()

    @Overridepublic int handleBeat(String namespaceId, String serviceName, String ip, int port, String cluster,RsInfo clientBeat, BeatInfoInstanceBuilder builder) throws NacosException {com.alibaba.nacos.naming.core.Instance instance = serviceManager.getInstance(namespaceId, serviceName, cluster, ip, port);// 如果服务实例还没有注册先注册if (instance == null) {if (clientBeat == null) {return NamingResponseCode.RESOURCE_NOT_FOUND;}Loggers.SRV_LOG.warn("[CLIENT-BEAT] The instance has been removed for health mechanism, "+ "perform data compensation operations, beat: {}, serviceName: {}", clientBeat, serviceName);instance = parseInstance(builder.setBeatInfo(clientBeat).setServiceName(serviceName).build());serviceManager.registerInstance(namespaceId, serviceName, instance);}// 从serviceMap获取服务Service service = serviceManager.getService(namespaceId, serviceName);// 校验服务不能为空,为空抛异常处理serviceManager.checkServiceIsNull(service, namespaceId, serviceName);if (clientBeat == null) {clientBeat = new RsInfo();clientBeat.setIp(ip);clientBeat.setPort(port);clientBeat.setCluster(cluster);}// 委托Service处理心跳service.processClientBeat(clientBeat);return NamingResponseCode.OK;}

主要逻辑:

  1. 如果服务实例还没有注册先注册
  2. 根据命名空间、服务名从本地缓存serviceMap获取服务
  3. 校验服务不能为空,为空抛异常处理
  4. 若clientBeat为空则新建封装,委托Service的processClientBeat(clientBeat)处理心跳

5.3、调用处理心跳任务

 

 ClientBeatProcessor也是Runnable的子类,构建ClientBeatProcessor后,底层还是委托GlobalExecutor的执行器封装中心提交执行任务的。

5.4、ClientBeatProcessor的run()逻辑

    @Overridepublic void run() {Service service = this.service;if (Loggers.EVT_LOG.isDebugEnabled()) {Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());}String ip = rsInfo.getIp();String clusterName = rsInfo.getCluster();int port = rsInfo.getPort();Cluster cluster = service.getClusterMap().get(clusterName);List instances = cluster.allIPs(true);for (Instance instance : instances) {if (instance.getIp().equals(ip) && instance.getPort() == port) {if (Loggers.EVT_LOG.isDebugEnabled()) {Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString());}// 设置当前心跳时间,用于服务健康检查比对是否大于设置阈值以决定服务是否剔除下线instance.setLastBeat(System.currentTimeMillis());// 既然接收到了客户端的心跳请求,证明服务可用了,这里做恢复服务健康状态并通知客户端// 服务可用了if (!instance.isMarked() && !instance.isHealthy()) {instance.setHealthy(true);Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",cluster.getService().getName(), ip, port, cluster.getName(),UtilsAndCommons.LOCALHOST_SITE);getPushService().serviceChanged(service);NotifyCenter.publishEvent(new NamingTraceEvent.HealthStateChangeTraceEvent(System.currentTimeMillis(),service.getNamespaceId(), service.getGroupName(), service.getName(), instance.getIp(),true, HealthStateChangeReason.HEARTBEAT_REFRESH));}}}}

设置当前心跳时间,用于服务健康检查比对是否大于设置阈值以决定服务是否剔除下线(步骤3.6)。既然接收到了客户端的心跳请求,证明服务可用了,这里做恢复服务健康状态并通知客户端服务可用了

相关内容

热门资讯

保存时出现了1个错误,导致这篇... 当保存文章时出现错误时,可以通过以下步骤解决问题:查看错误信息:查看错误提示信息可以帮助我们了解具体...
汇川伺服电机位置控制模式参数配... 1. 基本控制参数设置 1)设置位置控制模式   2)绝对值位置线性模...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
不一致的条件格式 要解决不一致的条件格式问题,可以按照以下步骤进行:确定条件格式的规则:首先,需要明确条件格式的规则是...
本地主机上的图像未显示 问题描述:在本地主机上显示图像时,图像未能正常显示。解决方法:以下是一些可能的解决方法,具体取决于问...
表格列调整大小出现问题 问题描述:表格列调整大小出现问题,无法正常调整列宽。解决方法:检查表格的布局方式是否正确。确保表格使...
表格中数据未显示 当表格中的数据未显示时,可能是由于以下几个原因导致的:HTML代码问题:检查表格的HTML代码是否正...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...