tidb节点重启后,服务无法重连

news2024/9/23 7:33:41

大家好,我是烤鸭:

    前几天遇到tidb节点重启后服务无法重连,确切地说是两个服务,一个可以正常重连,一个不行。

问题复现

由于线上执行慢SQL,导致TiDB 单个节点宕机重启。

其中A服务的3个节点和B服务的1个节点开始报错,重启后B服务恢复,A服务还在报错。

使用的组件版本:

A服务:tomcat-jdbc-9.0.71.jar
B服务:HikariCP-4.0.3.jar

数据库配置:

A服务:

spring.xxx-tidb-data-source.url=jdbc:mysql://xxx.xxx.xxx:6000/xxxA?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8

B服务:

spring.datasource.url=jdbc:mysql://xxx.xxx.xxx:6000/xxxB?useSSL=false&autoReconnect=true

可以看到两边的配置和连接池不同。

报错日志

无法重连服务的报错日志:

Caused by: com.mysql.cj.exceptions.ConnectionIsClosedException: No operations allowed after connection closed.
	at sun.reflect.GeneratedConstructorAccessor139.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
	at com.mysql.cj.NativeSession.checkClosed(NativeSession.java:762)
	at com.mysql.cj.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:569)
	at com.mysql.cj.jdbc.ConnectionImpl.prepareStatement$original$McwhO8C0(ConnectionImpl.java:1580)
	... 125 common frames omitted
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: The last packet successfully received from the server was 259,130,913 milliseconds ago. The last packet sent successfully to the server was 259,130,914 milliseconds ago.may or may not be greater than the server-side timeout (the driver was unable to determine the value of either the 'wait_timeout' or 'interactive_timeout' configuration values from the server.. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
	at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167)
	at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:520)
	at com.mysql.cj.protocol.a.NativeProtocol.checkErrorMessage(NativeProtocol.java:700)
	at com.mysql.cj.protocol.a.NativeProtocol.sendCommand(NativeProtocol.java:639)
	at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:987)
	at com.mysql.cj.NativeSession.execSQL(NativeSession.java:666)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:930)
	at com.mysql.cj.jdbc.ClientPreparedStatement.execute$original$mDKgOeFa(ClientPreparedStatement.java:371)
	at com.mysql.cj.jdbc.ClientPreparedStatement.execute$original$mDKgOeFa$accessor$UdYpLG4x(ClientPreparedStatement.java)
	at com.mysql.cj.jdbc.ClientPreparedStatement$auxiliary$BAL6Kq1Z.call(Unknown Source)
	at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
	at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java)
	at sun.reflect.GeneratedMethodAccessor261.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:118)

问题猜想

找到变量了,就顺着猜测查一下。

  • 数据库配置参数 autoReconnect=true
  • 连接池 tomcat-jdbc 和 HikariCP

报错日志都来源于 CJCommunicationsException 这个类,其实调用的是 ExceptionFactory.createLinkFailureMessageBasedOnHeuristics,报错日志来源于配置文件。

我保留了一部分一会可能用到的变量

LocalizedErrorMessages.properties

