六、Nacos源码系列:Nacos健康检查

目录

一、简介

二、健康检查流程

2.1、健康检查

2.2、客户端释放连接事件

2.3、客户端断开连接事件

2.4、小结

2.5、总结图

三、服务剔除


一、简介

Nacos作为注册中心不止提供了服务注册和服务发现的功能,还提供了服务可用性检测的功能,在Nacos 1.x的版本中,临时实例走的是distro协议,客户端向注册中心发送心跳来维持自身的健康(healthy)状态,持久实例则走的是Raft协议存储。

主要有两种检测机制:

  • 1)、客户端主动上报机制:主动向Nacos服务端发送心跳,告诉Nacos服务端是否自己还活着。
  • 2)、服务器端主动下探机制:Nacos服务端主动向每个Nacos客户端发起探活,如果探活成功,说明客户端还活着,如果探活失败,则服务端将会剔除客户端。

对于Nacos健康检测机制,主要是有两种服务实例类型:

  • 临时实例:客户端主动上报机制
  • 持久实例:服务端主动下探机制

在1.x版本中,临时实例每隔5秒会主动上报自己的健康状态,发送心跳,如果发送心跳的间隔时间超过15秒,Nacos服务器端会将服务标记为亚健康状态,如果超过30S没有发送心跳,那么服务实例会被从服务列表中剔除。

在Nacos 2.x版本以后,持久实例不变,还是通过服务端主动下探机制,但是临时实例变成通过长连接来判断实例是否健康。

  1. 长连接: 一个连接上可以连续发送多数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包,在Nacos 2.x之后,使用Grpc协议代替了http协议,长连接会保持客户端和服务端发送的状态,在源码中ConnectionManager 管理所有客户端的长连接。ConnectionManager每3秒检测所有超过20S内没有发生过通讯的客户端,向客户端发起ClientDetectionRequest探测请求,如果客户端在指定时间内成功响应,则检测通过,否则执行unregister方法移除Connection。

如果客户端持续和服务端进行通讯,服务端是不需要主动下探的,只有当客户端没有一直和服务端通信的时候,服务端才会主动下探操作。

二、健康检查流程

2.1、健康检查

在Nacos2.0之后,使用Grpc协议代替了http协议,Grpc是一个长连接的,长连接会保持客户端和服务端发送的状态,在Nacos源码中ConnectionManager 管理所有客户端的长连接。

ConnectionManager每隔3秒检测所有超过20S内没有发生过通讯的客户端,向客户端发起ClientDetectionRequest探测请求,如果客户端在指定时间内成功响应,则检测通过,否则执行unregister方法移除Connection。

我们从ConnectionManager类的源码开始分析:

ConnectionManager内部有一个map用于存放当前所有客户端的长连接信息:

/**
 * 连接集合
 * key: ConnectionId
 * value: Connection
 */
Map<String, Connection> connections = new ConcurrentHashMap<>();

当我们启动一个nacos客户端的时候,就会往connections里面保存这个连接信息。

在ConnectionManager类内部,我们发现了存在一个使用@PostConstruct注解标识的方法,说明构造方法执行后就会触发执行start():

/**
 * 应用启动的时候执行,首次执行延迟1s,运行中周期为3秒执行一次
 * Start Task:Expel the connection which active Time expire.
 */
@PostConstruct
public void start() {
    // 初始化runtimeConnectionEjector为NacosRuntimeConnectionEjector
    initConnectionEjector();
    // 开始执行不健康连接的剔除任务
    RpcScheduledExecutor.COMMON_SERVER_EXECUTOR.scheduleWithFixedDelay(() -> {
        // 调用com.alibaba.nacos.core.remote.NacosRuntimeConnectionEjector.doEject
        runtimeConnectionEjector.doEject();
    }, 1000L, 3000L, TimeUnit.MILLISECONDS);
    
}

可以看到,start()方法创建了一个定时任务,首次执行延迟1s,后面每隔3s执行一次,实际上就是执行不健康连接的剔除任务。

我们查看runtimeConnectionEjector.doEject()方法:

