ELI5 · 说人话

怎么让很多台机器
一起把活干对

? 一台装不下了,于是拆开 原来:一台机器 现在:很多台机器,只能靠网络喊话

分布式不是「把系统做得更强」的技巧。
是一台机器装不下也不敢只有一台,被逼着拆开。代价是——你从此再也不知道对面到底怎么了。

多台机器,一个说法

先问为什么

只有三个理由值得拆

分布式解决的从来不是「不够快」,是这三件单机根本办不到的事。

装不下
一台塞不进去

100 TB 数据、每秒 50 万次写。市面上最贵的单机也接不住,只能摊到很多台上。

挂不起
它一定会死一次

硬盘会坏、机房会断电。一台机器一年总有几小时是死的——你得允许死一台,站还开着。

离太远
光速也不够快

用户在东京,机房在美国东部。什么都不干,光跑一个来回就要 160 毫秒。只能把机器搬过去。

三条都不占,就别拆。一台好机器加一个从库,能顶到你想象不到的规模。

拆开那一刻

凭空多出一个新难题

单机里,你调一个函数,它要么返回、要么抛错,两种结果。
跨机器调用有第三种:没消息。而「没消息」是最要命的那种。

调用方 支付服务 扣款请求 请求丢了 看到:超时 钱没扣 调用方 支付服务 扣款请求 扣好了! 回复丢了 看到:超时 钱已经扣了 两边看到的一模一样——重试会扣两次,不重试可能白跑。

这就是分布式的地基问题:你分不清「它死了」和「它只是慢」。后面所有花哨的机制,本质上都在跟这一件事较劲。

断线那一刻

你只能二选一

两个机房之间的网线断了。两边都还活着,都还能收单,就是听不见对方。这时候你必须提前替系统做好选择。

机房 A 还活着,还在收单 机房 B 也活着,也在收单 网线断了 现在有人要下单。你选哪个? 要正确:宁可不办,也不办错 两边都回一句「现在说不准,稍后再来」。数据永远对,但这几分钟站是废的。 要能用:先办了,回头对账 两边照常下单。恢复后发现同一件货卖了两次,你得有一套规则去圆。 断线本身不是选项,是天气 网络一定会断。你能选的只有:断的那几分钟,要正确还是要能用。 转账选前一个,点赞和购物车选后一个。

点一个看看

别把这当成整个系统的一次表态。同一个系统里,不同功能选不同的——账户余额要正确,浏览记录能用就行。企业级的水平,就体现在能不能把这个刻度分清楚。

解法总纲

翻来覆去就四把刀

Raft、Kafka、分库分表、TCC……名词有几百个,动作只有四种。任何方案拆开看,都是这四把刀的组合。

同一份东西存好几份

机器一定会坏,所以任何数据至少三个副本,摆在不同机架、不同机房。死一台,站不抖。
代价:几份之间总要对表,这就引出了下一把刀。

吵起来就投票,少数服从多数

几份数据不一样时,谁说了算?摆奇数台,过半数同意才算数——这就是 Raft,也是 etcd 永远是 3 台或 5 台的原因。
过不了半数的那一边,自己闭嘴。

切成几块,各管一段

一份数据放不下,就按用户 ID 切成 16 份,每台只管自己那份,各查各的
代价很实在:跨片的查询、排序、事务全部变难。所以这刀最后再拔。

同一件事做几遍,结果都一样

既然分不清超时是哪种,那就放心重试——前提是每笔请求带一个唯一编号,服务端认号不认次数,第二次来直接把第一次的结果原样返回。
这叫幂等,是所有跨机器写接口的及格线。

最常问的那一题

一单跨四个服务,中间挂了怎么办

单机数据库一句 ROLLBACK 就撤了。跨四个服务,前两步已经在别人家的库里落了地——你撤不了,只能反着做一遍。

建订单 订单服务 扣库存 库存服务 扣钱 支付服务 ! 发货 物流服务 余额不够,失败了 → 把做过的反着做一遍 关掉订单 订单服务 库存还回去 库存服务 没走到这一步 跨服务没有「回滚」,只有「反向再做一次」。

这叫 Saga。三条铁律:每一步都要能被反着做;每个反向动作也要幂等(补偿本身也会重试);补偿失败要有人工兜底,别让它自己无限转圈。
至于两阶段提交(2PC)——它把所有参与者锁在原地等协调者发话,协调者一挂全体僵死,所以业务系统里基本不用。

会咬人的地方

五个躲不掉的坑

这些不是「优化项」,是不做就一定出事的东西。玩具和企业级的差别全在这一节。

网一断,两边都以为自己是老大,都在写

叫脑裂。恢复后两份数据都改过,谁也不是对的。对策:当主的条件是拿到过半数选票,拿不到的那一半必须自己下线——宁可少一半算力,也不能有两个主。租约(lease)要设得比故障检测超时还短。

拿两台机器的时间戳比先后,结果比反了

机器的墙上时钟会漂移,还会被 NTP 往回拨。永远别用时间戳判断因果。对策:用单调递增的版本号、序列号,或者带因果关系的逻辑时钟。真要靠时间(比如 Spanner),得先花钱买原子钟。

一个不起眼的下游变慢,整个站跟着挂

线程全堵在那儿等它,谁也接不了新活,一层层往上传染。对策四件套:超时(等 200 毫秒就撒手)、熔断(连续失败就先别打了)、舱壁(给每个下游单独的线程池,别共用)、降级(推荐位加载不出来就显示默认的)。

一个请求走了 12 个服务,出了事没人知道卡在哪

单机看日志就行,分布式里日志散在 12 台机器上,还对不上号。对策:入口生成一个 traceId 一路透传,日志、指标、链路三样都带上它。这套东西要在第一个服务拆出去的当天就建好,不是等出事再补。

同一个数据三个服务都在改,谁也说不清以谁为准

对策:每份数据只有一个主人服务,别人只能通过它的接口改,自己那份是只读副本。再配一个天天跑的对账任务,把不一致的挑出来——不是防止不一致,是保证发现得比用户早。

落地顺序

四步,最难的是第一步

一句话:先证明你真的需要它。分布式买来的每一分可用性,都用复杂度付了账。
  1. 1

    先别拆回到第二节那三条:装不下、挂不起、离太远。一条都不占就别动。拆早了,你会花 80% 的时间处理网络问题,而不是做业务。

  2. 2

    按业务切切的标准是:拆完每个服务能自己把一件事做完。要是每次请求都得叫上另外五个服务,你只是把函数调用换成了网络调用——更慢、更容易坏、还多了一堆超时要处理。

  3. 3

    写死它怎么坏每个依赖都要提前写清楚:超时多少、重试几次、失败了返回什么。写不出降级方案的依赖,就不该是依赖。顺手把 traceId 和监控铺上,别等出事再补。

  4. 4

    主动杀上线前自己 kill 一台看看,拔一次网线试试。没演练过的容灾方案等于没有——真出事那天你会发现主从切换的脚本三个月前就跑不通了。

最后一句实话:微服务是组织规模逼出来的产物,不是技术追求。三个人的团队拆十二个服务,是在给自己挖坑。

单机系统里,出故障是意外
分布式系统里,出故障是日常