前两天排查一个线上问题,花了四十分钟在一堆分布式链路的 tracing 数据里翻来翻去,最后同事过来看了一眼,直接在关键路径上加了行print,两分钟定位到问题。我盯着那行输出愣了半天,不是因为它多巧妙,而是因为它简单到让我想起一个词——caveman。
这个词在程序员圈子里其实流传了很久。你搜“caveman”能翻到各种梗图和段子,但抛开调侃,“caveman”在技术语境里指向的是一种做事方式:用最原始、最直白、最不依赖外部条件的手段,解决眼前最实际的问题。像穴居人钻木取火一样,不去想什么燃气灶、微波炉,手头有什么就用什么。
这篇内容就是想把“caveman”从头到尾聊透:它到底是什么、为什么在工具链满天飞的今天反而还这么好使、在哪些场景它是真救场、在哪些场景你千万别学它,以及我自己几次真刀真枪用“caveman 方案”翻盘的完整记录。适合正在被分布式、微服务、复杂的构建链路和各种抽象框架困扰的开发者看,也适合所有觉得“系统越来越复杂、出问题越来越难查”的人。
1. 为什么“穴居人”反而好使:三个核心信号
先别急着把 caveman 理解成“偷懒”或者“水平差”。实际上,我对这个词的理解是:它是工具箱里最底层、最不容易失效的那套锤子和凿子。现代技术栈一层套一层,任何一层出问题都会让上面所有层变得不可信,而 caveman 方案往往直接绕开了“层”,在物理现实里跟你打交道。
1.1 信号一:工具链越薄,故障面越小
我自己有个体会:一个东西越高级,它依赖的环境就越多。要用 Kubernetes 跑服务,你得先有容器运行时,得有网络插件,得有镜像仓库,还得有调度器;调度器本身又要依赖 etcd、证书、各种准入控制器。任何一个环节抖动,你都会看到一个“莫名其妙”的症状。
而所谓 caveman 式方案,通常是“一条命令能解释清楚自己在干什么”的方案。比如排查网络问题,我见过有人直接上nc -vz、curl -v、traceroute,三件套下来链路就清楚了;也见过有人先开一套 prometheus + grafana,再配一堆告警规则,最后发现是目标机器根本没开机。工具链越薄,你离真相越近。这不是夸张,是无数个排查现场验证过的经验。
1.2 信号二:直白代码的可读性红利
我之前接手过一个遗留系统,核心逻辑用了一个非常“聪明”的状态机框架,事件、条件、转换规则全部通过配置文件驱动。问题是,配置文件的语法是框架作者自己发明的,文档只有几段注释。业务方提了一个需求,我改了两天配置文件,跑起来还是不对。
后来我把状态机整个拆掉,改成 100 行朴素的switch-case,半小时改完,效果一模一样,而且同事后来看代码的时候说“终于知道这段逻辑在干嘛了”。
“直白”本身是有巨大红利的——它意味着后来者不需要先学会你的暗号才能帮你修 bug。代码首先是给人读的,其次才是给机器执行的。caveman 风格代码牺牲了一点点表达上的优雅,换来了极高的可读性和可维护性。在人员流动频繁的团队里,这比任何架构文档都实在。
1.3 信号三:从“坑”里爬出来的速度
系统越是复杂,出问题时的排查成本就越是指数上升。因为你需要回答的不只是“哪里坏了”,还有“坏的这一层在我这套架构里处于什么位置”“跟它相连的其它部分会不会也被带崩”。
caveman 思路在处理故障时有个天然优势:它倾向于先止血,再问责。而不是先调研、再开大会、再改架构、最后才动手。最简单的例子——线上服务 OOM 了,第一件事是重启还是看 dump?很多人会说“先留证据”,但服务在客户那边已经挂了,先把流量摘掉、重启、恢复服务,再把 dump 捞下来慢慢分析。这就是“先保证钻木能点着火,再研究怎么造打火机”。
这三条信号合在一起,能解释很多现象:为什么很多看起来“不够酷”的技术栈反而极其稳定,为什么老工程师遇到问题先ssh上去看日志而不是先开 APM,为什么有些系统十年没大重构也活得很好。
2. “穴居人”模式在真实场景里的四种典型形态
caveman 不是一个具体的工具,而是一类“怎么做事情”的思路。落到真实项目里,它大概会被我分成四种形态,分别对应调试、技术选型、架构设计和日常节奏。
2.1 形态一:Caveman Debugging——最简单粗暴的排查法
这个方法其实已经“梗化”了,叫“print 调试法”。正经点的名字是日志驱动排查。不管你是写 Python、Go、Java 还是 JavaScript,出问题第一反应不是上 debugger,不是开 tracing,而是在关键节点print一下变量、时间、返回值。
我见过不少年轻同事觉得 print 很 Low,非要用 IDE 断点、用分布式追踪、用 profiling。但断点没法在线上环境打,tracing 也不是每个系统都能覆盖到。反而是 print 出来的日志,跟着业务链路走,能让你快速把“数据在哪一步开始不对”给圈出来。Caveman debugging 的核心不是不用工具,而是工具的引入成本必须小于问题本身的复杂度。
2.2 形态二:Caveman Stack——能不引入就不引入
我这些年对技术选型的态度越来越保守。以前看到一个新出的消息队列、新数据库,恨不得马上迁过去体验一把;现在我的默认答案是:能用 MySQL 解决的,绝不上一个新的存储引擎;能用 Redis 解决的,绝不上一个独立的缓存中间件;能用定时任务解决的,绝不上一个 workflow 引擎。
举个例子。之前一个同事提议用 ElasticSearch 做搜索,我看了看数据量,一天新增不到几千条,总量也就几十万。直接让他在 MySQL 里建了个索引列,用LIKE模糊查询,配合分页一次性拉取,查询响应都在 200ms 以内。后来他有点不好意思,说“没想到数据量这么小的时候 MySQL 这么能扛”。对,这就是 caveman stack 的底气——数据库厂家花了那么多年优化存储引擎,不是让你一上来就绕开它的。
2.3 形态三:Caveman Architecture——符合认知水平的设计
架构设计领域现在有很多“美丽”的概念:微服务、事件溯源、CQRS、Serverless。这些概念本身没有错,但它们都有适用边界。caveman architecture 说的是:让架构的复杂度,恰好匹配你和你团队当前的认知水平。
我见过一个 20 人不到的团队,硬是拆了 17 个微服务。每个人都要维护两三个服务,部署一次要等二十分钟流水线跑完,联调要开好几个环境,出了问题根本不知道日志去哪找。后来他们新来的 CTO 干的第一件事是把服务合并成三个模块,单体部署。半年之后,迭代速度反而快了一倍。
这不是说微服务不行,是说如果你现在还没有足够的运维能力、监控体系、团队分工来支撑微服务,那单体架构就是更 caveman、更有效的选择。架构没有绝对的高级和低级,只有对不对得上当前阶段。
2.4 形态四:Caveman Workflow——回到纸笔和单线程
很多人没注意到,“caveman”还体现在日常办公方式上。文件夹里堆了几百个云文档,聊天软件里几十个群,一周到底哪些事做了哪些没做,脑子里一团浆糊。
我现在的待办清单是拿一个文本文件 +vim维护的,每天早上打开先看三件事:今天要交付什么、目前阻塞在谁那里、哪些承诺要到期了。就这三行,比任何项目管理软件都好用。因为工具本身不会帮你做决策,只会帮你记录决策。记录这件事,用最朴素的方式往往更不容易漏。
我还认识一个老运维,他的服务器清单是一张 A4 纸贴在工位上,上面手写着每台机器的 IP、用途、登录方式。我们都说他原始,但每次机器出问题,他都是第一个找到对应节点的人。后来想想,这种 caveman workflow 的信息组织方式,其实比几百条云文档更接近人的大脑习惯。
3. 实操:三次我用“穴居人”方案救场的完整记录
光讲概念没有说服力,我还是把自己印象最深的三个经历完整写出来。这三个案例分别对应排查问题、解决构建问题、以及架构取舍,都是我一步步走过来的,其中的排查链路很有参考价值。
3.1 案例一:一次内存泄漏,是jstack和top救了我
当时负责一个 Java 写的交易服务,上线两周后开始频繁 GC 停顿,响应越来越慢。我的第一反应是上专业的堆分析工具,于是折腾 JProfiler,又装了 Mat 插件,折腾半天连 license 都没搞定。
后来我退了一步,用最土的办法:先在服务器上top -H -p <pid>看线程 CPU 占用,再用jstack打印线程栈,两次jstack间隔二十秒对比哪个线程一直卡在某个方法上。结果很快就看到一个叫SchedulerTask的线程反复执行某段处理逻辑,再看代码,发现它每执行一次都会往一个静态Map里塞一条数据,而清空逻辑只处理成功的数据,失败的一直留着。就这么一行代码的问题,用一整天的专业工具没查出来,用二十行命令加肉眼对比二十分钟就定位了。
提示:线上问题排查的顺序建议是——先看操作系统层面(
top、free、df)、再看进程日志(journalctl、log文件)、最后才是上 profiling 工具。工具越重,你离现场越远。
3.2 案例二:构建依赖太多,最后靠“删”解决问题
另一个项目前端构建,每次打包要跑两三分钟,CI 排队更是常态化。大家天天喊优化,但方案全是“上缓存”“换更快的机器”“用 pnpm 的共享存储”。
我没急着优化性能,而是先看了一眼依赖树。结果发现一个用了三年多的库,实际只调用了其中两个工具函数。这个库本身依赖了一大堆其他库,每次构建光解析它就要花掉十几秒。而那两个函数其实加起来不到五十行。
我的做法很简单粗暴:把这个库从依赖里去掉,把那五十行代码手写进项目公共函数里。构建时间从两分半降到四十秒,没有任何功能回退。这事的启发是:很多“性能问题”其实是“复杂度问题”。不加分析地堆缓存、堆硬件,是拿复杂度去掩盖另一个复杂度,caveman 思路则是先把没必要的复杂度砍掉。
3.3 案例三:一个复杂架构回退成单体的过程
第三个故事是我参与的一个数据同步系统。最初架构用了三套不同用途的存储:一套做实时写入,一套做离线分析,一套做消息队列缓冲。每一条数据要在三个系统之间流转,一旦某个环节失败,对账就成了噩梦。
前前后后跟了半年,每次发版都在补漏。后来我们把链路重构成一个非常“土”的模型:主库只保留一份原始记录,加一个状态字段;同步逻辑就是定时任务扫描状态字段变化,然后按目标格式写出去。少了两套存储,少了一半代码,数据一致性反而更容易保证了,因为“当前状态”只有一份,不再需要三套系统之间互相参照对账。
这个案例让我意识到:复杂架构带来的问题,经常是它自己制造出来的“协调成本”。一个系统组件越少,需要协调的地方就越少,出错的概率也就越低。caveman 不是拒绝演进,而是拒绝在没有足够收益的情况下,提前引入协调复杂度。
4. 怎么平衡:穴居人思维不是倒退,而是取舍工具
聊到这里可能有人会问:那是不是以后所有东西都往最原始的写?当然不是。caveman 思维本身是一把尺子,不是一块铁板。关键是知道什么时候用、什么时候收。
4.1 判断标准一:这个复杂度是必需的吗
我在选每一个组件、框架、设计模式之前,都会先问自己一句:这一层抽象,是不是为了应对一个真实存在的复杂度?如果只是“别人都在用”或者“以后可能会用到”,那大概率会变成负担。
真实存在的复杂度可以来自很多方面:数据量大到单表撑不住、并发高到单机扛不住、团队规模大到代码库协作冲突严重、业务逻辑复杂到单文件无法维护。每一项都有可量化的触发条件,不是玄学。写下来,逐条对照,再决定要不要引入复杂度。
4.2 判断标准二:你的团队和系统处在哪个阶段
同样的方案,在创业期的三人团队是合理的,在万人规模的公司可能就是定时炸弹;反过来也一样。衡量标准不是“这个方案酷不酷”,而是“如果出现问题时,现有的人和环境能不能在一小时内搞清楚它”。
我自己列过一个简单的对照表,不一定严谨,但很实用:
| 场景 | 倾向 caveman | 倾向现代工具链 |
|---|---|---|
| 数据量 | 百万级以内,单库可扛 | 亿级以上,需要水平扩展 |
| 团队规模 | 一两个小组,认知半径小 | 多团队并行,需要明确边界 |
| 开发节奏 | 快速验证,小步快跑 | 长期稳定迭代,需要严格规范 |
| 故障影响 | 影响局部,可容忍少量丢失 | 核心链路,秒级不可用即事故 |
| 排查能力 | 主要靠日志和命令 | 有完整监控告警链路和专职团队 |
4.3 判断标准三:Revert 成本高不高
每次做一个设计决策,我都会顺带问:如果这条路走不通,退回来要花多久?用配置中心还是直接写配置文件?前者改起来不用发版,但引入了一套新系统和它的可用性问题;后者改起来要发版,但没有任何额外依赖。如果回退成本低,就大胆选 caveman;如果回退成本极高,才值得用更“重”的手段提前兜底。
比如数据库选型,一旦业务跑起来,数据迁移成本极高,这时候哪怕数据量不大,也该认真评估长期方案。而像缓存策略、日志格式、定时任务这种局部实现,完全可以从最朴素的方式开始,必要的时候再迭代。
4.4 什么时候千万别当穴居人
有几类场景,caveman 是完全不合适的:
第一类是安全敏感场景。身份认证、支付、密码存储这种地方,自己手写轮子的风险远大于收益。该用成熟库、该过合规审计的时候,千万别图省事。密码不能被反推出明文,加密算法要用经过验证的标准实现,这些是用无数代价换来的规则。
第二类是强一致性和高并发储备。如果你的业务确实需要分布式事务、需要水平扩展到很高量级,那早期就可以铺垫对应的技术栈,不要等到流量涨上来了才临时抱佛脚。
第三类是长期演进的核心领域。比如数据模型的设计,一开始建模很随意,后面要改就伤筋动骨。这种地方值得前期多想几天,别太 caveman。
我的经验是:caveman 适用于“实现手段”。至于需求分析、数据建模、安全设计,该较真的地方反而要扎实。这俩你区分开了,很多纠结都能迎刃而解。
5. 一些我觉得值得长期保留的“穴居人”习惯
聊到最后,我发现这几年自己真正沉淀下来的工作习惯,其实都很 caveman。这里整理几条,权当给各位做个参考。
第一,凡事先说“最小可复现路径”。遇到一个怪问题,别急着猜原因,先想办法在本地或者测试环境里用最少的步骤复现。复现不了,你对问题的理解就是虚的,后面的所有方案都可能是盲人摸象。
第二,代码提交之前先自测一遍主流程。不要依赖 CI 帮你兜底。这一段代码改完,影响到的核心路径你亲手跑一遍,比一百个自动化用例都让人安心。自动化测试是为回归准备的,但“自己先跑一遍”才是第一道防线。
第三,日志要写人话。我看到有人在生产环境里写if err != nil { log.Println(err) },关键是这行代码出现在业务逻辑中间,既不说明是哪一步出的错,也没带上上下文 ID。caveman 式日志不是不加日志,而是要把日志写到“三天后的自己不需要翻代码上下文也能看懂”的程度。
第四,每次排查完问题,都留一段记录。我自己的做法是在一个本地 Markdown 文件里按日期记录:现象是什么、怀疑方向有哪些、最终怎么定位、用了什么命令、是不是有更快的路径。说实话几个月之后翻回来看看,很多踩过的坑直接能帮你把“排查时间”从几小时压缩到几分钟。这可能是最低成本、最高回报的穴居人习惯了。
写在最后
技术圈每隔一段时间就会出现一批新概念、新框架、新平台,有时候会让人产生一种错位感——好像不追最新,马上就落伍了。但回到实际工作里,决定你在关键时刻能不能顶住的,往往不是手里工具的先进程度,而是你对问题的理解深度和手边最朴素手段的熟练度。
caveman 不是一种倒退,它是一种提醒:在层层叠叠的抽象之下,别忘了事物最本来的样子。数据就是数据,接口就是接口,调用链就是从左到右的一次执行。当所有的高级手段都失效时,能让你从坑里爬出来的,依然是你对基础命令、基础数据结构和基础逻辑的理解。
我现在处理问题仍然会习惯性地先问一句:如果拿最笨的办法,这一步该怎么完成?这个问题的答案永远都能让我更踏实。希望这篇文章,也能给你在复杂系统里添一份“钻木取火”的底气。