资讯动态

云原生架构下HAProxy的定位与落地实践

发布时间:2026/10/21 5:49:55 来源:尧图企业网站定制
前言说句实话这几年云原生课里讲入口流量的内容不少但单独把 HAProxy 拎出来认认真真讲一遍的内容真的不多。很多人一提负载均衡就条件反射想到 Nginx要么就是上了 Envoy 之后就懒得回头看 HAProxy。我在生产环境里从传统裸机时代一直用到 K8s 时代HAProxy 始终没有下线过反而在关键入口位置越站越稳。这篇主要想聊清楚一个事在云原生的架构里HAProxy 到底适合放在哪个位置、怎么配置才算用对、以及我踩过哪些坑之后摸索出一套还算顺手的落地方式。内容主线大概是先说为什么云原生里还是绕不开 HAProxy接着拆它的配置模型和转发机制然后给一套在 K8s 里从零落地的实操过程最后是调优思路和真实故障排查记录。适合正在维护生产集群的运维、SRE或者想给自己服务加一层可靠入口的后端开发。1. 云原生环境里的 HAProxy这个东西为什么还没被淘汰1.1 K8s 自带 Service 的局限Kubernetes 的 Service 解决了 Pod IP 漂移的问题但它本身更像一个默认值而不是一个完整的流量治理方案。Service 的负载均衡策略很基础ClusterIP 只能在集群内部访问NodePort 暴露端口会带来端口规划混乱和大量 NAT 转发LoadBalancer 类型又被云厂商的实现方式绑得很死你很难在自建集群里拿到好用的 LB。当你开始面对多域名、多租户、复杂路由规则、四层长连接透传这些需求时靠原生 Service 组合出来的方案会变得非常别扭。比如你想让同一个入口按域名去路由到集群内不同的命名空间或者想对后端的 MySQL、Redis 做数据库级别的代理原生方案基本都给不了你直接的答案。HAProxy 的价值就在这里。它不是一个只会轮询转发的傻子而是一个自带精确 ACL、健康检查、连接复用、会话保持、动态重配置的流量控制中枢。在 K8s 的 Pod 频繁重建、流量拓扑不断变化的背景下它的传统优势不但没有消失反而被放大了。1.2 三种主流部署模式怎么选我在不同集群里试过好几种部署方式现在比较主流的有三种各有利弊部署模式流量路径优点注意点独立节点裸部署外部流量 → HAProxy → 节点端口 → K8s Service故障隔离彻底HAProxy 挂掉不影响集群需要单独维护一套配置管理和进程监控K8s 内 Deployment外部流量 → Service LoadBalancer/NodePort → HAProxy Pod → 后端 Service部署简单能用 K8s 做服务发现和滚动升级需要处理 Pod 重建带来的连接中断尽量用 PDB 和优雅停机双级入口边缘 HAProxy 先接入集群内再放一层 HAProxy/Ingress分层治理安全策略和业务路由解耦链路变长排障成本增加如果集群规模不大、团队也没有专门的边缘网络设备我建议直接采用第二种方式把 HAProxy 跑在集群内配合 Service 暴露端口。等流量涨到一定规模后再在边缘独立部署一层专门做 TLS 卸载和 DDoS 清洗两层各司其职。1.3 和 Nginx、Envoy 的分工每次聊 HAProxy 都会有人问Nginx 不也能干这事吗Envoy 不是更时髦吗我的看法是工具没有高下之分只有匹配不匹配。Nginx 的强项在 HTTP 处理、缓存、静态文件配置语法对熟悉 Web 服务器的人来说门槛低。但 Nginx 在四层 TCP/UDP 代理这块不如 HAProxy 纯粹在多端口监听和连接数管理上也需要额外调。Envoy 的优势是动态配置、服务网格集成、可观测性但它的复杂度也高光是理解 xDS 协议就要花不少时间对于只想稳定抗住入口流量、不想引入一堆新概念的场景来说有点大材小用。HAProxy 的核心优势是专注。它没有花哨的插件生态但它把连接调度、健康检查、会话保持这几件事做到了极致单进程轻松扛住几十万并发连接配置文件和运行时 API 配合起来非常顺手。在云原生体系里我通常给每个入口场景做一个分工边缘和四层入口优先 HAProxyHTTP 网关优先内嵌业务逻辑南北向流量交给 HAProxy东西向流量交给 Service Mesh。2. HAProxy 的底层逻辑搞懂这几个机制才算入门2.1 控制的几个核心配置段HAProxy 配置文件的逻辑非常好记frontend 负责接收流量backend 负责定义后端服务器组listen 则是把前两者合在一起用。再就是 defaults 设置全局默认值global 设置进程级参数。看一个最经典的例子global maxconn 50000 log /dev/log local0 stats socket /var/run/haproxy.sock level admin defaults log global mode http option httplog option dontlognull retries 3 timeout connect 5s timeout client 30s timeout server 30s frontend web_front bind *:80 use_backend web_servers if { hdr(host) -i app.example.local } default_backend web_servers backend web_servers balance roundrobin server web1 192.0.2.10:8080 check server web2 192.0.2.11:8080 check这里面有个很容易忽略的点defaults里的参数会被后面的 frontend/backend 继承所以全局超时和选项最好放在 defaults 里统一定少在后端重复写。配置的继承逻辑做得很好你在 defaults 里写的任何参数后面没有单独覆盖就能一直传递下去。另一个重要的机制是控制面和数据面的分离。HAProxy 的配置是静态的运行时可以通过 socket 来动态修改已加载的配置比如临时摘掉某个后端节点、修改节点权重、查询当前连接数这些操作不需要重启进程。我常在发布前用 socket 把某台新节点权重调低观察几分钟再恢复整个过程对现网流量没有影响。2.2 四层与七层转发模式的选择HAProxy 最核心的区别在于mode tcp和mode http的取舍。四层模式下 HAProxy 基本不做报文内容解析流量转发只依赖 TCP 四元组信息延迟极低也能保留客户端源 IP适合数据库代理、Redis 集群入口、游戏长连接等场景。七层模式则是完整地解析 HTTP 协议可以做路径路由、域名路由、Header 改写、Cookie 插入、重定向、正则过滤等高级操作。我见过不少人是“不管三七二十一全上七层”结果后面接 MySQL 的时候发现模式不支持还得回头改。这里给个选型参考业务类型推荐模式说明HTTP API 网关mode http可以做路径路由、限流、Header 改写WebSocket 长连接mode http需要开启option http-server-close配合隧道超时MySQL / Redis / MQmode tcp尽量保持透明的二进制流透传任意 TCP 长连接服务mode tcp注意设置合理的timeout tunnelmode tcp配置起来很简单但要注意的是健康检查不能再依赖 HTTP 检查需要改用 tcp-check 或者类似自定义的协议探活。下面这种就是一套针对四层服务的检查配置listen mysql_rw bind 0.0.0.0:3306 mode tcp balance leastconn option tcp-check tcp-check connect tcp-check send ping\r\n tcp-check expect string pong server db1 192.0.2.20:3306 check inter 3s fall 3 rise 2 server db2 192.0.2.21:3306 check inter 3s fall 3 rise 2这里的leastconn权重算法对长连接场景很重要如果是 MongoDB、Redis 这种需要保持大量连接的场景roundrobin会把新连接平均分给所有后端但实际上连接被占用的状态不一样长连接容易倾斜leastconn 会主动选择当前活跃连接最少的那台后端分布均匀很多。2.3 健康检查与服务发现HAProxy 的健康检查是流量调度的根基。主动检查可以设置连接检查、HTTP 检查、自定义 TCP 检查甚至会专门针对 Redis 这类服务做协议级探活。出于习惯我基本每个后端都会配上check参数权重和兜底策略要结合 fall/rise 一起设计。inter 设置检查间隔fall 表示连续失败多少次判定后端不可用rise 表示连续成功多少次之后重新标记为可用。生产环境的推荐基线值是check inter 3s fall 3 rise 2不建议把间隔压到 1 秒以内否则后端服务在启动阶段还没就绪健康检查风暴会把进程压垮。HTTP 服务的健康检查可以做得更精细比如请求一个专门的健康检查 URL只认指定状态码backend api_servers mode http option httpchk GET /healthz HTTP/1.1 http-check expect status 200 server api1 192.0.2.30:8080 check inter 3s fall 3 rise 2 server api2 192.0.2.31:8080 check inter 3s fall 3 rise 2在 K8s 的落地场景里HAProxy 的配置往往不能直接写死后端 IP因为 Pod 的 IP 会变。这时候一般搭配一个配置同步脚本或 Operator监听 Service 的 Endpoints 变化把后端列表动态写进 HAProxy 的配置再触发 reload。也有直接使用支持 DNS 的服务发现的方案比如配置里写域名HAProxy 定期解析改起来更快但不够那么细粒度。2.4 会话保持与优雅关停会话保持是七层入口里高频出现的需求。HAProxy 最经典的做法是cookie插入给后端真实应用下发一个带后端标识的 Cookie后续请求带着这个 Cookie 直接落回同一台后端。还有 stick-table 机制它是无侵入式的HAProxy 自己根据客户端 IP 或某段 Header 学习并记录后端映射不需要应用配合改 Cookie。区别在于Cookie 模式适合应用可以接受额外 Cookie 的场景stick-table 更适合不想动应用逻辑的场景。不过 stick-table 的内存占用要规划好比例公式是每 10 万个条目大概几十 MB 内存不能随手设一个巨大的 size 不管。优雅关停很多人会忽略。传统做法把进程 kill 掉会断掉存量连接。HAProxy 支持通过 socket 执行disable server和shutdown sessions也可以使用带-sf参数的 reload 让旧进程把存量连接处理完再退出。在 K8s 环境里如果直接把 Pod 杀掉存量连接也会跟着断所以配置preStop钩子非常关键我会在后面实操部分详细讲。2.5 热加载与配置一致性HAProxy 2.x 之后默认使用 master-worker 模式reload 时新的 worker 进程会接管监听端口旧 worker 保持已有连接直到自然结束。这个过程比想象中的简单很多不是把进程停了再启而是重新加载配置后主进程完成 fd 和 socket 的交接。但坑也在这里。如果 reload 频率太高比如配置同步脚本每隔几秒就触发一次可能会积累一堆旧进程还没退完连接又转移到下一个进程上。我见过的生产故障里HAProxy 连接数飙升最后发现是 reload 太频繁导致的孤儿进程太多。优雅做法是给同步脚本加一个冷却机制配置变更 10 秒内的重复变更都合并成一次 reload这也算我在运维中吃到教训后总结出来的经验。3. 在 K8s 集群里落地 HAProxy一次从零到一的操作记录3.1 部署形态先想明白我在某模拟项目里尝试过在 K8s 中部署 HAProxy给一个多业务集群做入口。当时的网络环境是三节点的自建集群需要同时暴露 HTTP 域名路由和一组 TCP 长连接服务。服务规模不大没必要上独立边缘节点所以直接在集群内部署了一套 HAProxy用节点端口作为对外入口。动手之前先理清几个问题对外入口使用 NodePort 还是 LoadBalancer自建集群通常没有现成的云 LBNodePort 是最简单可靠的办法。需要支持哪些协议混合 HTTP 和 TCPHAProxy 的配置要区分 frontend 来监听不同端口。是否需要保留客户端真实 IP需要。因此使用了 externalTrafficPolicy: Local。配置更新机制先用 ConfigMap 挂载配置文件后续再引入同步脚本。这个架构的好处是HAProxy 本身是高可用的无状态代理任意一个副本掉线节点上的 Service 会把流量导到其他副本配置变更通过 ConfigMap 滚动更新不需要重装组件。3.2 用 Helm 部署 HAProxy Ingress Controller为了省去手动编写大量 YAML 的时间我选择用社区的 Helm Chart 来部署 HAProxy Ingress Controller。如果你的环境不方便用 Helm直接把 Chart 渲染出来的 YAML 应用到集群也是一样的效果。大概的操作路径是这样helm repo add haproxy-tech https://haproxy-ingress.github.io/charts helm repo update helm install haproxy-ingress haproxy-tech/haproxy-ingress \ --namespace ingress-nginx \ --create-namespace \ --set controller.kindDaemonSet \ --set controller.service.typeNodePort \ --set controller.service.nodePorts.http32080 \ --set controller.service.nodePorts.https32443副本形态我选择了 DaemonSet 而不是 Deployment原因是 DaemonSet 能保证集群内每个节点都有一个 HAProxy 副本配合 externalTrafficPolicy: Local 能避免跨节点的二次转发有点接近边缘入口的模型。不加污点的情况下DaemonSet 也很方便。安装完成后需要验证kubectl get pods -n ingress-nginx kubectl get svc -n ingress-nginx kubectl logs -n ingress-nginx -l app.kubernetes.io/namehaproxy-ingress第一次装完最容易遇到的问题就是容器反复 CrashLoopBackOff。多数原因是 ConfigMap 里配置了语法错误的模板或者监听的端口被节点上其他进程占用了。优先看日志不要瞎猜。3.3 配置七层路由转发如果只是想给集群内的 HTTP 服务做一个入口最直接的方式就是定义 Ingress 对象。在这个方案里HAProxy Ingress Controller 会监听 Ingress 资源变化自动生成对应的 HAProxy 配置逻辑不需要人工维护 frontend/backend 定义这也是腾讯云、阿里云等很多托管 K8s 服务里的实际做法。下面是一个 Ingress 的配置示例apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress annotations: kubernetes.io/ingress.class: haproxy spec: rules: - host: app.example.local http: paths: - path: /api pathType: Prefix backend: service: name: api-service port: number: 8080 - path: / pathType: Prefix backend: service: name: web-service port: number: 80通过定义 Path 的路由规则可以实现同一个域名下/api前缀的请求打到 API 服务其余请求打到 Web 前端服务。实际生成的 HAProxy 逻辑内部是用 ACL 实现的所以性能依旧在线。但 Ingress Controller 这种模式有一个约束它只支持 HTTP 和 HTTPS 的七层转发如果想给 MySQL、Redis 这类 TCP 服务做代理就必须用 Controller 支持的 TCP Service 映射或者在集群里单独部署一个原生 HAProxy 实例来监听四层端口。3.4 四层 TCP 服务的暴露方式某模拟项目里需要把集群内的 Redis 服务暴露给外部客户端访问端口是 6379。如果直接用 NodePort 把 Service 暴露出去外部客户端拿到的不是真实接入 IP连日志也没法记录。更好的做法是在 HAProxy 的 ConfigMap 里增加 TCP 端口映射把 6379 端口的流量调度到后端的 Redis Service 上。使用 HAProxy Ingress Controller 时可以通过 ConfigMap 定义apiVersion: v1 kind: ConfigMap metadata: name: haproxy-ingress-tcp namespace: ingress-nginx data: 6379: default/redis-service:6379然后给 HAProxy 的 Service 增加一个 6379 的 NodePort 映射这样外部客户端通过节点 IP:30637 访问时流量会落到 HAProxy再由 HAProxy 转发到 Redis 服务。由于工作在四层模式客户端看到的就是真实的后端响应Redis 自己也能看到完整的 TCP 连接状态。有一点必须提醒NodePort 注册的端口范围默认是 30000-32767如果你要用 6379 这样的任意端口就得在 API Server 启动参数里调整--service-node-port-range。但这样做会带来端口冲突风险实际上我很少直接改范围通常映射成一个高位端口再在节点前面加一层防火墙转发或者在边缘路由器上做端口映射。3.5 容器里运行 HAProxy 的几个小细节在容器环境里跑 HAProxy 有几点和裸机不一样。首先HAProxy 默认以 root 用户绑定 80 端口的话在容器里会有问题建议以非 root 用户运行并显式设置setuid和setgid或使用受控的 SecurityContext。其次一定要给 HAProxy 容器配置足够的内存和文件描述符限制否则 maxconn 一调高就出现奇怪的问题。最后确保容器里 PID 1 进程是 HAProxy 本身这样信号量和进程退出都能被 K8s 正确处理。如果是用基础镜像自带的 init 进程有时候会吞掉 TERM 信号导致优雅关停失效我曾经被这个问题坑过一下午。下面是 Deployment 的核心片段注意 readinessProbe 用的是 TCP 检查而不是 HTTP因为 HAProxy 的监听端口可能在容器里是不走 HTTP 的containers: - name: haproxy image: haproxy:2.8-alpine args: [-c, -f, /usr/local/etc/haproxy/haproxy.cfg] ports: - containerPort: 80 name: http - containerPort: 6379 name: redis readinessProbe: tcpSocket: port: 80 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: 256Mi cpu: 250m limits: memory: 1Gi cpu: 1如果需要验证配置是否正确可以在本地先跑一遍haproxy -c -f /path/to/haproxy.cfg这一步能省去很多进容器里发现配置语法错误后的抓狂时间。4. 性能调优和问题排查实录核心参数与踩坑清单4.1 连接数、超时与线程模型调 HAProxy 不像想象中那样玄学核心其实就几个参数。第一个是maxconn它直接决定进程能接收多少并发连接。计算方式是看内存资源单连接所需内存包括 socket buffer 等内存足够时 maxconn 可以设很高但还要注意系统级ulimit -n的限制。如果文件描述符不够maxconn 设再大也没有用。建议 maxconn 值为可用系统文件描述符上限的四分之一剩下的四分之一留给连接正常的波动和监听端口。nbthread参数在 2.x 版本中很有用可以按 CPU 核数设定。但不要盲目超过物理核数否则线程切换的开销会反向抵消连接优势。CPU 亲和性使用cpu-map可以绑定但在容器环境下必要性不是特别高因为容器已经做了 CPU 限额。超时参数是常见的拦路虎。我的推荐基线是参数推荐值说明timeout connect5s与后端建立连接的超时不宜过短timeout client30s与客户端之间的空闲超时timeout server30s与后端之间的空闲超时timeout http-request10sHTTP 头接收超时timeout tunnel3600sWebSocket 和 TCP 长连接推荐设置timeout queue10s等待后端可用连接的超时如果业务里有长时间运行的任务比如导出报表、AI 推理请求一定要把 server 超时调大或者通过option http-server-close和timeout tunnel来配合处理。我遇到过坑默认的 30s server 超时直接断掉了一个耗时 80 秒的模型推理请求排查时从后端看应用也没报错最后才发现是 HAProxy 超时把连接断了那个体验相当曲折。4.2 常见故障对照表这部分内容想以故障速查表的方式给出来方便排查问题时直接对照。故障现象可能原因排查思路后端节点全部显示 DOWN健康检查路径错误curl 手动访问健康检查地址看状态码和响应体客户端 502 Bad Gateway后端服务未就绪或端口错误看 HAProxy 日志里 backend 状态检查后端 Pod 是否 Ready客户端 504 Gateway Timeout后端响应过慢超时太短调整 timeout server 或 timeout tunnel客户端拿到的源 IP 都是 Node IP缺少 realip / PROXY 协议支持开启 externalTrafficPolicy: Local或用 PROXY Protocol长连接流量集中在某台后端负载均衡算法不适合长连接改用 leastconn 配合 maxconn 权重调整后台访问间歇性不可用reload 导致旧进程连接泄漏降低 reload 频率使用 master-worker 模式日志里大量 connect and close健康检查间隔太短调大 inter 间隔降低误报率4.3 我踩过的几个特别值得说的坑第一个坑是七层模式下的连接复用问题。HAProxy 作为七层反向代理默认会对后端开启连接复用这本身是性能优化但如果你后端的接口有基于来源 IP 的限流逻辑它会看到所有请求都来自 HAProxy 的 Pod IP而不是客户端真实 IP。解决方法是开启option forwardfor把X-Forwarded-For头传给后端应用侧再取这个头来做真实 IP 识别。第二个坑是健康检查间隔设置过短导致的“探活风暴”。一次发布中后端接口的耗时在正常值之外有时会超时健康检查连续失败导致节点被摘除流量全打给剩余节点剩余节点也因为负载升高而响应变慢结果所有节点都被摘除服务整体不可用。后来我调整了 inter、fall、rise 的组合把检查节奏放慢到 3 秒间隔连续失败 3 次才摘除情况立刻好转。第三个坑是 reload 的高频触发。早期的自动化脚本用 event watch 监听 Service 的 Endpoints 变化Pod 每次重建都会触发一次 HAProxy reload。原本以为没多大问题直到某一天看到机器上堆了几十个 haproxy process才意识到 reload 不是免费的。现在这个脚本加入了 10 秒冷却窗口只对最新状态生效效果好了很多。第四个坑是关于 stats socket 的权限。为了开发方便曾一度把 socket 的 mode 设成 666任何容器都能通过 socket 修改运行中的配置后来被内部扫描发现后做了收紧。现在生产环境里 socket 都设置为 mode 600只保留管理员用户访问对外访问只暴露只读指标。4.4 可以抄作业的运行时管理小技巧最后分享几个我实际用下来非常顺手的小技巧。如果是原生 HAProxy 而不是 Ingress Controller可以用 socat 操作 runtime socket。比如临时摘掉一台后端echo set server web_servers/web1 state maint | socat unix-connect:/var/run/haproxy.sock stdio恢复则把 state 改为 ready。这个操作在发布场景中非常好用不需要改配置也不需要 reload。查看后端状态和连接数echo show servers state | socat unix-connect:/var/run/haproxy.sock stdio echo show info | socat unix-connect:/var/run/haproxy.sock stdio调试性能问题时show stat可以输出 CSV 格式的状态指标我经常把它拉出来算一段时间内的请求延迟分布比看监控曲线直观很多。还有一个关于优雅关闭的小经验。在 K8s 里如果 HAProxy 直接收到 SIGTERM它默认会等待活动连接自然结束但 PreStop 钩子时间有限超时后 K8s 还是会强杀。我的做法是给 HAProxy 的 Deployment 配置terminationGracePeriodSeconds: 60同时利用 PreStop 把 socket 里的后端节点全部切换到维护状态再触发进程 reload保证存量连接处理完再退出。这套组合在多次发布中没有一次出现连接中断的情况。我个人在实际操作里的体会是HAProxy 这个老家伙看着不起眼文档也写得比较理工男但只要你理解了它的配置模型和运行时 API它比很多所谓的“下一代网关”都要稳得多。现在的路线图里还计划在边缘节点引入 PROXY Protocol让集群内的负载均衡层也能拿到完整的客户端地址链有兴趣的朋友也可以从这点开始研究。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