ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HAProxy+Nginx+NFS+DNS:高可用负载均衡架构搭建全攻略

HAProxy+Nginx+NFS+DNS:高可用负载均衡架构搭建全攻略 先说一次翻车经历。几年前业务流量涨得很快我把服务从一台机器拆到三台虚拟机上以为“多台机器”就是“高可用”。结果第二天就出问题用户刚下的订单另一台服务器上查不到上传的头像刷新一下就404更离谱的是用户明明登录了换个节点Session就丢了。后来我用 haproxy nginx nfs dns 这套组合重新搭了一遍才把“高可用负载均衡”这几个字真正落到实处。这篇文章就把整套构建流程拆开讲包括每一层在扛什么、关键配置怎么给、以及我在线上踩过的坑希望帮你少走几个月的弯路。1. 为什么是这四个组件先把流量路径画清楚很多人一上来就问“用 nginx 不也能做负载均衡吗为什么还要 haproxy”、“文件放数据库不就行了为什么非要 NFS”——这些问题说明还没把流量路径和故障域想清楚。我先给出这套架构的完整链路再逐一解释每个组件存在的理由。1.1 请求从进来到落库经过哪些环节一个用户从浏览器发起请求到最终拿到响应会经过这样一条链路用户 - 公共DNS解析域名 - HAProxy双节点VIP - Nginx集群 - 后端应用Java/PHP/Node - MySQL Redis NFS每一层的职责分配很明确我习惯用一张表格说明层级组件核心职责如果这层挂了会发生什么入口解析DNS把域名解析到多个IP做第一层故障转移用户找不到服务器网站直接打不开接入层HAProxy Keepalived四层/七层流量分发VIP漂移流量无法进入内网业务路由Nginx7层路由、SSL卸载、静态资源、反向代理动态请求、静态资源全部断掉共享存储NFS多台Web节点共享同一份文件数据上传文件、Session在节点间不一致后端服务应用进程池执行业务逻辑业务不可用这个分层思路的核心是“各管一段、互不越权”。DNS管“用户能找到哪几个入口”HAProxy管“流量分发和入口故障转移”Nginx管“具体把请求转给谁、怎么做业务级路由”NFS管“多台机器怎么共享同一份数据”。每一层都有自己的冗余机制单点故障最多影响局部不会拖垮整条链路。1.2 为什么入口不直接用 NginxNginx 做负载均衡确实没问题但它有两个短板。第一Nginx 的 7 层处理强4 层并发能力和健康检查的精细度不如 HAProxy第二Nginx 本身不做 VIP 漂移你得额外配 keepalived等于把两件事耦合在一起。HAProxy 的定位就是“纯粹的分发器”它不缓存、不执行业务只专注于把流量按规则发给后端同时通过健康检查自动摘除故障节点。我见过不少团队用 Nginx keepalived 做入口也能跑但一到“某台后端半死不活、只对部分请求超时”的场景Nginx 的 http_check 不如 HAProxy 灵活。HAProxy 支持自定义健康检查请求、期望返回码、基于 ACK 或 Content 的匹配在复杂业务下更可靠。当然如果你团队对 Nginx 非常熟、量也不大Nginx 做入口不是不行只是我这套方案下面更偏 HAProxy。1.3 选择组件的两个关键判断标准判断一个组件要不要引入就看两件事它是不是解决了“单点”它是不是让“故障恢复”变得更快。基于这个标准我为每层做的选型是入口HAProxy Keepalived前者做分发后者管 VIP 漂移WebNginx既能当静态服务器又能当反代还天然支持多域名配置存储NFS 作为共享文件仓库数据库、缓存各司其职不能什么都往 NFS 塞DNS公共解析服务或自建 BIND配合健康检查脚本完成故障转移。这套组合的好处是每一层替换成本低、社区资料多、故障边界清晰。下面我按流量路径从 DNS 层开始逐层拆解配置和原理。2. DNS 层最便宜的故障转移却总被人忽略DNS 是很多人搭架构时最容易忽略的一层。原因很简单——它太“基础”了基础到让人觉得不用配置。但 DNS 恰恰是决定“用户到底访问哪台机器”的第一公里。这一层如果没设计好后面 HAProxy 再稳也白搭。2.1 多 A 记录与 TTLDNS 层做高可用的核心机制首先需要明确一点DNS 不负责均衡流量它只负责“把一个域名解析成多个 IP”。我们通常给同一个域名配置两条或多条 A 记录指向不同的公网 IP或不同机房的 VIP。例如www.example.com. 300 IN A 203.0.113.10 www.example.com. 300 IN A 203.0.113.11浏览器拿到这两个 IP 后默认会按顺序优先访问第一个只有第一个连不上才会尝试第二个。严格来说这不叫负载均衡而是“入口冗余”。但高可用架构要的就是冗余——当第一个入口挂了用户还能自动走到第二个入口。TTL 值至关重要。TTL 决定了递归DNS和浏览器缓存这条记录的时间。我见过有人把 TTL 设成 3600 秒结果机房故障后临时切换 IP老用户最长一小时之后才恢复访问。在高可用场景下我的建议是正常状态下 TTL 可以设 300 秒预期到可能要做故障切换时提前把 TTL 降到 60 秒永远不要为了“减少DNS查询”把 TTL 调得很大除非你能接受故障恢复以小时计。2.2 用云解析的健康检查还是自建 DNS 脚本如果网站本身就在云上最简单可靠的方式是使用云厂商的 DNS 解析服务配合“云解析健康检查”功能。你只需要把两个后端的 IP 地址配置成“源站”云解析会定期发 HTTP/HTTPS 或 TCP 探活一旦某个 IP 连续失败自动从解析结果中摘除。这类功能我用下来最大的感受是省钱、省心故障切换大概在 30 秒到 1 分钟内收敛对大部分业务都够用。如果是自建机房、不想依赖云厂商那就用 BIND 脚本实现基本切换逻辑。核心思路是写一个脚本定时 curl 两个 HAProxy 节点的健康地址比如http://192.168.1.5/healthz如果 A 节点挂了就用nsupdate动态把域名 A 记录改成 B 节点并同时把 TTL 拨到 60 秒。原理不复杂但脚本要处理“节点恢复后是否回切”的问题我建议默认不回切让人工决策避免 DNS 反复横跳导致整体雪上加霜。2.3 DNS 层必须避开的坑第一个坑是“只配一条 A 记录”。如果只解析到一个 IP那 DNS 层就完全没有冗余HAProxy 再高可用也白搭。第二个坑是“健康检查只看进程不看业务”。我之前在云解析里配健康检查时直接探http://IP/结果 HAProxy 进程活着但后端全挂了时健康检查依然返回 200DNS 根本不会切换。正确做法是给 HAProxy 配一个只依赖后端业务状态的独立健康检查路径比如/healthz这个路径会检查后端池子是否至少有一个节点存活否则返回 503。这样 DNS 层才能感知到真实业务可用性。如果要做更细的“按地域就近解析”一般需要自建 DNS 或上 GSLB 设备成本会更高。对绝大多数中小团队来说多 A 记录 健康检查 低 TTL 已经足够支撑“机房级故障下用户自动切入口”的需求。更细的精准分流交给下一层 HAProxy 去处理。3. HAProxy 和 Keepalived接入层的双活与 VIP 漂移这一层是整个架构的心脏。没有这一层DNS 解析到再多的 IP 也没意义因为流量进到内网后需要有节点来“接住”并做分发。HAProxy 前面说的“入口冗余”具体由 Keepalived 的虚拟 IP 机制实现。3.1 VIP 漂移的原理与 Keepalived 配置先讲清楚 VIPVirtual IP是什么它并不是绑定在某台物理网卡上的真实 IP而是由两台 HAProxy 节点共享的一个 IP。正常情况下VIP 落在主节点上所有请求都先到主节点主节点故障后备用节点通过 VRRP 协议“抢”到这个 VIP流量自动切到备用节点。Keepalived 的配置我习惯写成这样global_defs { router_id LB1 } vrrp_script check_haproxy { script /usr/local/bin/check_haproxy.sh interval 2 weight 2 fall 2 rise 2 } vrrp_instance VI_1 { state BACKUP interface eth0 virtual_router_id 51 priority 100 advert_int 1 unicast_src_ip 192.168.1.5 unicast_peer { 192.168.1.6 } authentication { auth_type PASS auth_pass lbsecret } virtual_ipaddress { 192.168.1.100/24 } track_script { check_haproxy } notify_master /etc/keepalived/notify_master.sh notify_backup /etc/keepalived/notify_backup.sh }注意这里的两点经验两台节点都用state BACKUP配合不同的priority。这样默认由 priority 高的一台当主避免“双主”导致的 IP 冲突。生产环境建议用unicast_peer单播配置不依赖组播减少交换机开启 multicast 带来的不确定性。check_haproxy.sh脚本的作用是如果 HAProxy 进程都没了就主动停掉 Keepalived让 VIP 立刻漂移走。脚本内容很简单#!/bin/bash if ! pgrep -x haproxy /dev/null; then systemctl stop keepalived exit 1 fi exit 0这段逻辑的重点是进程级别挂了就停掉 Keepalived让备用节点接管但进程活着、业务异常则交给 HAProxy 自己的健康检查来摘后端。3.2 HAProxy 核心配置mode、ACL、健康检查HAProxy 的配置说多不多但真正用到生产环境时我建议从一份“能支撑基本业务的骨架”起步global log /dev/log local0 maxconn 65535 chroot /var/lib/haproxy user haproxy group haproxy daemon nbproc 1 nbthread 4 defaults log global mode http option httplog option dontlognull option http-server-close timeout connect 5s timeout client 30s timeout server 30s retries 3 frontend web_front bind *:80 # 生产环境这里会在前面再加一层TLS卸载或直接用443 acl is_api path_beg /api/ acl is_static path_beg /static/ /images/ /css/ use_backend api_servers if is_api use_backend static_servers if is_static default_backend web_servers backend web_servers balance roundrobin option httpchk HEAD /healthz http-check expect status 200 server web1 192.168.1.11:80 check inter 3s fall 2 rise 3 server web2 192.168.1.12:80 check inter 3s fall 2 rise 3 backend api_servers balance leastconn option httpchk GET /api/health http-check expect status 200 server api1 192.168.1.21:8080 check inter 3s fall 2 rise 3 server api2 192.168.1.22:8080 check inter 3s fall 2 rise 3配置里的参数不是随手写的。我解释几个关键点mode http适用于大多数 Web 流量可以做 7 层路由、ACL、按 URL 前缀分流。如果后面要透传数据库之类的原始 TCP 协议那 backend 就需要单独开mode tcp。balance roundrobin适合后端处理能力相近的场景leastconn适合长连接、API 型服务避免某个节点连接数堆积。option httpchk HEAD /healthz做健康检查时探活频率是 inter 3s失败 2 次摘除成功 3 次恢复。这个频率在生产环境是“稳妥折中”不会因为过于频繁增加后端压力也不会因为太慢让坏节点继续接流量。3.3 连接跟踪与“切主不掉线”的坑Keepalived 完成 VIP 漂移后很多人以为一切就自动恢复了。但有一个容易被忽略的环节当主 HAProxy 突然宕机VIP 漂移到备用节点此时原本通过主节点建立的长连接会瞬间断开。HTTP 短连接影响不大但 WebSocket、数据库长连接这类场景会感知到明显抖动。HAProxy 在这方面的处理办法是“优雅退出”。当你需要主动切主做升级时不要直接把主节点关机而是对 HAProxy 发送信号SIGTTOUkill -TTOU $(cat /run/haproxy.pid)这会告诉 HAProxy“暂停接收新连接”让正在处理的请求继续完成等连接自然收尾后再切走。等旧进程彻底退出后再执行 Keepalived 的漂移逻辑。这样就把用户对切主的感知降到最低。我每次升级前都会按这个顺序操作先排空连接再停 Keepalived最后重启主节点。顺序反了一定会出现“切主瞬间大量连接重置”的问题。4. Nginx 层业务路由、SSL 卸载和那几项关键参数从 HAProxy 出来的流量会被送到 Nginx 节点。这一层不是简单的“再分发一次”而是要承担真正的 Web 服务职责。我见过很多架构把 Nginx 只当反代忽略了它本身的性能调优导致入口层和业务层之间出现不必要的瓶颈。4.1 Nginx 的多站点配置与注意事项Nginx 最大的优势是灵活的多站点配置。开发环境可以轻松配置多个域名指向不同站点生产环境则用 server_name 区分不同业务域。我在热词里看到“多端口 nginx 开发环境多站点自定义域名配置”这种需求其实核心就是两条server { listen 80; server_name site1.example.com; root /data/www/site1; } server { listen 80; server_name site2.example.com; root /data/www/site2; }改完配置后本地访问site1.example.com需要把域名解析到本机 IP。生产环境也同样HAProxy 会根据 Host 头把流量转发到不同组而 Nginx 则按域名把请求路由到不同后端。这层设计的关键是域名越多越要把“静态资源”和“动态请求”分开。静态文件直接在 Nginx 上读 NFS 目录返回动态请求才代理到后端应用。4.2 proxy_pass 的斜杠陷阱与后端路由Nginx 反向代理最常见的坑就是proxy_pass的路径拼接问题。下面这两段配置效果完全不同location /api/ { proxy_pass http://backend_app/; }这种情况下/api/前缀会被后端 URL 里的根路径替换掉。假如请求是https://example.com/api/user/1后端实际收到的是http://backend_app/user/1。如果你也想保留/api就要写成location /api/ { proxy_pass http://backend_app; }没有尾部斜杠时Nginx 会把完整的原始 URI包括 /api传给后端。这个细节很多新手栽过跟头改了半天发现接口 404其实不是后端问题是路径被吃掉了。配合后端集群时我建议的 upstream 配置是upstream backend_app { server 192.168.1.31:8080 weight3 max_fails2 fail_timeout10s; server 192.168.1.32:8080 weight2 max_fails2 fail_timeout10s; keepalive 32; } server { listen 80; server_name www.example.com; location /api/ { proxy_pass http://backend_app/; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意keepalive 32的作用它让 Nginx 与后端应用之间保持长连接避免每次请求都重新 TCP 握手。默认情况下proxy_set_header Connection 会让 Nginx 把客户端的长连接转成与后端的长连接这个改动能明显减少握手开销尤其是高并发时。4.3 性能参数与 SSL 证书的坑Nginx 的默认配置往往是“能跑”但不是“效率最优”。生产环境我会重点调整这几个参数worker_processes auto; worker_connections 4096; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; gzip on; gzip_types text/plain application/javascript application/json;worker_processes auto让 Nginx 自动匹配 CPU 核数sendfile on让静态文件直接从内核缓冲区发送不经过用户态拷贝大文件传输效率显著提升gzip对文本类资源能省很多带宽。别小看这些参数我在压测里见过同样一台机器只调这几个值QPS 就能涨 30% 以上。关于 SSL 证书热词里有一条“nginx 替换 ssl 证书不生效”这类问题十有八九是没搞清楚reload和restart的区别。Nginx 会对nginx -s reload做平滑重载但 SSL 证书是加载到 worker 进程里的老连接可能仍然用旧证书彻底生效需要nginx -t nginx -s reload之后再确认没有旧的 worker 进程残留。如果还不行检查证书文件路径和权限尤其是ssl_certificate_key是否有nginx用户可读权限。实际排查时加一句openssl s_client -connect domain:443 -servername domain看证书有效期比盯着错误日志快得多。5. NFS 共享存储解决“多台机器数据不一致”的根本矛盾HAProxy 和 Nginx 解决了流量怎么分的问题但分完之后还有一个更棘手的问题用户上传的图片、生成的临时文件、Session 数据到底存在哪台机器上如果存在 Web1 上用户下一次请求被分到 Web2文件就没了。这就是我开头说的“订单查不到、头像 404”的根源。NFS 就是用来解决这个根本矛盾的。5.1 服务端 exports 配置与安全NFS 服务端我建议独立一台机器至少 4 核 16G 内存起步磁盘用 SSD。系统如果是 Ubuntu 22.04 这类新版系统配置 NFS 已经很方便了。首先要安装服务端apt install nfs-kernel-server然后在/etc/exports里声明要共享的目录/data/www 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)说明几个参数rw表示读写sync表示写入时先落盘再返回是更安全的推荐选项no_root_squash允许 root 用户写入的时候保留 root 权限方便运维操作但如果目录是在跨安全域共享建议别加no_subtree_check减少无谓的目录权限校验提升性能。保存后执行exportfs -rav让配置生效再通过showmount -e确认服务端共享列表。这里有个很常见的坑/etc/exports里的网段写错客户端 mount 时一直报access denied排查半天才发现是网段漏了。5.2 客户端挂载参数与 fstab 自动恢复客户端挂载 NFS 时我推荐的配置是mount -t nfs4 -o hard,intr,rsize65536,wsize65536,timeo15,retrans2 192.168.1.50:/data/www /data/www对应写到/etc/fstab则是192.168.1.50:/data/www /data/www nfs4 auto,_netdev,nofail,hard,intr,rsize65536,wsize65536,timeo15,retrans2 0 0这里说下最容易纠结的hard和soft之争。hard模式如果 NFS 服务端挂了客户端挂载点上的读写操作会一直重试直到恢复。好处是数据一致性强坏处是一旦 NFS 长时间不恢复所有依赖这个目录的服务进程会被“卡死”进而拖垮整条 Web 链路。soft模式超时后返回 IO 错误进程不会卡死。但应用层不一定能正确处理 NFS IO 错误反而可能导致数据写一半、状态混乱。我的建议是生产环境优先hardintr加外部监控脚本然后再加上_netdev和nofail防止开机时 NFS 还没就绪导致启动失败。真正要避免“NFS 挂死拖垮全部”的关键是把 NFS 的数据放“冷”一点别把所有热读写都压在共享目录上。5.3 性能优化与 Session/热文件应该放哪NFS 性能优化的核心参数是rsize和wsize默认值很多时候是 4096调成 65536 后大文件读写效率提升非常明显。还有一个技巧是readdir优化如果你共享目录下有大量小文件加上actimeo30或提高目录项缓存时间Nginx 访问静态文件时能明显降低重复 lookup 的延迟。但这里必须泼一盆冷水NFS 不是用来扛热数据的。高并发场景下Session 这类热点数据一定要迁到 Redis不要让每个请求都打 NFS 做文件锁。我早期把 Session 文件直接放在 NFS 上结果高峰期 NFS 的 IO 飙到 100%整个集群被拖得响应缓慢。后来把 Session 彻底迁到 RedisNFS 只保留上传文件、代码发布目录、归档日志这些真正需要“跨节点共享且不需要秒级同步”的数据轻松了很多。共享目录里还有一个隐形杀手文件锁。如果在应用里用flock或者依赖session_set_save_handler写文件锁NFSv4 的锁机制在不同客户端之间协调很费劲严重的会导致某个节点一直拿不到锁。规避办法很简单——别在 NFS 上做文件锁锁统一放到 Redis 做一个分布式锁组件。6. 故障演练这套架构到底能不能稳得住拔网线才知道配置写完不是结束真正的考验是“出故障时能不能自动恢复”。我强烈建议在正式上线前做一次系统的故障演练把每一层能挂的点都挂一遍看看整个链路怎么反应。这不是为了作秀而是让自己对这套系统真正做到心里有数。6.1 故障矩阵与验证命令我列一张表统一描述“故障点、预期行为、如何验证”故障点预期行为验证方式DNS 某个 A 记录指向的入口宕机云解析健康检查摘除该 IP新请求走向另一入口dig trace www.example.com观察是否只剩健康 IP主 HAProxy 宕机Keepalived 自动漂移 VIP 到备节点ip addr看 VIP 是否在备机上出现某个后端 Nginx 宕机HAProxy 健康检查摘除该节点流量只走存活节点连续 curl 观察响应来源 IPNFS 服务重启共享目录短暂卡顿后恢复之后一切正常df -h、ls /data/www是否能正常列出演练时我常用的几个命令组合# 查看 VIP 在哪台机器 ip addr | grep 192.168.1.100 # 不断请求观察响应来自哪台后端 for i in $(seq 1 100); do curl -s https://www.example.com/healthz -w \n; done # 查看 HAProxy 当前后端健康状态 echo show servers state | socat stdio /var/lib/haproxy/admin.sock生产环境如果开了 HAProxy 的 stats 页面直接看后端状态会更直观。注意 stats 页面不要暴露公网否则等于把后端 IP 列表直接送给攻击者。6.2 我演练时遇到的两个真实问题第一次我在测试环境演练“主 HAProxy 宕机”本来预期 VIP 几秒内漂移结果等了快半分钟还没切。排查发现 Keepalived 的check_haproxy脚本里我写的是pgrep -x haproxy但系统里实际跑着两个 haproxy worker 进程进程名分别是haproxy和haproxy2不对仔细一看是二进制路径下还有一层 wrapper实际上进程名叫haproxy的两份没问题。真正原因是我在脚本里又加了sleep 5去做网络检查把整个检测周期拖到了十几秒。后来我把脚本改成“进程探活 本机 80 端口探活”双管齐下检测延迟降到了 2 秒内。第二个问题更有代表性切主完成后我再用 curl 打 VIP发现连接超时。原因是主机的连接跟踪表里的旧条目还没过期而 VIP 已经跑到备机上导致部分新连接被旧的 conntrack 记录干扰。解决方法是切主前先对主节点执行sysctl -w net.netfilter.nf_conntrack_max65535加大上限同时在 keepalived 的notify_master脚本里对新节点执行conntrack -F清掉旧条目。这个细节文档里很少写但实际切主时影响很大。6.3 高可用的最底线认知演练完一轮之后有些朋友会有种错觉系统好稳丢一台机器完全不影响。但我要说句实话——这套架构能扛住的是“单点故障”扛不住的是“全链路雪崩”。比如 NFS 挂了且没在短时间内恢复所有 Nginx 节点都会被卡住HAProxy 健康检查探测 Nginx 时也会因 NFS 阻塞而超时最终 HAProxy 把所有 Nginx 都标记为 down整个入口直接全断。所以监控上一定要重点盯 NFS 的可用性。我习惯给每台 NFS 客户端配一个独立探活脚本#!/bin/bash if ! timeout 5 mountpoint -q /data/www; then echo NFS mount lost at $(date) /var/log/nfs_alert.log systemctl restart nfs-client.target fiNFS 的恢复手段并不是“重启 Nginx 就好”先恢复共享目录再让 Nginx 重新加载步骤错了会带来连环报警。最后再分享一个经验。上线之后每隔一段时间就做一次“主动拔网线”演练不要只做一次就觉得万事大吉。配置会被改、内核参数会被调、网络环境会变唯一能确认高可用的方式就是不断制造故障、观察自动恢复速度、并压住恢复时间。这套 haproxy nginx nfs dns 的架构本身不复杂复杂的是你在它身上积累的故障应对经验。真到了半夜报警响起来的那一天你会发现之前所有演练时流的汗都是值得的。
返回列表