public void doEject() {
    try {

        Loggers.CONNECTION.info("Connection check task start");

        Map<String, Connection> connections = connectionManager.connections;
        int totalCount = connections.size();
        MetricsMonitor.getLongConnectionMonitor().set(totalCount);
        int currentSdkClientCount = connectionManager.currentSdkClientCount();
        
        Loggers.CONNECTION.info("Long connection metrics detail ,Total count ={}, sdkCount={},clusterCount={}",
                totalCount, currentSdkClientCount, (totalCount - currentSdkClientCount));

        // 超时的连接集合
        Set<String> outDatedConnections = new HashSet<>();
        long now = System.currentTimeMillis();
        for (Map.Entry<String, Connection> entry : connections.entrySet()) {
            Connection client = entry.getValue();
            // client.getMetaInfo().getLastActiveTime(): 客户端最近一次活跃时间
            // 客户端最近一次活跃时间距离当前时间超过20s的客户端,服务端会发起请求探活,如果失败或者超过指定时间内未响应则剔除服务。
            if (now - client.getMetaInfo().getLastActiveTime() >= KEEP_ALIVE_TIME) {
                outDatedConnections.add(client.getMetaInfo().getConnectionId());
            }
        }
        
        // check out date connection
        Loggers.CONNECTION.info("Out dated connection ,size={}", outDatedConnections.size());
        if (CollectionUtils.isNotEmpty(outDatedConnections)) {
            // 记录成功探活的客户端连接的集合
            Set<String> successConnections = new HashSet<>();
            final CountDownLatch latch = new CountDownLatch(outDatedConnections.size());
            for (String outDateConnectionId : outDatedConnections) {
                try {
                    Connection connection = connectionManager.getConnection(outDateConnectionId);
                    if (connection != null) {
                        // 创建一个客户端检测请求
                        ClientDetectionRequest clientDetectionRequest = new ClientDetectionRequest();
                        connection.asyncRequest(clientDetectionRequest, new RequestCallBack() {
                            @Override
                            public Executor getExecutor() {
                                return null;
                            }
                            
                            @Override
                            public long getTimeout() {
                                return 5000L;
                            }
                            
                            @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.CONNECTION.info("[{}]send connection active request ", outDateConnectionId);
                    } else {
                        latch.countDown();
                    }
                    
                } catch (ConnectionAlreadyClosedException e) {
                    latch.countDown();
                } catch (Exception e) {
                    Loggers.CONNECTION.error("[{}]Error occurs when check client active detection ,error={}",
                            outDateConnectionId, e);
                    latch.countDown();
                }
            }
            
            latch.await(5000L, TimeUnit.MILLISECONDS);
            Loggers.CONNECTION.info("Out dated connection check successCount={}", successConnections.size());
            
            for (String outDateConnectionId : outDatedConnections) {
                // 不在探活成功的集合,说明探活失败,执行注销连接操作
                if (!successConnections.contains(outDateConnectionId)) {
                    Loggers.CONNECTION.info("[{}]Unregister Out dated connection....", outDateConnectionId);
                    // 注销过期连接
                    connectionManager.unregister(outDateConnectionId);
                }
            }
        }
        
        Loggers.CONNECTION.info("Connection check task end");
        
    } catch (Throwable e) {
        Loggers.CONNECTION.error("Error occurs during connection check... ", e);
    }
}

 如上代码,比较容易看懂,总体逻辑就是:

  • 1、拿到当前所有的连接;
  • 2、循环判断每个连接,判断下最近一次活跃时间距离当前时间,是不是超过20s,如果超过20s,将连接ID加入到一个过期连接集合中放着;
  • 3、循环过期连接集合中的每个连接,Nacos服务端主动发起一个探活,如果探活成功,将连接ID加入到探活成功的集合中;
  • 4、比较过期连接集合、探活成功集合,两者的差集,就是真正探活失败,需要剔除的那些连接,将会执行注销连接操作;

针对探活失败的那些连接,需要执行注销连接,具体代码如下:

// 注销过期连接
connectionManager.unregister(outDateConnectionId);

public synchronized void unregister(String connectionId) {
    // 根据connectionId从连接集合中移除这个连接
    // Map<String, Connection> connections = new ConcurrentHashMap<>();
    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();
        LOGGER.info("[{}]Connection unregistered successfully. ", connectionId);

        // 通知其它客户端,这个连接断开了
        clientConnectionEventListenerRegistry.notifyClientDisConnected(remove);
    }
}

unregister()方法首先根据connectionId从连接集合中移除这个连接,然后通知其它客户端,这个连接断开了。

继续跟踪clientConnectionEventListenerRegistry.notifyClientDisConnected(remove)的源码:

public void notifyClientDisConnected(final Connection connection) {
    
    for (ClientConnectionEventListener clientConnectionEventListener : clientConnectionEventListeners) {
        try {
            clientConnectionEventListener.clientDisConnected(connection);
        } catch (Throwable throwable) {
            Loggers.REMOTE.info("[NotifyClientDisConnected] failed for listener {}",
                    clientConnectionEventListener.getName(), throwable);
        }
    }
    
}

ClientConnectionEventListener其实就是客户端连接事件的一些监听器,看下其类图:

ClientConnectionEventListener主要有三个子类,这里我们关注ConnectionBasedClientManager。

我们查看ConnectionBasedClientManager#clientDisConnected()的源码:

public void clientDisConnected(Connection connect) {
    clientDisconnected(connect.getMetaInfo().getConnectionId());
}

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();
    boolean isResponsible = isResponsibleClient(client);
    // 发布客户端释放连接事件
    /**
     * 具体处理是在:{@link com.alibaba.nacos.naming.core.v2.index.ClientServiceIndexesManager.onEvent}
     * 主要做了下面几个事情:
     * 1、从订阅者列表中移除所有服务对这个客户端的引用
     * 2、从发布者列表中移除所有服务对这个客户端的引用
     */
    NotifyCenter.publishEvent(new ClientOperationEvent.ClientReleaseEvent(client, isResponsible));

    // 发布客户端断开连接事件
    /**
     * 具体处理是在:{@link com.alibaba.nacos.naming.core.v2.metadata.NamingMetadataManager.onEvent}
     * 主要做了下面几个事情:
     * 1、将服务实例元数据添加到过期集合中
     */
    NotifyCenter.publishEvent(new ClientEvent.ClientDisconnectEvent(client, isResponsible));
    return true;
}

