01-整体架构与高可用

整体架构与高可用

双 Master、Keepalived VIP、Nginx 双栈、8004 Agent 8h 长稳

0. 一句话

8004 台 Agent,双 Master 各 2C2G,跑 8 小时长稳,DB 连接稳定在 17-20 不爬升。高可用不靠堆机器,靠无状态设计和路由黏性。

1. 问题

监控系统有个逃不掉的要求:Agent 端不能因为 Master 挂了一次就丢数据、断采集。单机 Master 的方案我直接排除了------不是会不会挂的问题,是挂了之后 8004 台机器全部失联,这个事故等级我担不起。

另一个问题容易被忽略:Agent 上报必须"黏"在同一个 Master 上。如果同一台机器这次打到 Master-01、下次打到 Master-02,它的指标就被切碎成两份,告警关联、趋势对比全是错的。

所以两个目标:

  1. Master 挂了,另一台秒级顶上,Agent 无感
  2. 同一台 Agent 始终路由到同一台 Master,数据不切碎

2. 架构

bash 复制代码
Agent × 8004
    ↓ 上报(mTLS)
Nginx 四层 :8080(stream,hash $remote_addr consistent,双节点)
    ↓
Master-01 / Master-02 :8080(双节点,共享 MySQL,不互调 HTTP)
    ↓
MySQL 8.0(单库 mon_master)
    ↑
Nginx 七层 :443(http,round-robin,双节点)
    ↑
server-api :8001 × 2 + 前端 Vue(静态文件走 443)

2.1 Keepalived VIP:组播的坑,单播来填

双节点跑 VRRP,对外暴露一个 VIP。VRRP 默认用组播地址 224.0.0.18 互发心跳,我在虚拟化环境第一次部署就踩了坑:桥接网卡上组播帧经常被交换机丢弃,两个节点收不到对方的通告,都认为自己是主------脑裂,VIP 同时出现在两台机器上,流量就乱了。

解法是显式配置 unicast_peer 指定对端 IP,VRRP 心跳改走单播。组播变单播,丢弃问题绕开了,代价只是配置多写两行。

健康检查不是只盯 VIP 本身(那是 Keepalived 自己活着,业务早挂了),而是 track_script 同时探 Nginx 进程和 Master 进程:主节点 priority 100,任一脚本失败就降优先级,backup(priority 90)检测到比自己高了就抢占 VIP。探测脚本失败 → 降优先级 → 漂移,整个链路是"业务不健康就切",不是"Keepalived 不健康才切"。

2.2 Nginx 双栈:七层给人,四层给机

一台 Nginx 上同时起七层和四层两种配置,作用完全不同。

七层(http,:443) ------人流量:Web 页面 + API 请求。upstream api_backend + round-robin,双 API 节点分摊,TLS 终止在 Nginx 层。

四层(stream,:8080) ------机流量:Agent 上报。hash $remote_addr consistent 黏性路由,同一 IP 永远落同一 Master。

为什么四层不是七层?两个原因。第一,Agent 上报是 mTLS 双向认证,证书校验(CA 验签、吊销检查)必须在 Master 完成;Nginx 七层一旦做 TLS 终止,客户端证书链就被吞了,没法把原始 mTLS 会话透传给后端,硬要做(proxy_ssl 重新握手)又引入证书分发和兼容问题。四层只做 TCP 流转发,TLS 流原样透给后端 Master,mTLS 在两端正常协商,Nginx 不参与、不知道证书内容。第二,四层没有 HTTP 协议解析开销,8004 台 Agent 每 10s 一轮的上报 QPS 下,四层的 CPU 和延迟都更省。

consistent 是关键,这里有个技术细节值得说:普通 hash $remote_addr 是对节点数取模,节点增删时几乎全部 Agent 都会重新哈希、全部迁移;consistent(一致性哈希)把 Agent 的 IP 映射到一个哈希环上,节点增删只影响环上相邻的一段,只有约 1/N 的 Agent 需要迁移,其余不动。双 Master 故障切换时,本来就该把流量切给对端,一致性哈希的价值在"日常不迁移、切换只动该动的"。

2.3 双 Master 无状态:状态放 DB,天然一致

Master 之间不互调 HTTP,这是刻意设计------互调就有循环依赖和故障传播。状态全部放共享 MySQL:

  • 配置:master_config 表,atomic.Value 存指针,DB 10s 轮询检测变更,所有 goroutine 下一轮循环读到新值,无锁读
  • 告警:alert 表,AlertSyncInterval 5s 从 DB 全量同步活跃告警到内存,缓存与 DB 不一致时以 DB 为准。两个细节:告警防抖状态(CoolDown)也持久化到 DB,Master 重启后从 DB 恢复冷却期,不会重启即告警风暴;离线巡检直接查 DB 活跃告警(FindActiveAlert),绕过内存缓存,双 Master 并行巡检时不会重复插入同一条告警
  • 设备:agent_info 表,注册/续签/心跳都直接写 DB