CommunicationsException.2=\ is longer than the server configured value of 
CommunicationsException.3=''wait_timeout''
CommunicationsException.4=''interactive_timeout''
CommunicationsException.5=may or may not be greater than the server-side timeout 
CommunicationsException.6=(the driver was unable to determine the value of either the 
CommunicationsException.7=''wait_timeout'' or ''interactive_timeout'' configuration values from 
CommunicationsException.8=the server.
CommunicationsException.11=. You should consider either expiring and/or testing connection validity 
CommunicationsException.12=before use in your application, increasing the server configured values for client timeouts, 
CommunicationsException.13=or using the Connector/J connection property ''autoReconnect=true'' to avoid this problem.
CommunicationsException.20=Communications link failure

...

Connection.0=Unable to connect to database.
Connection.1=Cannot connect to MySQL server on {0}:{1}.\n\nMake sure that there is a MySQL server running on the machine/port you are trying to connect to and that the machine this software is running on is able to connect to this host/port (i.e. not firewalled). Also make sure that the server has not been started with the --skip-networking flag.\n\n
Connection.2=No operations allowed after connection closed.
Connection.3=Can''t call commit when autocommit=true
Connection.4=Communications link failure during commit(). Transaction resolution unknown.
Connection.5=Unknown Java encoding for the character set with index ''{0}''. Use the ''customCharsetMapping'' property to force it.
Connection.6=Unknown character set index ''{0}'' received from server. The appropriate client character set can be forced via the ''characterEncoding'' property.
Connection.7=Can''t map {0} given for characterSetResults to a supported MySQL encoding.
Connection.8=Unable to use encoding: {0}

看看源码

从报错日志入手,根据链接状态和上次发送、接收时间判断,拼一些异常原因。

可以看出报错日志有几个组成部分:

1、上次发送packet的时间距离现在超过8小时,getLastPacketSentTime > 28800秒(8小时)

2、当前链接已关闭:No operations allowed after connection closed

ExceptionFactory

public static String createLinkFailureMessageBasedOnHeuristics(PropertySet propertySet, ServerSession serverSession,
            PacketSentTimeHolder packetSentTimeHolder, PacketReceivedTimeHolder packetReceivedTimeHolder, Throwable underlyingException) {
        long serverTimeoutSeconds = 0;
        boolean isInteractiveClient = false;

        long lastPacketReceivedTimeMs = packetReceivedTimeHolder == null ? 0L : packetReceivedTimeHolder.getLastPacketReceivedTime();
        long lastPacketSentTimeMs = packetSentTimeHolder.getLastPacketSentTime();
        if (lastPacketSentTimeMs > lastPacketReceivedTimeMs) {
            lastPacketSentTimeMs = packetSentTimeHolder.getPreviousPacketSentTime();
        }

        if (propertySet != null) {
            isInteractiveClient = propertySet.getBooleanProperty(PropertyKey.interactiveClient).getValue();

            String serverTimeoutSecondsStr = null;

            if (serverSession != null) {
                serverTimeoutSecondsStr = isInteractiveClient ? serverSession.getServerVariable("interactive_timeout")
                        : serverSession.getServerVariable("wait_timeout");
            }

            if (serverTimeoutSecondsStr != null) {
                try {
                    serverTimeoutSeconds = Long.parseLong(serverTimeoutSecondsStr);
                } catch (NumberFormatException nfe) {
                    serverTimeoutSeconds = 0;
                }
            }
        }

        StringBuilder exceptionMessageBuf = new StringBuilder();

        long nowMs = System.currentTimeMillis();

        if (lastPacketSentTimeMs == 0) {
            lastPacketSentTimeMs = nowMs;
        }

        long timeSinceLastPacketSentMs = (nowMs - lastPacketSentTimeMs);
        long timeSinceLastPacketSeconds = timeSinceLastPacketSentMs / 1000;

        long timeSinceLastPacketReceivedMs = (nowMs - lastPacketReceivedTimeMs);

        int dueToTimeout = DUE_TO_TIMEOUT_FALSE;

        StringBuilder timeoutMessageBuf = null;

        if (serverTimeoutSeconds != 0) {
            if (timeSinceLastPacketSeconds > serverTimeoutSeconds) {
                dueToTimeout = DUE_TO_TIMEOUT_TRUE;

                timeoutMessageBuf = new StringBuilder();
                timeoutMessageBuf.append(Messages.getString("CommunicationsException.2"));
                timeoutMessageBuf.append(Messages.getString(isInteractiveClient ? "CommunicationsException.4" : "CommunicationsException.3"));
            }
		  // 上次发送成功的时间距离现在超过8小时
        } else if (timeSinceLastPacketSeconds > DEFAULT_WAIT_TIMEOUT_SECONDS) {
            dueToTimeout = DUE_TO_TIMEOUT_MAYBE;
			
            timeoutMessageBuf = new StringBuilder();
            timeoutMessageBuf.append(Messages.getString("CommunicationsException.5"));
            timeoutMessageBuf.append(Messages.getString("CommunicationsException.6"));
            timeoutMessageBuf.append(Messages.getString("CommunicationsException.7"));
            timeoutMessageBuf.append(Messages.getString("CommunicationsException.8"));
        }

        if (dueToTimeout == DUE_TO_TIMEOUT_TRUE || dueToTimeout == DUE_TO_TIMEOUT_MAYBE) {

            exceptionMessageBuf.append(lastPacketReceivedTimeMs != 0
                    ? Messages.getString("CommunicationsException.ServerPacketTimingInfo",
                            new Object[] { Long.valueOf(timeSinceLastPacketReceivedMs), Long.valueOf(timeSinceLastPacketSentMs) })
                    : Messages.getString("CommunicationsException.ServerPacketTimingInfoNoRecv", new Object[] { Long.valueOf(timeSinceLastPacketSentMs) }));

            if (timeoutMessageBuf != null) {
                exceptionMessageBuf.append(timeoutMessageBuf);
            }

            exceptionMessageBuf.append(Messages.getString("CommunicationsException.11"));
            exceptionMessageBuf.append(Messages.getString("CommunicationsException.12"));
            exceptionMessageBuf.append(Messages.getString("CommunicationsException.13"));

        } else {
            //
            // Attempt to determine the reason for the underlying exception (we can only make a best-guess here)
            //
            if (underlyingException instanceof BindException) {
                String localSocketAddress = propertySet.getStringProperty(PropertyKey.localSocketAddress).getValue();
                boolean interfaceNotAvaliable;
                try {
                    interfaceNotAvaliable = localSocketAddress != null && NetworkInterface.getByName(localSocketAddress) == null;
                } catch (SocketException e1) {
                    interfaceNotAvaliable = false;
                }
                exceptionMessageBuf.append(interfaceNotAvaliable ? Messages.getString("CommunicationsException.LocalSocketAddressNotAvailable")
                        : Messages.getString("CommunicationsException.TooManyClientConnections"));
            }
        }

        if (exceptionMessageBuf.length() == 0) {
            // We haven't figured out a good reason, so copy it.
            exceptionMessageBuf.append(Messages.getString("CommunicationsException.20"));

            if (propertySet.getBooleanProperty(PropertyKey.maintainTimeStats).getValue() && !propertySet.getBooleanProperty(PropertyKey.paranoid).getValue()) {
                exceptionMessageBuf.append("\n\n");
                exceptionMessageBuf.append(lastPacketReceivedTimeMs != 0
                        ? Messages.getString("CommunicationsException.ServerPacketTimingInfo",
                                new Object[] { Long.valueOf(timeSinceLastPacketReceivedMs), Long.valueOf(timeSinceLastPacketSentMs) })
                        : Messages.getString("CommunicationsException.ServerPacketTimingInfoNoRecv", new Object[] { Long.valueOf(timeSinceLastPacketSentMs) }));
            }
        }

        return exceptionMessageBuf.toString();
    }

再根据上边的两个猜测,找一下对应的源码。

猜想1:autoReconnect 这个参数是有用的

网上很多说这个参数只针对mysql5以下,这个倒是没找到,官网关于这个参数的说明。
https://dev.mysql.com/doc/connectors/en/connector-j-connp-props-high-availability-and-clustering.html#cj-conn-prop_autoReconnect

如果链接断开的情况下,配置了这个参数可以发起重试。如果不配置的话,就会一直报错。

在这里插入图片描述

猜想2:HikariCP 会自动重连,tomcat-jdbc不会

可以看到 tomcat-jdbc 执行 invoke,getConnetion一气呵成,在默认配置下,也没有任何校验,如果因为第三方导致的链接断开,就麻烦了。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (compare(ISCLOSED_VAL,method)) {
            return Boolean.valueOf(isClosed());
        }
        if (compare(CLOSE_VAL,method)) {
            if (connection==null)
             {
              return null; //noop for already closed.
            }
            PooledConnection poolc = this.connection;
            this.connection = null;
            pool.returnConnection(poolc);
            return null;
        } else if (compare(TOSTRING_VAL,method)) {
            return this.toString();
        } else if (compare(GETCONNECTION_VAL,method) && connection!=null) {
            return connection.getConnection();
        } else if (method.getDeclaringClass().isAssignableFrom(XAConnection.class)) {
            try {
                return method.invoke(connection.getXAConnection(),args);
            }catch (Throwable t) {
                if (t instanceof InvocationTargetException) {
                    throw t.getCause() != null ? t.getCause() : t;
                } else {
                    throw t;
                }
            }
        }
        if (isClosed()) {
          throw new SQLException("Connection has already been closed.");
        }
        if (compare(UNWRAP_VAL,method)) {
            return unwrap((Class<?>)args[0]);
        } else if (compare(ISWRAPPERFOR_VAL,method)) {
            return Boolean.valueOf(this.isWrapperFor((Class<?>)args[0]));
        }
        try {
            PooledConnection poolc = connection;
            if (poolc!=null) {
                return method.invoke(poolc.getConnection(),args);
            } else {
                throw new SQLException("Connection has already been closed.");
            }
        }catch (Throwable t) {
            if (t instanceof InvocationTargetException) {
                throw t.getCause() != null ? t.getCause() : t;
            } else {
                throw t;
            }
        }
    }

