上班路上,我把分布式想通了

87 阅读6分钟

上班路上,我把分布式想通了

今天早上挤地铁的时候,脑子里突然把一些东西串起来了。

地铁上人挤人,手机也懒得掏,脑子反而闲下来了。不知道怎么就想到昨天排查的那个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

主节点挂掉后:

123.jpg

服务之间怎么玩?

有了基础设施,服务之间怎么协作呢?

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看起来功能挺多的,调度中心、执行器、任务管理……但你拆开来看,它其实就是这么几个东西的组合:

  1. 分布式锁:保证同一时刻只有一个调度中心在调度
  2. 任务池:时间轮 + MySQL队列
  3. 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的流复制……这些我后面会继续写。


最后想问问大家:

  • 你们在做分布式架构的时候,有没有类似的"突然想通"的时刻?
  • 上面这个分类方式,你觉得有没有遗漏的地方?
  • 有没有什么中间件,你觉得看起来复杂但其实拆开来很简单的?

欢迎在评论区聊聊,互相交流一下。毕竟架构这东西,很多时候不是看书看出来的,是踩坑踩出来的。