负载均衡
Croupier HA 多实例架构(Server 多实例 HA)有两个流量入口,各自需要负载均衡:
- Dashboard API(L7 / HTTP):Dashboard 前置网关分流到各 Server 实例
:18780 - Agent 接入(L4 / TCP):Agent 的自行开发 transport 长连接(
:19090,长度前缀分帧 + 多路复用)打散到各 Server 实例
为什么 Agent 必须经 L4 LB 而不是直连:直连会导致实例故障时该实例名下所有 Agent 失联需人工干预;经 LB 打散后,断连重连自动分发到存活实例 + 重新注册更新 owner(架构文档 §6 故障转移时间线,全程无人工干预)。
部署模式约束:单活 + 冷备(当前推荐,双活前置已补齐待切换验证)
前提认知:Agent 会话表与函数注册 registry 是 Server 实例的进程内存态,跨实例同步仅覆盖部分路径(/ops/nodes、agent 列表、/functions/instances 走共享归属表聚合——instances 已于 2026-09-11 补齐)。invoke 执行路径已在 2026-09-11 完成归属表接入与转发补全,双活下的调用正确性不再依赖「请求恰好落到持有 agent 的实例」:
- 候选集兜底(RemoteAgentSource):本地候选为空或 failover 耗尽本地候选时,同步查共享归属表 +
agent_sessions快照表(1s 预算,出错降级回no live agent语义不放大故障)补远端候选,选中的远端候选经 mesh 转发到 owner 实例执行 - 三分支转发:同步 invoke、异步任务(start_task,与 invoke 同款 failover——lb 路由失败换候选重试、失败尝试的 task_runs 行标 failed、耗尽映射 503)、任务取消(cancel_task,task routing miss 时从共享
task_runs解析 agent)、广播(候选集 local ∪ remote,按 AgentID 去重本地优先)都走同一帧格式(kind区分),owner 侧定向投递并落审计(不重查 policy,信任边界见docs/architecture/server-ha-multi-instance.md§5.3) refreshRemoteSnapshots的 30s 周期回灌从正确性依赖降级为性能优化层(减少热路径同步查库)- registry 内存会话生命周期与归属表对齐(2026-09-11 补齐,三级清理):本实例断连即时清(
RemoveAgentIfStale的 notAfter 校验挡住断连瞬间重连注册的竞态)、重启恢复按归属表活跃全集过滤快照行、30s 对账周期清理「归属表无行且 5min 无心跳」的孤儿副本——断连 agent 不再以僵尸候选滞留函数视图/lb/broadcast 候选至 ExpireAt(24h)。DBagent_sessions快照行不删不改、自然过期(归属表是跨实例查询的唯一入口,无归属行的快照行无消费者);单实例(cluster 未启用)行为不变:断连即时清生效,重启恢复仍全量
已知边界(切双活前须知):
- mesh 互联为内网明文 TCP、无对端认证——互联端口必须仅集群内网可达
- 哈希路由(
route: hash)跨实例不保证稳定落点:仅本地候选耗尽才扩远端 - 异步任务事件不走 mesh 回传:事件经 owner 落共享库(
task_events),caller 的 HTTP 轮询/SSE 从共享库读取(既有链路不变)
因此当前部署形态仍是单活 + 冷备(双活切换是独立操作,前置条件已凑齐):
| 层 | 配置 | 故障语义 |
|---|---|---|
| L4(haproxy.cfg) | server croupier-server2 ... check backup | agent 全部连主实例;主实例摘除后 agent 重连自动落 backup 并重新注册 |
| L7(nginx-main.conf) | split_clients ... 100% croupier-server | API 全量指向主实例;主实例故障时人工提升此处切到 server2(换一处分流值) |
切换双活(前置已补齐,操作时点自定):
- L7 分流值从
100% croupier-server改为按比例(如50%/50%),nginx -s reload - L4 去掉 server2 的
backup标记(agent 连接开始打散) - 观察:
/ops/nodes两实例都有 agent 归属;dashboard 发起 invoke/async/broadcast 三类调用混合落在两实例均成功(owner 侧审计记录出现在对端实例);server2日志出现cluster: refreshed remote agent snapshot/ owner 侧审计 - 回滚即恢复
100%+backup(agent 会随重连自然回归主实例,期间跨实例调用经转发兜底)
背景概念:L4 / L7 / VRRP
L4 / L7 按协议分层定义
L4/L7 指的是 OSI 模型中的协议层次——负载均衡器工作在哪一层,决定了它看得见什么、能按什么分发:
| 层 | OSI 名称 | LB 看得见什么 | 分发依据 | 典型实现 |
|---|---|---|---|---|
| L4 | 传输层 | IP + 端口(TCP/UDP 头),不解析应用内容 | 五元组哈希、最少连接 | LVS、HAProxy(mode tcp)、nginx stream、云 NLB |
| L7 | 应用层 | 完整 HTTP 请求(方法/路径/Header/Cookie) | 路径路由、Header、会话粘滞、灰度 | nginx http、HAProxy(mode http)、Envoy、云 ALB |
两者的取舍:
- L4 透传字节流:协议无关(自行开发协议也能跑)、长连接天然友好、开销低;但看不见请求内容,只能按「连接」粒度分发,也无法做基于内容的路由
- L7 理解应用语义:能按路径/Header 分流、改写、重试、灰度;代价是必须解析(甚至终结)应用层协议
对 Croupier 的映射:Dashboard API 是标准 HTTP,天然适合 L7;Agent 用的是自行开发二进制分帧协议(长度前缀 + protobuf),LB 无法也无需理解,L4 透传即可——这也是本文两个入口分层选型的依据。
顺带一提 LVS(Linux Virtual Server):工作在内核态的 L4 转发(IPVS),不经过用户态协议栈,性能是所有方案中最高的;代价是配置与生态复杂度,单公司千级节点规模内通常用不到。
VRRP:解决「LB 自己是单点」
VRRP(Virtual Router Redundancy Protocol,RFC 5798) 是一个网络层高可用协议,与负载均衡本身无关:
- 多台主机组成一个虚拟路由器,共享一个虚拟 IP(VIP);对外只暴露 VIP,客户端永远连 VIP
- 协议自动选举 Master(按 priority 最高者)持有 VIP 并应答流量,其余为 Backup 待命
- Master 周期性广播通告(默认 1s 一次);Backup 连续约 3 个周期收不到通告即认定 Master 死亡,按 priority 接管 VIP——秒级故障转移,客户端无感知
- keepalived 是 Linux 上 VRRP 的守护进程实现,常与 nginx/HAProxy 成对部署:LB 进程做分发,keepalived 保证「跑 LB 的这台机器」挂了 VIP 能漂到备机
注意 VRRP 依赖二层组播/单播可达,多数公有云 VPC 内不可用——云上的等价物是云 NLB(跨可用区 + 健康检查 + 免运维)。
概念澄清:keepalived 不是负载均衡器
三者的关系经常被混淆:
- nginx(stream 模块)、HAProxy:负载均衡器,真正分发流量
- keepalived:VRRP 守护进程,只负责 VIP 漂移(主备高可用),本身不负载均衡;通常与 nginx/HAProxy 成对部署,解决「LB 自己挂了」的问题
- 第三种自建高可用 LB 形态是 keepalived + LVS(内核态转发,性能最高)
方案对比
L4 能力对比(nginx stream vs HAProxy)
| 维度 | nginx stream | HAProxy | 对 Croupier 的影响 |
|---|---|---|---|
| 运行时 DNS 重解析 | ❌ upstream 域名只在启动/reload 时解析;实例重建换 IP 后旧 IP 仍被使用(502/连接失败),需手动 reload | ✅ resolvers 块 + resolve-prefer ipv4,容器 IP 变动自动跟随 | 最关键差异:compose 重建 server 容器后,nginx 需 --force-recreate dashboard 兜底,HAProxy 无需任何动作 |
| 主动健康检查 | ❌ 开源版被动探测(max_fails/fail_timeout,连接失败才累计标记) | ✅ option tcp-check / http-check 主动探活,摘除半死实例 | Agent 是长连接多路复用:Server 进程僵死但 TCP 连接未断时,被动探测摘不干净,调用会持续打到死实例 |
| 优雅下线 | ❌ reload 直接断存量连接 | ✅ stats socket 运行时 set server ... state drain:存量连接排空、新连接停入 | Server 实例滚动升级时,HAProxy 可零断连切换 |
| 可观测性 | access log 为主 | ✅ 内置 stats 页:每后端会话数/队列/错误率实时可见 | 排查「agent 连不上/连不均」时直接看图 |
| 性能 | epoll,数万并发连接 | epoll,同量级;均低于 LVS(内核态) | 单公司多游戏(千级节点内)两者都不是瓶颈,不必纠结 |
| 组件成本 | ✅ 复用 dashboard 镜像,零新增 | ❌ 新增组件(镜像/配置/监控面) | 小团队 nginx 的核心优势 |
| L7 能力 | ✅ SPA 静态资源 + API 反代 + SSE 调优一体 | ✅ 但静态资源服务弱于 nginx | Dashboard 已经需要 nginx 服务静态资源 |
结论
就 L4 负载均衡职能而言,HAProxy 全面占优(运行时重解析 + 主动健康检查 + 优雅下线 + 可观测性);nginx stream 唯一优势是复用现有 dashboard 容器、零新增组件。
推荐按部署形态选择:
| 部署形态 | 推荐 | 理由 |
|---|---|---|
单机 compose(docker-compose.deploy.yml) | HAProxy(现状,独立 haproxy 服务) | 统一形态:L4 能力完整(重解析/健康检查/stats),单机与多宿主无需两套配置 |
| 多宿主容器化生产 | HAProxy(agent L4 入口) | IP 变动自动跟随 + 主动摘除僵死实例 + stats 排查;dashboard 的 L7 仍由 nginx 承担 |
| LB 自身不允许单点 | keepalived + HAProxy(主备 VIP 漂移);公有云等价物是云 NLB | VRRP 需要网络层支持,公有云内用 NLB 免运维 |
| Kubernetes | Service(ClusterIP) | kube-proxy 天然负载均衡 + Endpoints 就绪探针,无需自建 LB 层 |
配置示例
nginx stream(备选:复用 dashboard 容器)
# docker/nginx-main.conf(部署产物的一部分)
stream {
upstream croupier_agent_lb {
least_conn; # 按活跃会话数,贴合长连接
server croupier-server:19090; # 单活 + 冷备(见「部署模式约束」):
server croupier-server2:19090 backup; # 主实例故障后 agent 重连落 backup
}
server {
listen 19090;
proxy_pass croupier_agent_lb;
proxy_timeout 1h; # agent 心跳 30s 保活,1h 空闲兜底
proxy_connect_timeout 3s; # 故障快速切换
}
}注意事项:
- upstream 域名只在启动/reload 解析;server 实例重建换 IP 后:
docker compose up -d --force-recreate dashboard - 被动探测:实例僵死但连接未断时不会被摘除,依赖 agent 侧心跳超时重连兜底
HAProxy(本仓库默认,docker/configs/haproxy.cfg)
# /etc/haproxy/haproxy.cfg(agent L4 入口)
resolvers docker_dns
nameserver dns1 127.0.0.11:53 # docker 内嵌 DNS;裸机部署换成环境 DNS
resolve_retries 3
timeout resolve 1s
hold valid 10s # 10s 重解析,实例重建换 IP 自动跟随
defaults
mode tcp
timeout connect 3s
timeout client 1h # agent 长连接会话
timeout server 1h
listen croupier_agent_lb
bind *:19090
balance leastconn
# 主动健康检查:TCP 建连探活(server 的 control listener)
option tcp-check
default-server inter 2s fall 3 rise 2 resolvers docker_dns init-addr libc,none
# 单活 + 冷备(见「部署模式约束」):server2 仅在主实例摘除后承接重连
server server1 croupier-server:19090 check
server server2 croupier-server2:19090 check backup
listen stats # 排查连接分布
bind *:8404
stats enable
stats uri /statsDashboard 的 L7(API 反代 + SPA + SSE)仍由 nginx 承担——两层各司其职:nginx 管「人」(HTTP),HAProxy 管「机器」(agent TCP)。
keepalived + HAProxy(LB 自身高可用)
# /etc/keepalived/keepalived.conf(主 LB 宿主;备机 priority 略低、其余相同)
vrrp_instance VI_CROUPIER {
state MASTER
interface eth0
virtual_router_id 51
priority 100
advert_int 1
virtual_ipaddress {
10.0.0.100/24 # VIP:agent/Dashboard 统一连这个地址
}
}两台宿主各跑一个 HAProxy(配置完全相同),keepalived 维持 VIP 在主上;主挂了 VIP 秒级漂移到备,agent 断连重连自动到备——agent 侧依然零改动。
注意:VRRP 依赖网络层组播/单播支持,多数公有云 VPC 内不可用——云上等价方案是云 NLB(跨可用区 + 健康检查 + 免运维),直接替代 keepalived+HAProxy 整层。
Agent 侧零改动原则
无论入口层怎么换型,Agent 不需要任何改动:单地址连接 + 断线退避重连 + 重新注册(owner 随之更新到新实例)。换 LB 只需改 agent.yaml 的 server.addr 指向新入口。
迁移路径(nginx stream [HAProxy]
- 起新 HAProxy(指向现有 croupier-server/croupier-server2:19090)
- 灰度:先改一个 agent 的
server.addr指向 HAProxy,验证注册与调用正常 - 全量 agent 切换;观察 HAProxy stats 会话分布
- nginx stream 块下线(dashboard 回归纯 L7)
整个过程 Server 集群无感知(成员表按连接归属自动更新 owner)。
LB 监控(设计定案)
状态:Accepted(方案定案,分阶段落地)。核心原则:管道用开源生态,展示做平台原生;数据源可配置,不绑定任何 LB 品牌。
设计演进与结论
监控 LB 的方案经历三轮讨论收敛:
server 端自行开发 LB stats 解析适配器(拉 haproxy CSV)—— 否决:重复造轮子,等价于重新实现 haproxy exporter 的一部分;且绑死 haproxy,换 LB 就废跳转/iframe 嵌 Grafana—— 否决为主案:体验割裂(另一套 UI)、权限绕过平台 RBAC(Grafana 需配匿名/auth-proxy)- 最终定案:采集与存储用 Prometheus exporter 生态(开源),展示层由平台原生渲染(直读 Prometheus API + Ant Design Charts),Grafana 仅作深度分析外链兜底
分层架构
HAProxy 2.4+ 内置 exporter(零新增容器,cfg 一行)
→ Prometheus(存储/历史/聚合/告警,开源 TSDB)
→ Server 只读代理端点(metric 白名单 + 平台 RBAC 鉴权收口)
→ Dashboard 原生图表页 /ops/lb(与 dbmon 同模式:server 拉数、原生渲染)自己做 VS 不做的边界:
| 层 | 自行开发 or 开源 | 理由 |
|---|---|---|
| 采集(exporter) | 开源 | 每个 LB 都有官方/社区 exporter,追不完也不该追 |
| 存储/查询/告警引擎 | 开源(Prometheus) | TSDB 自行开发纯属重复劳动 |
| 查询代理 + 图表 | 自行开发(薄两层) | 鉴权收口 + 体验一致;dbmon 页是同模式先例 |
| 深度分析大盘 | 开源(Grafana 外链) | 平台只做日常对账,不复制 Grafana |
开源 exporter 对照表
| LB | 方案 | 接入成本 |
|---|---|---|
| HAProxy | 2.4+ 内置 Prometheus exporter(http-request use-service prometheus-exporter)——本仓库 haproxy 2.9 原生支持 | 极低(改 cfg) |
| nginx | nginx-prometheus-exporter(nginxinc 官方) | 低(sidecar) |
| keepalived | keepalived-exporter(VRRP 状态/漂移) | 低 |
| LVS/IPVS | prometheus-ipvs-exporter(需宿主权限) | 中 |
| 云 NLB | 云厂商 exporter(AWS yace / 阿里云 aliyun-exporter) | 中(云凭据) |
| 通用兜底 | blackbox_exporter(TCP 探测入口可达/延迟——无管理接口也适用) | 极低 |
Grafana 官方大盘:HAProxy 2(dashboard ID 12693)、NGINX(12708),导入即用。
平台侧核心图表(原生渲染)
| 图 | PromQL(现成) | 对账意义 |
|---|---|---|
| 各后端会话分布 | haproxy_backend_current_sessions | 与归属表 agentCount 对照 |
| 后端健康状态 | haproxy_server_status | UP/DOWN/NMA 实时可见 |
| 请求速率/错误率 | haproxy_backend_http_requests_total / haproxy_backend_errors_total | 流量异常发现 |
| 健康检查失败 | haproxy_server_check_failures | 半死实例探测 |
对账列(僵尸探测器):/ops/cluster 每实例展示「LB 会话数 vs 归属表 agent 数」——两者长期不一致(连接在、心跳停)即半开连接信号,正是 HA 双实例排障中最缺的一屏可见性。
配置化(不写死任何地址)
# server.yaml——部署拓扑事实,支持环境变量覆盖(与 cluster 段同模式)
ops:
prometheusUrl: http://prometheus:9090 # 空 = /ops/lb 页隐藏(单实例/无遥测栈优雅降级)
grafanaUrl: http://grafana:3000 # 可选,配置后出现"深度分析"外链- 未配置 → 页面不出现,零依赖
- 换 LB / 加 keepalived → 只改 prometheus.yml 的 scrape job,平台侧零改动
- 配置归属支持层(ops 段),不进站点配置(业务运营开关)——遵循信号分层定案
网络注意点(部署)
telemetry 栈(docker-compose.telemetry.yaml)与 deploy 栈(docker-compose.deploy.yml)是两个独立 compose 网络。Server 连 Prometheus 需要任选其一:
- 共享 external network(
croupier-telemetry声明为 external 并挂给 deploy 栈) prometheusUrl指宿主映射端口(http://<host>:19092)
HAProxy exporter 端点只需集群内可达(scrape 走容器网),无需暴露宿主端口——顺带解决 stats :8404 裸奔问题(stats 页可加 basic auth 或完全收回内网,平台代理走 exporter 通道)。
与 agent 视角三方对账的关系(并行推进)
Prometheus 生态覆盖的是支持层(LB 管理面数据);平台仍需自行开发业务数据面真实性(生态覆盖不了):
三方对账:
LB 视角 :agent 的 TCP 会话在实例 X(Prometheus / exporter)
归属表视角:实例 X claim 了该 agent,心跳新鲜(cluster_agent_owners)
agent 视角:agent 自报"我连着实例 X"(注册响应携带 instanceId,心跳回传)三方不一致的组合即可精确定位故障形态(半开连接 / 路由漂移 / 注册丢失)。agent 视角为零配置通用实现(不依赖任何 LB 类型),与本节 Prometheus 管道互补。
落地阶段
| 阶段 | 内容 | 依赖 |
|---|---|---|
| P0 | haproxy.cfg 开 exporter + telemetry prometheus.yml 加 scrape + 文档 | 无(纯配置) |
| P1 | ops.prometheusUrl 配置 + server 只读代理端点(metric 白名单)+ /ops/lb 原生图表 + cluster 对账列 | P0 |
| P2 | agent 视角三方对账(注册响应带 instanceId + 心跳自报 owner + nodes 页对账列) | 无(独立于 P0/P1) |
| P3 | Grafana 外链配置化 + blackbox_exporter 数据面探测标配 | P1 |
