上周团队复盘会上,小李苦笑着承认:”我当初坚持用上Kubernetes,纯粹是因为看了几篇大厂案例,觉得不上就落伍了。”话音刚落,会议室里响起一片会心的笑声。这不怪他,我们都一样——自以为理性客观的工程师,其实每天都在被大脑的各种认知偏差牵着鼻子走。

干了二十年开发,我最大的感悟不是技术有多难,而是认清自己有多难。今天想跟大家聊聊,那些年在技术决策中,我是怎么被自己的大脑”坑”的,以及后来学会的一些避坑方法。

锚定效应:那个该死的”第一印象”

2018年,我负责重构公司的订单系统。第一次开会时,CTO随口说了句:”听说现在都用微服务了,挺火的。”就这一句话,像锚一样沉在我心里。接下来两周,我所有的调研都围绕着”怎么把单体拆成微服务”,而不是”我们的核心问题到底是什么”。

我选择性忽略了团队只有5个人、业务变化快、QPS不到500这些关键事实。当我洋洋洒洒拿出那份50页的《微服务化改造方案》时,老板问了个致命问题:”我们现在的问题,是单体架构导致的吗?”

我愣住了。真实情况是,我们的痛点是业务逻辑混乱、数据库设计不合理,跟单体还是微服务半毛钱关系没有。但那个”微服务”的锚,让我自动过滤了所有反对证据。

代码级的锚定更可怕。记得有一次排查性能问题,我第一眼看到某个SQL查询,就认定它是罪魁祸首。接下来三个小时,我不断优化这条SQL,加索引、改写查询、甚至考虑上缓存。结果最后发现,真正的问题是上游接口返回了冗余数据,导致内存溢出。那条SQL的性能其实完全够用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 错误示范:被锚定思维困住
def optimize_query():
# 第一眼就认定是数据库问题
query = "SELECT * FROM orders WHERE status = 'pending'" # 全表扫描!
# 疯狂加索引...
add_index('orders', 'status')
# 改写成分页查询...
query = "SELECT * FROM orders WHERE status = 'pending' LIMIT 1000"
# 上Redis缓存...
cache_key = "pending_orders"

# 三天后才发现,真正的问题是:
# orders表只有2000条数据,根本不是瓶颈
# 真正慢的是下游的process_order()函数,里面有同步调用的第三方接口

避坑方法:我现在做技术决策,会强制自己写一份”反向论证”。如果我想上微服务,就必须先写一份《为什么我们现在不该用微服务》的文档,列出至少5条反对理由。这招特管用,因为它强迫大脑跳出第一印象的陷阱。

确认偏误:只相信自己愿意相信的

2020年,我迷上了TypeScript。说实话,这确实是门好语言,但问题出在我是”带着答案找问题”。当时团队还在用纯JavaScript,我一心想推动TypeScript落地。

我开始”有选择地”收集证据:把GitHub上所有关于TypeScript的赞美文章都收藏起来,把同事遇到的bug都归结为”没有类型检查”。有个实习生提了一句:”我们的单元测试覆盖率其实挺高的,很多类型错误都能被测出来。”我当场就反驳:”那不一样!”——现在回想,我只是在保护自己的观点。

最讽刺的是,当我终于说服团队花了两个月迁移到TypeScript后,生产环境的bug数量并没有显著下降。我们的主要问题一直是业务逻辑复杂、需求变更频繁,类型系统能解决一部分问题,但远不是银弹。我浪费了团队两个月时间,只是为了证明”我是对的”。

代码审查中的确认偏误更隐蔽。作为资深工程师,我们很容易陷入”我写的代码肯定没问题”的思维。有一次我提交了一个PR,里面用了一个比较晦涩的设计模式。同事review时提了一句:”这个是不是有点过度设计了?”我立刻找了三篇关于这个设计模式的文章发过去,证明自己是对的。但冷静下来想想,同事只是委婉地表达”这段代码不好维护”,而我却在拼命证明”我的技术选择很高级”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 我当时写的"高级"代码
class OrderProcessor {
constructor(strategy) {
this.strategy = strategy;
}

process(order) {
return this.strategy.execute(order);
}
}

class VIPOrderStrategy { /* ... */ }
class NormalOrderStrategy { /* ... */ }
class DiscountOrderStrategy { /* ... */ }

// 用了策略模式,看起来很酷
// 但实际上业务逻辑就是简单的if-else:
// if (order.type === 'vip') { ... }
// else if (order.type === 'discount') { ... }
// 同事的真实意思是:就三行逻辑,值得上设计模式吗?

文章插图

避坑方法:我现在养成了一个习惯,叫”主动寻找反对者”。在做重要决策前,我会专门找一两个平时观点不同的同事,请他们”使劲挑毛病”。不是为了说服他们,而是为了听到不同的声音。同时,我会把”如果我错了,最早什么时候能发现”这个问题写下来,设定一个验证节点。

沉没成本谬误:越陷越深的重构泥潭

这是我最惨痛的经历,没有之一。

2019年,我决定重写公司的老后台系统。原系统是PHP写的,确实代码质量堪忧。我信誓旦旦地说:”给我三个月,还你一个全新的Java微服务架构。”

三个月过去了,完成了30%。老板问还要继续吗?我说都投入这么多了,不能前功尽弃。六个月过去了,完成了60%。团队开始疲惫,但我坚持:”现在放弃,前面的努力都白费了。”

九个月过去了,完成了80%,但业务已经等不及了。老系统还在跑,新系统迟迟上不了线。最后我们不得不做了个屈辱的决定:把新系统砍掉,在老系统上打补丁继续跑。我浪费了团队九个月时间,不是因为技术选型错了,而是因为”已经投入了九个月”这个沉没成本,让我失去了理性判断的能力。