另外每个 Master 每 30s 做一次自检,写 master_status 表:进程内调一次 /health 路由(不占网络端口)、DB ping + SELECT 1、再带上 Worker 数、队列长度、goroutine 数、堆内存、GC 频率这些运行时指标。这张表就是双 Master 健康状态的唯一事实源,API 侧的"Master 节点状态"页面直接读它。

为什么这样?两个 Master 各自持有本地状态,切主时状态会分叉;状态放 DB,天然一致------代价是多一次轮询的延迟(秒级),监控场景完全容忍。

3. 取舍

为什么双 Master 只有 2C2G 够用? 上报链路不在 Master 的 HTTP 层,在三级流水线(队列→Worker→攒批)。瓶颈在应用层 Worker 池(min 2 / max 8),不在 Master 实例数量。加 Master 解决不了 Worker 池吞吐------这是压测后才明白的,36004 Agent 瞬时压力下 DB 连接只有 33,瓶颈明确在 Worker 池。

为什么不用 Redis 做缓存? 双 Master 若各自持有本地缓存,切主时缓存会分叉。缓存放 DB 天然一致,10s 轮询延迟可容忍。API 网关侧的查询缓存是"全量快照 + 原子指针交换"(每 10s 重建),也不需要 Redis。

为什么前端要挂 443 而不是独立端口? 减少暴露面:对外只有一个 443 端口,HTTP/HTTPS 流量全在这,防火墙规则简单。

4. 验证

压测数据来自 docs/reports/压测报告-2核2G支撑8004Agent长稳.md,阶梯加压:

阶段 Agent 数 时长 DB 连接 现象
基线 800 --- 16-18 正常
长稳 8004 8h+ 17-20 不爬升,分钟增量 48,000 行(= 8004×6),零积压零丢失
中压 10004 ~4h 24-37 仍稳定
瞬时 20004 2min 30-33 正常
瞬时 36004 3min 33 分钟增量卡在 48,000(预期 216,000),Worker 池到顶

结论:Agent 数翻 4.5 倍,DB 连接不到翻倍------攒批生效;36004 时瓶颈在应用层 Worker 池,不在架构层、不在 MySQL。高可用架构在 8h 长稳 + 36004 瞬时下没有一处崩溃点。

故障切换验证:Keepalived 健康检查探到 Master 异常 → 降优先级 → VIP 漂移 → Agent 四层 hash 仍路由到 VIP → 落到另一台 Master。因为 Master 无状态,切换后无需任何状态迁移;Agent 侧唯一的感知是本次上报可能失败一次,走离线缓存补发兜住。

5. 回看

高可用这块做对的是"无状态 + 黏性路由"的组合:无状态让切换变得简单,黏性让数据不切碎。做错或者说没想到的是------高可用解决了"Master 挂了怎么办",但没解决"Master 没挂却写不动"(Worker 池瓶颈),这是压测才暴露的。

另一个回看:双 Master 其实没有发挥"负载分担"的价值------两个 2C2G 各自独立处理全部 Agent(hash 分摊),单 Master 也有 17000 Agent 的拐点。双 Master 的价值是可用性,不是性能。如果当初明白这点,压测时的注意力会早一点放到 Worker 池上。

下一篇写技术栈选型。

相关推荐
程序员爱钓鱼3 小时前
Go if 判断详解
前端·后端·go
名字还没想好☜17 小时前
Go 的 unsafe.Pointer 实战:零拷贝 []byte↔string 转换与三条铁律
开发语言·后端·golang·go·unsafe
阿里云云原生18 小时前
阿里云联合 Datadog,补齐 Go 可观测性最后短板
云原生·go
可观测性用观测云19 小时前
零码改造!Go 语言应用上报观测云完整最佳实践
go
斐波那契数列21 小时前
参考react 实现一个golang gui
go
程序员爱钓鱼1 天前
Go 运算符详解
后端·面试·go
angryshan2 天前
goLang配置断点流程
go·bug
名字还没想好☜2 天前
Go 的 sync.Cond 实战:用条件变量做等待/通知,比忙轮询省 CPU
开发语言·数据库·后端·golang·go
程序员爱钓鱼2 天前
Go 输入输出详解
后端·面试·go