有状态服务同城双活

wallhaven-ly9kzp

服务介绍

serverA是一个数据仓库服务,提供数据的版本管理,储存和下载等功能。

线上服务变更大致可分为代码变更、配置变更、数据变更,serverA广泛用于线上数据变更。

由于数据处理对磁盘 IO 性能要求较高(如顺序写入和随机读取),相比网络存储(如Ceph,对象存储等),本地磁盘能提供更低的延迟和更高的吞吐量,因此被选为存储介质。这也使其归属于有状态服务。

同城双活架构

flowchart TB
    %% 定义城市A
    subgraph 城市A
        direction LR
        subgraph IDC1["IDC1"]
            clientA1["客户端"]
            ingressA1["接入层(nginx)"]
            serverA1["serverA"]
            storageA1["存储层"]
            clientA1 --> ingressA1
            ingressA1 --> serverA1
            serverA1 --> storageA1

            subgraph storageA1["存储层"]
                mysql1[(MySQL主)]
                redis1[(Redis)]
                zk1_1[(ZK)]
                zk1_2[(ZK)]
            end
        end

        subgraph IDC2["IDC2"]
            clientA2["客户端"]
            ingressA2["接入层(nginx)"]
            serverA2["serverA"]
            storageA2["存储层"]
            clientA2 --> ingressA2
            ingressA2 --> serverA2
            serverA2 --> storageA2

            subgraph storageA2["存储层"]
                mysql2[(MySQL从)]
                redis2[(Redis)]
                zk2_1[(ZK)]
                zk2_2[(ZK)]
            end
        end

        subgraph IDC3["IDC3"]
            storageA3["存储层"]

            subgraph storageA3["存储层"]
                zk3[(ZK)]
            end
        end
    end

    %% 外部IDC(交换IDC4和IDC6)
    subgraph IDC5
        client4["客户端"]
    end
    subgraph IDC4
        client5["客户端"]
    end
    subgraph IDC6
        client6["客户端"]
    end

    %% 客户端连接
    client6 --> ingressA2
    client5 --> ingressA1
    client4 --> ingressA1

    %% 添加样式
    classDef client fill:#f9f,stroke:#333;
    classDef ingress fill:#bbf,stroke:#333;
    classDef serverA fill:#bfb,stroke:#333;
    classDef 存储层 fill:#fbb,stroke:#333;
    classDef mysql fill:#ffd,stroke:#333;
    classDef redis fill:#fdd,stroke:#333;
    classDef zk fill:#dfd,stroke:#333;

    class clientA1,clientA2,client4,client5,client6 client;
    class ingressA1,ingressA2 ingress;
    class serverA1,serverA2 serverA;
    class storageA1,storageA2,storageA3 存储层;
    class mysql1,mysql2 mysql;
    class redis1,redis2 redis;
    class zk1_1,zk1_2,zk2_1,zk2_2,zk3 zk;

插个题外话,这里用mermaid画的架构图,推荐大家使用。

mermaid VS PNG

优点

  1. 可以方便的修改
  2. 版本管理友好,可以和代码一起放到git仓库。

缺点

  1. 有门槛,需要掌握专门的语法。
    1. 这个缺点现在已不是问题,通过AI可以方便的用自然语言画图。

整体来说是利大于弊的。

为什么没有做单元化,实现异地多活?

serverA的业务场景存在跨机房使用,例如IDC1上传的数据,在IDC2进行下载使用,本身就没有机房流量闭环。

如果将用户上传的数据同步到所有IDC,但是实际只有部分IDC使用数据,那么将会浪费大量带宽,磁盘。

存储层双活

存储层会通过同城的网络专线进行数据传输,保证较低的延迟。

Mysql

Mysql通过主从部署,IDC2里serverA的写流量会通过中间件转发到IDC1里的Mysql主库。

主从同步通过 binlog 实现,Master 记录写操作日志,Slave 拉取并重放日志,支持异步模式(高性能但有丢失风险)和半同步模式(折中方案,半同步模式要求 Master 收到至少一个 Slave 确认后提交事务,平衡了性能与安全性)。

