分布式系统填坑记:被 CAP 定理按在地上摩擦的血泪史
前言:只要加机器就能解决性能问题?
刚入行的时候,我以为分布式系统就是“一台机器扛不住,就加两台”。
直到又一次,生产环境的 Redis 集群即使有 3 个副本,依然读到了旧数据,导致用户账户余额显示错误,差点引发资损。
排查了一整夜,最后发现是网络分区导致的数据不一致。
那一刻,CAP 定理 像一块砖头一样狠狠地砸在了我的脸上的。
这篇文章不是教科书式的理论分析,而是我在分布式泥潭里摸爬滚打出来的血泪经验。
1. CAP 定理:不可能三角
Eric Brewer 告诉我们,在一个分布式处理数据系统中,不可能同时满足:
- C (Consistency, 一致性): 每次读取都能读到最新的写入。(不是事务的一致性,而是线性一致性)。
- A (Availability, 可用性): 每个请求都能在有限时间内得到非错的响应。(哪怕数据是旧的)。
- P (Partition Tolerance, 分区容错性): 允许网络丢包、断连。
因为网络肯定会断(光缆被挖断,交换机重启),所以 P 是必须满足的。
剩下的就是 CP vs AP 的抉择。
血淋淋的案例:Redis vs Zookeeper
Zookeeper 是 CP 系统。
当主节点挂掉,选主期间(几秒到几十秒),整个集群拒绝服务。它宁可不干活,也不能告诉你错误的信息。这适合做服务发现、分布式锁。Redis (主从+Sentinel) 倾向于 AP。
Master 挂了,Slave 还没同步完最新的数据。Sentinel 提升 Slave 为新 Master。此时 client 读到的数据就丢失了刚才那几毫秒的写入。但服务一直可用。
我的那个余额 bug,就是因为我们把 Redis 当作强一致性数据库来用了。
2. 强一致性的代价:Paxos 与 Raft
为了追求一致性,我们引入了共识算法。最著名的其实是 Raft(Etcd, TiKV 都在用)。
Raft 的核心是 Look-on-Majority(少数服从多数)。
- Leader Election: 大家都投我,我就是老大。
- Log Replication: 我收到写入请求,先记小本本(Log),然后群发给 Follower。
- Commit: 收到超过半数 Follower 的 “OK”,我才告诉 Client “写成功了”。
这个过程虽然安全,但是慢。
每次写入都要跨网络往返 RTT。如果你的节点跨机房(比如北京-上海),光速限制了你的 TPS 上限。
脑裂 (Split Brain)
最可怕的场景:网络把机房切成两半。
- A区:3个节点。
- B区:2个节点。
Raft 很聪明,A区能凑齐多数,继续选主工作;B区凑不齐,自动停止写服务。
但如果你用的自研的简单主从复制(MySQL 主主同步),没有 Arbiter,可能两边都觉得自己是 Master,都在接受写入。等网络恢复时,数据就彻底乱了(冲突)。
3. 最终一致性 (Eventual Consistency)
鉴于强一致性太贵,互联网公司大都选择了妥协:BASE 理论。
Basically Available, Soft state, Eventual consistency.
典型的代表是 Dynamo (Amazon) 和 Cassandra。
它们引入了 Quorum 机制:$N, W, R$。
- $N$: 副本数 (3)
- $W$: 写入多少个算成功 (1) -> 极速写入,但极不安全。
- $R$: 读取多少个算成功 (1)
如果我们设置 $W=1, R=1$,那就是极致的 AP。
如果我们设置 $W + R > N$(比如 $W=2, R=2$),根据鸽巢原理,读取的副本里一定包含最新的那个写入。这就是强一致性了吗?
不!还有时钟问题。
4. 最大的坑:分布式时钟
在单机上,System.currentTimeMillis() 是可信的。
在分布式系统里,时间是不可信的。
机器 A 和机器 B 的时间可能差几百毫秒。Google 的 TrueTime API 用原子钟和卫星才把误差压到 7ms。
当你在 Cassandra 里用 Last-Write-Wins (LWW) 策略解决冲突时,那个“Last”到底是谁的 Last?
如果一个旧数据的机器时间快了 1 秒,它可能会覆盖掉新数据!
Vector Clock (向量时钟) 是解决方案,但它带来了存储成本的膨胀。
5. 总结:如何在泥潭中求生
在设计分布式系统时,永远要问自己三个问题:
- 数据丢了能不能接受? (不能 -> CP / Raft)
- 服务停几秒能不能接受? (不能 -> AP / Gossip)
- 如果网络断了,我的系统会变成什么样? (脑裂?降级?)
没有完美的架构,只有最适合业务场景的权衡 (Trade-off)。
如果你的业务只是统计点赞数,请毫不犹豫地拥抱 AP,丢几个赞没人会在意。
如果你的业务是银行转账,请死守 CP,哪怕系统因为网络抖动不可用,也不能算错一分钱。
把复杂性留给中间件(Etcd, ZooKeeper),把简单留给业务逻辑。 这就是我填了无数坑之后的所谓“智慧”。