上班路上,我把分布式想通了
今天早上挤地铁的时候,脑子里突然把一些东西串起来了。
地铁上人挤人,手机也懒得掏,脑子反而闲下来了。不知道怎么就想到昨天排查的那个Redis集群问题,然后想到PostgreSQL的主从切换,又想到前阵子折腾的xxl-job……
想着想着,突然发现这些东西长得好像:
- 都是一堆节点组成集群
- 都要选出一个主来
- 都要把数据同步到其他节点
- 都怕脑裂、都怕数据不一致
然后就有种感觉:这不都是一回事吗?
做架构这么多年,各种中间件用了一堆,什么RocketMQ、Redis、PostgreSQL、Kubernetes……每个都能单独拿出来讲三天三夜。但今天突然觉得,这些东西本质上都在解决同一个问题。
我试着用一句话概括:技术架构的核心就是分布式,业务架构的核心就是微服务。
不一定精准,但方向大概是这么个意思。
写到这里我犹豫了一下,要不要把这种"不精准"的感悟写出来。毕竟网上那些教程都是严谨的,什么CAP定理、BASE理论、分布式一致性的各种级别……我这种大白话的归纳,会不会被喷"不专业"?
后来想想,算了,还是写。
精准的教程到处都有,官方文档、源码分析、最佳实践,搜一下一大堆。但这些东西有个问题:它们告诉你"是什么",不告诉你"为什么要这样想"。
我今天这个感悟,说白了就是做了二十年之后脑子里形成的一个直觉。这种直觉没法从教程里学,它是踩了无数坑、做了无数选型、半夜被无数次告警叫醒之后,自然长出来的一个东西。
教程是地图,这种感性归纳是方向感。有地图的人能按图索骥,有方向感的人扔到陌生地方也能大概知道往哪走。两个都有用,但后者更难得。
所以这篇文章不追求精准,就是聊聊我的"感觉"。如果你觉得哪里不对,欢迎来喷,我们评论区见。
好,言归正传,今天就聊聊技术架构这块。
分布式到底在搞什么?
想来想去,分布式要解决的东西其实就这么几样:
- 数据库要分布式、要高可用(PostgreSQL、MySQL)
- 缓存要分布式、要高可用(Redis)
- 文件系统要分布式、要高可用(MinIO、S3)
- 业务系统要分布式、要高可用(Nginx、Higress、Spring Gateway)
没了。就这些。
graph TB
subgraph 接入层
A[Nginx / Higress / Spring Gateway]
end
subgraph 业务层
B1[服务A]
B2[服务B]
B3[服务C]
end
subgraph 存储层
C1[(数据库<br/>PG/MySQL)]
C2[(缓存<br/>Redis)]
C3[(文件<br/>MinIO)]
end
A --> B1
A --> B2
A --> B3
B1 --> C1
B1 --> C2
B2 --> C1
B2 --> C3
B3 --> C2
B3 --> C3
你看,不管是数据库、缓存、文件系统还是业务系统,它们要解决的问题是一样的:数据怎么复制、故障怎么切换、状态怎么同步。
把这些串起来需要什么?
光有这些组件还不够,还得有东西把它们串起来。我当时在地铁上想了想,大概是这么几个:
分布式选主
集群里谁是老大?这个问题看起来简单,实际上挺复杂的。Raft协议就是干这个的。我之前写Simple Raft系列的时候,才真正搞明白这里面的门道。
sequenceDiagram
participant F as Follower
participant C as Candidate
participant L as Leader
Note over F: 超时,没收到心跳
F->>C: 转变为Candidate
C->>C: term++,投票给自己
C->>F: 请求投票 RequestVote
F->>C: 投票响应
Note over C: 获得多数票
C->>L: 成为Leader
L->>F: 开始发送心跳
分布式锁
多个节点要操作同一个资源,怎么保证不打架?分布式锁。Redisson、ZooKeeper都是干这个的。
sequenceDiagram
participant S1 as 服务实例1
participant R as Redis
participant S2 as 服务实例2
S1->>R: SETNX lock_key value1 EX 30
R-->>S1: OK(获取锁成功)
S2->>R: SETNX lock_key value2 EX 30
R-->>S2: FAIL(锁被占用)
Note over S1: 执行业务逻辑...
S1->>R: DEL lock_key
R-->>S1: OK(释放锁)
数据复制
这个各家有各家的叫法,数据库里叫WAL或者binlog,消息队列里叫commitlog,但本质上都是一个东西——日志复制。
graph LR
subgraph 主节点
A[写操作] --> B[WAL/Binlog]
end
subgraph 从节点1
B --> C[重放日志]
C --> D[(数据副本)]
end
subgraph 从节点2
B --> E[重放日志]
E --> F[(数据副本)]
end
主从切换
主节点挂了怎么办?Keepalived + 虚拟IP,经典套路了。
graph TB
subgraph VIP漂移
VIP[虚拟IP: 192.168.1.100]
end
subgraph 主节点
M[Master<br/>Keepalived MASTER]
end
subgraph 从节点
S[Slave<br/>Keepalived BACKUP]
end
VIP --> |正常情况| M
M --> |主从复制| S
style M fill:#90EE90
style S fill:#FFB6C1
主节点挂掉后:
服务之间怎么玩?
有了基础设施,服务之间怎么协作呢?
RPC调用
服务之间要通信,最直接的就是RPC。Dubbo、gRPC、Feign,底层都是这个套路。
sequenceDiagram
participant C as 服务消费者
participant R as 注册中心
participant P as 服务提供者
P->>R: 注册服务
C->>R: 订阅服务
R-->>C: 推送服务地址列表
C->>P: RPC调用
P-->>C: 返回结果
分布式事务
跨服务的操作要保持一致性怎么办?这块我用Seata比较多,之前也写过源码分析。说实话这块坑挺多的,AT模式看起来简单,真到生产环境各种问题。
sequenceDiagram
participant TM as 事务管理器
participant TC as 事务协调者
participant RM1 as 资源管理器1
participant RM2 as 资源管理器2
TM->>TC: 开启全局事务
TC-->>TM: 返回XID
TM->>RM1: 执行分支事务(携带XID)
RM1->>TC: 注册分支事务
RM1-->>TM: 本地提交
TM->>RM2: 执行分支事务(携带XID)
RM2->>TC: 注册分支事务
RM2-->>TM: 本地提交
TM->>TC: 提交全局事务
TC->>RM1: 通知提交
TC->>RM2: 通知提交
xxl-job,想通了就很简单
这个是我今天在地铁上想明白的一个点。
xxl-job看起来功能挺多的,调度中心、执行器、任务管理……但你拆开来看,它其实就是这么几个东西的组合:
- 分布式锁:保证同一时刻只有一个调度中心在调度
- 任务池:时间轮 + MySQL队列
- RPC:调度中心和执行器之间的通信
就这三样,组合起来就是一个分布式任务调度系统。
graph TB
subgraph 调度中心
A[时间轮] --> B[任务队列<br/>MySQL]
B --> C[调度线程]
D[分布式锁] --> C
end
subgraph 执行器集群
E[执行器1]
F[执行器2]
G[执行器3]
end
C --> |RPC调用| E
C --> |RPC调用| F
C --> |RPC调用| G
时间轮这个东西也挺有意思的,本质上就是把任务按执行时间分桶:
graph LR
subgraph 时间轮
S0[槽0<br/>当前]
S1[槽1<br/>1秒后]
S2[槽2<br/>2秒后]
S3[槽3<br/>3秒后]
S4[...]
S5[槽59<br/>59秒后]
end
S0 --> S1 --> S2 --> S3 --> S4 --> S5
S5 --> |循环| S0
T1[任务A] --> S2
T2[任务B] --> S2
T3[任务C] --> S5
想通了这个,再去看DolphinScheduler,它不就是在xxl-job的基础上加了个DAG吗?任务之间有依赖关系,形成一个有向无环图:
graph LR
A[数据抽取] --> B[数据清洗]
A --> C[数据校验]
B --> D[数据聚合]
C --> D
D --> E[数据入库]
E --> F[生成报表]
再往上就是运维的事了
有了上面这些,分布式系统的骨架就搭起来了。再往上做什么?
- 高可用:主备切换、故障转移
- 备份:定期备份、增量备份
- 恢复:故障恢复、数据恢复
- 监控:Prometheus + Grafana,或者Nightingale
- 补偿:任务失败重试、数据对账
graph TB
subgraph 监控告警
M[Prometheus/Nightingale] --> G[Grafana]
M --> A[告警中心]
end
subgraph 日志系统
L[ELK/Loki]
end
subgraph 链路追踪
T[SkyWalking/Jaeger]
end
subgraph 业务系统
S[微服务集群]
end
S --> M
S --> L
S --> T
A --> |告警通知| N[运维人员]
如此而已。
画个全景图
把上面说的都放一起,大概就是这么个样子:
graph TB
subgraph 接入层
GW[网关<br/>Nginx/Higress/Gateway]
end
subgraph 服务层
SVC1[服务A]
SVC2[服务B]
SVC3[服务C]
end
subgraph 中间件层
MQ[消息队列<br/>RocketMQ/Kafka]
CACHE[缓存<br/>Redis Cluster]
REG[注册中心<br/>Nacos]
LOCK[分布式锁<br/>Redisson]
JOB[任务调度<br/>xxl-job]
end
subgraph 存储层
DB[(数据库<br/>PG/MySQL)]
FILE[(文件存储<br/>MinIO)]
end
subgraph 运维层
MON[监控<br/>Prometheus]
LOG[日志<br/>ELK]
TRACE[链路<br/>SkyWalking]
end
GW --> SVC1
GW --> SVC2
GW --> SVC3
SVC1 --> MQ
SVC2 --> MQ
SVC1 --> CACHE
SVC2 --> CACHE
SVC3 --> CACHE
SVC1 --> REG
SVC2 --> REG
SVC3 --> REG
SVC1 --> DB
SVC2 --> DB
SVC3 --> FILE
SVC1 --> LOCK
JOB --> SVC1
JOB --> SVC2
SVC1 --> MON
SVC1 --> LOG
SVC1 --> TRACE
写在最后
这篇文章算是我的一个阶段性总结吧。做了这么多年架构,有时候反而容易陷在细节里,忘了退一步看全局。
今天早上地铁上的那个"顿悟",可能也算不上什么真正的顿悟,就是把以前零散的东西串起来了。写下来一方面是自己理理思路,另一方面也希望能给其他人一些启发。
当然,这里面每一个点展开都能写一篇长文。比如Raft协议的细节、Redis Cluster的槽位分配、PostgreSQL的流复制……这些我后面会继续写。
最后想问问大家:
- 你们在做分布式架构的时候,有没有类似的"突然想通"的时刻?
- 上面这个分类方式,你觉得有没有遗漏的地方?
- 有没有什么中间件,你觉得看起来复杂但其实拆开来很简单的?
欢迎在评论区聊聊,互相交流一下。毕竟架构这东西,很多时候不是看书看出来的,是踩坑踩出来的。