可以看到,关键的逻辑就是发布了两个事件:客户端释放连接事件、客户端断开连接事件

2.2、客户端释放连接事件

具体处理是在com.alibaba.nacos.naming.core.v2.index.ClientServiceIndexesManager.onEvent:

public void onEvent(Event event) {
    if (event instanceof ClientOperationEvent.ClientReleaseEvent) {
        // 处理客户端释放连接事件
        handleClientDisconnect((ClientOperationEvent.ClientReleaseEvent) event);
    } else if (event instanceof ClientOperationEvent) {
        // 处理排除ClientReleaseEvent后的其它客户端操作事件
        handleClientOperation((ClientOperationEvent) event);
    }
}

private void handleClientDisconnect(ClientOperationEvent.ClientReleaseEvent event) {
    Client client = event.getClient();
    for (Service each : client.getAllSubscribeService()) {
        // 从订阅者列表中移除所有服务对这个客户端的引用
        // private final ConcurrentMap<Service, Set<String>> subscriberIndexes = new ConcurrentHashMap<>();
        // key: Service      value: 客户端ID集合
        removeSubscriberIndexes(each, client.getClientId());
    }
    DeregisterInstanceReason reason = event.isNative()
            ? DeregisterInstanceReason.NATIVE_DISCONNECTED : DeregisterInstanceReason.SYNCED_DISCONNECTED;
    long currentTimeMillis = System.currentTimeMillis();
    for (Service each : client.getAllPublishedService()) {
        // 从发布者列表中移除所有服务对这个客户端的引用
        removePublisherIndexes(each, client.getClientId());
        InstancePublishInfo instance = client.getInstancePublishInfo(each);
        NotifyCenter.publishEvent(new DeregisterInstanceTraceEvent(currentTimeMillis,
                "", false, reason, each.getNamespace(), each.getGroup(), each.getName(),
                instance.getIp(), instance.getPort()));
    }
}