在这里插入图片描述

HikariCP

HikarPool.getConnection 会校验当前是否存活,如果不存活,会关闭当前链接,并会重新创建连接。

在这里插入图片描述

问题解决

当天的解决方案比较简单,重启服务就行。不过如果想彻底解决这个问题的话。

  • 升级连接池,tomcat-jdbc 升级 druid 或者 HikariCP ,springboot 2.0以上默认是 HikariCP

  • 修改配置,如果还使用 tomcat-jdbc :

    增加以下配置:

    # 连接存活检查
    spring.datasource.tomcat.validation-query='SELECT 1'
    # 指定多少ms执行一次连接校验.	
    spring.datasource.tomcat.validation-interval=3000
    # 指定连接校验查询的超时时间
    spring.datasource.tomcat.validation-query-timeout=-1
    
    或者连接地址增加 autoReconnect=true
    

总结

有些问题不经常遇到,即便是预发1比1的环境可能也没办法避免。除了在事故处理的时候可以完善流程,就只能尽量保持架构统一,尤其是老项目。业务侧增加全方面中间件的监控,尤其是存活检测这种。坑是无穷无尽的,踩完了记得遇到相同的跳过去。

相关文章

https://zhuanlan.zhihu.com/p/361130333?utm_id=0

https://blog.csdn.net/zhangxin09/article/details/124901850