代码层面的沉没成本更常见。多少次,我们看到一段烂代码,心想”既然都这样了,就继续堆上去吧”。上周我就遇到这个情况:一个2000行的React组件,我花了半天时间想重构它。中途同事说:”要不我们重写一个吧?”我第一反应是:”不行,我都看了半天了,现在放弃太亏了。”

这就是典型的沉没成本思维——我已经投入的时间,不应该影响我接下来的决策。那段代码该不该重构,取决于重构的收益和成本,而不是我已经看了多久。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 沉没成本谬误的典型场景
def legacy_function():
# 这个函数有500行,各种if-else嵌套
# 你已经花了3小时试图理解它
# 现在发现逻辑根本是错的

# 错误思维:都看了3小时了,必须把它改好!
# 正确思维:这个函数是否值得挽救?还是重写更划算?

# 我现在的做法是,设定一个"止损点"
# 比如:如果1小时内搞不定,就考虑重写
# 而不是:我已经投入了X小时,必须继续
pass

避坑方法:我现在做决策,会刻意忽略”已经投入了多少”,只问自己一个问题:”如果今天是从零开始,我会选择继续做这件事吗?”如果答案是否定的,那就果断止损。同时,我会给任何超过一周的项目设定”继续/放弃”的决策点,避免被沉没成本拖死。

从众效应:被大厂案例牵着鼻子走

这个坑,我想每个工程师都踩过。

2021年,看到某大厂技术博客说他们用Service Mesh解决了所有微服务通信问题,我立刻心动了。没仔细分析我们的规模(30个微服务)、团队能力(没人懂Istio)、业务特点(内部系统,流量不大),我就启动了Service Mesh调研。

我告诉自己:”大厂都用,肯定没错。”但忽略了最关键的问题:大厂的问题,我们有吗?就像看到马云穿布鞋,你也买一双,但人家是穿腻了皮鞋想换换口味,你是连双像样的鞋都没有。

折腾了两个月,我们终于把Istio跑起来了。然后发现,服务间的调用 tracing 确实好用了,但配置复杂度、排错难度、性能损耗都超出了预期。最关键的是,我们的核心痛点是业务逻辑混乱,不是服务间通信问题。Service Mesh解决了一个我们不存在的问题。

技术选型中的从众特别危险,因为我们都害怕”落伍”。看到React 19出了新特性,立刻想升级;看到Vite构建快,马上想换掉Webpack;看到Rust性能强,琢磨着要不要用Rust重写。但很少停下来问:这解决了我们当前的什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# docker-compose.yml 里的"跟风"配置
version: '3.8'

services:
web:
image: our-app:latest
# 看到大厂都用Kubernetes,我们在docker-compose里也搞一堆配置
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3

# 但实际情况是:这只是开发环境,只有一个人在用
# 这些配置除了增加复杂度,没有任何实际价值

文章插图

避坑方法:我现在看到任何”大厂实践”,都会先问自己三个问题:

  1. 他们解决的问题,我们有吗?
  2. 他们的规模,我们达到了吗?
  3. 他们的团队能力,我们具备吗?

如果答案都是否定的,那就先放一放。技术没有高低贵贱,只有适合不适合。

过度自信:老司机最容易翻的车

作为”资深”工程师,这是我最近两年最警惕的陷阱。

去年,产品经理说要加个”简单”的秒杀功能。我想都没想就说:”这个简单,一天搞定。”——过度自信了。我忽略了库存扣减的并发问题、恶意刷单、前端重复提交、数据库压力等一系列细节。结果这个”一天”的功能,最后做了两周,还出了一次库存超卖的事故。

过度自信在代码审查中更致命。资深工程师写的代码,大家往往不好意思深挑毛病,自己也会下意识地觉得”我写的肯定没问题”。去年我提交了一个缓存优化方案,没写单元测试,心想”这么简单的逻辑,不会错的”。结果上线后,在边缘情况下导致了缓存雪崩,整个服务挂了20分钟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 过度自信的经典代码
public class CacheService {
// 我当时的"简单"缓存方案
public String getData(String key) {
String value = redis.get(key);
if (value == null) {
// 这里过度自信了:假设数据库永远不会挂
value = database.query(key);
// 假设设置缓存永远不会失败
redis.set(key, value, 3600);
}
return value;
}

// 实际上需要考虑:
// 1. 数据库查询异常
// 2. Redis连接失败
// 3. 缓存穿透、击穿、雪崩
// 4. 并发写问题
// 5. 数据一致性
// ...
}

避坑方法:我现在给自己定了两条铁律:

  1. 任何评估,先乘以1.5倍。说一天完成的,按1.5天计划;说一周的,按10天计划。
  2. 强制写单元测试,尤其是对自己觉得”简单”的代码。简单代码往往意味着思考不充分。

写在最后:与大脑和解

讲了这么多被大脑”坑”的经历,你可能会觉得认知偏差很可怕。但其实,认知偏差是大脑为了节省能量而进化的机制,它让我们能快速决策。问题在于,技术决策往往需要慢思考。

我现在做技术决策,会用一个简单的 checklist:

  • 有没有写反向论证?
  • 有没有找到至少一个反对者?
  • 是否忽略了沉没成本?
  • 这个方案解决了我们当前的什么问题?
  • 时间评估是否乘以了1.5?
  • 有没有设定止损点?

最重要的,是保持谦逊。无论经验多丰富,都要承认自己会被认知偏差影响。当我开始意识到”我可能又在大脑坑我”时,反而能做出更理性的决策。

技术决策没有银弹,但认清自己的局限,或许是最好的开始。就像那句老话:知道的越多,越知道自己不知道。在认知偏差这件事上,意识到自己可能会被坑,就是避免被坑的第一步。

共勉。