深夜两点半,我盯着屏幕上那行红色的报错,一边翻着搜索引擎里零零散散的解决方案,一边在笔记里写下第37条报错备忘。这个场景应该很多同行都不陌生。做开发这十年,我越来越觉得"报错"这个东西,本质上不是程序在为难你,而是程序在用自己的方式告诉你:我哪里不对劲了,你顺着线索来找。真正折磨人的不是那行红色的英文,而是你两眼一抹黑、完全没有排查思路的时刻。
这篇"一些报错记录"不是什么高深的技术教程,就是把我在实际项目里收集到的、有代表性的报错案例拆开揉碎,从数据库到构建打包、从运行时报错到那些冷门到搜不到答案的犄角旮旯,讲清楚每一类报错背后的逻辑链路。不管你是刚入行的新手,还是已经写了好几年代码的老兵,我都建议养成记录报错的习惯——你今天踩的坑,百分之八十会在三个月后的另一个项目里换个马甲重新出现。
1. 数据库报错的两种典型死法:SQL语法与启动时序
数据库相关的报错在开发里出现频率极高,但大部分时候翻来覆去就是两类问题:一类是SQL本身写得有问题,另一类是数据库服务的启动顺序或残留状态出了问题。这两类报错的表现形式完全不同,排查思路也是两套打法。
1.1 MySQL 1064:先把报错里的"near"位置读清楚
MySQL的1064语法错误,可能是所有数据库报错里最常被搜索的一种。就拿"mysql1064报错怎么解决"这个热搜来说,几乎每天都有新人遇到。这类报错的提示信息长这样:
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'desc limit 10' at line 1很多新手看到"check the manual"就直接懵了,心想我总不能去翻几百页的官方文档吧。其实这个报错最有价值的信息就是near后面跟着的那一小段内容。它告诉你MySQL解析器是在哪个位置开始读不懂的。拿上面的例子来说,问题就出在desc上。DESC是MySQL的保留关键字,用来做降序排序或者查看表结构,如果你拿它当字段名,不加上反引号,1064就是家常便饭。
我自己的排查套路是三步走:
- 把报错里
near后面的内容连同一整条SQL一起复制出来,先做"格式化"。很多时候你写的一长串SQL在编辑器和命令行里看着没问题,一旦粘到客户端工具里换行、缩进全乱了,你根本看不清各个子句的边界在哪。格式化之后,SELECT、FROM、WHERE、GROUP BY这些关键子句一目了然,语法错误多半就暴露了。 - 二分法删减SQL。如果你格式化之后还是看不出来,就从中间开始删掉一半子句,保留完整结构继续执行,逐步缩小范围。我见过很多次情况是,问题不在报错语句本身,而是子查询里少写了一个别名或者一个逗号。
- 检查保留字和引号。字段名跟关键字重名是最容易踩的坑,另一个坑是字符串值用了双引号而MySQL的
sql_mode又没开ANSI_QUOTES,结果字符串被当成了标识符。
这里多说一句:1064报错还有个容易忽略的变体,就是字符集导致的"看不见"的字符。比如你在Word里编辑SQL,复制进来的时候带了弯引号或者不间断空格,肉眼完全看不出来,但MySQL就是报语法错误。遇到这种情况,把SQL粘贴到能显示空白字符的编辑器里查一遍。
1.2 ClickHouse 重启报错:failed to flush system log already exists
比起MySQL的语法错误,ClickHouse重启时报的failed to flush system log already exists这种错误明显更让人头大。语法错误至少报错位置明确,这种服务启动失败的错误,往往意味着你连数据库都进不去,排查手段极其有限。
这个报错出现的典型场景是这样的:服务器异常断电,或者ClickHouse进程被kill -9强杀,然后你重新启动服务,发现起不来了,日志里反复出现类似Failed to flush system log table ... already exists这样的信息。
这背后是什么原因呢?ClickHouse内部有一套系统日志表,比如system.query_log、system.query_thread_log、system.trace_log这些,它们记录了查询历史、线程信息和追踪数据。正常运行的时候,ClickHouse会定期把内存里的日志数据异步刷新(flush)到磁盘上的系统表里。如果进程上次是异常终止的,刷新操作只做了一半——元数据已经写进去了,但数据还没来得及完整落盘。下次重启的时候,系统尝试再次执行刷新,发现那个对象已经存在,于是直接抛错,服务启动流程中断。
这个问题的麻烦之处在于,你不能像修业务数据那样直接跑一条SQL去改,因为服务压根没起来,没有查询接口可以用。我的处理步骤是这样的:
- 第一步,找一台新机器或者先手动停下服务,把整个数据目录做完整备份。这个操作虽然慢,但必须有,操作元数据目录的容错率很低,一个
rm下去可能整个库都废了。 - 第二步,进到ClickHouse的数据目录下,找到
metadata相关的目录,定位到系统日志表的目录结构。这里需要特别留意带"log"字样的子目录,往往就是上次flush残留的元数据。 - 第三步,在确认备份没问题的情况下,把这些残留的system log相关目录改名,而不是直接删除。改名的好处是,如果你判断失误,还能改回来;而且ClickHouse重启检测不到那个已存在的对象,就会自动重新初始化系统表。
- 最后重新启动ClickHouse服务,观察日志确认系统表重建成功。
这类报错给到我的教训是:数据库服务的优雅启停不是可有可无的仪式。kill -9解决不了一个卡死的进程时确实省事,但代价是一个潜在的启动炸弹。能用clickhouse stop或systemctl stop让服务自己处理完待刷新的数据,就别图快。
2. 构建与打包的报错,最容易被环境细节坑到
如果你说数据库报错还能靠SQL功底解决,那构建打包阶段的报错就完全是另外一回事了。这类报错最坑的地方在于:代码在别人机器上能编译,偏偏到你机器上就挂;或者IDE里运行得好好的,一换成命令行打包就失败。你很难说是代码的问题,越来越怀疑是自己的开发环境有什么不干净的东西。
2.1 IntelliJ Maven 打包报错:从堆栈尾部倒着找原因
"Intellij+maven项目打包报错"这个热搜词我太熟了,几乎每个用Java做后端开发的人都遇到过。Maven打包报错的形态五花八门,但有一个通用排查原则可以先记住:永远从堆栈最底部开始看,而不是最上面。
很多新手看报错习惯从第一行看起,对着最上面的[ERROR]一行研究半天。但实际上Maven的报错信息是层层包裹的,最顶部往往是"Maven execution failed"这样的笼统描述,真正有用的Caused by藏在最下面。举个例子,报错信息长这样:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project demo: Compilation failure [ERROR] /path/to/xxx.java:[18,43] 程序包com.alibaba.fastjson不存在别看上面那行那么长,真正告诉你答案的就是程序包com.alibaba.fastjson不存在。这说明编译ClassPath里压根没有fastjson这个依赖。这个时候去检查pom.xml,多半能发现以下四种情况之一:
- 依赖坐标写错了,比如groupId写成了另一个公司的。
- 依赖声明了
<scope>provided</scope>或<scope>test</scope>,结果你在主代码里引用了。 - 父级pom里的
<dependencyManagement>锁了版本,导致依赖没有真正传递下来。 - 本地仓库里的包损坏了,Maven拉取到一半中断,剩一个残缺的jar文件。
针对最后一种情况还有个实用技巧:如果你怀疑本地仓库有半截文件,直接进~/.m2/repository找到对应路径删掉当前目录,然后重新执行mvn clean package让Maven重新下载。别手动去改jar包,那是越弄越乱。
我还遇到过一种编码相关的Maven打包报错,特别隐蔽:代码里有中文注释,本地IDE把文件按GBK保存,Linux服务器上构建时默认读UTF-8,结果中文注释变成了乱码,导致编译失败。排查方式是在pom.xml里显式声明<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>,同时把编辑器里文件的编码统一切换到UTF-8,一劳永逸。
2.2 Tauri Windows:link.exe not found 的修复链路
这两年桌面应用开发里Tauri很火,但Tauri在Windows上的入门报错也格外劝退新人。"tauri windows报错link.exe not found"这个搜索词,基本上每个Tauri新手都会搜一遍。我第一次遇到这个报错的时候也很困惑:我电脑上明明装了Visual Studio Code,能写Rust代码,怎么cargo tauri dev跑起来就找不到link.exe呢?
这里要先把一个概念说清楚:Tauri的构建链在Windows上依赖的是MSVC工具链,也就是微软的C++编译器和链接器,而不是你写Rust代码时用的编辑器或编译器前端。link.exe是MSVC的链接器,负责把编译出来的目标文件链接成最终的可执行文件。你光装了Rust的MSVC工具链(也就是rustup默认安装的stable-x86_64-pc-windows-msvc),但没有装Visual Studio Build Tools,那么link.exe就根本不存在,cargo编译到链接那一步就直接罢工了。
解决链路其实很清晰:
- 打开Visual Studio Installer,找到"使用C++的桌面开发"这个工作负载,勾选安装。这里面要重点确认包含MSVC v143(或对应版本)的C++编译工具和Windows 10/11 SDK。只装这两个组件,别贪多。
- 安装完成后,
link.exe通常会被放在类似C:\Program Files\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.38.33130\bin\Hostx64\x64这样的路径下。 - 重点来了:装完Build Tools之后,要关掉当前所有的终端窗口再重新打开。因为环境变量是在安装过程中注入的,已打开的终端窗口不会自动刷新环境变量,你不重启终端,照样找不到link.exe。这个细节我当初折腾了一下午才反应过来。
还有一个容易踩的坑是:系统里同时装了MinGW或Git自带的Unix工具链,PATH环境变量里它们的路径排在MSVC前面。有些工具在查找link.exe的时候会优先命中Git带的那个链接器,结果因为版本不兼容又报新错。解决办法是调整PATH顺序,或者干脆在Tauri项目用的终端里先确认where link.exe的输出到底指向哪里。
顺便说一句,遇到"link.exe not found"的时候,不要一上来就重装Rust。网络上很多答案会误导你去改Rust的工具链,但实际上你的Rust环境大概率是好的,问题只出在缺了微软的C++工具集。
3. 运行时报错,不是代码错了,是环境、权限和编码在捣乱
编译通过了、打包成功了的项目,不代表就万事大吉了。运行阶段冒出来的报错往往最让人抓狂,因为代码看起来哪哪都正常,但程序就是跑不起来。这类报错我总结下来,大部分根子不在逻辑层面,而在环境、权限和编码这三个"隐形因素"上。
3.1 MediaRecorder.start() 的 start failed 排查
安卓开发里用MediaRecorder录制视频,几乎每个人都会遇到这个报错:java.lang.RuntimeException: start failed.。关键这个报错异常信息极度吝啬,就一句话,不告诉你具体是哪一步配置出了问题。我调试这个问题时,把Android官方文档反复翻了好几遍,最后梳理出了一套排查顺序。
先说一下MediaRecorder的使用流程,很多人前脚刚new完对象,后脚就直接调start(),然后抱怨怎么失败了。MediaRecorder是一个状态机,你必须按固定顺序完成配置,它才愿意开始工作:
- 依次调用
setAudioSource和setVideoSource指定音视频源。 - 调用
setOutputFormat设置输出封装格式,比如MPEG_4。 - 调用
setOutputFile指定输出文件路径。这一步特别容易翻车,你给的文件路径父目录如果不存在,start阶段就是会失败——系统不会自动帮你创建目录。 - 设置音视频编码器和参数,
setAudioEncoder、setVideoEncoder,缺一个都会在start阶段报错。 - 调
prepare(),如果prepare抛异常,说明前面的配置还有问题。 - 最后再调
start()。
如果你全部配置顺序都正确了,仍然报start failed,那就需要检查运行环境了。常见的有这么几类:
- 权限问题:Android 6.0以上需要运行时动态申请
RECORD_AUDIO和CAMERA权限,你只在Manifest里声明了但没在代码里请求,start照样失败。 - 文件写入问题:输出路径在外部存储时,需要考虑
WRITE_EXTERNAL_STORAGE权限;在应用私有目录时,目录要保证存在且可写。 - 摄像头被占用:如果你在同一个Activity里先打开了Camera预览,又创建了一个新的MediaRecorder去录制,而没有
release()掉Camera,MediaRecorder无法获得摄像头资源,也会直接抛start failed。
我的经验是:遇到这类运行时报错,先别怀疑代码逻辑,按"编码器配置 > 文件目录 > 权限 > 摄像头占用"的顺序逐个排查,通常五分钟内能找到答案。千万不要在start前后加一堆try-catch只打印日志,而不去确认上述前置条件。
3.2 VSCode运行Java乱码:一半是编码,一半是终端
VSCode里运行Java程序,控制台输出中文全是乱码,这个问题搜"VSCode运行java报错乱码"能找到一大批同病相怜的人。乱码的本质就四个字:字符集不对。Java源码文件在VSCode里默认按UTF-8保存,但Windows系统控制台(cmd或者Windows Terminal的某些配置)默认用的是GBK编码,两边对不上,中文就成了一堆看到就头疼的符号。
这里面有几个层面要分别处理:
- 源码编译层面:如果你的Java文件里有中文,运行
javac编译时如果不指定编码,javac会用平台默认编码(Windows上是GBK)去读取UTF-8的源文件,这时候中文注释就会变成乱码,严重的话直接编译报错。解决办法是编译命令加上-encoding UTF-8,或者在Maven的pom.xml里把project.build.sourceEncoding设置为UTF-8。 - 运行输出层面:VSCode的终端输出中文乱码,通常是因为Java进程输出的字节流是UTF-8,而终端按GBK解码显示。需要在VSCode的
settings.json里做两件事:一个是把terminal.integrated.profiles.windows配置的默认终端Shell设置成支持UTF-8,另一个是在"java.debug.settings"相关的配置里把控制台输出编码统一处理。 - 一个快速自检技巧:在Java代码里打印
System.getProperty("file.encoding"),看看运行环境拿到的默认编码是什么。如果显示的是GBK,而你的源码是UTF-8,那乱码就是板上钉钉的事。
这里我强烈建议大家统一一个原则:源文件、编译参数、运行终端三者的编码必须一致。我自己在Windows上做Java开发,一律把工作区编码设为UTF-8,编译参数强制指定UTF-8,终端通过VSCode设置改成UTF-8,三管齐下之后乱码再也没出现过。
3.3 一个常见的JavaScript运行时错误:undefined is not a function
"javascript运行时报错"这个热搜词覆盖的范围太广了,市面上九成的JavaScript运行时错误都能让人当场血压升高。我挑一个最典型的来讲:TypeError: xxx is not a function。这类报错的信息直译就是"你调用的这个东西不是函数",程序员的第一反应通常是:不可能啊,我明明定义过这个函数。
我排查过很多次这种问题,发现它出现的原因不外乎三类:
- 变量覆盖:你在某个作用域里用
var声明了一个与外部函数同名的变量,赋值成了数字或字符串,导致后续调用时拿到的不是原来的函数。 - 异步时序错位:你从接口异步获取一个对象,这个对象里有个方法,但在接口响应回来之前你就调用了这个方法。此时
xxx还是undefined,你强行调用它,自然就报undefined is not a function。 - 导入导出不匹配:ESModule里你
import { getData }导入,但实际上导出的是export default { getData },拿到的就是一个对象,不是函数。这类错误在TypeScript里还能靠类型检查拦住,在纯JavaScript里就只能靠运行时报错来发现。
排查建议就一条:在报错那一行前面加console.log打印出这个变量的类型。typeof输出清清楚楚,如果你看到undefined,那就是异步还没回来;如果是number,那就是变量被覆盖。定位到根因再动手修,别看着报错就瞎猜。
4. 冷门但值得记一笔的报错
有些报错一年见不了几次,但一旦遇到,搜索引擎上的有效答案寥寥无几,那种"全世界只剩我一个人"的孤独感,写过代码的人都懂。这里我挑几个近期收集到的小众报错,不一定全面,但每一条都是我实际折腾出结果来的。
4.1 NVIDIA 屏蔽ECC:当GPU计算卡在硬件层面"报警"
"NVIDIA 屏蔽ecc报错"这个热词一看就是玩深度学习训练的人搜的。我自己在服务器上跑模型训练时也遇到过:nvidia-smi里显示了ECC相关的错误信息,服务器本身的日志也弹了硬件报错提示。
先普及一个概念:ECC是显存错误检查和纠正机制,它能在数据读取时检测到单比特错误并自动纠正,对长时间跑大规模计算的场景非常重要。但它也不是没有代价——开启ECC会占用一部分显存带宽和容量,性能有少许损耗。所以有些做深度学习训练的同学为了压榨出那一点性能,会选择屏蔽ECC,也就是关闭这个纠错功能。
如果真的决定要关,命令很直接,用管理员权限执行:
nvidia-smi -e 0这个命令的作用是禁用当前GPU的ECC支持。执行之后nvidia-smi再查看,ECC相关的状态就会变成Disabled。想恢复的话,把0改成1再执行一次,就是重新启用。
但如果你问我的个人看法:我要泼一盆冷水。ECC报错的本质是硬件已经开始出现异常了,它是在提醒你显存可能有坏块或正在劣化。屏蔽ECC只是让系统不再报告这个异常,相当于把仪表盘的警报灯线剪了,问题本身还在那。如果是测试机自己折腾着玩,那关了就关了;如果是生产环境或者正在跑正式训练任务,正确的做法是记录GPU的序列号和报错时间,走设备售后检测流程,而不是急着关闭这个功能。我见过有人关了ECC之后继续训练,结果模型训练到一半损失值突然发散,最后排查发现就是显存数据被静默写错了,这类问题造成的浪费比那一点性能损耗大得多。
4.2 gloo报错:分布式通信库在"找队友"时翻车
gloo是Facebook开源的分布式通信库,很多AI框架在分布式训练时拿它来做多机多卡之间的数据交换。"gloo报错应该如何改"搜索量虽然不高,但提问的人基本都是卡在分布式训练任务起不来。
gloo最常见的报错有两大类。第一类是关于网络接口的,典型提示是找不到可用的网卡或选择了错误的接口。很多服务器有多个网卡,管理口、业务口、高速互联口混在一起,gloo不知道选哪个的时候就会报错。这类问题的解决思路是显式指定网卡:通过环境变量GLOO_SOCKET_IFNAME强制它使用你想要的那块网卡,比如:
export GLOO_SOCKET_IFNAME=eth0第二类是节点间通信超时或握手失败。这类问题的排查思路要顺着链路一层一层看:先确认节点间的端口能通(可以用nc -zv挨个测试常见端口),再检查防火墙规则有没有放行gloo用的通信端口,最后确认共享内存目录(/dev/shm)有足够的空间。遇到过容器环境里/dev/shm默认只有64MB,跑着跑着gloo直接报错的案例——把共享内存调大到几个GB就解决了。
4.3 报错信息本身可能是线索:一个CTF查询系统的"怪报错"
最后聊一个比较有意思的案例:一个CTF比赛里的"小查询系统",功能非常简单,输入ID就能查询到对应信息。但选手们测试时发现,有些输入会让系统返回一个"奇怪的报错",这个报错信息里不仅带出了部分SQL查询语句,还泄露了服务器上文件路径的细节。
这个场景放到安全圈很好理解:报错信息是信息泄露的经典入口。很多开发者为了方便排查,会把数据库操作的详细错误直接原样抛给前端用户。用户输入的内容如果被直接拼接进了SQL语句,一旦输入了特殊字符,SQL语法就被破坏,数据库的报错信息里通常会包含查询语句片段。这些片段对攻击者来说就是深入分析系统结构的线索。CTF题目会故意保留这种报错来作为解题突破口,而真实业务系统里这就属于严重的安全隐患了。
这个案例给到开发侧的启示其实很简单:生产环境的报错信息必须做脱敏和收敛。前端用户能看到的,永远应该是"系统繁忙,请稍后再试"这类无害提示,而完整的堆栈信息应该写入服务端日志,并配合日志采集系统做监控告警,而不是直接吐给浏览器页面。判断一个系统的成熟度,有时候就看你把报错信息藏得多好。
做技术这些年,我一直保持着记报错笔记的习惯。每解决一个问题,就顺手把报错原文、根因分析和解决命令写进一个专门的文档里,按关键词打标签,方便日后搜索。刚开始觉得这事情很花时间,写多了才发现这是效率回报最高的一项投资——很多时候你以为又在面对一个新技术难题,翻出笔记一看,三年前就解决过一模一样的坑。如果你也是一线开发者,真心建议从今天开始给报错建档。它不会让你少遇到问题,但会让你在遇到问题时,知道该去哪里找答案。