在IDC1机房故障的情况下,可通过切主操作,使IDC2正常提供服务。

Redis

Redis Sentinel 可实现主从自动切换,Redis Cluster 提供分片和高可用,适合容灾场景。

由于跨机房的网络专线存在延迟,导致主从复制存在毫秒级延迟,IDC1里写入的数据,IDC2里不能马上可见。

对于一致性敏感的操作,例如通过redis实现分布式锁,就需要应用层保证单机房闭环。也就是IDC1里的应用加的锁,只有IDC1里使用,IDC2里不会用到。

或者仅仅把Redis当做缓存来使用。

ZK

ZK采用 221 部署(共 5 节点),基于 Paxos 协议,需至少 3 节点存活以维持服务,适合跨 IDC 的高可用场景。IDC1,IDC2,IDC3任意机房挂掉,整个ZK集群依旧可以提供服务。

机房故障下切流

通过 DNS 切流和备用域名,保障了服务在机房故障下的可用性。

flowchart TD
    A[故障感知]
    B[DNS 解析切换]
    C[TTL 生效]
    D[流量切至存活机房]
    E[使用备用域名]

    A --> B
    B --> C
    C --> D
    A --> E
    E --> D

    classDef event fill:#f9f,stroke:#333;       %% Light Pink for events/triggers
    classDef primaryAction fill:#bbf,stroke:#333;  %% Light Blue for primary actions
    classDef alternativeAction fill:#fbb,stroke:#333; %% Light Red for alternative actions
    classDef outcome fill:#bfb,stroke:#333;      %% Light Green for outcomes

    class A event;
    class B,C primaryAction;
    class E alternativeAction;
    class D outcome;

DNS切流

在IDC1或IDC2机房故障的时候,需要操作各个机房的DNS服务,将serverA域名指向故障机房的解析,改为指向存活机房。这个止损动作可以考虑自动化实现,减少人工干预,提升切换效率。

各IDC的域名解析TTL建议设置为60s,避免DNS缓存时间过长,影响切流生效时间。

备用域名

serverA包含了前端,用户在内网通过域名访问。

在IDC1机房故障时,需要通过域名解析平台,将域名切到IDC2。但这强依赖于域名解析平台。

出于稳定性,服务重要性,以及降低RT(故障恢复时间)等因素考虑,可以通过配置备用域名的方式来解决。

新增一个备用域名,解析到IDC2,在IDC1故障时,直接用备用域名访问到前端页面。

基础管控服务建议统一备用域名规范。例如命名,主域名a.b.com,备用域名a-backup.b.com。这样便于故障期间各团队同学能准确使用备用域名进行止损操作。

数据一致性

IDC之间

用户数据存到serverA,这个动作抽象为一个备份任务。创建备份任务时,同时往ZK里的两个队列写入任务。IDC1内的serverA从队列1里消费备份任务,IDC2内的serverA从队列2里消费备份任务。达到最终一致性效果。

flowchart TD
    P["备份任务生产者"]

    subgraph Zookeeper
        direction LR
        Q1["ZK Queue 1 (for IDC1)"]
        Q2["ZK Queue 2 (for IDC2)"]
    end

    subgraph IDC1
        direction LR
        S1_IDC1["serverA instance 1"]
        S2_IDC1["serverA instance 2"]
    end

    subgraph IDC2
        direction LR
        S1_IDC2["serverA instance 1"]
        S2_IDC2["serverA instance 2"]
    end

    P --> Q1
    P --> Q2

    Q1 --> S1_IDC1
    Q1 --> S2_IDC1
    Q2 --> S1_IDC2
    Q2 --> S2_IDC2

    classDef producer fill:#f9f,stroke:#333;
    classDef zk fill:#dfd,stroke:#333;
    classDef idc fill:#bbf,stroke:#333;
    classDef serverA fill:#bfb,stroke:#333;

    class P producer;
    class Q1,Q2 zk;
    class IDC1,IDC2 idc;
    class S1_IDC1,S2_IDC1,S1_IDC2,S2_IDC2 serverA;

