Skip to content

负载均衡 ​

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)。DB agent_sessions 快照行不删不改、自然过期(归属表是跨实例查询的唯一入口,无归属行的快照行无消费者);单实例(cluster 未启用)行为不变:断连即时清生效,重启恢复仍全量

已知边界(切双活前须知):

  • mesh 互联为内网明文 TCP、无对端认证——互联端口必须仅集群内网可达
  • 哈希路由(route: hash)跨实例不保证稳定落点:仅本地候选耗尽才扩远端
  • 异步任务事件不走 mesh 回传:事件经 owner 落共享库(task_events),caller 的 HTTP 轮询/SSE 从共享库读取(既有链路不变)

因此当前部署形态仍是单活 + 冷备(双活切换是独立操作,前置条件已凑齐):

层配置故障语义
L4(haproxy.cfg)server croupier-server2 ... check backupagent 全部连主实例;主实例摘除后 agent 重连自动落 backup 并重新注册
L7(nginx-main.conf)split_clients ... 100% croupier-serverAPI 全量指向主实例;主实例故障时人工提升此处切到 server2(换一处分流值)

切换双活(前置已补齐,操作时点自定):

  1. L7 分流值从 100% croupier-server 改为按比例(如 50%/50%),nginx -s reload
  2. L4 去掉 server2 的 backup 标记(agent 连接开始打散)
  3. 观察:/ops/nodes 两实例都有 agent 归属;dashboard 发起 invoke/async/broadcast 三类调用混合落在两实例均成功(owner 侧审计记录出现在对端实例);server2 日志出现 cluster: refreshed remote agent snapshot / owner 侧审计
  4. 回滚即恢复 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 streamHAProxy对 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 调优一体✅ 但静态资源服务弱于 nginxDashboard 已经需要 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 漂移);公有云等价物是云 NLBVRRP 需要网络层支持,公有云内用 NLB 免运维
KubernetesService(ClusterIP)kube-proxy 天然负载均衡 + Endpoints 就绪探针,无需自建 LB 层

配置示例 ​

nginx stream(备选:复用 dashboard 容器) ​

nginx
# 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) ​

haproxy
# /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 /stats

Dashboard 的 L7(API 反代 + SPA + SSE)仍由 nginx 承担——两层各司其职:nginx 管「人」(HTTP),HAProxy 管「机器」(agent TCP)。

keepalived + HAProxy(LB 自身高可用) ​

conf
# /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] ​

  1. 起新 HAProxy(指向现有 croupier-server/croupier-server2:19090)
  2. 灰度:先改一个 agent 的 server.addr 指向 HAProxy,验证注册与调用正常
  3. 全量 agent 切换;观察 HAProxy stats 会话分布
  4. nginx stream 块下线(dashboard 回归纯 L7)

整个过程 Server 集群无感知(成员表按连接归属自动更新 owner)。

LB 监控(设计定案) ​

状态:Accepted(方案定案,分阶段落地)。核心原则:管道用开源生态,展示做平台原生;数据源可配置,不绑定任何 LB 品牌。

设计演进与结论 ​

监控 LB 的方案经历三轮讨论收敛:

  1. server 端自行开发 LB stats 解析适配器(拉 haproxy CSV) —— 否决:重复造轮子,等价于重新实现 haproxy exporter 的一部分;且绑死 haproxy,换 LB 就废
  2. 跳转/iframe 嵌 Grafana —— 否决为主案:体验割裂(另一套 UI)、权限绕过平台 RBAC(Grafana 需配匿名/auth-proxy)
  3. 最终定案:采集与存储用 Prometheus exporter 生态(开源),展示层由平台原生渲染(直读 Prometheus API + Ant Design Charts),Grafana 仅作深度分析外链兜底

分层架构 ​

text
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方案接入成本
HAProxy2.4+ 内置 Prometheus exporter(http-request use-service prometheus-exporter)——本仓库 haproxy 2.9 原生支持极低(改 cfg)
nginxnginx-prometheus-exporter(nginxinc 官方)低(sidecar)
keepalivedkeepalived-exporter(VRRP 状态/漂移)低
LVS/IPVSprometheus-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_statusUP/DOWN/NMA 实时可见
请求速率/错误率haproxy_backend_http_requests_total / haproxy_backend_errors_total流量异常发现
健康检查失败haproxy_server_check_failures半死实例探测

对账列(僵尸探测器):/ops/cluster 每实例展示「LB 会话数 vs 归属表 agent 数」——两者长期不一致(连接在、心跳停)即半开连接信号,正是 HA 双实例排障中最缺的一屏可见性。

配置化(不写死任何地址) ​

yaml
# 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 需要任选其一:

  1. 共享 external network(croupier-telemetry 声明为 external 并挂给 deploy 栈)
  2. prometheusUrl 指宿主映射端口(http://<host>:19092)

HAProxy exporter 端点只需集群内可达(scrape 走容器网),无需暴露宿主端口——顺带解决 stats :8404 裸奔问题(stats 页可加 basic auth 或完全收回内网,平台代理走 exporter 通道)。

与 agent 视角三方对账的关系(并行推进) ​

Prometheus 生态覆盖的是支持层(LB 管理面数据);平台仍需自行开发业务数据面真实性(生态覆盖不了):

text
三方对账:
  LB 视角   :agent 的 TCP 会话在实例 X(Prometheus / exporter)
  归属表视角:实例 X claim 了该 agent,心跳新鲜(cluster_agent_owners)
  agent 视角:agent 自报"我连着实例 X"(注册响应携带 instanceId,心跳回传)

三方不一致的组合即可精确定位故障形态(半开连接 / 路由漂移 / 注册丢失)。agent 视角为零配置通用实现(不依赖任何 LB 类型),与本节 Prometheus 管道互补。

落地阶段 ​

阶段内容依赖
P0haproxy.cfg 开 exporter + telemetry prometheus.yml 加 scrape + 文档无(纯配置)
P1ops.prometheusUrl 配置 + server 只读代理端点(metric 白名单)+ /ops/lb 原生图表 + cluster 对账列P0
P2agent 视角三方对账(注册响应带 instanceId + 心跳自报 owner + nodes 页对账列)无(独立于 P0/P1)
P3Grafana 外链配置化 + blackbox_exporter 数据面探测标配P1