caveman 这个词,翻成中文就是“穴居人”。要是放在几年前,你说一个人的代码是“caveman 写的”,那绝对是骂人——潜台词是这代码原始、粗糙、没有进化。但这两年风向有点变了,我身边越来越多开发者开始主动把项目命名为 caveman,甚至把自己的工作方式也往“穴居人”方向调整。最开始我以为是玩梗,直到自己也尝试了一把,才意识到这个词背后藏着一种相当清醒的技术态度:在工具链越来越臃肿的时代,主动选择简单的工具、简单的方法、简单的架构,反而能活得很好。这篇文章我想聊聊我理解的“caveman”式技术思路,以及我是怎么把它落进真实项目里的。
1. 当“穴居人”不再是贬义词:技术圈里的返璞归真
1.1 极繁主义时代的自然反弹
先描述一个很多人都有过的场景:新项目初始化,为了写一个返回 “hello world” 的接口,你得先装 Node 或 Python 运行环境,然后初始化包管理器,拉一堆依赖,配一下构建工具,再写几层目录结构。等真正开始写业务逻辑,半小时已经过去了。如果中途某个依赖版本和系统环境冲突,那时间还得翻倍。
这就是最近几年的技术常态。工具链越造越复杂,框架一个套一个,一个简单需求背后要背负几十层抽象。不是说这些工具不好,而是它们被用在了太多根本不需要它们的地方。很多小型项目、个人项目、内部工具,明明一把凿子就能解决的事情,硬生生上了全套工业流水线。
于是出现了一股反潮流:一部分人开始做减法。他们不追新框架,不搞微服务拆分,不碰容器编排,能用一个 shell 脚本解决的问题绝不写 Java 服务。“caveman”这个词就是在这种氛围下被重新捡起来的——它代表的不是退化,而是主动选择的简化。
1.2 “caveman”在技术语境里的三个常见含义
我观察下来,“caveman”在开发者圈子里主要有三层含义:
第一是caveman debugging,也就是“穴居人式调试”。指的是不用断点调试器、不用复杂 profiling 工具,而是用最原始的 print、echo、console.log 在代码里打桩,靠输出信息推断问题。听起来简陋,但很多场景下它比调试器更直接、更普适。
第二是caveman stack,极简技术栈。一个项目能用标准库解决的,绝不引第三方依赖;能用单文件的,绝不建多级目录;能直接操作数据的,绝不套一层 ORM。整个项目的依赖数量一只手数得过来,任何一台机器拉下来就能跑。
第三是caveman process,极简做事流程。不搞繁重的文档流程和过度设计,问题想清楚就动手,代码改完就跑,跑完就交付。用最原始的沟通方式同步信息,用最朴素的方式跟踪进度。
1.3 为什么“简单”本身就是一种技术优势
很多人一听“极简”就觉得是偷懒,是能力不足。但我的实际体会是,简单带来的技术优势是实打实的:
依赖少意味着攻击面小、故障点少。一个只有几层依赖的系统,出问题时排查链路非常短。你的系统里装的每一个第三方库,都意味着别人代码的问题可能传导到你这里来。反过来,依赖越少,可控性越高。
代码人人能读,交接成本极低。一个用标准库写成的脚本,任何有基础的人都能看懂。你不需要读者先熟悉某个冷门框架的体系和心智模型,只需要他懂这门语言本身。这一点在团队协作和开源项目里尤其值钱。
可复制性强,能稳定运行很久。依赖越多,时间越长越容易失效。五年后某个依赖不维护了,你的项目就跟着遭殃。极简项目哪怕十年没人动,只要系统还能跑基本的命令,它就能活。
提示:caveman 不等于“不用工具”。它更准确的定义是:能用标准库解决的,绝不引入框架;能用单文件的,绝不引入目录树;能用十分钟理解的,绝不让后人花十小时破译。
2. 我搭了一套“caveman”极简工具箱:从选型到落地
2.1 需求描述与选型原则
说个我自己的真实项目。有一台闲置的小主机,我拿它当家庭服务器用,需要完成几件事:定时备份指定目录到外接硬盘,每天早上发一封网络连通性报告,定期把本地 Markdown 文章发布成静态博客,偶尔跑一些数据处理脚本。
这些需求如果用“标准方案”来做,大概会变成:装一套监控系统、配一个定时任务框架、搭一个容器环境、用静态站点生成器加主题插件。但我给自己定了一条规则:除了一开始系统自带的软件包,不额外安装任何重型服务。所有任务尽可能用 shell 脚本、cron、以及各语言的标准库完成。
选型原则其实就三条:标准库优先、单文件优先、不起常驻服务。所谓标准库优先,就是同一个功能,Python 标准库能做的绝不用第三方包;单文件优先,就是每个独立功能尽量收进一个文件里;不起常驻服务,就是能靠 cron 每天跑一次解决的,不搞一个守护进程天天挂着。
2.2 具体工具选型表
| 需求 | 选用的工具 | 选择原因 |
|---|---|---|
| 定时调度 | cron | 系统自带,可靠运行了四十年 |
| 目录备份 | rsync + tar | 增量同步加归档,两条命令解决 |
| 连通性检测 | ping + shell 脚本 | 不需要装专门的监控组件 |
| 静态博客生成 | pandoc + Makefile | 一个转换器加一个构建命令 |
| 数据处理 | awk / python 标准库 | 内存占用小,一台破机器也能跑 |
| 日志查看 | journalctl / tail | 系统自带,无需引入日志平台 |
这张表你要是仔细看,会发现每一个选择都挺“无聊”的,没有任何炫技成分。但换来的结果是:整个服务器上跑着的定时任务加起来,占用的系统资源可以忽略不计,而且从来没有因为工具本身的 bug 出过问题。
2.3 一套可复用的骨架脚本
我挑两个核心脚本分享出来,你直接改改路径就能用。
第一个是备份脚本,核心逻辑是先做增量同步,再打一个带日期的归档包:
#!/bin/bash # backup.sh - 增量同步目录并打归档包 BACKUP_SRC="${1:-/home/user/data}" BACKUP_DST="${2:-/mnt/backup}" # 归档文件按日期命名,保留最近30份 STAMP=$(date +%F) DIR_NAME=$(basename "$BACKUP_SRC") # 第一步:rsync 增量同步到暂存区 rsync -a --delete "$BACKUP_SRC" "$BACKUP_DST/$DIR_NAME-live/" # 第二步:打包成独立 tar.gz,防止误删或源数据出问题后有逃生通道 tar czf "$BACKUP_DST/$DIR_NAME-$STAMP.tar.gz" -C "$BACKUP_DST" "$DIR_NAME-live" # 清理超过30天的归档 find "$BACKUP_DST" -name "$DIR_NAME-*.tar.gz" -mtime +30 -delete第二步的 tar 很多人会省略,觉得 rsync 做完镜像就够了。但我的习惯是两种都留:rsync 镜像方便随时直接访问最新文件,tar 包是时间快照,万一哪天误删了文件又没发现,还能从历史快照里捞回来。
第二个是静态博客发布脚本,整站只有一个输入目录、一个输出目录、一个博客模板:
#!/bin/bash # publish.sh - 把 Markdown 文章发布成静态 HTML INPUT_DIR="content" OUTPUT_DIR="docs" TEMPLATE="tmpl.html" mkdir -p "$OUTPUT_DIR" for md in "$INPUT_DIR"/*.md; do name=$(basename "$md" .md) # 用 pandoc 把 md 转成 HTML 片段,再嵌入到统一模板里 pandoc "$md" --template="$TEMPLATE" -o "$OUTPUT_DIR/$name.html" done # 顺手生成一个简单的索引页 ls "$OUTPUT_DIR" | sed 's/\.html$//' > index_list.txt整套博客系统,下载下来也就两个脚本加一个模板文件。不用数据库、不用后台管理界面、不用构建插件,你在本地写完文章,扔进 content 目录,跑一下 publish.sh,完事。
2.4 落地后的效果与维护成本
这套配置我在那台小主机上跑了快一年。一年里没有为“维护工具”本身花过任何时间,cron 到点执行,执行完写日志,日志没人看也不会出事。偶尔要做点小改动,整个系统只有几个文件,打开看一眼就知道改哪里。
同样的需求,我见过别人用重型方案做完的样子:监控服务隔段时间要升级,升级完要适配新接口,推送通道时不时失联,还有时不时的性能告警……说实话,把我的方案和他的方案摆在一起,功能和稳定性都不在一个层级(我的反而更稳),但复杂度差了一整个量级。
这不是说他的方案不该用,而是说他的场景可能有几十台机器、多人协作、跨团队告警,这当然需要那套体系。但对于我这样一台小主机、两三个定时任务的需求,caveman 思路就是最优解。
3. 黑暗洞穴探路法:只用 print 打日志的高效排错套路
3.1 为什么调试器不是万能的
聊完了项目选型,来说说日常更常碰到的排错问题。很多新人习惯一遇到 bug 就上调试器,断点一打、变量一看、逐步执行,觉得这才是“正规军”的做法。但真实世界里,有大量场景调试器根本用不上:你连的是远程服务器,没有图形界面;问题出在已经上线的服务里,不能随便重启打断;你排查的是一个别人的黑盒组件,根本没有源码让你断点。
我这些年踩过的坑告诉我,print 调试法才是真正通用的排错手段。学名叫 caveman debugging,也就是穴居人式调试。它不需要特殊环境,只需要你能改代码加输出,或者看日志。
下面对比一下两种方式的实际差异:
| 对比项 | print 调试法 | 调试器 |
|---|---|---|
| 对运行环境要求 | 只要能运行和输出即可 | 需要开发环境、断点支持 |
| 对线上服务的影响 | 只需加日志,无需停止服务 | 打断执行,影响线上请求 |
| 对第三方组件的适用性 | 通过看输入输出判断 | 看不到内部实现,无法断点 |
| 排错速度 | 一次加一个输出,直接定位 | 需要复现问题,逐步跟踪 |
| 学习门槛 | 几乎为零 | 需要理解断点、条件、堆栈概念 |
3.2 一套可复制的“print 排错”流程
我的经验是,print 调试法没太多技巧,但要按流程走,乱打桩反而浪费时间。我通常是这样做的:
第一步,在一个可能出错的分支前后加输出。比如函数有返回值,就在调用处把返回值打出来;有多个分支条件,就在每个分支入口打印一条标记。这一步的目的是确定问题到底出没出在这个函数。
第二步,二分法定位。如果函数范围太大,就在函数中间位置加一个输出,看执行到不到。到了,说明问题在后半段;没到,说明问题在前半段。反复对半切,很快就能筛到具体一行。
第三步,确认数据而不是猜测。很多人喜欢盯着代码“看”出问题,但我更倾向于直接把实际运行的数据打出来。很多时候你以为变量应该是某个值,一打印出来发现完全不是。打印实际数据永远比脑内模拟更可靠。
举例说明。我有一次排查一个定时任务:脚本偶尔会把配置文件覆盖成空白。代码扫了几遍没看出毛病,于是我在写文件前后各加两行输出:
echo "BEFORE_WRITE: $(date +%s) -- content length: ${#CONFIG_CONTENT}" echo "OVERWRITE_FLAG: ${OVERWRITE:-unset}" >> "$LOG"跑了两天,日志显示覆盖动作每次都发生在凌晨 2:17,非常有规律。我顺着时间去查 cron,发现有一条别的定时任务在 2:15 左右运行,它内部会生成一份临时的配置文件,路径和正式配置路径因为一个~和绝对路径的混用恰好指到了同一个文件。这个纯靠断点调试很难发现,因为问题根本不在业务逻辑里,而是在两个任务的环境变量解析方式上。print 日志把时间和动作关联起来,问题一目了然。
3.3 比 print 更“caveman”的底层排错工具
除了在代码里打桩,命令行世界里还有一批“穴居人级别”的排错工具,每一个都历史悠久,但威力不减。遇到情况我一般从它们开始查。
- strace:跟踪系统调用,看一个程序到底执行了哪些系统操作。适合排查文件权限、网络连接、启动报错。
- tcpdump:抓包工具,直接看网络层数据。适合排查服务之间连不上、协议不对、报文被截断。
- lsof:列出当前进程打开的文件和端口。适合排查端口被谁占着、文件为什么删不掉。
- ltrace:跟踪动态库函数调用,比 strace 更靠近用户态。适合定位第三方库内部的调用过程。
- journalctl / tail:看日志。很多时候问题根本不需要调试器,日志里明明白白写着,只是没人去看。
这些工具的共同特点是一样的:直接用命令观察程序的真实行为,不依赖任何图形界面和现代开发环境。在一个连最小化系统都可能缺东西的环境里,它们反而是最后一道防线。
3.4 什么时候该放下火把,拿起探照灯
当然,print 调试法也不是万能的。我碰到过一种情况,就果断放弃了 print 路线:多线程并发问题。几个线程同时在写同一个数据,靠 print 打印数据往往是“事后诸葛亮”,打印出来的时候数据早就已经是最终状态,根本看不到中间时序。这种场景我直接上专门的线程分析工具,或者干脆用调试器在关键行加断点,观察各个线程的当前状态。
还有内存泄漏问题,靠 print 也几乎不可能定位。变量内容看得再多,也看不到堆内存的增长曲线。这种时候必须借助 profiling 工具。
所以我的完整判断是:日常八成以上的 bug,print 调试法完全够用,而且更快;剩下两成涉及并发、性能、复杂内存问题时,老老实实换专业工具。关键是先搞清楚自己在哪个类型的问题里,不要拿火把去探有沼泽的地方。
4. caveman 式决策清单:什么时候该返璞归真,什么时候该“进化”
4.1 适合 caveman 思路的场景
坦白说,caveman 思路不是所有项目的最优解,但它适用的场景比很多人想象中宽得多。我梳理了几个典型场景,你可以对照看:
第一是个人项目和维护边界清晰的小工具。项目最大的使用者和维护者都是自己,逻辑复杂度可控,不需要多人协作。这种情况上重型框架纯属自找麻烦。
第二是原型验证和一次性脚本。有时候只是想验证一个想法是否可行,或者批量处理一次数据。这种场景最重要的是快速出结果,架构合理性和可扩展性不重要。用 shell 或 Python 标准库一把梭,跑完就扔,完全不心疼。
第三是嵌入式系统和资源受限环境。小内存、弱 CPU、存储有限的设备,压根跑不动 JVM 或 Node 那一套。反而awk、sh、sqlite3这类轻量工具能完美适配。这里 caveman 不是一种审美,而是硬性约束下的必然选择。
第四是管道式的数据处理任务。数据从 A 走到 B,中间做几次清洗、转换、过滤。用 shell 管道把 grep、awk、sort 串联起来,几十行就能解决一个“看起来需要写程序”的需求。这种场景我见过太多人急着上框架,结果搞出个一百兆的服务就为了做个字段统计。
4.2 caveman 思路明显不适用的场景
有些场景强行 caveman 就是坑团队。最常见的包括:
多人协作的大规模项目。几十个人在同一个仓库里开发,没有类型约束、没有统一框架,局面很快会失控。人多了就必须靠工具链约束人的行为,这时候成熟的框架、严格的类型、自动化的 CI 流程反而是保障。
安全合规要求高的系统。支付、医疗、政务类项目,需要完整的操作审计、权限控制、安全加固。这种场景靠“简单”是无法满足要求的,必须上专门的框架和流程。
需要长期演进的业务系统。业务逻辑会持续增加,需求不断变化,一个前期图简单选型的数据结构,后面可能成为扩展的天花板。这种情况下该做的抽象、该解的耦,省不了。
4.3 一张实用决策清单
我把自己多年的判断标准压缩成一张清单,遇到项目先对照一遍:
| 判断维度 | 倾向简单方案 | 倾向重型方案 |
|---|---|---|
| 使用者规模 | 我自己或两三人 | 几十人以上团队 |
| 变更频率 | 偶尔改动 | 持续高频迭代 |
| 故障成本 | 故障影响可控 | 故障等于严重事故 |
| 运行环境 | 资源受限、环境简单 | 标准云环境、资源充裕 |
| 生命周期 | 一次性或短中期工具 | 计划运行五年以上 |
| 调试能力 | 需在无法打断的线上环境运行 | 有完整测试和预发环境 |
| 第三方依赖风险 | 希望长期零维护 | 有人专职维护依赖体系 |
这个清单不一定是公式,但足够帮你踩下第一脚刹车:每当你准备给一个简单需求引入复杂的重型工具时,先拿出这几个问题来回一遍,答案自然清晰。
4.4 如何把“极简基因”融入复杂系统
最后这点我觉得最有实际价值——哪怕是在大型系统里面,你也可以保留 caveman 的小模块,多点“极简基因”反而能对冲整体复杂性。
比如一个大型后端服务,日志收集、数据清洗这类边角功能,完全可以用 shell 脚本在后台跑,没必要再引入一套流处理框架。再比如内部的管理后台,静态报表用数据库原生查询加一个简单脚本生成就行,不一定非要上全套可视化大屏。项目的核心业务部分可以用复杂的架构保证扩展性,但项目的非核心部分,用力简化,既能降低维护压力,又能让团队把精力集中在真正重要的事情上。
5. 做了一段时间“技术穴居人”后的真实体会
5.1 最大的变化:维护成本变成了一件看得见的事
做了一段时间极简风格之后,最大的变化是我对“维护成本”这个概念有了实感。以前用重型框架,维护成本是隐藏的——依赖更新、版本升级、API 变化,这些都像暗礁一样潜在水下。现在用极简方案,整个项目的维护成本几乎等于它表面看到的样子:一个脚本、几行命令,出问题看一眼就明白。
这种感觉有点像你从全自动驾驶换成了手动挡:操作确实多了一点,但你对车子的状态、路面的情况有了更直接的感知。对于像我这种喜欢掌控感的人来说,这种“笨一点但清楚”的体验反而很踏实。
5.2 几个意外的收获和教训
收获有几个是意料之外的。第一个是代码审查变得异常轻松,因为要点一目了然,不用在多个模块之间跳来跳去。第二个是交接和协作变得顺畅,别人接手我的脚本时不需要先上一周培训课,看一下代码就能改。第三个是夜间告警基本断粮,极简系统太稳定了,我现在很少被半夜的告警短信吵醒。
教训也有。最深刻的一条是:极简不等于不思进取,别把“用难度换成就感的陷阱”误当成极简。有人为了显得极简,放着好好的成熟库不用,硬是徒手写了一个轮子,结果轮子漏洞百出。真正的 caveman 是在多个可行方案里选择最简单的那个,而不是切掉自己需要的能力。省复杂度可以,省功能没必要。
另外一个教训是:极简系统也需要文档。我一开始觉得代码这么短谁需要文档,结果三个月后自己都记不清某个参数为什么要那样传。后来我在每个脚本开头加几行注释,把设计思路和注意事项写清楚,之后才真的实现了“零成本维护”。
5.3 给想尝试 caveman 风格的人几个具体建议
第一,从全新项目开始尝试。别急着把正在运行的重型系统推翻重写,挑一个全新的小需求,用极简方式实现一遍,感受一下具体差别。第二,设置一个“复杂度预算”。比如先给项目定死:依赖不超过五个文件,前后端逻辑加起来不超过一千行。超了就想办法简化。第三,定期复盘。每隔两三个月问自己一次:项目里有没有哪部分复杂度是其实不需要的?有没有哪个依赖是可以去掉的?主动做减法,复杂度才不会悄悄长回去。
最后分享一个我很喜欢的小习惯:每个脚本的开头都有一段给未来自己的留言。比如开头加一行注释:“半年后你还想维护这个脚本的话,记得先看这里。”每次改完脚本,把思路变化追加进去。等未来某天因为一个线头问题翻回这个文件时,你会感谢当时的自己。这就是我理解的 caveman 精神——用最简单的方式,为未来的自己留好火种。