整体架构与高可用
双 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,它的指标就被切碎成两份,告警关联、趋势对比全是错的。
所以两个目标:
- Master 挂了,另一台秒级顶上,Agent 无感
- 同一台 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表,AlertSyncInterval5s 从 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 池上。
下一篇写技术栈选型。