主要做了两件事情:

  • 1、从订阅者列表中移除所有服务对这个客户端的引用;
  • 2、从发布者列表中移除所有服务对这个客户端的引用;

2.3、客户端断开连接事件

具体处理是在com.alibaba.nacos.naming.core.v2.metadata.NamingMetadataManager.onEvent:

public void onEvent(Event event) {
    if (event instanceof MetadataEvent.InstanceMetadataEvent) {
        // 处理实例元数据事件
        handleInstanceMetadataEvent((MetadataEvent.InstanceMetadataEvent) event);
    } else if (event instanceof MetadataEvent.ServiceMetadataEvent) {
        // 处理服务元数据事件
        handleServiceMetadataEvent((MetadataEvent.ServiceMetadataEvent) event);
    } else {
        // 处理客户端断开连接事件
        handleClientDisconnectEvent((ClientEvent.ClientDisconnectEvent) event);
    }
}

private void handleClientDisconnectEvent(ClientEvent.ClientDisconnectEvent event) {
    for (Service each : event.getClient().getAllPublishedService()) {
        String metadataId = event.getClient().getInstancePublishInfo(each).getMetadataId();
        if (containInstanceMetadata(each, metadataId)) {
            // 实例已过期,将实例元数据添加到过期集合中
            updateExpiredInfo(true, ExpiredMetadataInfo.newExpiredInstanceMetadata(each, metadataId));
        }
    }
}

主要做了一件事情:

  • 1、判断实例元数据是否存在,存在的话,将它标志已过期,添加到过期集合中;

2.4、小结

以上就是Nacos服务端健康检查的整体流程,总结一下:

  • 1、入口在ConnectionManager.start()方法,该方法有注解@PostConstruct;
  • 2、start()方法启动了一个定时任务,3s定时调度一次(每次结束后延迟3s);
  • 3、判断哪些客户端最近一次活跃时间已经超过20s,如果超过,判断为连接过期,并把过期的client存放到过期集合中;
  • 4、Nacos服务端会对过期的client进行一次探活操作,如果失败或者指定时间内还没有响应,直接剔除该客户端;
  • 5、剔除客户端的过程,发布了两个事件:客户端释放连接事件、客户端断开连接事件。拿到订阅者列表、发布者列表,移除掉所有服务对这个client的引用,保证服务不会引用到过期的client;

2.5、总结图

三、服务剔除

前面健康检查我们主要分析了ConnectionBasedClientManager这个类,细心的朋友可能会发现ConnectionBasedClientManager的构造方法其实启动了一个定时任务,如下所示:

public ConnectionBasedClientManager() {
    // 启动了一个定时任务,无延迟,每隔5s执行一次
    // 具体就是执行ExpiredClientCleaner.run()方法
    GlobalExecutor
            .scheduleExpiredClientCleaner(new ExpiredClientCleaner(this), 0, Constants.DEFAULT_HEART_BEAT_INTERVAL,
                    TimeUnit.MILLISECONDS);
}

 这个定时任务,每隔5s就会执行一次,具体就是执行ExpiredClientCleaner.run()方法:

private static class ExpiredClientCleaner implements Runnable {
    
    private final ConnectionBasedClientManager clientManager;
    
    public ExpiredClientCleaner(ConnectionBasedClientManager clientManager) {
        this.clientManager = clientManager;
    }
    
    @Override
    public void run() {
        long currentTime = System.currentTimeMillis();
        for (String each : clientManager.allClientId()) {
            // 判断客户端是否超时
            ConnectionBasedClient client = (ConnectionBasedClient) clientManager.getClient(each);
            if (null != client && client.isExpire(currentTime)) {
                // 超时连接处理
                clientManager.clientDisconnected(each);
            }
        }
    }
}