在数据不一致的时间窗口内,serverA是否对外提供服务,取决于控制面服务内相应的开关。满足不同用户对于机房之间数据不一致的不同要求。

IDC内

IDC内会部署2各实例,实现高可用。在消费备份任务时,会先抢zk里的分布式锁。拿到锁的实例执行备份任务。同时两个实例之间通过nfs互挂彼此的数据目录。serverA在备份数据时,会将数据同时写入到自己的数据目录和peer的nfs挂载路径。

存在的问题

架构扩展性差,IDC内只能部署两个实例。不过nfs互挂稳定性很好,没出过什么事故,用不着改造。

但后面随着业务的发展,数仓的数据越来越多,流量也越来越大。IDC内两个实例,在高峰期时,带宽被打满,架构问题迫在眉睫。

新架构

针对 v1 架构的扩展性和双写耦合问题,v2 架构进行了如下改进。

zk里的备份任务队列,不再是两个队列(部署serverA的IDC各一个队列),而是每个serverA实例一个队列,独立消费。IDC内,可以水平扩展多个serverA实例,各自消费自己队列里的任务。

flowchart TD
    P["备份任务生产者"]

    subgraph Zookeeper_Queues ["Zookeeper (Per-Instance Queues)"]
        direction LR
        %% Queues for IDC1
        Q_S1_IDC1["ZK Queue (IDC1-Srv1)"]
        Q_S2_IDC1["ZK Queue (IDC1-Srv2)"]
        Q_SN_IDC1["ZK Queue (IDC1-SrvN)"]
        %% Queues for IDC2
        Q_S1_IDC2["ZK Queue (IDC2-Srv1)"]
        Q_S2_IDC2["ZK Queue (IDC2-Srv2)"]
        Q_SN_IDC2["ZK Queue (IDC2-SrvN)"]
    end

    subgraph IDC1 ["IDC1"]
        direction TB
        S1_IDC1["serverA instance 1"]
        S2_IDC1["serverA instance 2"]
        SN_IDC1["serverA instance N"]
    end

    subgraph IDC2 ["IDC2"]
        direction TB
        S1_IDC2["serverA instance 1"]
        S2_IDC2["serverA instance 2"]
        SN_IDC2["serverA instance N"]
    end

    %% Producer to Queues
    P --> Q_S1_IDC1
    P --> Q_S2_IDC1
    P --> Q_SN_IDC1
    P --> Q_S1_IDC2
    P --> Q_S2_IDC2
    P --> Q_SN_IDC2

    %% Queues to serverA instances
    Q_S1_IDC1 --> S1_IDC1
    Q_S2_IDC1 --> S2_IDC1
    Q_SN_IDC1 --> SN_IDC1
    Q_S1_IDC2 --> S1_IDC2
    Q_S2_IDC2 --> S2_IDC2
    Q_SN_IDC2 --> SN_IDC2

    classDef producer fill:#f9f,stroke:#333;
    classDef zk fill:#dfd,stroke:#333;
    classDef idc fill:#bbf,stroke:#333;
    classDef serverA fill:#bfb,stroke:#333;

    class P producer;
    class Q_S1_IDC1,Q_S2_IDC1,Q_SN_IDC1,Q_S1_IDC2,Q_S2_IDC2,Q_SN_IDC2 zk;
    class IDC1,IDC2 idc;
    class S1_IDC1,S2_IDC1,SN_IDC1,S1_IDC2,S2_IDC2,SN_IDC2 serverA;

每个serverA实例执行完成备份任务后,状态写入 DB 后,客户端可动态选择可用实例下载数据,避免了 v1 双写失败导致的服务不可用问题,且吞吐量提升可达数倍(视实例数而定)。

资料

  1. 搞懂异地多活,看这篇就够了
作者:Yuyy
博客:https://yuyy.info
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