https://blog.csdn.net/u014398624/article/details/47746473

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Nicn的刷题日常之杨氏矩阵(三种方法求解,逐级递增详解,手把手教学,建议三连收藏)

目录 1.杨氏矩阵知识普及&#xff1a;什么是样式矩阵 2.题目描述 3.解题 3.1暴力求解&#xff0c;遍历法 3.2巧妙解题&#xff1a;对角元素法 3.3将巧解法封装为函数 4.结语 1.杨氏矩阵知识普及&#xff1a;什么是样式矩阵 杨氏矩阵&#xff0c;是对组合表示理论和…

计算机网络_1.6.3 计算机网络体系结构分层思想举例

1.6.3 计算机网络体系结构分层思想举例 1、实例引入&#xff08;用户在主机中使用浏览器访问web服务器&#xff09;2、从五层原理体系结构的角度研究该实例3、练习题 笔记来源&#xff1a; B站 《深入浅出计算机网络》课程 本节通过一个常见的网络应用实例&#xff0c;来介绍计…

“type-check“ exited with 1.

博主介绍&#xff1a;✌全网粉丝5W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

ProcessSlot构建流程分析

ProcessorSlot ProcessorSlot构建流程 // com.alibaba.csp.sentinel.CtSph#lookProcessChain private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)throws BlockException {// 省略创建 Context 的代码// 黑盒…

gorm day1

gorm day1 gorm简介gorm声明模型 代码样例基本来自官方文档 Gorm简介 什么是ORM&#xff1f; 对象关系映射(Objection Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库(如mysql数据库&#xff09;存在的互不匹配现象的计数。简单来说&#xff0c;ORM是通…

【annie/lux 快速下载哔哩哔哩视频】全网最简单,只需要5步!!!

1.首先 现在annie更名为lux 官网地址&#xff1a;https://github.com/iawia002/lux/releases 2.进入官网之后如图所示 3.下载lux软件 4.下载lux 这里需要说明一下 如果不下载这个的话也可以下载视频 但是视频和音频是分开的&#xff0c;你的视频没有声音 5.下载视频

04、全文检索 -- Solr -- 管理 Solr 的 core(使用命令和图形界面创建、删除 core,以及对core 目录下的各文件进行详细介绍)

目录 管理 Solr 的 core创建 Core方式1&#xff1a;solr 命令创建演示&#xff1a;使用 solr 命令创建 Core&#xff1a;演示&#xff1a;命令删除 Core&#xff08;彻底删除&#xff09; 方式2&#xff1a;图形界面创建Web控制台创建CoreWeb控制台删除 Core&#xff08;未彻底…

软件测试学习笔记-测试用例的编写

7中测试分类 按照阶段可划分单元测试、集成测试、系统测试、验收测试。代码可见度划分黑盒测试、灰盒测试、白盒测试 单元测试&#xff1a;针对源代码的测试 集成测试&#xff1a;针对接口进行测试 系统测试&#xff1a;针对功能和非功能的测试 验收测试&#xff1a;公测、内测…

js中执行上下文和执行栈是什么