上面这个clientManager.clientDisconnected(each)超时连接处理,我们在前面已经分析过了,这里不再分析,关键的逻辑就是发布了两个事件:客户端释放连接事件、客户端断开连接事件。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/365019.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vue3+Vite】路由机制router 快速学习 第四期

文章目录 路由简介路由是什么路由的作用 一、路由入门案例1. 创建项目 导入路由依赖2. 准备页面和组件3. 准备路由配置4. main.js引入router配置 二、路由重定向三、编程式路由(useRouter)四、路由传参(useRoute)五、路由守卫总结 路由简介 路由是什么 路由就是根据不同的 URL…

第八篇:node模版引擎Handlebars及他的高级用法(动态参数)

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! ​ 目录 &#x1f4d8; 引言&#xff1a; &#x1f…

Armv8-M的TrustZone技术之在安全状态和非安全状态之间切换

Armv8-M安全扩展允许在安全和非安全软件之间直接调用。 Armv8-M处理器提供了几条指令来处理状态转换: 下图显示了安全状态转换。 如果入口点的第一条指令是SG且位于非安全可调用内存位置中,则允许从非安全到安全软件的直接API函数调用。 当非安全程序调用安全API时,API通过…

前妻(C#)-基础03-枚举-预处理指令

前妻C#-基础语法03 枚举关于控制台IO及注释C#预处理指令 枚举 枚举是用户定义的整数类型。在声明一个枚举时&#xff0c;要指定改枚举的实例可以包含的一组可接受的值。不仅如此&#xff0c;还可以给值指定易于记忆的名称&#xff0c;如果在代码的某个地方&#xff0c;要试图把…

Java学习day24:线程的同步和锁(例题+知识点详解)

声明&#xff1a;该专栏本人重新过一遍java知识点时候的笔记汇总&#xff0c;主要是每天的知识点题解&#xff0c;算是让自己巩固复习&#xff0c;也希望能给初学的朋友们一点帮助&#xff0c;大佬们不喜勿喷(抱拳了老铁&#xff01;) 往期回顾 Java学习day23&#xff1a;线程构…

ElasticSearch 8.x 使用 snapshot(快照)进行数据迁移

ElasticSearch 1、ElasticSearch学习随笔之基础介绍 2、ElasticSearch学习随笔之简单操作 3、ElasticSearch学习随笔之java api 操作 4、ElasticSearch学习随笔之SpringBoot Starter 操作 5、ElasticSearch学习随笔之嵌套操作 6、ElasticSearch学习随笔之分词算法 7、ElasticS…

儿童护眼台灯怎么选择?一文教你如何选择儿童护眼台灯

护眼台灯是家长最常为孩子购买的用品之一&#xff0c;但是大部分人对它的了解并不多&#xff0c;很多人购买之后反而会觉得眼睛更容易疲劳&#xff0c;有不适的情况&#xff01;最主要的原因是因为挑选的台灯不够专业&#xff0c;次要原因则是使用方法不正确。所以今天跟大家讲…

BI是什么?

企业SaaS服务2B或者其他2C&#xff0c;数据都会越集越多&#xff0c;丰富的数据可视化展示成为销售及老板们的需求&#xff1b;当然通过如下BI了解 你需要思考是不是只是能做、想做、值得做一个&#xff0c;如果写简单的数据统计&#xff0c;至于用不用的上BI都另说&#xff0…

本地部署GeoServe服务并结合内网穿透实现任意浏览器远程访问

文章目录 前言1.安装GeoServer2. windows 安装 cpolar3. 创建公网访问地址4. 公网访问Geo Servcer服务5. 固定公网HTTP地址 前言 GeoServer是OGC Web服务器规范的J2EE实现&#xff0c;利用GeoServer可以方便地发布地图数据&#xff0c;允许用户对要素数据进行更新、删除、插入…

C#学习笔记_类(Class)

类的定义 类的定义是以关键字 class 开始&#xff0c;后跟类的名称。类的主体&#xff0c;包含在一对花括号内。 语法格式如下&#xff1a; 访问标识符 class 类名 {//变量定义访问标识符 数据类型 变量名;访问标识符 数据类型 变量名;访问标识符 数据类型 变量名;......//方…

【Linux】-多线程的知识都收尾(线程池,封装的线程,单例模式,自旋锁)

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

深度剖析Sentinel热点规则

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 深度剖析Sentinel热点规则 前言核心概念解析&#xff1a;数字守护者的起源核心概念解析&#xff1a;简单示例演示&#xff1a; 参数索引&#xff1a;规则的基石参数索引的作用&#xff1a;不同场景下选…

2024美国大学生数学建模美赛选题建议+初步分析

总的来说&#xff0c;去年算是美赛环境题元年&#xff0c;去年的开放度是较高的&#xff0c;今年每种赛题类型相对而言平均了起来 提示&#xff1a;DS C君认为的难度&#xff1a;E<BCF<AD&#xff0c;开放度&#xff1a;DBCE<A<F。 以下为A-F题选题建议及初步分析…

小型内衣裤洗衣机哪个牌子好?家用小型洗衣机推荐

相信对于很多用户而言&#xff0c;宁愿强撑着疲惫的身子手洗内衣裤&#xff0c;也不愿把内衣裤与外穿衣物一起放进洗衣机洗。内衣裤与外穿衣物的脏污情况不同&#xff0c;内衣裤是贴身衣物&#xff0c;上面留有人体的汗液和分泌物&#xff0c;有可能带有大量真菌。而外衣上则是…

动环系统断电告警的防误报

机房一般接入的市电为三相380伏特&#xff0c;也有用单向220伏特的。UPS本身提供断电告警的功能&#xff0c;这个告警在各种种类的UPS中都是提供的&#xff0c;不同电压的市电输入都支持&#xff1b;三相电另外有缺相告警事件。但这些告警事件存在抖动或者误判。 瞬间的低压或…

LiveGBS流媒体平台GB/T28181功能-支持配置开启 HTTPS 服务什么时候需要开启HTTPS服务

LiveGBS功能支持配置开启 HTTPS 服务什么时候需要开启HTTPS服务 1、配置开启HTTPS1.1、准备https证书1.1.1、选择Nginx类型证书下载 1.2、配置 LiveCMS 开启 HTTPS1.2.1 web页面配置1.2.2 配置文件配置 2、验证HTTPS服务3、为什么要开启HTTPS3.1、安全性要求3.2、功能需求 4、搭…

Linux基础知识合集

整理了一下学习的一些关于Linux的一些基础知识&#xff0c;同学们也可以通过公众号菜单栏查看&#xff01; 一、基础知识 Linux基础知识 Linux命令行基础学习 Linux用户与组概念初识 Linux文件与目录权限基础 Linux中文件内容的查看 Linux系统之计划任务管理 二、服务器管理 Vm…

Vue中使用 Element-ui form和 el-dialog 进行自定义表单校验清除表单状态

文章目录 问题分析 问题 在使用 Element-ui el-form 和 el-dialog 进行自定义表单校验时&#xff0c;出现点击编辑按钮之后再带年纪新增按钮&#xff0c;出现如下情况&#xff0c;新增弹出表单进行了一次表单验证&#xff0c;而这时不应该要表单验证的 分析 在寻找多种解决…

深信服技术认证“SCCA-C”划重点:深信服云计算关键技术

为帮助大家更加系统化地学习云计算知识&#xff0c;高效通过云计算工程师认证&#xff0c;深信服特推出“SCCA-C认证备考秘笈”&#xff0c;共十期内容。“考试重点”内容框架&#xff0c;帮助大家快速get重点知识。 划重点来啦 *点击图片放大展示 深信服云计算认证&#xff08…

【无刷电机学习】电流采样电路硬件方案

【仅作自学记录&#xff0c;不出于任何商业目的】 目录 AD8210 INA282 INA240 INA199 AD8210 【AD8210数据手册】 在典型应用中&#xff0c;AD8210放大由负载电流通过分流电阻产生的小差分输入电压。AD8210抑制高共模电压(高达65V)&#xff0c;并提供接地参考缓冲输出&…