如果你维护过一组规模不小的HTTP服务你一定遇到过这种尴尬局面后端拆成了十几个子服务前端入口却还在用端口号转发要么给每个服务单独开端口要么在业务代码里写死跳转逻辑。我接手某个内部系统时就是这么个烂摊子折腾到线上接口超时才下决心把入口全部收敛到HAProxy七层代理上。这篇内容不聊官方文档里那些基础介绍只讲我实际配下来的路径从四层和七层的选型逻辑到frontend/backend、ACL路由、健康检查、会话保持、TLS终结再到高可用和性能调优最后附上我踩过的坑。适合手里有多个HTTP后端服务、想用一个入口统一管理的人参考。1. 为什么非要用七层四层代理解决不了的那些麻烦1.1 四层和七层的分界线到底在哪先别急着写配置得把选型逻辑想清楚。很多人以为负载均衡就是“把流量均分给几台机器”这个理解在四层模式下勉强成立一旦到了七层玩法完全不同。四层代理工作在传输层只认IP和端口。它拿到一个TCP连接根据目标地址和后端列表做转发完全不关心里面跑的是HTTP还是别的协议。好处是性能极高、延迟极低坏处是对内容一无所知同一个端口进来的请求只能被送到同一组后端想做用户维度、路径维度的拆分得靠业务代码自己处理。七层代理则是在HTTP层面工作能读取请求行、Host头、URL路径、Cookie、请求体。HAProxy把请求完整解析之后再按你写的规则决定发给谁。这意味着你可以做到“同一个域名/api开头的进API集群/static开头的进静态集群Host匹配ws.example.com的进WebSocket集群”这种能力对微服务架构来说几乎是刚需。我用一个例子说明白。某跨平台系统上线初期只有两个子服务我直接给前端分配了两个端口端口10给用户服务端口11给订单服务看起来也还行。等子服务涨到十几个问题就爆了每个服务都要开新端口前端同学要维护一长串端口列表上线新模块时运维要开防火墙端口更离谱的是有一次前端把端口搞混用户信息串到了订单服务上线上反馈诡异了半个小时才定位到。这事的根因就是入口层没有按照业务语义来路由而是在用底层端口硬扛。1.2 七层代理真正解决的四个核心问题从那次之后我对入口层的设计就一句话能用七层语义解决的问题绝不用四层端口硬解。具体来说HAProxy七层代理帮我解决的是这四个核心问题。第一是路由拆分。一个域名可以对应任意多个后端集群按Host、按路径前缀、按查询参数、按请求头都可以拆。后端服务的演进不再影响前端的访问方式。第二是请求和响应的改写。七层模式下代理能往请求里加X-Forwarded-For、X-Forwarded-Proto后端拿到的就是真实客户端IP和原始协议不用再靠网络层面的IP透传来猜。还能在响应头里统一注入安全策略比如HSTS、CSP。第三是内容感知的健康检查。四层只能看端口通不通七层可以主动发一个HTTP请求检查接口返回的状态码和响应体。这样后端进程即使没崩、只是业务逻辑卡死HAProxy也能把它摘除。第四是会话保持。HTTP本身无状态但如果后端有本地Session或者依赖同一台机器的缓存就需要把同一个用户的请求固定到同一台后端。七层代理可以通过插入Cookie来实现四层代理很难优雅地做到这一点。我把四层和七层几个关键维度对比放在下面方便你评估自己的场景对比维度四层TCP模式七层HTTP模式工作层级传输层基于IP和端口应用层解析HTTP协议路由依据目标IP和端口Host、URL、Header、Cookie内容改写不支持支持请求/响应头改写健康检查端口连通性HTTP状态码和响应体内容会话保持基于源IP粗糙基于Cookie精细协议支持任意TCP/UDP协议HTTP、HTTPS、WebSocket、gRPC等性能损耗极低有解析开销但现代CPU可忽略1.3 什么时候别硬上七层七层不是万能药。如果你的场景是数据库连接、Redis、内网TCP长连接这类对时延极度敏感、且没有HTTP语义的服务那老老实实开nginx stream或者HAProxy的tcp模式别为了“统一架构”硬把一切塞进七层。另外还有一个容易被忽略的点七层代理要解析报文意味着它必须看到完整的请求对于一些极端大包上传场景内存和buffer的规划要提前想好否则会直接碰到性能上限。我给团队做选型时有个原则HTTP流量默认走七层非HTTP流量一律走四层。这个原则到现在也没有变过。2. 最该搞清楚的核心配置frontend与backend怎么搭2.1 一套完整可跑的HTTP七层代理配置HAProxy的核心概念其实就三个frontend是流量入口负责接收请求backend是后端资源池定义一组真实服务器listen则是把两者绑定在一起的简写方式。配置结构非常清晰我直接给一套我当时上线的精简版配置。global maxconn 50000 log /dev/log local0 log /dev/log local1 notice chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin nbthread 4 cpu-map auto 1-4 0-3 defaults mode http log global option httplog option dontlognull option http-server-close option forwardfor except 127.0.0.1/8 timeout connect 5s timeout client 30s timeout server 30s frontend web_front bind *:80 bind *:443 ssl crt /etc/haproxy/certs/example.pem http-request set-header X-Forwarded-Proto https if { ssl_fc } default_backend api_servers backend api_servers balance roundrobin option httpchk GET /healthz http-check expect status 200 server api-1 192.168.1.11:8080 check inter 3s fall 3 rise 2 server api-2 192.168.1.12:8080 check inter 3s fall 3 rise 2 server api-3 192.168.1.13:8080 check inter 3s fall 3 rise 2这套配置跑起来并不难难的是理解每一行的理由。2.2 每条关键指令的意图和推荐值很多人配置haproxy喜欢复制网上的模板结果出了问题不知道怎么调。这不对。我逐个说几个最容易被忽视的点。option http-server-close是连接治理的关键。它的作用是让HAProxy在与客户端通信时采用HTTP/1.1的短连接模型——每个请求处理完后HAProxy主动关闭与客户端的长连接但后端连接可以按需复用。这样做的最大收益是省文件描述符避免客户端把连接挂在那里不释放。注意如果你的服务是WebSocket或者需要长轮询这条可能要配合超时调整。timeout connect定义的是HAProxy向后端发起TCP连接的最大等待时间。别设太短内网一般2-5秒足够跨机房可以放宽到10秒。timeout client和timeout server分别管客户端和后端的读写超时。静态页面和简单API给30秒绰绰有余但大文件上传、WebSocket场景要单独处理后面细说。balance roundrobin是轮询算法。HAProxy默认就是roundrobin此外还有leastconn按最少连接数、source按源IP哈希、uri按URL哈希。无状态API用roundrobin和leastconn都行有缓存亲和需求的再用uri哈希。maxconn 50000也不是随手写的要根据内存和预期并发来算。我习惯用这条经验公式预估峰值并发连接数 单连接平均占用内存系数约32KB× 预估QPS × 平均响应时间秒。一个机器如果分给HAProxy 512MB内存50万连接数对应的内存需求量级大概在1.5GB左右超出了就得调小否则会在内存压力下直接OOM。2.3 headers改写让后端看到真实客户端七层代理有个天然好处就是能整齐地给请求补“身份信息”。option forwardfor会自动追加X-Forwarded-For头把真实客户端IP放进这个列表。如果你的Nginx在更靠前的位置需要在这里写except子句排除可信代理的IP段否则IP列表里会混入代理节点本身后端取IP时容易取错。另外一个必须做的操作是X-Forwarded-Proto。很多站点升级HTTPS后后端通过判断$_SERVER[HTTPS]或者req.is_secure()来决定是否下发HTTPS链接如果代理层终结了TLS但没有告诉后端“原始请求是HTTPS”后端会以为所有请求都是明文HTTP生成一堆错误的http链接这个bug排查起来特别痛苦。http-request set-header X-Forwarded-Proto https if { ssl_fc }这行的意思简单说如果HAProxy与客户端之间是SSL连接就把X-Forwarded-Proto设置为https否则不设置。配置好了之后我在后端日志里看到的客户端IP和协议才真正和用户对上排查线上问题省了一半时间。3. ACL规则按Host、URL、Headers把流量切开3.1 ACL条件到底有哪些什么时候用哪个七层代理最强的能力在ACL访问控制列表。你可以在frontend里定义一堆条件每个条件对应一个动作——通常是use_backend把请求导到指定的后端集群。HAProxy的ACL写法很直观一条ACL由名称、条件类型和匹配模式组成acl is_api path_beg /api acl is_ws hdr(host) -i ws.example.compath_beg表示路径以指定字符串开头如果请求路径是/api/v1/order就能匹配到is_api。hdr(host)用来匹配请求头中的Host字段-i表示大小写不敏感。这些是最常用的两个。我整理了一份我日常用的ACL条件速查表ACL条件语法匹配目标典型场景path_beg /apiURL路径前缀按模块拆分后端path_reg ^/v[0-9]/userURL正则匹配带版本号的路径hdr(host) -i api.example.comHost请求头多域名共用一个入口urlp(id) -m str 123查询参数按参数值分流hdr_beg(cookie) SESSIONCookie前缀老系统会话分流src 192.168.0.0/16客户端IP段内网访问特殊后端匹配逻辑可以有组合if is_api只走单个条件if is_api or is_ws做或运算if is_api !is_vip做且和否定组合用小括号做分组的时候注意外层要用大括号包裹例如use_backend api_servers if { path_beg /api } !{ hdr(host) -i internal.example.com }3.2 一个frontend切三个backend的实例举一个我配置过多次的组合场景同一个公网入口想让/api下的请求进API集群/static下的请求进静态文件集群同时把Host为ws.example.com的请求转发到WebSocket后端。frontend web_front bind *:80 mode http acl is_api path_beg /api acl is_static path_beg /static acl is_ws hdr(host) -i ws.example.com use_backend ws_servers if is_ws use_backend static_servers if is_static use_backend api_servers if is_api default_backend static_servers这里有一个顺序问题必须提。HAProxy的use_backend规则是按配置顺序从上到下匹配的命中第一条之后就不再继续往下看。所以要把最具体的规则放前面把兜底的放最后。比如ws.example.com的请求同时可能满足hdr(host)和某个path前缀如果不先把ws规则放在最前面它会被别的backend截走。我之前犯过一个错误把is_api写在最前面结果WebSocket的握手请求也带上了/api路径于是被劫持到API集群连接直接失败。那个问题排查了很久最后通过haproxy -c验证配置和查看统计页面才发现ACL顺序把规则吞掉了。3.3 ACL匹配顺序和大小写这些隐蔽坑ACL有几个容易踩的隐形坑都是真实见过的问题我一个个说。第一个是路径大小写。HAProxy的path_beg默认区分大小写path_beg /API不会命中/api/test。很多前端的路由生成规则不统一一批请求是/api另一批是/API如果你只配了一个斜杠开头的小写规则后面这批流量会全部落到default_backend。解决办法是缩写成path_beg -i /api大小写一起匹配或者在前面先加一个ACL把统一成小写。第二个是path_reg正则的贪婪匹配。HAProxy里path_reg用的是正则表达式引擎如果你写^/v[0-9]它会尽可能匹配更多数字但一般不会造成太大问题。真正需要小心的是那些没有加锚点的正则比如path_reg /user可以命中/user/info也可能命中/admin/user如果你的期望只是限定前缀请用path_beg别用path_reg性能和语义都更准确。第三个是Host的大小写。HTTP规范里Host头是大小写不敏感的但严格模式下HAProxy比较时仍会区分大小写所以匹配域名时一定要加-i。我见过有个同事写hdr(host) api.example.com线上偶尔有大写Host的客户端直接落到后端兜底组的例子原因就在这里。ACL是七层代理里最需要花心思的部分。配置不复杂但逻辑一定要先在纸面上列清楚什么路径、什么Host该去哪哪些是例外写成一棵树再映射到ACL规则里不然等流量一多规则之间互相打架你连猜都猜不到问题出在哪。4. 健康检查与故障转移别让后端“半死不活”拖垮入口4.1 从TCP检查到HTTP内容检查健康检查是负载均衡器的生命线。默认情况下如果某个后端挂了而HAProxy没有及时发现请求还是会被送过去然后用户看到的就是超时或502。四层模式下最简单的检查是TCP连接检查HAProxy按时往后端的IP:端口发起连接能连上就算活着。这种检查非常快但也有个致命缺点后端进程可能只是端口还开着但业务线程已经卡死或者内存已经泄漏到无法正常响应请求。四层检查感知不到这些流量会继续灌进去。七层代理的正确做法是主动发HTTP请求。HAProxy通过option httpchk来启用比如backend api_servers option httpchk GET /healthz HTTP/1.1 http-check expect status 200 server api-1 192.168.1.11:8080 check inter 3s fall 3 rise 2这段配置的意思是每3秒inter 3s向后端发起一次GET /healthz请求连续成功2次rise 2才算健康连续失败3次fall 3才标记为宕机。健康检查请求走的是独立的连接不占用业务session表所以不用担心影响正常流量。为什么推荐单独做一个轻量级健康检查端点而不是直接check首页因为首页往往包含数据库查询、模板渲染等耗时操作健康检查请求可能会拖慢甚至反过来给后端制造压力。理想的做法是后端专门提供一个只检查本进程存活、不访问外部依赖的/healthz接口响应时间控制在几十毫秒内。4.2 慢启动、备用池和摘除节奏健康检查不只是“能连就连”的开关节奏控制很重要。一个重要参数是slowstart它控制后端重新加入服务后权重从0逐渐恢复的时间。没有slowstart时一批后端同时重启完瞬间被塞入大量流量可能再次把服务打挂。有了slowstart比如slowstart 10sHAProxy会在10秒内让权重逐步恢复到正常值后端的压力增长就平滑多了。还有一个容易忽略的是备用池。HAProxy支持给backend配置备用服务器主服务器全部宕机时备胎才顶上。配置方式是在server行后面加backup参数server api-1 192.168.1.11:8080 check inter 3s fall 3 rise 2 server api-2 192.168.1.12:8080 check inter 3s fall 3 rise 2 server api-bak 192.168.1.200:8080 check inter 3s fall 3 rise 2 backup这个设计可以用在维护窗口先在备用机上拉好服务再把主服务器逐个摘掉业务无感切换。比直接在DNS层面切要稳得多。摘除节奏也要讲究。fall 3意味着连续3次健康检查失败才摘除假设检查周期是3秒那就约9秒后才摘除。如果检查周期太长比如默认10秒一次宕机后要30秒才摘这个时间对在线业务来说已经不可接受了。所以生产环境建议把inter调到2-3秒同时fall不要设太高。4.3 一次故障转移的完整复盘这里记录一个真实发生的排查过程能帮你直观理解健康检查怎么工作。某内部系统某个接口突然大量超时。我第一时间登录HAProxy看show stat命令的输出发现其中一个后端的状态是DOWN但奇怪的是TCP端口检查一直显示正常说明进程没退出。再看监控面板那台机器CPU跑满内存接近上限业务日志里全是数据库连接池超时异常——典型的“半死不活”状态。当时的HAProxy配置用的是默认的TCP检查所以它认为后端还活着继续往里发流量。我花了大半个下午才定位到问题根源就是健康检查太“浅”。后面我把配置改成HTTP检查并让业务侧增加一个只返回本进程状态的/healthz端点这个问题再也没有出现过。这次复盘给我留下的经验是健康检查的深度决定了你系统自我修复的上限。检查越贴近用户的真实请求越能提前发现异常。七层代理既然已经具备HTTP解析能力那就别浪费在TCP端口上。5. 会话保持和TLS终结从客户端侧绕不开的进阶玩法5.1 Cookie会话保持和stick-table怎么选无状态服务的架构趋势下最好别依赖会话保持。但现实中总有些老系统用户登录状态直接存在后端进程的内存里或者依赖于本地缓存这时候就需要让同一个客户端稳定地打到同一台后端。HAProxy做会话保持有两种常见方案Cookie注入和stick-table。Cookie注入是最推荐的方式。HAProxy在第一次响应时给客户端种一个Cookie后续请求带着这个CookieHAProxy根据Cookie值里的哈希直接路由到对应的后端。配置如下backend api_servers cookie SERVERID insert indirect nocache server api-1 192.168.1.11:8080 cookie s1 check server api-2 192.168.1.12:8080 cookie s2 checkinsert表示如果客户端没有带CookieHAProxy注入一个indirect表示后端始终看不到这个Cookie避免干扰业务nocache防止中间缓存把带Cookie的响应也缓存了。这套方案的优势在于即使中间有一层CDN或Nginx也能正确传递Cookie。stick-table则是把“客户端IP和选中后端”的对应关系记在HAProxy本地的表里。配置起来简单但适用范围窄一旦多台HAProxy共享虚IP时每台机器里的表不一致可能把同一个用户的请求在不同HAProxy之间切来切去导致会话丢失。Cookie方案不依赖服务器端状态在双机或集群部署下更可靠。5.2 TLS终结配置及证书链的坑七层代理最常见的用法之一是做TLS终结客户端到HAProxy是HTTPSHAProxy到后端是明文HTTP方便后端统一处理证书也方便HAProxy解析HTTP头做路由。核心配置就是bind行加ssl和crt参数frontend web_front bind *:443 ssl crt /etc/haproxy/certs/example.pem这里有几个容易踩的坑。第一个是证书文件的命名证书文件必须包含私钥和证书链顺序是私钥在前、证书在后多个证书的链可以拼在一个文件里。我刚开始配置时把证书和私钥分成了两个文件结果HAProxy启动后一直报错拒绝加载。后来才知道HAProxy要求一个PEM文件里把私钥和完整证书链串在一起。第二个是证书更新。证书在某个固定日期过期是常态但线上证书文件如果被直接覆盖替换HAProxy不会自动热加载。你需要执行haproxy -c -f /etc/haproxy/haproxy.cfg验证配置后再用systemctl reload haproxy来触发重新加载。如果集群里有多台HAProxy记得挨个reload别把流量瞬间全打到一台上去。第三个是上游是否还要加密。如果后端服务在云厂商内网通常内网通信是可信的直接用明文HTTP即可如果后端跨了不可信网络就要考虑开启TLS透传或者在backend里用server的ssl verify none参数让HAProxy也以TLS连接到后端这属于两层TLS的范畴配置时要前后端证书先对齐不然会出现后端的连接握手失败。5.3 WebSocket和长连接的超时处理七层代理处理WebSocket长连接时最大的坑是超时时间。默认的timeout client和timeout server都是30秒HTTP短请求完全够用但WebSocket连接可能保持几分钟、几十分钟如果沿用默认值连接会在30秒后被HAProxy强制关闭前端表现为莫名其妙的断连。解决方法是单独建一个backend并设置timeout tunnel。HAProxy对升级后的长连接使用tunnel模式timeout tunnel 3600s把这类连接的空闲超时拉长到1小时远超普通请求的超时时间。同时可以关掉针对该backend的HTTP健康检查改用TCP检查因为业务层的WebSocket服务未必按HTTP协议响应。backend ws_servers mode http timeout tunnel 3600s timeout client 30s timeout server 30s server ws-1 192.168.1.21:8080 check inter 3s fall 3 rise 2gRPC、SSE这类长连接协议的处理思路类似都是靠timeout tunnel以及必要时调整buffer来稳住连接。长连接场景里七层代理的意义不只是转发流量更是在连接生命周期管理上提供精细化的超时模型。6. 性能调优和多实例高可用从能跑到跑得稳6.1 maxconn到底设多少我给出一个可复现的计算思路HAProxy性能调优的第一件事是搞清楚maxconn该填多少。这个数字不能拍脑袋我分享一套可复现的思路。首先估算峰值QPS和平均响应时间。假设你的业务峰值是每秒2000个请求后端平均响应时间200毫秒。按照Little定律系统内的并发请求数量大约是2000 × 0.2 400。这里说的是活动请求数还要考虑HTTP keep-alive带来的空闲连接一般空闲连接是活动连接的好几倍比如5倍那就是2000个连接。再加上健康检查连接、内部管理连接保守估计3000个连接足够。如果你的机器内存是2GB单个连接缓冲区大约占用几十KB3000个连接对应的内存占用也就100MB上下完全没压力。真正吃内存的往往是缓冲区处理大请求时tune.bufsize会对内存有线性放大如果改到64KB连接数就要相应收紧。生产配置上我通常还会关注nbthread。多线程模式让HAProxy充分利用多核CPU但线程数不要盲目设成和CPU核数一样建议从4开始压测看曲线。一个常见的误区是线程越多吞吐越高实际上线程过多会导致锁竞争加剧QPS不升反降。另外有一些tune参数建议保持默认除非你明确知道在调什么。比如tune.maxrewrite它和HTTP头大小有关日志头和Cookie会占用这部分空间。改太小会导致加了proxy头后buffer不够改太大则浪费内存。只有在做超大头解析时才需要动它。6.2 内核参数和文件描述符HAProxy性能瓶颈往往不在进程本身而在系统限制。最常见的就是文件描述符不够。每个TCP连接至少占一个fd如果系统ulimit默认是1024那maxconn设置再高也没用。启动前要保证软硬限制都够大ulimit -n 1000000同时内核里net.core.somaxconn决定监听队列长度Backlog太小会导致SYN包被丢弃表现为连接成功率下降。一般调到65535比较稳妥。还有一个容易忽略的问题后端连接的大量TIME_WAIT。HAProxy主动关闭到后端的短连接时会产生大量TIME_WAIT状态的socket。如果net.ipv4.ip_local_port_range范围不够端口会被耗尽表现为新连接无法建立。我习惯把端口范围扩大同时开启net.ipv4.tcp_tw_reuse只对主动连接方有效避免端口耗尽问题。sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.core.somaxconn65535这些参数要写进sysctl配置文件里否则重启就丢了。很多半夜报警的连接数突增、偶发超时排查下来就是这些内核参数在生产环境没生效。6.3 keepalived双主备与无感reload单台HAProxy做得再好也只解决了入口的“功能”问题没解决“可用性”问题。HAProxy进程挂掉、机器重启、网络抖动都会导致入口不可用。所以生产环境至少要两套HAProxy配合keepalived做虚IP漂移。架构很经典两台HAProxy机器组一个虚拟IP主节点承载流量备用节点通过VRRP协议实时检测主节点状态。主节点挂了备用节点接管虚拟IP客户端几乎无感知。HAProxy侧的配合点是reload要尽量无感。systemctl reload haproxy实际上是通过向旧进程发送USR2信号等待旧进程优雅退出、新进程平滑接手。这个过程中旧连接会维持到自然结束新连接由新进程接管所以业务基本无感知。但有几个前提配置里不要用daemon模式的旧写法socket配置要正确reload前一定用haproxy -c -f /etc/haproxy/haproxy.cfg验证配置文件。如果两个HAProxy之间需要同步stick-table比如会话保持依赖stick-table还得额外配置peers机制否则主备切换后会话表是空的用户会被随机打到别的后端。Cookie方案就没有这个烦恼这也再次印证了前面说的能靠Cookie就别过度依赖服务器端状态。我实际配下来的经验是HAProxy的reload非常稳健前提是配置文件别乱写尤其是不能在配置变更时把listen段删掉再重来尽量用use_backend和default_backend的动态切换。另外每次部署后习惯看一眼show info确认运行版本和启动时间别让老进程一直占着更新不生效。最后分享一个保住我多次的小习惯所有HAProxy改动不管是新增一个backend还是改一条ACL都必须先跑一遍haproxy -c -f /etc/haproxy/haproxy.cfg通过之后再reload。七层代理的配置语法虽然不复杂但一个笔误可能会导致整个frontend拒绝启动线上事故往往就是这么来的。这套从四层选型、frontend/backend基础、ACL路由、健康检查、会话保持到性能调优和高可用的完整路径就是我把入口层从端口转发收敛到统一七层代理的完整实践。真做下来你会发现入口收敛这件事越早做越省钱。
阅读完成 · 觉得有帮助?