你以为是搭乐高,其实是在做数学题——每加一块积木,就得拆掉另一块。
系统设计不是选“最好的方案”,而是选“你最愿意忍受哪种痛苦”。 同一个问题有十种解法,每种解法都在让你用A换B。搞懂这50个权衡点,比背一百张架构图管用一万倍。
别信“最佳实践”——系统架构圈最大的谎言
你去翻技术博客,满屏都是“某某大厂的最佳实践”。好像照着抄就能复制人家的成功。
但真相是什么?
这个世界上压根不存在“最佳实践”,只存在“最适合你当时处境的最不坏选择”。
这话反直觉到让人想拍桌子。但你说说看,Netflix的微服务架构,搬到一家只有三个开发者的创业公司,能叫“最佳”吗?人家一个团队几十上百人,有专门的基础设施组、SRE组、可观测性组——你连个运维都没有,抄过来就是给自己挖坟。
系统设计这件事,本质上是在资源约束下做选择题。你的资源永远有限:钱有限、人有限、时间有限、机器有限。所以你永远不可能“全都要”——你只能决定“先要什么,后要什么,以及愿意用什么去换”。
这就怪了:明明每个架构师都知道“权衡”这个词,为什么一到实战就翻车?
因为大多数人的“权衡”是嘴上说说,真到做决策的时候,脑子里只有一条路径——看别人怎么做,自己就怎么做。根本不问“我的场景跟别人的场景,差在哪里”。
大家都说加索引快,但没人告诉你加索引会让写入慢三倍
数据库索引这玩意儿,教科书里写得明明白白:索引能加速查询。
然后你就开始给表建索引。一个不够建两个,两个不够建五个。查询确实快了,美滋滋。
但过了一个月,你发现写入越来越慢。批量插入几万条数据,从原来的几秒钟变成了好几分钟。你慌了,查来查去,最后发现罪魁祸首就是那些索引。
索引的本质是拿写入性能换读取性能。
每次你往表里插一条数据,数据库不光是存数据本身——它还要去更新每一个索引。索引越多,写入的时候要干的事就越多。一张表上有五个索引,写一条数据就等于写六次。
更狠的是,如果你用的主键是随机值(比如UUID),每次插入都要在B+树的随机位置写数据,磁盘I/O直接爆炸。
这就叫“拿A换B”。你选择了查询快,就得接受写入慢。没有“查询又快写入又快”这种好事——至少在同一个数据库里没有。
那怎么办?你得算账:你的系统是读多写少,还是写多读少?读多写少,多建索引没问题。写多读少,索引就得精打细算。
这个道理简单吧?但你去看看多少人的生产环境,表上挂着十几个索引,写入慢成狗了还不知道为什么。
加延迟反而降延迟——Kafka教你的反常识一课
接着说一个更离谱的。
你有个消息队列,生产者往里面发消息。默认情况下,来一条发一条——看着挺快对吧?但Kafka的官方文档告诉你:把linger.ms从0改成5,端到端延迟能从27.5毫秒降到7.5毫秒。
什么?主动等5毫秒,反而让总延迟减少了20毫秒?
这听起来像诈骗。但原理特别简单:你一条一条发,每次都要建连接、发请求、等确认——网络往返的开销巨大。但如果攒一批再发,平摊下来每个请求的平均开销就小多了。
有家公司实测:linger.ms=0的时候,每秒要发2800次请求;改成5毫秒之后,请求次数降到了1100。请求少了,网络拥塞也少了,整体反而更快。
这就是系统设计最迷人的地方:直觉告诉你“快就是快”,但现实告诉你“有时候慢一点反而更快”。
所以你选方案的时候,不能光看表面。延迟和吞吐量不是“选一个”的关系——它们通过排队理论绑在一起。你调整了A,B可能跟着变,而且变化方向可能跟你想的完全相反。
微服务:花更多的钱,买更复杂的痛苦
好了,重头戏来了。
2016年前后,微服务像一场宗教运动席卷了整个技术圈。技术大会上人人都在讲微服务,讲Netflix怎么拆的,讲Amazon怎么做的。你如果还在用单体架构,都不好意思跟人打招呼。
然后呢?
有一家公司,把单体应用拆成了47个微服务。三年后,他们花在排查分布式链路追踪上的时间,比写新功能还多。“独立”的服务通过共享数据库和同步调用紧紧耦合在一起。每次部署都要协调五个团队。可观测性的账单眼看就要突破六位数。
他们只是用一套更昂贵的问题,换掉了原来那套。
这不是孤例。Shopify、Segment这些当年的微服务布道者,现在正在往回撤——回到一种叫“模块化单体”的东西。这不是倒退,是进化。
为什么?因为微服务的代价很多人根本没想到:
第一,运维复杂度爆炸。 以前你监控一个应用,现在要管几十个服务。每个服务都有自己的部署流水线、日志配置、指标看板、健康检查、熔断器、重试策略、超时设置。你还得引入服务网格、分布式追踪、API网关、服务发现……你只是把应用复杂度换成了基础设施复杂度——而后者更难理解和调试。
第二,分布式调试是噩梦。 一个用户的请求失败了,你要查日志——但这个请求穿过了12个服务。你得一个一个服务查过去, tracing系统没配好就抓瞎。
第三,分布式事务能把人逼疯。 订单服务调库存服务,库存服务调物流服务——任何一个环节出问题,数据就乱了。你得用Saga、用补偿事务、用各种奇技淫巧来兜底。
有数据显示,采用微服务架构的平台故障率比单体低41%,但初期开发成本高出27%。你省下来的故障时间,全花在了开发和运维上。
微服务不是银弹,它只是把“单体的痛苦”换成了“分布式的痛苦” 。你要问自己:你更愿意忍受哪种痛苦?
如果你的团队不到10个人,业务逻辑没那么复杂——老老实实用单体。单体架构在小规模场景下效率比分布式系统更高,因为所有调用都在进程内,没有网络开销。
别为了“以后能扩展”去搞微服务。大多数系统根本活不到“以后” 。
CAP定理:网络断了,你要钱还是要命?
接着说分布式系统里最经典的一道选择题。
你的系统部署在两个机房。突然光缆被挖断了,两个机房连不上了。
这时候来了一个用户,往A机房写了一笔钱。B机房收不到这条更新。
如果B机房的用户来查余额——你怎么办?
选项一:让B机房说“系统维护中,稍后再试”。 这叫“牺牲可用性,保一致性”——用户查不到余额,但至少不会查到错的。
选项二:让B机房把旧余额告诉用户。 这叫“牺牲一致性,保可用性”——用户能查到钱,但可能是错的。
你只能选一个。 CAP定理说得明明白白:分布式系统在发生网络分区的时候,一致性和可用性你只能保一个。
金融系统选CP——宁可让你查不到,也不能让你查到错的。社交媒体选AP——宁可让你看到旧数据,也不能让你刷不出来。
没有对错,只有选择。但很多人根本不知道自己选的是什么——默认用了某个数据库,默认接受了它的取舍,然后出了问题才傻眼。
事件溯源:别把整个系统都搭进去
再说一个技术圈特别容易上头的东西:事件溯源(Event Sourcing)。
这玩意儿听起来很酷——不存当前状态,存每一次变化。想查当前状态?把事件从头到尾重放一遍就行。
好处很明显:完整的审计追踪、可以回溯到任意时间点、方便做复杂的事件驱动架构。
但Greg Young(事件溯源的布道者之一)自己说了:把整个系统建立在事件溯源上,是一种常见的反模式。
为什么?
事件溯源迫使你为每一次数据变化都生成一个事件。 如果你的系统有几百个实体,每个实体每天变化几百次——事件存储会膨胀到你根本管不过来。
而且从事件存储里读取数据非常困难。你想查一个用户的当前状态?重放几千个事件。你想做一个简单的报表?得写复杂的聚合逻辑。
CQRS和事件溯源都不是顶层的架构,应当有选择性地在少部分场景中使用。
只在真正需要完整审计追踪、法律合规要求的核心领域用——比如金融交易、订单系统。别为了炫技把整个系统都搭进去。
每一个选择都在让你失去什么
回到开头那句话。
系统设计没有“最好的方案”,只有“你最愿意忍受的代价”。
加索引?你获得了查询速度,但失去了写入速度。
用微服务?你获得了独立部署的能力,但失去了运维的简单性。
用事件溯源?你获得了完整的审计追踪,但失去了查询的便利性。
用同步复制?你获得了数据一致性,但失去了写入的低延迟。
用缓存?你获得了读取速度,但失去了数据的实时性。
每一个“得到”的背后,都藏着一个“失去”。
真正厉害的系统设计师,不是能画出多漂亮的架构图——是能说清楚“我选择这个方案,放弃了什么,以及我为什么愿意放弃它” 。
下次你再做技术决策的时候,别问“哪个方案最好”。问自己三个问题:
第一,我的场景里什么最重要?
第二,我愿意用什么去换它?
第三,如果这个交换出了问题,我能承受吗?
这三个问题答不上来,你选什么都是蒙的。
有个团队花了一年时间把单体拆成微服务,上线第一个月就遇到网络分区。他们的服务发现挂了,整个系统不可用四小时。复盘的时候发现——他们从来没讨论过“如果服务发现挂了怎么办”。他们只讨论了“怎么拆”,没讨论“拆完之后怎么活”。
这个团队后来把一半的服务合并了回去。
你觉得他们做对了吗?