
去年接手一个电商活动项目的入口架构改造活动流量一上来那台扛了两年的Nginx单机直接出现大面积502CPU一路冲到95%以上连接数打满运维群里全是告警。也是从那次之后我下定决心把入口层彻底重构最终落地的方案就是LVS DR模式做四层流量入口Keepalived负责VIP漂移和健康检查后端挂一组Nginx做七层反向代理再转发到业务服务。这篇记录会把整套LVSNginx反向代理高可用方案的选型逻辑、部署细节、故障演练、调优避坑完整讲清楚适合正在规划入口高可用或者已经在用Nginx但天天担心单点故障的团队参考。1. 单点故障砸到脸上之后为什么要做LVSNginx分层1.1 那台Nginx单机是怎么被打垮的当时我们的结构非常简单一台8核16G的Nginx前面接DNS后面挂四台业务节点。日常流量下它非常轻松CPU常年不超过10%我也就没把它当成瓶颈。真正的问题出现在活动预热阶段运营在首页挂了资源位瞬时并发从几百跳到几千Nginx开始频繁出现502。查下来CPU逼近100%worker进程全部处于active状态连接数直接顶到内核限制系统日志里全是too many open files。更麻烦的是Nginx单点问题让所有退路都很难走。想扩容只能人工改DNS切流量TTL生效慢而且改错了就是全站故障想滚动升级Nginx配置必须先跟业务约窗口因为reload期间一旦配置写错影响的就是整个入口。后来后端有一台Java节点内存溢出Nginx的upstream虽然自动摘除了它但我意识到只要Nginx这一层自己挂掉后面的一切防护都白搭。1.2 LVS、HAProxy、Nginx upstream、云SLB怎么选入口层选型时我列了四类方案LVS、HAProxy、Nginx自身的upstream负载均衡、云厂商SLB。它们的定位其实不太一样。LVS跑在内核态通过ipvs框架做四层转发性能极高转发过程不解析HTTP内容可以支撑非常大的并发连接。配合Keepalived做VIP漂移是自建机房里最经典的入口方案。HAProxy是用户态的四层/七层代理配置灵活健康检查能力非常细支持HTTP状态码匹配这种高级检查性能比LVS略低但在绝大多数业务量级下完全够用。Nginx upstream本质是七层反向代理适合做业务路由但如果把所有入口流量直接压在Nginx上HTTP解析成本会成为天花板而且Nginx本身也需要靠Keepalived或DNS来保证高可用。云SLB是托管产品开箱即用可自建机房根本没有这个选项部分合规场景也要求VIP落在自己网段内。我做选型时的思路很直接入口层不需要处理复杂的业务逻辑只需要快、稳、能漂移VIP所以选LVS DR模式业务路由层需要正则匹配、rewrite、多站点转发这些七层能力交给Nginx。HAProxy能力当然很强但团队当时对Nginx更熟悉没必要为了引入而引入。1.3 四层入口加七层反代的逻辑很多人会问既然Nginx也能做负载均衡为什么非要在前面加一层LVS答案是性能和职责边界完全不同。LVS本质是在内核态改报文并转发它不关心请求里的URI、Cookie、Header只按照调度算法把进来的TCP连接送到某台后端服务器。而Nginx的每一条请求都要走完整的HTTP解析、location匹配、upstream选择流程这带来灵活性的同时也有CPU开销。把这两层分开LVS负责摊平入口洪峰Nginx负责理解请求并做精细路由各自只做自己擅长的事。这个架构里还有一个隐含优势LVS的故障域和Nginx的故障域是隔离的LVS挂了VIP漂移到备机Nginx挂了好歹还有另一台不会出现单点连坐。整套架构里Keepalived是粘合剂。它通过VRRP协议在主备两台LVS之间共享一个VIP同时通过TCP_CHECK持续探测后端Nginx的80端口。健康检查一旦失败就自动把故障节点从LVS调度列表中摘除恢复后再加回来。也正因为Keepalived承担了探活和漂移两个关键动作它的配置准确性直接决定这套高可用架构到底可不可用。2. DR模式是首选但这两处机制理解不透必翻车2.1 NAT、TUN、DR的差异与生产选型LVS的三种工作模式经常被拿来当面试题讲但真正落地时很多人只记住了名字没搞懂响应路径的差异。NAT模式下客户端请求到达LVSLVS改写目标IP和端口后转发给真实服务器RSRS处理完再把响应回给LVSLVS改写源IP后返回客户端。问题在于所有响应流量都要从LVS过一遍LVS的网卡带宽就是整个系统的天花板而且RS的默认网关必须指向LVS网络改动面很大。TUN模式用IPIP隧道把请求封装后发给RSRS解封装后直接响应客户端解决了回程瓶颈但需要内核支持ipip模块跨网段部署还得处理隧道MTU分片问题排查起来很吃力。DR模式则是LVS和RS在同一二层网络内LVS只改写数据帧的目标MACRS处理完直接把响应包通过自身网关发出去流量根本不回LVS。生产选型我的经验是只要RS和LVS在同一交换机二层网络优先DR模式。它性能损耗最小、配置最简单、响应也不绕路。TUN模式我实际项目中用得很少主要是隧道封装带来的MTU问题在复杂网络里容易出幺蛾子出了问题不好定位。2.2 DR模式的响应路径流量怎么进、怎么出DR模式是最容易让新手困惑的因为它有个反直觉的点明明客户端请求是发给VIP的但RS收到请求后构造响应时的源IP还是VIP这个包却能直接发给客户端不经过LVS。原因在于TCP连接是靠四元组标识的源IP、源端口、目标IP、目标端口。客户端连接的是VIP的80端口RS处理完业务后把响应源IP写成VIP、目标IP写成客户端IP封装成IP包丢给默认网关由网关路由回客户端。整个过程中LVS改的是二层MAC地址而不是三层IP地址所以RS没有任何理由认为这个请求不是发给自己的。我理解这个机制时用了一个类比LVS就像一个小区门口的收发室它只负责在包裹上贴上具体楼栋房间号然后让快递员直接送过去寄件人回信时并不需要回到收发室中转。这个设计带来一个关键收益响应带宽不再受LVS网卡限制。请求流量通常远小于响应流量LVS只需要承受入口方向的请求吞吐量这在图片、文件下载这类响应体积大的业务里尤其占便宜。2.3 VIP挂在loopback与ARP抑制DR模式虽然性能好但有个必须处理的副作用。因为VIP同时存在于LVS和所有RS上当客户端或交换机做ARP广播询问谁是192.168.1.200时如果任何一台RS都跳出来应答ARP表就会混乱流量会被错误地直接送到某台RSLVS的调度逻辑就失效了。解决办法分两步。第一步把VIP绑到RS的loopback接口上不绑在物理网卡eth0上目的就是不让VIP的IP地址出现在对外的ARP应答接口上。第二步设置内核ARP参数让RS尽量不响应与VIP相关的地址解析请求。我在每台后端Nginx上执行下面这套初始化ifconfig lo:0 192.168.1.200 netmask 255.255.255.255 broadcast 192.168.1.200 route add -host 192.168.1.200 dev lo:0 cat /etc/sysctl.conf EOF net.ipv4.conf.all.arp_ignore 1 net.ipv4.conf.all.arp_announce 2 net.ipv4.conf.eth0.arp_ignore 1 net.ipv4.conf.eth0.arp_announce 2 net.ipv4.conf.lo.arp_ignore 1 net.ipv4.conf.lo.arp_announce 2 EOF sysctl -p这里有两个细节值得说。arp_ignore 1表示只应答目标IP地址是本机某个接口IP的ARP请求arp_announce 2表示使用最优本地地址做应答。设置完之后可以用arp -a在客户端或交换机上确认VIP对应的MAC应该是LVS主节点的物理网卡MAC而不是任何一台RS的MAC。如果发现第一条请求还是被送到RS多半是ARP缓存没老化等缓存过期后才会恢复正常。3. 从IP规划到KeepalivedLVSNginx全流程部署3.1 主机角色与IP规划先看一套完整的最小化集群规划。下面的IP段来自我的测试环境落地时替换成自己的网段即可。角色IP地址说明LVS主节点192.168.1.11运行Keepalived并管理LVS规则初始持有VIPLVS备节点192.168.1.12运行Keepalived主节点故障时接管VIPVIP192.168.1.200对外提供服务的虚拟地址Nginx节点1192.168.1.21七层反向代理也是LVS的RSNginx节点2192.168.1.22七层反向代理也是LVS的RS后端业务节点1192.168.1.31部署Java或Go业务服务端口8011后端业务节点2192.168.1.32部署Java或Go业务服务端口8011这套架构里客户端只访问VIPLVS把流量调度到两台Nginx的80端口Nginx根据域名、URI等七层信息继续转发到后端业务节点。测试环境建议至少用三台机器两台LVS两台Nginx两台后端业务。如果条件有限可以先把Nginx和后端业务合并部署但生产环境最好不要这样压榨否则故障隔离效果会打折扣。3.2 Keepalived主备配置实战两台LVS节点都要安装Keepalived和ipvsadm。以CentOS 7/8为例yum install -y keepalived ipvsadm systemctl enable --now keepalived主节点/etc/keepalived/keepalived.conf的关键配置如下global_defs { router_id LVS_NGINX_HA } vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass ha9527 } virtual_ipaddress { 192.168.1.200/32 dev eth0 label eth0:VIP } unicast_src_ip 192.168.1.11 unicast_peer { 192.168.1.12 } } virtual_server 192.168.1.200 80 { delay_loop 6 lb_algo rr lb_kind DR persistence_timeout 600 protocol TCP real_server 192.168.1.21 80 { weight 1 TCP_CHECK { connect_port 80 connect_timeout 3 nb_get_retry 3 delay_before_retry 2 } } real_server 192.168.1.22 80 { weight 1 TCP_CHECK { connect_port 80 connect_timeout 3 nb_get_retry 3 delay_before_retry 2 } } }备份节点的配置只有三处不同state改为BACKUPpriority改为90unicast_src_ip与unicast_peer对调其余全部保持一致。这里有个我在生产环境坚持了很多年的习惯主备之间的VRRP通告使用单播而不是默认的组播。Keepalived默认通过224.0.0.18组播地址同步状态但在交换机设置了端口隔离、防火墙策略或部分云内网里组播报文可能被直接丢弃。一旦主备互相收不到通告两边都会认为对方挂了同时抢占VIP形成脑裂。用配置里的unicast_src_ip加unicast_peer明确指定对端IP从根本上绕开组播不可达的问题。关于参数advert_int 1表示每1秒发一次VRRP通告三秒没收到对端通告就判定对方故障。不要为了追求极速切换把它压到0.5秒以下否则一次网络抖动就可能造成频繁误切换。persistence_timeout控制了来自同一来源IP的请求是否始终调度到同一台RS对有会话保持需求的业务很有用我一般设600秒。3.3 后端Nginx节点的ARP与VIP配置这部分对应第2节原理配置必须在每台RS上单独完成。我建议把命令写成一个初始化脚本并在/etc/rc.d/rc.local里追加确保重启后依然生效# 绑定VIP到lo接口 echo ifconfig lo:0 192.168.1.200 netmask 255.255.255.255 broadcast 192.168.1.200 /etc/rc.d/rc.local echo route add -host 192.168.1.200 dev lo:0 /etc/rc.d/rc.local chmod x /etc/rc.d/rc.local # ARP抑制 cat /etc/sysctl.conf EOF net.ipv4.conf.all.arp_ignore 1 net.ipv4.conf.all.arp_announce 2 net.ipv4.conf.eth0.arp_ignore 1 net.ipv4.conf.eth0.arp_announce 2 net.ipv4.conf.lo.arp_ignore 1 net.ipv4.conf.lo.arp_announce 2 EOF sysctl -p配置完成后在Nginx节点上执行curl http://192.168.1.200能正常返回页面说明VIP绑定、路由和ARP配置都通了。这里提醒一个低级但常见的坑绑在lo上的VIP掩码必须写成32位也就是255.255.255.255不能随手写成24位。写成24位会导致VIP参与子网广播行为ARP应答逻辑直接乱掉轻则流量失衡重则整个VIP不通。3.4 Nginx反向代理配置细节两台Nginx的配置逻辑基本一致都是作为LVS的RS监听在80端口然后按照域名做七层转发。下面是一份生产常用的配置片段upstream backend_orders { server 192.168.1.31:8011 max_fails2 fail_timeout10s; server 192.168.1.32:8011 max_fails2 fail_timeout10s; keepalive 32; } server { listen 80; server_name orders.example.com; location / { proxy_pass http://backend_orders; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 60s; } }几个容易忽视的配置点。第一max_fails和fail_timeout控制Nginx对后端的失败判定默认是1次和10秒我习惯调到2次和10秒避免后端启动阶段的一次抖动就触发摘除。第二proxy_http_version 1.1和Connection 是开启upstream keepalive的前提不设置的话Nginx与后端之间的TCP连接无法复用长连接池等于没配。第三DR模式下LVS不修改源IP所以Nginx能拿到真实客户端IP这对日志分析和限流策略非常有价值。这套反代同样适用于代理一些特殊服务。比如我最近把本地的Ollama推理服务也挂到了入口后面逻辑完全一样只是upstream指向127.0.0.1:11434但必须把proxy_read_timeout调大到120秒以上否则一次长推理请求会被Nginx提前断开。如果站点需要跨域记得在location里补上add_header Access-Control-Allow-Origin和相关OPTIONS处理不然前端会报invalid cors request。4. 主备切换的故障演练哪些测试是真的有效的4.1 模拟Keepalived进程故障配置完成不代表高可用成立。我见过太多配好放了一年没动过真故障才发现脚本有问题的案例。所以部署完的第一件事就是把整套集群拉进测试环境做三场演练。第一场模拟LVS主节点整体故障。最直接的方式是在主节点执行pkill keepalived此时观察备用节点正常情况下几秒内它会主动把VIP绑定到自己的eth0上ip addr show eth0 # 输出中应该出现 inet 192.168.1.200/32 scope global eth0:VIP同时客户端访问VIP应当依然正常。再去备用节点执行ipvsadm -Ln确认LVS规则也一起同步过来了。如果出现VIP漂移了但ipvs规则没有同步多半是Keepalived配置里virtual_server块和vrrp_instance块的绑定关系有问题需要重新reload才能补齐。4.2 模拟Nginx进程故障第二场模拟某台Nginx节点故障。因为Keepalived的TCP_CHECK监控的是Nginx的80端口直接在其中一台Nginx上执行systemctl stop nginx等几个健康检查周期后在主节点执行ipvsadm -Ln仔细观察输出。故障Nginx的IP并不会从列表里消失但它的Available字段会变成0意味着新连接不会再调度到它。把Nginx重新启动再过几个周期它又会自动恢复。这里要强调TCP_CHECK只检查端口通不通端口通不代表HTTP层正常。如果Nginx进程活着但worker异常接收连接后不返回响应TCP_CHECK是发现不了的。要更严格可以考虑HTTP_GET检查让Keepalived请求指定路径并匹配返回码。4.3 模拟后端业务节点故障第三场模拟的是Nginx后面的业务节点故障。在业务节点停掉服务后Nginx的max_fails机制会在几次失败后把它标记为不可用。但如果配了upstream keepalive有一个坑需要特别注意keepalive池里的空闲长连接不会立刻感知到对端进程已经挂掉要等下一次请求复用这个连接失败后Nginx才会切换到健康节点。在低流量场景下这意味着首次故障请求可能还是会报一次502。我的解决办法是打开proxy_next_upstream让Nginx在碰到连接错误时直接换下一台后端再试一次proxy_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 2;这样即使第一条连接已经失效客户端也几乎无感。这也是我对入口架构容错必须分层做的理解LVS负责摘掉故障NginxNginx负责摘掉故障业务节点两者不能互相替代。4.4 切换演练中暴露的坑演练中我踩过几个真实的坑写出来帮大家排雷。第一个坑是切换时间比预期长。如果主节点是被直接断网而不是优雅停机Keepalived需要连续三个advert_int周期没收到对端通告才判定故障。advert_int1时最快也要3秒左右才切换再叠加TCP_CHECK的若干超时周期最坏情况下业务感知的故障时间可能到10秒。对切换时间有小于1秒要求的业务不能只靠Keepalived需要在应用层做重试或链路冗余。第二个坑是脑裂。有一次我在测试环境模拟主备之间的交换机端口隔离因为早期配置用了组播两边同时抢到了VIP整个入口出现瞬时连接错乱。从那之后我不仅全面改用单播还加了一条校验规则任何节点在绑定VIP之前先检查对端是否还持有VIP发现对端同样持有VIP超过5秒就主动释放原理就是宁可丢VIP也不能脑裂。第三个坑是关于证书的。入口如果挂了多个域名证书必须同步更新到两台Nginx上。只换主节点而忽略备节点的话平时看不出来一旦VIP漂移用户访问就会看到证书错误。我现在用Ansible批量下发证书把两台Nginx的证书和配置统一管理避免这种隐性不一致。5. 生产环境调优连接超时、健康检查和Zabbix监控5.1 LVS连接跟踪与超时调优LVS在内核里维护一张连接跟踪表每条连接记录会在超时后老化删除。默认值对多数场景够用但在长连接较多的业务里执行ipvsadm -Lnc会看到积累了海量FIN_WAIT或CLOSE_WAIT状态的半开连接白白占用连接表内存。可以用下面的命令调整三类连接的超时时间ipvsadm --set 900 120 60三个参数分别对应TCP会话超时、TCP半开连接超时、UDP超时。短连接为主的业务我会把第一个降到600加快表项回收有大量长轮询和WebSocket连接的业务保持900甚至更高。要注意这个设置是覆盖式的节点发生切换后需要重新执行建议写成一个开机脚本或者在Keepalived的MASTER状态脚本里执行。连接表容量也要关注执行cat /proc/sys/net/netfilter/nf_conntrack_max看上限。如果业务峰值接近这个值需要适当调大同时注意内存占用echo 65536 /proc/sys/net/netfilter/nf_conntrack_max5.2 Nginx worker与keepalive连接池Nginx侧首先要确认worker数量和连接数配置不是默认值。我的通用起点如下worker_processes auto; worker_rlimit_nofile 65535; events { use epoll; worker_connections 65535; }worker_connections 65535表示每个worker最多同时处理65535个连接总并发量接近worker数量乘以这个值。但这个理论值同时受worker_rlimit_nofile和系统ulimit -n限制三者必须同步调大只改一个会先撞到另一个瓶颈。这也是很多人在压测时觉得明明配置了65535却不生效的原因。另一个经常被误解的参数是upstream keepalive池。很多人配置了keepalive 32;但不知道这个数字代表每台worker里为每台后端维护的空闲连接数上限不是总连接数。假设worker有4个、后端有2台空闲连接上限大约是4×32×2。这个值不是越大越好设得过高会占用大量文件描述符反而压缩了整体并发能力。我一般从32起步看压测时的waiting队列再微调。5.3 健康检测参数节奏回到Keepalived的TCP_CHECK配置参数节奏有一条原则既要快速发现故障又不能因为偶发超时就误摘除。我常用的组合在上面配置里已经写了细看是这样connect_timeout 3单次TCP连接超时3秒。nb_get_retry 3连续失败3次才判定RS不可用。delay_before_retry 2每次失败后等待2秒再重试。按这个节奏推算一台RS从开始故障到真正摘除大约经历3加2加3加2加3合计13秒左右。如果想让摘除更快可以把nb_get_retry减到2但必须有配套的后端快速恢复能力否则健康节点会在高压力下被频繁摘除又加回加重抖动。这个参数组合没有标准答案一定要根据自己业务的故障容忍度来调。5.4 用Zabbix盯住入口层高可用架构如果没有监控真的发生切换也没人知道。我用Zabbix从三个层面盯住整套入口。第一层是LVS节点本身。写一个自定义脚本检查VIP是否存在执行ip addr show | grep 192.168.1.200再检查LVS规则是否完整解析ipvsadm -Ln的输出并监控每一台RS的Available状态字段。只要Available为0立即触发告警。第二层是Nginx状态。开启stub_status模块在server块加location /nginx_status { stub_status; allow 127.0.0.1; allow 192.168.1.0/24; deny all; }Zabbix采集active connections、accepts、handled、reading、writing、waiting这些指标。waiting队列持续偏高通常说明keepalive连接池配置过小或后端响应变慢。第三层是端到端可用性。Zabbix配置一个web scenario每隔30秒请求一次VIP上的探测URL状态码非200就告警。这个探测能兜住所有下层组件的问题是我最依赖的一个监控项。如果团队已经有Prometheus用blackbox_exporter做HTTP探活也可以达到同样效果思路一致。6. 这套高可用架构的边界什么时候别硬套6.1 适合这套方案的典型场景这套LVSNginx的组合最适合的是自建机房或物理机环境的业务入口尤其是对VIP稳定性要求高、需要横向扩展入口能力的团队。具体来说有几个特征入口流量以HTTP/HTTPS为主也有少量TCP端口转发需求团队已经有Nginx基础设施熟悉它的配置和调优想在入口层获得接近线性的吞吐扩展能力同时不希望被云厂商SLB绑定业务防火墙策略、第三方回调白名单等场景需要固定的VIP。这些条件下LVS DR加Keepalived加Nginx是一套经过长周期验证的成熟组合社区资料多问题好排查排障链路清晰。6.2 不建议硬套的场景反过来有三种场景我不会推荐硬套这套方案。第一跨机房容灾。LVS DR模式依赖二层网络跨机房做VIP漂移不仅广播域撕不开切换时间也不可控。跨机房容灾更适合DNS GSLB、Anycast或者云厂商的跨地域负载均衡。第二容器化环境。在Kubernetes集群里人肉维护LVS和Keepalived属于过度设计。K8s自身的Service、Ingress Controller配合云LoadBalancer或MetalLB已经完整覆盖了负载均衡和高可用需求容器节点的网络命名空间和ARP抑制规则会让你维护成本成倍上升。第三极简场景。如果只有两台后端、流量只有几百QPS直接Nginx upstream加Keepalived就足够了。引入LVS意味着多一层架构、多一点排障复杂度只有高可用带来的价值大于运维成本时才值得。6.3 如果重新设计我会怎么选最后分享一点个人经验。如果再让我重新做一次选型我会先回答三个问题团队对Nginx的掌握程度够不够深是不是必须自建VIP流量预期有没有超过单机Nginx的能力如果三个问题的答案都是是我依然会选LVS DR加Keepalived加Nginx这套组合。它经过了太多次大促和故障演练的验证性能、稳定性、可排查性都足够成熟。如果第一个问题答案是否我建议直接用云SLB或者HAProxy把复杂度交给更成熟的产品第二个问题答案是否其实可以用DNS多IP加权轮询替代VIP漂移前提是能把DNS的TTL压到一分钟以内第三个问题答案是否那么Keepalived加Nginx已经够用真没必要为了上LVS而上LVS。架构没有好不好只有合不合适先看清楚自己的约束条件再谈方案。