文章目录 一、执行上下文二、生命周期创建阶段This Binding词法环境变量环境 执行阶段回收阶段 二、执行栈参考文献 一、执行上下文 简单的来说&#xff0c;执行上下文是一种对Javascript代码执行环境的抽象概念&#xff0c;也就是说只要有Javascript代码运行&#xff0c;那么…

关于Django部署

首先了解一下开发环境服务器跟生产环境服务器有何不同。 一、我们通过 python manage.py runserver 启动开发环境服务器&#xff0c;这条命令背后做了哪些事情&#xff1f; 1、首先加载Django项目的设置&#xff08;settings&#xff09; 2、检查数据库迁移&#xff0c;确保数…

这种学习单片机的顺序是否合理?

这种学习单片机的顺序是否合理&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01…

【Linux Day15 TCP网络通讯】

TCP网络通讯 TCP编程流程 接口介绍 socket()方法是用来创建一个套接字&#xff0c;有了套接字就可以通过网络进行数据的收发。创建套接字时要指定使用的服务类型&#xff0c;使用 TCP 协议选择流式服务&#xff08;SOCK_STREAM&#xff09;。 **bind()方法是用来指定套接字使…

绝地求生:海外博主呼吁PUBG2开发提上日程,PUBG2能否继往日荣光

海外PUBG博主WackyJacky101发推&#xff1a;PUBG 现在的平均玩家人数继续增加&#xff0c;但假期过后这里的势头似乎正在放缓&#xff01;现在是保持势头并宣布制作 PUBG 2 的最佳时机&#xff01; 大家好&#xff0c;我是闲游盒。PUBG作为最早的独立大逃杀游戏之一&#xff0c…

你今年过年回去吗?

#过年 我是一名21岁刚毕业的大学生&#xff0c;专业是软件技术&#xff0c;主修c#&#xff0c;之前在上海实习了一年&#xff0c;正式工作后来到了深圳&#xff0c;进入了一家电商公司实习。至于我为什么转行了&#xff0c;大家懂的都懂 现在是20240203晚上19.39&#xff0c;还…

算法设计与分析实验:回溯

目录 一、组合总和 1.1 具体思路 1.2 思路展示 1.3 代码实现 1.4 复杂度分析 1.5 运行结果 二、全排列 2.1 具体思路 2.2 思路展示 2.3 代码实现 2.4 复杂度分析 2.5 运行结果 三、N皇后问题 3.1 具体思路 3.2 思路展示 3.3 代码实现 3.4 复杂度分析 3.5 运行…

两个重要极限【高数笔记】

【第一个&#xff1a;lim &#xff08;sinx / x&#xff09; 1, x -- > 0】 1.本质&#xff1a; lim &#xff08;sin‘&#xff1f;’ / ‘&#xff1f;’&#xff09; 1, ‘&#xff1f;’ -- > 0&#xff1b;保证‘&#xff1f;’ -- > 0,与趋向无关 2.例题&#x…

Harbor介绍、整体架构和安装

1.Harbor介绍 Harbor 是由 VMware 开源的一款云原生制品仓库&#xff0c;Harbor 的核心功能是存储和管理 Artifact。Harbor 允许用户用命令行工具对容器镜像及其他 Artifact 进行推送和拉取&#xff0c;并提供了图形管理界面帮助用户查看和管理这些 Artifact。在 Harbor 2.0 版…

LangChain 81 LangGraph 从入门到精通三

LangChain系列文章 LangChain 60 深入理解LangChain 表达式语言23 multiple chains链透传参数 LangChain Expression Language (LCEL)LangChain 61 深入理解LangChain 表达式语言24 multiple chains链透传参数 LangChain Expression Language (LCEL)LangChain 62 深入理解Lang…

Logback学习

logback 1、logback介绍 Logback是由log4j创始人设计的另一个开源日志组件&#xff0c;性能比log4j要好。 lockback优点&#xff1a; 内核重写、测试充分、初始化内存加载更小&#xff0c;这一切让logback性能和log4j相比有诸多倍的提升。logback非常自然地直接实现了slf4j…

Open3d计算点云法向量,可视化(代码)

Open3d使用estimate_normals函数来计算法向量。其参数设置Open3d提供了3中参数搜索的方法&#xff08;所有计算的法向量模长为1&#xff09;&#xff1a; open3d.geometry.KDTreeSearchParamKNN(knn20) # 计算近邻的20个点 open3d.geometry.KDTreeSearc…