从零开始:keepalived + LVS 负载均衡与高可用完全教程
适用对象 :完全没有接触过负载均衡/高可用的读者。
配套环境 :三台 Ubuntu 服务器(
192.168.1.5/.6/.7),虚拟 IP192.168.1.123,网卡名enp1s0。阅读方式 :从头往下读。每一章都建立在前一章之上,跳读会有障碍。
术语约定:正文中首次出现的关键名词用「」标注,附录 A 有完整术语字典可随时回查。
目录
- [第 0 章 我们要解决什么问题](#第 0 章 我们要解决什么问题)
- [第 1 章 网络基础复习(理解 LVS 的钥匙)](#第 1 章 网络基础复习(理解 LVS 的钥匙))
- [第 2 章 负载均衡是什么](#第 2 章 负载均衡是什么)
- [第 3 章 LVS 详解](#第 3 章 LVS 详解)
- [第 4 章 keepalived 详解](#第 4 章 keepalived 详解)
- [第 5 章 把两者组合起来](#第 5 章 把两者组合起来)
- [第 6 章 必知的内核参数](#第 6 章 必知的内核参数)
- [第 7 章 网络诊断工具箱](#第 7 章 网络诊断工具箱)
- [第 8 章 网络边界:NAT 与公网映射](#第 8 章 网络边界:NAT 与公网映射)
- [第 9 章 实战排错:本项目 8 个真实案例](#第 9 章 实战排错:本项目 8 个真实案例)
- [第 10 章 从 LVS 到 MySQL 集群](#第 10 章 从 LVS 到 MySQL 集群)
- [第 11 章 附录](#第 11 章 附录)
第 0 章 我们要解决什么问题
0.1 一台服务器的烦恼
假设你有一台服务器,上面跑着一个网页。它会遇到三种麻烦:
麻烦一:扛不住。 一台机器每秒能处理 1000 个请求。某天访问量涨到 3000,它就卡死了。
麻烦二:会坏。 硬盘坏了、电源烧了、系统崩了、你手滑打了个错命令------服务就没了。修好要半小时,这半小时里所有用户都在看错误页面。
麻烦三:要升级。 你想更新网页内容,得重启服务。重启的几秒里,用户访问会失败。
这三个麻烦,换更贵的服务器 只能缓解第一个(这叫「垂直扩展 / Scale Up」:把机器换强)。但第二种麻烦换不掉------越贵的机器坏了损失越大。
真正的解法是:用多台普通机器组成一个整体,对外表现得像一台永不宕机的机器。 这叫「水平扩展 / Scale Out」。
名词:高可用(High Availability,HA)
指系统在部分组件发生故障时,仍能继续对外提供服务的能力。核心思路是「冗余」------同一件事至少有两台机器能干,坏一台还有一台。
名词:集群(Cluster)多台计算机通过网络连接、协同工作,对外表现为一个整体的系统。
0.2 可用性怎么算:「几个 9」
业界用「几个 9」来描述可用性,含义是一年中有多少时间是不可用的:
| 可用性 | 年停机时间 | 通俗说法 |
|---|---|---|
| 99%(2 个 9) | 3.65 天 | 一般业务 |
| 99.9%(3 个 9) | 8.76 小时 | 要求不低了 |
| 99.99%(4 个 9) | 52.6 分钟 | 在线业务底线 |
| 99.999%(5 个 9) | 5.26 分钟 | 电信级 |
关键结论:单台机器的可用性上限,大概在 99.9% 左右(硬件故障 + 计划内维护 + 人为失误)。想上 99.99%,必须靠多机冗余 + 自动切换。
「自动切换」很重要。如果服务器挂了要你手动去改配置、切流量,那个切换时间是以分钟甚至小时计的,根本达不到 3 个 9。
名词:故障转移(Failover)
主节点故障时,备用节点自动接管其工作的过程。关键词是自动------不需要人干预。
0.3 我们需要的两样东西
把问题拆开,其实要解决的是两件事:
第一件:分摊流量。 把用户的请求分散到多台机器上,每台只干一部分活。
→ 这是「负载均衡(Load Balancing,LB) 」要解决的。本教程用的是 LVS。
第二件:不让入口成为单点。 负载均衡器自己也是台机器,它坏了怎么办?
→ 这是「高可用 」要解决的。本教程用的是 keepalived。
所以最终的方案是:LVS 负责分摊流量,keepalived 负责让 LVS 自己不死。 两者配合,构成一个完整的入口层。
0.4 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| 垂直扩展 | Scale Up | 把单台机器换更强 |
| 水平扩展 | Scale Out | 用多台普通机器组成整体 |
| 可用性 | Availability | 系统正常提供服务的时间比例 |
| 高可用 | High Availability / HA | 部分组件故障时仍能提供服务 |
| 集群 | Cluster | 多台机器协同,对外像一个整体 |
| 负载均衡 | Load Balancing / LB | 把请求分散到多台后端 |
| 故障转移 | Failover | 故障时自动切换到备用节点 |
| 冗余 | Redundancy | 同一功能有多份,坏一份不影响 |
第 1 章 网络基础复习(理解 LVS 的钥匙)
这一章是整份教程的地基。LVS-DR 的核心原理,本质上是 MAC 地址和 IP 地址分工的问题。 如果这一章读懂了,第 3 章会非常轻松。
1.1 一次网页访问发生了什么
你在浏览器输入 http://192.168.1.123,按下回车。接下来发生的事,简化后是这样:
你的电脑 服务器
│ │
│ ① DNS 解析:域名 → IP 地址 │ (我们直接用 IP,跳过这步)
│ │
│ ② ARP:这个 IP 的 MAC 地址是? │
│ ───── 广播询问 ────────────────►│
│ ◄──── 回答:是 aa:bb:cc:dd:ee:ff │
│ │
│ ③ TCP 三次握手 │
│ ───── SYN ─────────────────────►│
│ ◄──── SYN + ACK ───────────────│
│ ───── ACK ─────────────────────►│
│ │
│ ④ HTTP 请求 │
│ ───── GET / HTTP/1.1 ──────────►│
│ ◄──── HTTP/1.1 200 OK ─────────│
│ │
│ ⑤ 四次挥手断开 │
第 ② 步就是理解 LVS 的关键,我们展开讲。
1.2 MAC 地址与 IP 地址的分工 ★
这是本章最重要的一节。
MAC 地址(Media Access Control address)
- 烧在网卡上的物理地址,形如
94:c6:91:73:45:aa,48 位 - 作用范围:同一个局域网(二层网络)内
- 交换机就是靠 MAC 地址来决定把数据帧从哪个端口发出去的
IP 地址
- 逻辑地址,形如
192.168.1.5,32 位(IPv4) - 作用范围:整个互联网(三层网络)
- 路由器靠 IP 地址来决定把数据包往哪个方向转发
一个数据包在网络上传递时,这两层地址是同时存在的:
┌──────────────────────────────────────────┐
│ 目标 MAC: 94:c6:91:73:45:aa ← 交换机看这个 │
├──────────────────────────────────────────┤
│ 目标 IP : 192.168.1.123 ← 路由器看这个 │
├──────────────────────────────────────────┤
│ TCP 端口: 80 │
├──────────────────────────────────────────┤
│ 数据(HTTP 请求内容) │
└──────────────────────────────────────────┘
关键洞察 :这个包每经过一个路由器,MAC 地址会被换掉 (变成下一跳的 MAC),但 IP 地址从头到尾不变(除非有 NAT,第 8 章讲)。
为什么会这样?
想象寄快递:
- IP 地址 = 收件人的家庭住址(
北京市朝阳区xx路1号)------全程不变,是"最终目的地" - MAC 地址 = 从当前中转站到下一站的运输车牌号------每经过一个中转站就换一次,是"这一段怎么走"
这个类比直接解释了 LVS-DR 模式:
DR 模式做的事情,就是只换"运输车牌号"(MAC),不动"家庭住址"(IP)。
为什么不换住址?因为一旦换了住址,后端服务器回复的时候,快递单上的寄件人地址就变成了负载均衡器的地址,用户会以为是负载均衡器发的------这会让负载均衡器变成回程必经之路(那就是 NAT 模式了,见 3.3.1)。
而只换车牌号 的代价是:换了车牌号,包裹只能在同一个中转站管辖区内传递。这就是 DR 模式要求"必须在同一个二层网络"的根本原因。
1.3 ARP:IP 到 MAC 的翻译
刚才说,交换机看 MAC、路由器看 IP。那么问题来了:我知道目标 IP,但我怎么知道目标 MAC?
这就是「ARP」(Address Resolution Protocol,地址解析协议)要干的事。
工作过程:
你的电脑(192.168.1.100)想访问 192.168.1.123
① 你的电脑大喊(广播到整个局域网):
"谁是 192.168.1.123?请把你的 MAC 地址告诉我!"
(目标 MAC 填 ff:ff:ff:ff:ff:ff,即广播地址,所有设备都会收到)
② 持有 192.168.1.123 的设备回应(单播):
"我是!我的 MAC 是 94:c6:91:73:45:aa"
③ 你的电脑把这个对应关系记进「ARP 缓存」,一段时间内不用再问
查看你本机的 ARP 缓存:
bash
ip neigh # Linux
arp -a # Windows / 老版 Linux
为什么 ARP 对 LVS 这么重要?
因为我们的 VIP 192.168.1.123 会同时存在于两台机器上(后文会讲为什么)。如果两台机器都回答"我是 192.168.1.123",你的电脑会一会儿记 .5 的 MAC,一会儿记 .6 的 MAC------流量就会乱跳。
名词:ARP 冲突 / ARP 抢占
多台设备对同一个 IP 都做 ARP 应答,导致上游设备的 ARP 缓存来回翻转,流量飘忽不定。
所以我们必须让没持有 VIP 的那台机器闭嘴,不许回答 。这就是第 6 章要讲的 arp_ignore / arp_announce 参数。
1.4 网关与子网
子网(Subnet) :把一个大的 IP 范围切分成若干小块,每块是一个子网。
192.168.1.0/24 里的 /24 表示前 24 位是"网络部分",后 8 位是"主机部分",所以这个子网包含 192.168.1.1 ~ 192.168.1.254 共 254 个可用地址。
网关(Gateway) :当目标 IP 不在本子网内时,把包交给谁。
判断逻辑:
目标 192.168.1.50 → 和我 192.168.1.100 在同一子网 → 直接 ARP 问它,直接发
目标 8.8.8.8 → 不在同一子网 → 把包交给网关(通常是路由器),由它继续转发
为什么 LVS-DR 要求"同一个二层网络"?
因为 DR 只用 MAC 地址转发。MAC 地址只在同一个局域网内有效------如果后端服务器在另一个网段,中间隔着路由器,那需要靠 IP 地址做路由,而 DR 恰恰不改 IP。链条就断了。
查看路由表:
bash
ip route
# 默认输出示例:
# default via 192.168.1.1 dev enp1s0 ← 默认网关
# 192.168.1.0/24 dev enp1s0 proto kernel scope link src 192.168.1.5
default via ... 那行就是默认网关------当响应要发给一个不在本网段的客户端时,就走这条路。记住这一点,第 8 章讲"三角路由"时会用到。
1.5 端口与 TCP 连接
IP 地址 定位"哪台机器",**端口(Port)**定位"机器上的哪个程序"。
常见端口:
| 端口 | 服务 |
|---|---|
| 22 | SSH(远程登录) |
| 80 | HTTP(网页) |
| 443 | HTTPS(加密网页) |
| 3306 | MySQL 数据库 |
| 6446 / 6447 | MySQL Router 读写 / 只读 |
| 33061 | MySQL 组复制通信 |
TCP 连接 用一个「四元组」唯一标识:
(源 IP, 源端口, 目标 IP, 目标端口)
例:(192.168.1.100, 49378, 192.168.1.123, 80)
这个四元组是全网络唯一的。理解这一点很重要------因为当负载均衡器把请求转给后端时,这个四元组保持不变(DR 模式不改 IP、不改端口),后端才能正确地把响应发给客户端。
查看本机监听的端口:
bash
ss -lntp
# -l 只看 LISTEN 状态的
# -n 不做域名解析(显示数字,更快)
# -t 只看 TCP
# -p 显示是哪个进程(需要 root)
名词:三次握手(Three-way Handshake)
TCP 建立连接的过程:客户端发 SYN → 服务端回 SYN+ACK → 客户端发 ACK。三次是为了让双方都确认「我能发出去,也能收到」。
1.6 什么是虚拟 IP(VIP)★
这是 LVS 方案里最"反直觉"的概念,但理解了它,一切都通了。
正常情况下,一个 IP 地址是配置在某块物理网卡上 的。比如 192.168.1.5 配在 enp1s0 上。
但 Linux 允许你把一个 IP 配在虚拟接口 上,比如 lo:0(lo 是"loopback 环回接口",本来是给 127.0.0.1 用的):
bash
ip addr add 192.168.1.123/32 dev lo label lo:0
这条命令的意思是:在我这台机器上,"192.168.1.123 就是我自己"。
关键点:
- 加了这行以后,这台机器收到目标 IP 是 192.168.1.123 的包时,会收下(而不是丢掉转走)
- 但这个地址不会自动对外通告------你不主动去 ARP,别人不知道在你这里
名词:VIP(Virtual IP,虚拟 IP)
一个不属于任何单一物理机器的 IP 地址,由多台机器按协议(如 VRRP)共享,同一时刻只有一台机器对外"持有"它。是负载均衡和高可用的核心机制。
名词:lo:0Linux 的环回接口别名。
lo是lo:0的简写形式。给它挂 VIP 是 LVS-DR 的标准做法,因为它比物理网卡"低调"------不会主动参与物理网络的 ARP 竞争。
为什么要挂 /32 而不是 /24?
/32 表示"就这一个地址,没有同子网的邻居"。这样内核不会认为 192.168.1.0/24 整个网段都在本地(否则会引发奇怪的路由行为)。在 DR 模式里这是必须的写法。
1.7 本章小结
如果这一章只记三句话:
- MAC 管一段路,IP 管全程。 DR 只改 MAC 不改 IP,所以要求同二层。
- ARP 决定"这个 IP 的 MAC 是谁"。 VIP 出现在多台机器上时,必须让多余的机器闭嘴。
- VIP 是"能让机器收下不属于自己物理网卡的包"的机制。 它挂
lo:0,掩码/32。
1.8 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| MAC 地址 | MAC Address | 网卡物理地址,二层寻址,同一局域网内有效 |
| IP 地址 | IP Address | 逻辑地址,三层寻址,全程有效 |
| 数据帧 | Frame | 二层的数据单位(含 MAC 头) |
| 数据包 | Packet | 三层的数据单位(含 IP 头) |
| ARP | Address Resolution Protocol | 把 IP 解析成 MAC 的协议 |
| ARP 缓存 | ARP Cache | 本机记录的 IP→MAC 对应表 |
| 广播 | Broadcast | 发给局域网内所有设备的包(MAC 为 ff:ff:ff:ff:ff:ff) |
| 子网 | Subnet | 一个 IP 地址范围,用 CIDR(如 /24)表示 |
| CIDR | Classless Inter-Domain Routing | 用 /数字 表示网络位长度,如 /24 = 255.255.255.0 |
| 网关 | Gateway | 目标不在本子网时,包交给谁 |
| 路由表 | Routing Table | 内核记录"发给谁走哪条路"的表 |
| 端口 | Port | 定位机器上的具体程序,0-65535 |
| 四元组 | 4-tuple | 源IP+源端口+目标IP+目标端口,唯一标识一条连接 |
| 三次握手 | Three-way Handshake | TCP 建连:SYN → SYN+ACK → ACK |
| 四次挥手 | Four-way Handshake | TCP 断连:FIN → ACK → FIN → ACK |
| VIP | Virtual IP | 虚拟 IP,多机共享,同一时刻一台持有 |
| lo / lo:0 | Loopback Interface | 环回接口,用于挂 VIP 而不影响物理网络 |
第 2 章 负载均衡是什么
2.1 为什么需要负载均衡
上一章说,用多台机器分摊流量。但用户的浏览器只知道一个地址(比如 http://192.168.1.123),它不知道后面有三台服务器。
所以必须在中间放一个"接待员":用户只管找接待员,接待员负责把活分给后面的工人。
用户们
│
▼
┌───────────┐
│ 接待员 │ ← 负载均衡器
└───────────┘
│ │ │
▼ ▼ ▼
工人 工人 工人 ← 后端服务器
这个"接待员"就是负载均衡器。它需要做到:
- 分发:把请求按某种规则分给后端(轮询、按连接数、按权重......)
- 健康检查:发现某个后端挂了,就不再往它那儿分
- 透明:用户完全感觉不到后面有多台机器
2.2 四层负载均衡 vs 七层负载均衡 ★
这是选择技术方案时的第一个岔路口。
回顾一下网络分层模型(简化版):
| 层 | 名称 | 典型协议 | 你能看到什么信息 |
|---|---|---|---|
| 7 | 应用层 | HTTP, FTP, DNS | URL、Cookie、请求头、内容 |
| 4 | 传输层 | TCP, UDP | 源/目标端口 |
| 3 | 网络层 | IP, ICMP | 源/目标 IP |
| 2 | 数据链路层 | Ethernet, ARP | MAC 地址 |
四层负载均衡(L4)
工作在传输层,只看 IP + 端口。
- 收到一个 TCP 连接,原封不动地"转"给后端
- 不认识 HTTP,看不懂 URL、Cookie
- 代表产品:LVS、F5、HAProxy(TCP 模式)
七层负载均衡(L7)
工作在应用层,能读懂 HTTP 内容。
- 会终止 TCP 连接:客户端和负载均衡器建一条连接,负载均衡器和后端再建一条
- 能按 URL 分流(
/api走 A 组,/static走 B 组)、能按 Cookie 做会话保持、能改请求头 - 代表产品:nginx、HAProxy(HTTP 模式)、Traefik
对比:
| 维度 | 四层(LVS) | 七层(nginx) |
|---|---|---|
| 转发依据 | IP + 端口 | URL / Host / Cookie / 请求头 |
| 是否终止连接 | 否 | 是 |
| 性能 | 极高(内核态处理) | 高(用户态处理) |
| 吞吐能力 | 单机可达数十万并发 | 单机数万并发 |
| 灵活性 | 低 | 高 |
| 能否做 SSL 卸载 | 否 | 是 |
| 能否改包内容 | 否 | 是 |
为什么 LVS 快这么多? 三个原因:
- 在内核里干活 。数据包不用从内核态拷贝到用户态再拷回去(这叫「零拷贝」层面的优化)。
- 不终止连接。nginx 要维护两倍的连接(客户端→nginx,nginx→后端),每连接都要占用内存和文件描述符;LVS 只"骗"一下 MAC 地址就转走了。
- DR 模式下响应不经过它。下行流量(通常比上行大得多,比如下载图片、视频)完全不占用负载均衡器的资源。
结论:入口层用 LVS 抗量,需要精细路由时在后面再加一层 nginx。 我们这次就是纯 LVS。
2.3 常见方案对比
| 方案 | 层次 | 特点 | 适用场景 |
|---|---|---|---|
| LVS | L4 | 内核态,性能最强,配置略"硬核" | 高并发入口 |
| nginx | L7 | 配置友好,功能丰富,性能次之 | 通用 Web 网关 |
| HAProxy | L4/L7 | 专业负载均衡,功能全面 | 需要精细健康检查 |
| F5 / A10 | L4-L7 | 硬件设备,贵,稳定 | 大型企业 |
| 云 LB(如 CLB) | L4/L7 | 免运维,按量付费 | 上云场景 |
| DNS 轮询 | - | 最简单,但无法感知故障 | 粗略分流 |
| LVS + keepalived | L4 | 开源 + 高可用 | 本项目 |
2.4 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| 负载均衡器 | Load Balancer / LB | 把请求分发给后端的中间设备 |
| 后端服务器 | Backend Server | 真正处理请求的机器 |
| 四层 | L4 / Layer 4 | 传输层,看 IP + 端口 |
| 七层 | L7 / Layer 7 | 应用层,能读 HTTP 内容 |
| 终止连接 | Connection Termination | 负载均衡器分别和两端建连 |
| SSL 卸载 | SSL Offloading | 由负载均衡器负责加解密,减轻后端负担 |
| 零拷贝 | Zero-copy | 数据不在内核/用户态间反复拷贝的优化 |
| 并发 | Concurrency | 同时处理的连接数 |
第 3 章 LVS 详解
3.1 LVS 是什么
LVS(Linux Virtual Server,Linux 虚拟服务器)是章文嵩博士在 1998 年发起的开源项目。
它由两部分组成:
- IPVS (IP Virtual Server)------内核模块,真正干活的。从 Linux 2.4 内核开始就内置了。
- ipvsadm ------用户态命令行工具,用来查看和修改 IPVS 的转发表。
名词:IPVS
Linux 内核中的一个模块,实现四层负载均衡的转发逻辑。它挂在 Netfilter(内核的包处理框架)的钩子上,包一进内核就被它截获处理。
名词:Netfilter / iptablesLinux 内核的包过滤框架。Netfilter 是内核里的框架名,iptables 是操作它的命令行工具。IPVS 和 iptables 都挂在这个框架的钩子上,所以它们会互相影响(这是本项目踩坑的一个来源,见第 9 章)。
注意:
iptables是"防火墙规则",ipvsadm是"负载均衡转发表",两者是不同的东西,但都在内核包里处理链路上。
安装:
bash
apt-get install -y ipvsadm
一个重要的前提 :IPVS 需要一个内核参数 net.ipv4.ip_forward = 1(开启转发)才能真正工作。这个参数会被 K8s 等软件改动,是排查时的常见陷阱(见第 6 章)。
3.2 核心名词
先统一术语,这是读 LVS 文档的基础:
客户端
(CIP)
│
▼
┌──────────────────────────────┐
│ 负载均衡器 │
│ Director / LB / 调度器 │
│ 对外地址: VIP 192.168.1.123 │
│ 对内地址: DIP 192.168.1.5 │
└──────────────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ RealServer │ │ RealServer │
│ RIP .5 │ │ RIP .6 │
│ 跑 nginx │ │ 跑 nginx │
└──────────────┘ └──────────────┘
| 名词 | 全称 | 含义 | 本项目取值 |
|---|---|---|---|
| Director | - | 负载均衡器 / 调度器。又叫 LB、VS(Virtual Server) | 192.168.1.5 和 .6(两台都是) |
| RealServer | RS | 真正干活的服务器 | 192.168.1.5 和 .6 |
| VIP | Virtual IP | 对外提供服务的虚拟 IP | 192.168.1.123 |
| DIP | Director IP | 调度器的真实 IP(内网) | 192.168.1.5 / .6 |
| RIP | RealServer IP | 后端服务器的真实 IP | 192.168.1.5 / .6 |
| CIP | Client IP | 客户端的 IP | 任意 |
注意 :本项目的架构比较特殊------同一台机器既是 Director 又是 RealServer。正常情况下 DIR 和 RS 是分开的两组机器,但小规模场景下"既是调度者又是干活的人"很常见,也完全合法。
这么做的另一个好处:不需要额外买两台只做调度的机器,两台机器就凑齐了「两个调度器 + 两个干活的人」。
3.3 三种工作模式
LVS 有三种转发模式。理解这三种模式的区别,就等于理解了 LVS 的全部。
3.3.1 NAT 模式(Network Address Translation)
做法 :调度器改写数据包的目标 IP ,把 VIP:80 改成 RIP:80;后端回复时,再把源 IP 从 RIP 改回 VIP。
① 客户端 → 调度器
源: 客户端IP:随机端口
目标: VIP:80
② 调度器改写目标地址,转发给后端
源: 客户端IP:随机端口
目标: RIP:80 ← 改了这个
③ 后端处理,回复
源: RIP:80 ← 后端以为客户端就是发起方
目标: 客户端IP:随机端口
④ 但后端的默认网关不是调度器!包会直接从后端出去,
源 IP 是 RIP,客户端不认识这个 IP → 连接失败
所以 NAT 模式有一个硬性要求:
所有 RealServer 的默认网关必须指向调度器的 DIP。
这样第 ④ 步的包才会回到调度器,由调度器把源 IP 改回 VIP,再发给客户端。
② 调度器转发(改目标)
③ 后端回复,走网关回到调度器
源: RIP:80
目标: 客户端IP:随机端口
④ 调度器改写源地址
源: VIP:80 ← 改回 VIP
目标: 客户端IP:随机端口
→ 发给客户端
优点 :后端可以是任意操作系统,不要求同网段。
缺点:
- 调度器成为双向瓶颈------所有请求和响应都要过它
- 需要改包的 IP 和端口,开销大(要重新算 TCP 校验和)
- 所有后端必须把网关改成调度器,配置侵入性强
3.3.2 TUN 模式(IP Tunneling)
做法 :调度器把原始数据包整个封装到一个新的 IP 包里,通过 IP 隧道(IPIP)发给后端。
原始包: [ 目标IP=VIP | 数据 ]
↓ 封装
新包: [ 目标IP=RIP | [ 目标IP=VIP | 数据 ] ]
后端解封装,看到里面是 目标IP=VIP 的包。因为后端也在 lo 上配了 VIP,所以能收下。处理完后,响应直接发给客户端(源 IP 是 VIP)。
优点 :后端可以跨网段,跨机房也能用。响应不经过调度器。
缺点:需要内核支持 IPIP 隧道,配置相对复杂;封包解包有额外开销。
3.3.3 DR 模式(Direct Routing,直接路由)★
这是我们用的模式。它是性能最高的方式。
做法 :调度器只改数据包的目标 MAC 地址,IP 一个字节都不动,直接把帧从原网卡发出去。
① 客户端 → 调度器
MAC: 客户端 → 调度器
IP : 客户端 → VIP
② 调度器只改 MAC,从同一个网卡发回交换机
MAC: 调度器 → 后端 ← 只改了这个
IP : 客户端 → VIP ← 完全不动!
③ 交换机按 MAC 把帧交给后端
后端发现目标 IP 是 VIP,而自己 lo:0 上正好有 VIP
→ 收下,交给 nginx
④ nginx 响应
源 IP: VIP ← 因为当初收到的请求源就是 VIP
目标IP: 客户端IP
后端查路由表,客户端不在本地网段 → 走默认网关直接发出
★ 响应根本不经过调度器
三个硬性要求(后面 3.4 详细讲):
- 后端必须在
lo:0上有 VIP(否则收不下这个"目标不是自己"的包) - 后端绝对不能响应 VIP 的 ARP 请求(否则抢走流量)
- 调度器和后端必须在同一个二层网络(因为只靠 MAC 转发)
优点:
- 性能最高:不改 IP,不用重算校验和;响应不经过调度器
- 调度器只承担上行流量,抗压能力极强
缺点:
- 必须同一二层网络
- 后端配置有一定侵入性(要加 VIP、改内核参数)
三种模式对比总结:
| 维度 | NAT | TUN | DR |
|---|---|---|---|
| 改什么 | 目标/源 IP + 端口 | 整体封装 | 只改 MAC |
| 响应路径 | 经过调度器 | 直达客户端 | 直达客户端 |
| 要求同网段 | 否 | 否 | 是 |
| 后端要改网关 | 是 | 否 | 否 |
| 性能 | 低 | 中 | 最高 |
| 跨网段 | 可以 | 可以 | 不行 |
| 配置复杂度 | 中 | 高 | 中 |
3.4 DR 模式三条铁律
这三条是 DR 模式能不能跑通的全部。违反任何一条,都会以"诡异现象"的形式出现,而不是给你明确的报错。
铁律一:后端必须在 lo:0 上配 VIP,且掩码是 /32
bash
ip addr add 192.168.1.123/32 dev lo label lo:0
为什么? 因为 DR 模式不改 IP,后端收到的包目标 IP 仍然是 VIP。如果后端不认为"VIP 是我自己",内核会把这个包当成"需要转发的包",要么丢掉,要么按路由表转走。
为什么用 lo 而不是 enp1s0?
挂在 lo 上,这个地址就是"内部知道、对外不说"的状态。挂在物理网卡上,内核会主动参与物理网络的 ARP。
铁律二:后端绝对不能有 IPVS 转发表 ★★★
这是本项目踩过的最大的坑,也是 DR 模式最反直觉的一点。
想一下:后端收到了一个"目标 IP 是 VIP"的包。如果后端自己也配置了 virtual_server VIP:80 的 IPVS 规则,那么内核的处理逻辑是:
后端收到包 → 查自己的 IPVS 转发表 → 发现匹配 VIP:80 → 按规则又转出去!
于是包在调度器和后端之间来回打转 ,形成转发循环。最终因为 TTL 耗尽或连接表打满而失败。
推论:BACKUP 状态的机器,ipvsadm -Ln 必须为空。
这一条必须靠代码保证,而不能靠"记得手动清理"。本项目的做法是用 keepalived 的钩子(见 4.6):
成为 MASTER → 执行脚本建表
降为 BACKUP → 执行脚本清表
铁律三:调度器与后端必须在同一个二层网络
原因在 1.2 讲过:只改 MAC 就意味着帧只能在同一个广播域里传递。
推论:不能把 VIP 通过端口映射暴露到公网------这条的深层原因见第 8 章的"三角路由"。
3.5 调度算法
LVS 支持十几种调度算法,常用的有:
| 算法 | 简称 | 逻辑 | 适用 |
|---|---|---|---|
| 轮询 | rr |
依次分给每台,1→2→3→1→2→3 | 后端性能相同、请求耗时相近 |
| 加权轮询 | wrr |
按权重比例分 | 机器配置不同 |
| 最少连接 | lc |
分给当前连接数最少的 | 请求耗时差异大 |
| 加权最少连接 | wlc |
lc + 权重(默认算法) |
通用 |
| 源地址哈希 | sh |
同一客户端 IP 总是分到同一台 | 需要会话保持 |
| 目标地址哈希 | dh |
同一目标 IP 总是分到同一台 | 缓存服务器场景 |
| 最短延迟 | sed |
按响应延迟选 | 长连接场景 |
本项目用的是 rr(轮询),因为两台机器配置相同、跑的都是同一个静态页面,轮询最直观也最公平。
为什么不能用
rr去做 MySQL? 这是第 10 章的核心议题。简单说:MySQL 是有状态的,写请求必须只到主库,平均分发会导致数据不一致。
3.6 ipvsadm 命令详解
bash
# ---------- 查看 ----------
ipvsadm -Ln # 看虚拟服务和后端列表(最常用)
ipvsadm -Lnc # 看具体连接
ipvsadm -Ln --stats # 看累计连接数、字节数(判断"到底有没有流量")
ipvsadm -L --rate # 看速率
# ---------- 增加 ----------
ipvsadm -A -t 192.168.1.123:80 -s rr
# -A 增加一个虚拟服务(Add service)
# -t 指定 TCP(对应 -u 是 UDP)
# 192.168.1.123:80 VIP:端口
# -s rr 调度算法 = 轮询
ipvsadm -a -t 192.168.1.123:80 -r 192.168.1.5:80 -g -w 1
# -a 增加一个后端(Add server)
# -r 后端地址
# -g 转发模式 = DR(gatewaying)
# 对应:-m = NAT(masq),-i = TUN(ipip)
# -w 1 权重 = 1
# ---------- 删除 ----------
ipvsadm -d -t 192.168.1.123:80 -r 192.168.1.5:80 # 删一个后端
ipvsadm -D -t 192.168.1.123:80 # 删一个虚拟服务
ipvsadm -C # ★ 清空所有规则(危险!)
# ---------- 保存与恢复 ----------
ipvsadm-save > /etc/ipvs.rules
ipvsadm-restore < /etc/ipvs.rules
ipvsadm -Ln 输出的解读:
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.1.123:80 rr
-> 192.168.1.5:80 Route 1 2 479
-> 192.168.1.6:80 Route 1 0 0
| 字段 | 含义 |
|---|---|
Prot |
协议(TCP / UDP / FWM) |
LocalAddress:Port |
虚拟服务的 VIP:端口 |
Scheduler |
调度算法 |
RemoteAddress:Port |
后端地址 |
Forward |
转发模式(Route=DR,Masq=NAT,Tunnel=TUN) |
Weight |
权重 |
ActiveConn |
当前活跃连接数(正在传输数据) |
InActConn |
非活跃连接数(连接还在,但没在传数据,比如 TIME_WAIT) |
⚠️ 新手最容易误读的字段 :
InActConn=479不代表"有 479 个连接卡住了",它只是"479 个还没超时回收的表项"。判断"有没有真实流量",要看--stats里的累计值,而不是瞬时连接数。
3.7 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| LVS | Linux Virtual Server | 开源四层负载均衡项目 |
| IPVS | IP Virtual Server | 内核模块,LVS 的真正实现 |
| Netfilter | - | Linux 内核包处理框架 |
| iptables | - | 操作 Netfilter 规则的命令行工具(防火墙) |
| Director | - | 调度器 / 负载均衡器 |
| RealServer / RS | - | 真正处理请求的后端服务器 |
| DIP | Director IP | 调度器的真实 IP |
| RIP | RealServer IP | 后端的真实 IP |
| CIP | Client IP | 客户端 IP |
| NAT 模式 | Network Address Translation | 改 IP 转发,响应必过调度器 |
| TUN 模式 | IP Tunneling | IPIP 封装转发 |
| DR 模式 | Direct Routing | 只改 MAC,响应直达客户端 |
| 转发循环 | Forwarding Loop | 后端自己也有 IPVS 表导致的包打转 |
| 调度算法 | Scheduling Algorithm | 决定请求分给哪台后端 |
| rr / wrr / lc / wlc | - | 轮询 / 加权轮询 / 最少连接 / 加权最少连接 |
| TIME_WAIT | - | TCP 连接关闭后的等待状态,占表项但不占资源 |
第 4 章 keepalived 详解
4.1 keepalived 解决什么问题
到第 3 章为止,我们有了一个能分摊流量的 LVS。但还有个致命问题:
调度器自己挂了怎么办?
一台调度器挂了,用户就完全访问不到服务了------这叫「单点故障」(Single Point of Failure,SPOF)。
于是我们需要第二台机器作为备用调度器。但接下来的问题是:
VIP
192.168.1.123到底该配在哪台机器上?主调度器活着的时候配在主上;主挂了,要自动挪到备上。这个"自动挪"的过程,谁来负责?
keepalived 就是干这个的。
它做两件事:
- 管 VIP 的归属------通过 VRRP 协议,决定"现在谁持有 VIP"
- 做健康检查------发现本机服务挂了,主动让出 VIP
名词:keepalived
一个用 C 写的轻量级进程,最初是为 LVS 设计的高可用解决方案。名字含义是"保持存活"。
安装:
bash
apt-get install -y keepalived
4.2 VRRP 协议原理
VRRP(Virtual Router Redundancy Protocol,虚拟路由器冗余协议)是一个标准的网络协议(RFC 3768 / RFC 5798),keepalived 是它的实现之一。
核心思想 :把多台机器组成一个「虚拟路由器组」,这个组对外表现为一个"虚拟路由器",拥有一个虚拟 IP(就是我们的 VIP)。组内的机器通过选举决定谁当"组长"(MASTER),组长持有 VIP。
┌──────────────────────────────────┐
│ 虚拟路由器组 (VRID = 51) │
│ 对外 IP: 192.168.1.123 │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ .5 │ │ .6 │ │
│ │ MASTER │◄──►│ BACKUP │ │
│ │ pri=100 │VRRP│ pri=90 │ │
│ │ 持有 VIP │心跳│ 待命 │ │
│ └──────────┘ └──────────┘ │
└──────────────────────────────────┘
VRRP 的通信细节:
| 项 | 值 |
|---|---|
| 组播地址 | 224.0.0.18 |
| 协议号 | 112 |
| 报文类型 | Advertisement(通告) |
| 默认发送间隔 | 1 秒 |
| 承载协议 | IP 协议(不是 TCP/UDP,是直接封装在 IP 里) |
工作流程:
① 启动时,所有节点都发出自己的 Advertisement,里面带着自己的优先级
② 收到别人的通告后,比较优先级:
我的优先级更高 → 我继续发
别人的更高 → 我闭嘴,转为 BACKUP
③ 优先级最高的成为 MASTER,把 VIP 配到自己网卡上
④ MASTER 每秒发一次 Advertisement,说"我还活着"
⑤ BACKUP 一直在听。如果连续 3 秒(3 × advert_int)没收到 →
认为 MASTER 挂了 → 重新选举 → 优先级最高的那个升为 MASTER,接管 VIP
名词:Advertisement(通告报文)
MASTER 周期性发出的"我还在"的广播。可以理解为心跳。
两个关键概念:
名词:组播(Multicast)
发给"某一组"设备的包,而不是广播(发所有设备)也不是单播(发一台)。VRRP 用
224.0.0.18这个专门的组播地址。为什么要用组播而不是单播? 因为组内的机器是动态的、可能有 3 台以上,组播可以让所有成员都收到,而不需要 MASTER 知道完整的成员列表。
名词:Master_Down_Interval(MASTER 判死时间)BACKUP 判定 MASTER 已挂所需的等待时间。标准公式约为
3 × Advertisement_Interval + Skew_Time。advert_int=1时大约是 3 秒出头。这个值决定了故障切换的速度------调到 3 秒意味着最坏情况下服务中断约 3 秒。
4.3 选举与优先级
priority(优先级) :决定谁当 MASTER。范围 0-255,可用的推荐值是 1-254,默认 100。
规则:
- 优先级大的当 MASTER
- 优先级相同时,IP 地址大的当 MASTER(兜底规则)
- 优先级 255 是保留给"IP 地址持有者"的,不要用
- 优先级 0 表示"主动放弃 MASTER 身份"
重要:VRRP 组的"身份"由三样东西共同决定
(VRID, 认证方式+密码, 网卡/网段)
三样完全相同的机器才属于同一个组。其中任何一样不一致,就会出现"两台都认为自己是 MASTER"或"两台都在等对方"的诡异状态。
名词:VRID(Virtual Router ID,虚拟路由器标识)
VRRP 组的编号,范围 1-255。同一个局域网内,不同的 VRRP 组必须用不同的 VRID ,否则会互相干扰(因为它们都往
224.0.0.18发通告,但组 ID 相同会被误认为同组)。本项目的教训 :网页集群用了 VRID 51。如果将来要给 MySQL 也做 keepalived VIP,必须换成 52,否则两套 VRRP 会打架。
4.4 抢占模式 vs 非抢占模式
抢占(Preempt,默认行为)
M A S T E R (.5, pri=100) ──挂了──► BACKUP (.6) 升为 MASTER
M A S T E R (.5) 恢复 ──► 发现 .6 优先级更低 ──► 抢回 MASTER 身份
问题:主节点重启、修好、或者只是网络抖了一下,就会发生一次"抢回",每次切换都有秒级服务中断 。如果主节点反复重启,就会反复切换(「震荡 / flapping」)。
非抢占(nopreempt)
M A S T E R (.5) ──挂了──► .6 升为 MASTER
.5 恢复 ──► 虽然我优先级高, 但我不抢了, 老实当 BACKUP
好处:稳定,不会来回切。
坏处:可能出现"性能较差的机器当了主"。
怎么选?
| 场景 | 建议 |
|---|---|
| 主备机器配置相同 | 用 nopreempt,追求稳定 |
| 主备配置差异大(但要一致对外) | 不用 nopreempt(让强机器当主) |
| 频繁重启的机器 | 一定要用 nopreempt |
本项目用了 nopreempt------两台机器配置相同,稳定优先。
⚠️
nopreempt的一个坑 :它需要配合state BACKUP使用(两台都写state BACKUP)。如果一台写state MASTER一台写state BACKUP,nopreempt可能不生效。
4.5 健康检查 track_script
VIP 归属解决了"机器挂了怎么办"。但还有一种情况:机器还活着,但上面的服务挂了。
比如 nginx 进程崩溃了(或者被人误杀),机器本身没问题,keepalived 完全不知道,VIP 还挂在这台机器上------但用户访问会得到"连接被拒绝"。
解法:让 keepalived 顺便检查服务,服务不健康就主动让出 VIP。
这就是 vrrp_script + track_script:
conf
vrrp_script chk_nginx {
script "/usr/bin/killall -0 nginx" # 要执行的命令,返回 0 = 健康
interval 2 # 每 2 秒执行一次
weight -20 # 不健康时,给自己的优先级加 -20
fall 2 # 连续 2 次失败才算"真的挂了"
rise 2 # 连续 2 次成功才算"真的恢复了"
}
vrrp_instance VI_1 {
# ... 其他配置 ...
track_script {
chk_nginx # 把这个检查挂到实例上
}
}
权重机制详解(这是最容易搞混的地方):
假设 .5 的 priority=100,.6 的 priority=90,weight=-20。
正常状态:
.5: 100(基础)+ 0(健康,不加不减)= 100
.6: 90 (基础)+ 0 = 90
→ .5 是 MASTER
.5 的 nginx 挂了:
.5: 100 + (-20) = 80
.6: 90 + 0 = 90
→ 80 < 90,.6 变成 MASTER,接管 VIP
.5 的 nginx 恢复了:
.5: 100 + 0 = 100
→ 100 > 90,如果没配 nopreempt,.5 抢回 MASTER
权重的正负号含义:
| 权重 | 含义 | 典型用途 |
|---|---|---|
负值 (如 -20) |
不健康时降低自己优先级 | 最常用:服务挂了就让位 |
正值 (如 +30) |
健康时提高自己优先级 | 想让"更健康的机器"优先当主 |
fall和rise的意义(防抖) :如果不配,那么"检查失败一次就切换"------网络抖动、检查命令执行慢一点,都会触发误切换。配fall 2表示"连续 2 次失败才算数",配rise 2表示"连续 2 次成功才算恢复"。
killall -0 是什么?
bash
killall -0 nginx
# -0 表示发"0 号信号" ------ 实际上不发送任何信号,
# 只是检查"这个进程存在吗、我有权限吗"
# 返回 0 = 进程存在;返回非 0 = 不存在
这是一个非常轻量的"进程存活检查"。更严谨的做法是检查服务是否真的能响应:
conf
vrrp_script chk_nginx {
script "/usr/bin/curl -fsS -m 2 -o /dev/null http://127.0.0.1/health"
# -f HTTP 状态码 >= 400 时返回非 0
# -s 静默
# -m 2 超时 2 秒
interval 2
weight -20
}
区别 :killall -0 只能发现"进程死了",发现不了"进程活着但服务假死"(比如死循环、数据库连接耗尽)。用 curl 检查能发现后者。
4.6 notify 钩子机制 ★
这是本项目修掉最大那个 bug 的关键机制。
keepalived 在状态发生变化时,可以调用你自己指定的脚本,这叫「钩子(hook)」。
四种状态变化钩子:
| 钩子 | 触发时机 |
|---|---|
notify_master |
本机变成 MASTER |
notify_backup |
本机变成 BACKUP |
notify_fault |
本机进入 FAULT 状态(健康检查失败等) |
notify_stop |
keepalived 服务停止时 |
notify |
任何状态变化都触发(会传入状态参数) |
本项目的用法:
conf
vrrp_instance VI_1 {
# ...
notify_master "/etc/keepalived/lvs-ipvs-add.sh"
notify_backup "/etc/keepalived/lvs-ipvs-del.sh"
notify_fault "/etc/keepalived/lvs-ipvs-del.sh"
notify_stop "/etc/keepalived/lvs-ipvs-del.sh"
}
/etc/keepalived/lvs-ipvs-add.sh:
bash
#!/bin/bash
# 只在成为 MASTER 时执行
ipvsadm -C 2>/dev/null || true # 先清空,防止规则重复叠加
ipvsadm -A -t 192.168.1.123:80 -s rr # 建虚拟服务
ipvsadm -a -t 192.168.1.123:80 -r 192.168.1.5:80 -g -w 1
ipvsadm -a -t 192.168.1.123:80 -r 192.168.1.6:80 -g -w 1
logger -t lvs "IPVS rules ADDED (this node is MASTER)"
/etc/keepalived/lvs-ipvs-del.sh:
bash
#!/bin/bash
# 在降为 BACKUP / FAIL / STOP 时执行
ipvsadm -C 2>/dev/null || true
logger -t lvs "IPVS rules REMOVED (this node is BACKUP)"
为什么这个设计如此重要?
回到铁律二:BACKUP 机器上不能有 IPVS 表。
- 错误做法 :在
keepalived.conf里静态写virtual_server段
→ 后果:两台机器都会生成 IPVS 表(因为配置文件一样),导致转发循环 - 正确做法 :用
notify_master钩子动态建表
→ 效果:只有真正的 MASTER 才有表,这是代码层面的保证,而不是靠"记住手动清理"
这个坑的完整复盘见第 9 章案例 1。
几个实战注意事项:
- 钩子脚本必须有执行权限 :
chmod +x /etc/keepalived/*.sh - 脚本里不要用
set -e后再让某条命令失败 ,否则脚本会中途退出(本项目用的|| true就是这个作用) enable_script_security+script_user root:keepalived 默认降权执行脚本。如果脚本需要 root 权限(比如ipvsadm),必须显式配置:
conf
global_defs {
enable_script_security
script_user root
}
- 排查钩子有没有跑,看系统日志:
bash
journalctl -u keepalived -n 50 --no-pager
# 脚本里写的 logger -t lvs "..." 也会出现在这里
4.7 keepalived.conf 逐行详解
下面是本项目 LVS 版的实际配置,逐行注释:
conf
# ==================== 全局配置 ====================
global_defs {
router_id LVS_NODE_1 # 本节点标识,同组内必须唯一(只是个名字)
enable_script_security # 启用脚本安全(防止非 root 用户执行脚本)
script_user root # 允许脚本以 root 身份运行(ipvsadm 需要)
}
# ==================== 健康检查脚本定义 ====================
vrrp_script chk_nginx {
script "/usr/bin/killall -0 nginx" # 检查命令,返回 0 = 健康
interval 2 # 执行间隔(秒)
weight -20 # 不健康时优先级减 20
fall 2 # 连续 2 次失败才判定为故障
rise 2 # 连续 2 次成功才判定为恢复
}
# ==================== VRRP 实例 ====================
vrrp_instance VI_1 {
state MASTER # 初始意向(实际角色由选举决定)
interface enp1s0 # VRRP 报文从哪块网卡收发 / VIP 挂在哪
virtual_router_id 51 # ★ VRRP 组编号,同组必须一致,同网段不能撞
priority 100 # ★ 优先级,大的当 MASTER
advert_int 1 # 心跳间隔(秒),默认 1
nopreempt # ★ 非抢占:修好后不主动抢回 MASTER
authentication { # 认证,同组必须完全一致
auth_type PASS # 密码认证
auth_pass HA_lvs_2026 # 密码,前 8 位有效
}
virtual_ipaddress { # 本组管理的 VIP
192.168.1.123/24 dev enp1s0
# 注意:这里写 /24 是因为要挂在物理网卡上参与 ARP
# 后端的 lo:0 上则是 /32(见铁律一)
}
track_script { # 挂载健康检查
chk_nginx
}
# ========== 状态变化时执行的钩子 ==========
notify_master "/etc/keepalived/lvs-ipvs-add.sh" # 当上 MASTER → 建 IPVS 表
notify_backup "/etc/keepalived/lvs-ipvs-del.sh" # 降为 BACKUP → 清 IPVS 表
notify_fault "/etc/keepalived/lvs-ipvs-del.sh"
notify_stop "/etc/keepalived/lvs-ipvs-del.sh"
}
逐条验证配置是否正确:
bash
# 检查语法
keepalived -t -f /etc/keepalived/keepalived.conf
# 重载配置(不断服务)
systemctl reload keepalived
# 看状态和日志
systemctl status keepalived
journalctl -u keepalived -f
4.8 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| keepalived | - | 实现 VRRP + 健康检查的高可用守护进程 |
| VRRP | Virtual Router Redundancy Protocol | 虚拟路由器冗余协议,决定 VIP 归属 |
| 虚拟路由器 | Virtual Router | 由多台机器组成、共享一个 VIP 的逻辑路由器 |
| VRID | Virtual Router ID | VRRP 组编号,1-255 |
| MASTER | - | 持有 VIP 的节点 |
| BACKUP | - | 待命的备用节点 |
| Advertisement | - | MASTER 周期性发出的"我活着"通告 |
| 组播 | Multicast | 发给特定一组设备的包(VRRP 用 224.0.0.18) |
| Master_Down_Interval | - | BACKUP 判定 MASTER 死亡的时间(约 3×advert_int) |
| priority | - | 优先级,决定谁是 MASTER |
| 抢占 | Preempt | 高优先级节点恢复后抢回 MASTER |
| 非抢占 | nopreempt | 恢复后不抢回,稳定优先 |
| 震荡 | Flapping | 状态反复切换 |
| 单点故障 | SPOF | 一个组件坏了整个系统就不可用 |
| 健康检查 | Health Check | 周期性检测服务是否正常 |
| track_script | - | keepalived 中挂载健康检查的配置项 |
| weight | - | 健康检查结果对优先级的加减值 |
| fall / rise | - | 连续几次失败/成功才切换状态(防抖) |
| notify 钩子 | notify hook | 状态变化时调用的自定义脚本 |
| 钩子 | Hook | 在特定时机被自动调用的脚本 |
第 5 章 把两者组合起来
5.1 完整架构图
互联网 / 局域网客户端
│
▼ http://192.168.1.123
┌─────────────────────────────────────────────────────┐
│ VIP 192.168.1.123 │
│ 由 keepalived 决定此刻挂在谁身上 │
└─────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ VRRP 心跳 224.0.0.18 │
│ 每 1 秒一次 │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 192.168.1.5 │ │ 192.168.1.6 │
│ hostname = server │ │ hostname = admin │
│ │ │ │
│ keepalived │ │ keepalived │
│ └ 角色: MASTER │ │ └ 角色: BACKUP │
│ └ pri: 100 │ │ └ pri: 90 │
│ └ 持有 VIP │ │ └ 不持有 VIP │
│ └ IPVS 表: 有 ★ │ │ └ IPVS 表: 空 ★ │
│ │ │ │
│ nginx(业务页面) │ │ nginx(业务页面) │
│ lo:0 有 VIP │ │ lo:0 有 VIP │
│ enp1s0: 有 VIP │ │ enp1s0: 无 VIP │
└──────────────────────┘ └──────────────────────┘
几个容易混淆的细节:
| 位置 | 有没有 VIP | 会不会回 ARP | 有没有 IPVS 表 |
|---|---|---|---|
MASTER 的 enp1s0 |
有 | 会(它是唯一该回应的) | 有(它是调度器) |
MASTER 的 lo:0 |
有 | 不会 | - |
BACKUP 的 enp1s0 |
无 | - | - |
BACKUP 的 lo:0 |
有 | 不会 | 空(铁律二) |
为什么两台机器的
lo:0上都有 VIP?因为当
.6升为 MASTER 时,它需要立刻能收下"目标 IP 是 VIP"的转发包。如果此刻才去配 VIP,会有延迟。预先配好、但不对外通告,是最优雅的做法。这也再次说明:
lo:0上的 VIP 是"内部识别用",enp1s0上的 VIP 是"对外通信+ARP 用"。用途不同。
5.2 一次请求的完整生命周期
假设当前 .5 是 MASTER,客户端 192.168.1.100 访问 http://192.168.1.123。
【阶段一:找路】
① 客户端查 ARP 缓存,问 "192.168.1.123 的 MAC 是谁?"
② .5 回答:我的 MAC 是 94:c6:91:73:45:aa(因为 VIP 在它的 enp1s0 上)
.6 收到询问,但因为 arp_ignore=1,VIP 不在它的 enp1s0 上 → 不回答 ✓
【阶段二:请求】
③ 客户端发出:
目标 MAC = 94:c6:91:73:45:aa(.5)
目标 IP = 192.168.1.123:80
④ .5 的 enp1s0 收到(MAC 是它的,收下)
⑤ 内核 Netfilter 钩子触发 IPVS:
- 查转发表,发现匹配 "192.168.1.123:80"
- 按 rr 算法选中 .6
- 建立连接记录:(客户端IP, 随机端口, VIP, 80) → .6
⑥ IPVS 改写目标 MAC 为 .6 的 MAC(94:c6:91:7e:06:29),
IP 头完全不动,从 enp1s0 发回交换机
【阶段三:处理】
⑦ 交换机按 MAC 转发给 .6
⑧ .6 收到包,检查目标 IP = 192.168.1.123
→ 内核发现"这是我的地址吗?"
→ 查本地地址列表,发现 lo:0 上配着 192.168.1.123
→ 是的,是我的 → 收下,交给 nginx ✓
(这就是 lo:0 上必须配 VIP 的原因)
⑨ nginx 处理请求,生成响应:
源 IP = 192.168.1.123
目标 IP = 192.168.1.100
【阶段四:响应】
⑩ .6 查路由表:
192.168.1.100 在 192.168.1.0/24 网段内 → 本地直连
(如果客户端在外网 → 走默认网关 192.168.1.1)
⑪ 直接从 enp1s0 发出 → 交换机 → 客户端
★ 注意第 ⑪ 步:响应完全不经过 .5,这是 DR 模式的性能优势所在
把这张流程记住,几乎所有 LVS 的排错都是在这 11 步里找"哪一步断了"。
5.3 一次故障切换的完整过程
场景 A:.5 整机宕机
T+0s .5 断电
T+1s .5 没有发出 VRRP Advertisement
T+2s 依然没有
T+3s .6 累计 3 次没收到 → 判定 MASTER 已死(Master_Down_Interval 到)
T+3s .6 发起选举 → 自己是组内唯一存活,优先级最高
T+3s .6 执行 notify_master 钩子:
/etc/keepalived/lvs-ipvs-add.sh
→ ipvsadm -A ... 建立 IPVS 表
T+3s .6 把 VIP 192.168.1.123 加到自己的 enp1s0
T+3s .6 开始发出自己的 Advertisement(宣告我是新 MASTER)
T+3s 新客户端 ARP 询问 VIP → .6 回答 → 流量来了
★ 网络中断时间:约 3 秒
★ 期间已经建立的连接会断开(客户端需要重连)
★ 客户端不需要做任何配置变更
场景 B:.5 活着,但 nginx 挂了
T+0s .5 上的 nginx 崩溃
T+0s .5 的 keepalived 仍然每隔 1 秒发 Advertisement(机器还活着)
T+2s chk_nginx 第 1 次执行,失败
T+4s chk_nginx 第 2 次执行,失败 → 达到 fall=2,判定为故障
T+4s .5 的优先级 100 + (-20) = 80
T+4s .6 收到 .5 的 Advertisement,发现里面优先级变成了 80
80 < 我的 90 → .6 认为应该由自己当 MASTER
T+4s .5 执行 notify_fault → 清空 IPVS 表,摘掉 VIP
T+4s .6 升为 MASTER → 建表 + 挂 VIP
★ 中断时间:约 4 秒(取决于 interval × fall)
★ .5 上的 keepalived 没停,只是"降级"
场景 C:.5 修好了
情况 1:配置了 nopreempt(本项目)
→ .5 恢复为 BACKUP,VIP 保持在 .6 上,不发生切换 ✓ 稳定
情况 2:没有 nopreempt(默认抢占)
→ .5 优先级恢复为 100 > 90 → .5 抢回 MASTER
→ 又一次 3 秒中断
→ 如果 .5 反复重启,就会反复中断(震荡)
5.4 部署顺序与验证
部署顺序(在每台机器上执行对应编号):
bash
# 1. 安装软件
apt-get install -y nginx keepalived ipvsadm curl psmisc iproute2
# 2. 配置内核参数(ARP 抑制等,见第 6 章)
cat > /etc/sysctl.d/90-lvs-dr.conf <<EOF
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.lo.arp_ignore = 1
net.ipv4.conf.lo.arp_announce = 2
net.ipv4.conf.enp1s0.arp_ignore = 1
net.ipv4.conf.enp1s0.arp_announce = 2
net.ipv4.ip_nonlocal_bind = 1
EOF
sysctl -p /etc/sysctl.d/90-lvs-dr.conf
# 3. 在 lo:0 上挂 VIP(两台都做)
ip addr add 192.168.1.123/32 dev lo label lo:0
# 4. 写辅助脚本 + keepalived.conf(见 4.6 / 4.7)
# 5. 启动
systemctl enable --now keepalived nginx
验证清单(每一条都要过):
bash
# ① 角色是否正确(应该一台 MASTER 一台 BACKUP)
systemctl status keepalived | grep -i state
ip a show enp1s0 | grep 192.168.1.123 # 只有 MASTER 应该输出
# ② lo:0 上两台都要有 VIP
ip a show lo | grep 192.168.1.123
# ③ ★ 最关键的检查:BACKUP 的 IPVS 表必须为空
ipvsadm -Ln
# MASTER 应该有 1 个虚拟服务 + 2 个后端
# BACKUP 应该是空的(只有一行版本信息)
# ④ 内核参数生效值
sysctl net.ipv4.conf.all.arp_ignore net.ipv4.conf.all.arp_announce
# ⑤ 服务本身
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1/
# ⑥ 从客户端验证轮询(多跑几次看是否交替)
for i in {1..10}; do curl -s -m 3 -o /dev/null -w "%{http_code} " http://192.168.1.123; done
echo
5.5 本章小结
三句话总结整个方案:
- keepalived 决定"谁是入口"------通过 VRRP 选举出 MASTER,VIP 只在它身上。
- LVS 决定"请求给谁"------MASTER 上的 IPVS 表做轮询分发。
- DR 模式让响应绕过入口------所以入口几乎不会成为瓶颈。
两个必须靠代码保证的约束(不能靠人记):
- MASTER 有 IPVS 表,BACKUP 没有 → 用
notify_master/notify_backup钩子 - BACKUP 不回 VIP 的 ARP → 用
arp_ignore=1/arp_announce=2
第 6 章 必知的内核参数
6.1 内核参数是什么
Linux 内核有上千个可调参数,统称「sysctl 参数 」,放在 /proc/sys/ 下,以文件形式存在。
两种查看方式:
bash
# 方式一:直接读文件(看到的是原始值)
cat /proc/sys/net/ipv4/ip_forward
# 方式二:用 sysctl 命令(推荐,会自动转换 / 为 .)
sysctl net.ipv4.ip_forward
# 输出: net.ipv4.ip_forward = 1
# 只看值,不要名字(适合写脚本)
sysctl -n net.ipv4.ip_forward
# 输出: 1
修改方式:
bash
# 临时修改(立即生效,重启失效)
sysctl -w net.ipv4.ip_forward=1
# 永久修改:写到配置文件
echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/99-mynet.conf
sysctl --system # 重新加载所有配置文件
重要:
/etc/sysctl.d/和/etc/sysctl.conf的关系
/etc/sysctl.conf是传统的主配置文件/etc/sysctl.d/*.conf是"配置片段"目录,方便不同软件各自放自己的配置- 加载顺序:先
/etc/sysctl.conf,再按文件名字典序 加载/etc/sysctl.d/*.conf- 后面的会覆盖前面的(同名参数)
所以命名有讲究:数字前缀控制顺序。本项目用了
90-lvs-dr.conf、91-lvs-forward.conf,后来的清理脚本删*kube*时不会误伤。
6.2 ARP 抑制:arp_ignore / arp_announce ★
这两个参数是 DR 模式能工作的基础。
背景问题
我们两台机器的 lo:0 上都有 192.168.1.123。如果两台都对外回答"我是 192.168.1.123",会发生什么?
时刻 T1: 客户端问 "192.168.1.123 是谁?"
.5 回答: 我的 MAC 是 73:45:aa
.6 回答: 我的 MAC 是 7e:06:29
→ 客户端也不知道该用哪个,可能用最后收到的
时刻 T2: 客户端问 "192.168.1.123 是谁?"
→ 这次可能是 .5 先回答
结果:客户端的 ARP 缓存来回翻转,请求一会儿发到 .5、一会儿发到 .6
→ 表现为"概率性连不上"
arp_ignore:控制"要不要回答 ARP 请求"
取值范围 0-8,常用的三个:
| 值 | 行为 | 含义 |
|---|---|---|
| 0 | 只要本机有任意接口配了这个 IP,就回答 | 默认值,最宽松 |
| 1 | 只有当目标 IP 配在"收到请求的那个接口"上,才回答 | ★ 我们用的 |
| 2 | 在 1 的基础上,还要求源 IP 与接口在同一子网 | 更严格 |
| 8 | 不回答所有 ARP 请求 | 极端场景 |
为什么用 1?
场景:客户端从 enp1s0 发来 ARP 请求,问 192.168.1.123 是谁。
.5的enp1s0上有192.168.1.123(因为它是 MASTER)→ 回答 ✓.6的enp1s0上没有192.168.1.123(VIP 在它的lo:0上,不在 enp1s0)→ 不回答 ✓
完美!arp_ignore=1 精准地实现了"只有 MASTER 才回答 ARP"。
arp_announce:控制"发 ARP 请求时,源 IP 填什么"
取值范围 0-2:
| 值 | 行为 | 含义 |
|---|---|---|
| 0 | 可以用本机任意 IP 作为源地址 | 默认值,最宽松 |
| 1 | 尽量用与目标地址在同一子网的源地址 | 中间档 |
| 2 | 总是用最合适的本地地址(忽略请求里指定的源) | ★ 我们用的 |
为什么用 2?
防的是这种情况:.6 主动发起 ARP 请求时(比如要访问某个客户端),内核可能会"顺手"把源 IP 写成 VIP。这样对方就会记录"VIP 的 MAC 是 .6 的 MAC"------相当于 .6 自己把 VIP 泄露了出去(这叫「ARP 通告污染」)。
arp_announce=2 强制内核永远不要用 VIP 当源地址,从根上杜绝这个问题。
配置写法
bash
cat > /etc/sysctl.d/90-lvs-dr.conf <<'EOF'
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.lo.arp_ignore = 1
net.ipv4.conf.lo.arp_announce = 2
net.ipv4.conf.enp1s0.arp_ignore = 1
net.ipv4.conf.enp1s0.arp_announce = 2
net.ipv4.ip_nonlocal_bind = 1
EOF
sysctl -p /etc/sysctl.d/90-lvs-dr.conf
为什么要写这么多行(all / lo / enp1s0)?
因为 arp_ignore 和 arp_announce 不是"all 优先"或"接口优先",而是取最大值 。所以必须把 all 和每块接口都设上,才能保证任何路径下生效值都是 1 / 2。
这是本项目一个重要的经验:不要以为设了接口级就够了,
all也要设。
6.3 反向路径过滤:rp_filter ★
这个参数在干什么
「rp_filter 」(Reverse Path Filter,反向路径过滤)是一个安全特性 ,用来防「IP 欺骗」。
工作逻辑:
收到一个包,源 IP 是 X,从 enp1s0 进来的
内核问自己:如果我要发一个包给 X,按路由表该从哪块网卡出去?
├─ 答案也是 enp1s0 → 一致 → 放行 ✓
└─ 答案不是 enp1s0(而是 enp0s3 之类)→ 不一致 → 丢弃 ✗
为什么这能防欺骗?因为正常情况下,从一个方向来的包,回程也应该走同一个方向。如果有人伪造源 IP,回程路径就对不上。
三个取值
| 值 | 名称 | 行为 |
|---|---|---|
| 0 | 关闭 | 不检查,全部放行 |
| 1 | 严格模式 | 按"最具体的路由"检查,最严格 |
| 2 | 松散模式 | 只要"任意接口能到这个源"就放行,较宽松 |
为什么 DR 模式建议设为 0
理论上,DR 模式下包从 enp1s0 进来,源是客户端 IP,回客户端的路由出口也是 enp1s0 ------ 一致,不会被丢。
但实践中经常出问题,原因是:
- 回程可能走不同路径(多网卡、多路由的场景)
- 非对称路由的存在
- 上面说的
all和接口级取最大值 的问题
所以 LVS-DR 的部署文档普遍建议把它设为 0。
★ 本项目的重要教训:「相关性 ≠ 因果性」
我们当时遇到丢包,看到 rp_filter 的值是:
bash
sysctl net.ipv4.conf.all.rp_filter # 2
sysctl net.ipv4.conf.enp1s0.rp_filter # 0
all=2 看起来非常可疑。于是我们去改了 /etc/sysctl.d/10-network-security.conf,把 2 改成 0,重启后验证:
bash
sysctl net.ipv4.conf.all.rp_filter # 0 ✓ 改好了
但丢包现象一点没变。
结论:rp_filter 不是根因。 它只是一个"看起来非常像凶手"的干扰项。
这个教训值得单独记下来:
排查问题时,判断一个因素是不是原因,唯一的标准是"去掉它,现象是否消失" 。
一个参数"看起来很专业"、"和故障领域高度相关"、"解释起来很有说服力",这些都不能作为它是原因的证据。
如果当时花 5 分钟先做这个验证,能省掉后面一大圈弯路。
★ 另一个必知细节:all 和接口级取最大值
bash
sysctl net.ipv4.conf.all.rp_filter # 2
sysctl net.ipv4.conf.enp1s0.rp_filter # 0
最终生效的是 2,不是 0。 因为内核在判断时会同时看 all 和具体接口的值,取较大的那个。
这个规则对 rp_filter、arp_ignore、arp_announce 都适用。这是"我明明改了为什么不生效"的最常见原因。
正确做法:两处都设。
bash
cat > /etc/sysctl.d/99-lvs-rpfilter.conf <<'EOF'
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.lo.rp_filter = 0
net.ipv4.conf.enp1s0.rp_filter = 0
EOF
sysctl --system
6.4 转发开关:ip_forward
bash
sysctl net.ipv4.ip_forward
# 0 = 关闭转发(默认,普通主机)
# 1 = 开启转发(路由器/网关/负载均衡器需要)
为什么需要它? IPVS 的转发逻辑依赖内核的转发能力。ip_forward=0 时,内核直接丢弃"目标 IP 不是本机"的包,IPVS 就没机会工作了。
本项目的踩坑点:
K8s 会在 /etc/sysctl.d/ 下写自己的配置文件,其中包含 net.ipv4.ip_forward = 1。清理 K8s 时,如果无脑删掉 /etc/sysctl.d/*kube* 之类的文件,ip_forward 可能会回落到默认值 0,导致 LVS 静默失效。
解法:单独写一个文件,明确锁死这个值。
bash
cat > /etc/sysctl.d/91-lvs-forward.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF
sysctl --system
命名技巧 :用 91- 前缀,且文件名里不含 kube / flannel / docker 等关键词,这样清理脚本用 rm /etc/sysctl.d/*kube* 时就不会误伤。
6.5 非本地绑定:ip_nonlocal_bind
bash
sysctl net.ipv4.ip_nonlocal_bind
# 0 = 不允许程序绑定"不属于本机"的 IP 地址
# 1 = 允许
场景 :如果想让 nginx 或 HAProxy 监听 VIP,但 VIP 此刻不一定在这台机器上(比如 BACKUP 状态),默认会报错 Cannot assign requested address。
设成 1 就允许了。这是"预配置"的常用技巧------让服务在还没拿到 VIP 时就能启动并监听。
本项目的 LVS-DR 方案其实用不到它 (因为在 lo:0 上预先配了 VIP),但设成 1 无害且有备无患,所以一并配上了。
6.6 参数生效的优先级规则(为什么我改了不生效)
这是新手最容易踩的坑,单独开一节。
规则一:文件加载顺序
/etc/sysctl.conf
↓ 然后按文件名字典序
/etc/sysctl.d/10-network-security.conf
/etc/sysctl.d/90-lvs-dr.conf
/etc/sysctl.d/91-lvs-forward.conf
/etc/sysctl.d/99-sysctl.conf
↓
后加载的覆盖先加载的(同名参数)
所以 99- 开头的文件会覆盖 90- 开头的。如果某个软件的配置文件用 99- 命名,它可能悄悄覆盖你的设置。
规则二:all vs 接口级:取最大值
对 rp_filter、arp_ignore、arp_announce 这些参数:
生效值 = max( net.ipv4.conf.all.X , net.ipv4.conf.<接口>.X )
所以只改一处是不够的。
规则三:修改后必须重新加载
bash
sysctl --system # 重新加载所有配置文件(推荐)
sysctl -p /path/to/file # 只加载指定文件
sysctl -w key=value # 只改这一个(临时)
注意 :sysctl -w 的修改重启后会丢失。
规则四:确认"生效值",而不是"配置文件里的值"
这是最关键的。排查时永远用这条命令看真实状态:
bash
sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.enp1s0.rp_filter
不要只看配置文件内容就下结论。配置文件写了不等于生效(可能被后面的文件覆盖了)。
规则五:有些参数需要重启才生效
少部分参数(如和 netfilter 模块相关的一些)改完需要重启。大部分 arp_* / rp_filter / ip_forward 是即时生效的。
6.7 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| sysctl | - | Linux 内核参数的查看/修改工具 |
| 内核参数 | Kernel Parameter | 内核的可调配置项,位于 /proc/sys |
| arp_ignore | - | 控制本机在什么条件下回应 ARP 请求 |
| arp_announce | - | 控制本机发 ARP 时用哪个源 IP |
| rp_filter | Reverse Path Filter | 反向路径过滤,防 IP 欺骗的安全特性 |
| ip_forward | - | 内核是否转发非本机的包 |
| ip_nonlocal_bind | - | 是否允许绑定非本机 IP |
| 严格模式 | Strict Mode | rp_filter=1,按最具体路由检查 |
| 松散模式 | Loose Mode | rp_filter=2,任意接口可达即放行 |
| 取最大值 | max() semantics | all 与接口级参数同时存在时取大者 |
| 生效值 | Effective Value | 内核实际使用的值(区别于配置文件里的值) |
| ARP 污染 | ARP Pollution | 本机用 VIP 当源地址发 ARP,污染上游缓存 |
第 7 章 网络诊断工具箱
7.1 排错的基本原则
在讲工具之前,先讲方法。工具人人会用,方法才是差距所在。
原则一:先建立"应该是什么样"的预期
不知道"正常应该是什么样",就不可能判断"现在正不正常"。
所以每次排错的第一步不是敲命令,而是问自己:
- 正常情况下,这个命令的输出应该是什么?
- 现在这个输出,偏离在哪里?
原则二:分层排查,从上往下或从下往上,但不要跳
┌─────────────────────────────────────────────┐
│ 7 应用层 nginx 在跑吗?本机 curl 通吗? │
├─────────────────────────────────────────────┤
│ 4 传输层 80 端口在监听吗?防火墙放行了吗? │
├─────────────────────────────────────────────┤
│ 3 网络层 能 ping 通真实 IP 吗?路由对吗? │
├─────────────────────────────────────────────┤
│ 2 链路层 ARP 表里 VIP 的 MAC 是谁? │
├─────────────────────────────────────────────┤
│ 内核转发 IPVS 表对吗?参数生效值是多少? │
└─────────────────────────────────────────────┘
顺序不能跳。 如果一上来就怀疑内核转发,会在参数里绕很久;但如果一直停在上面几层,永远找不到真正的问题。
原则三:区分"请求失败"和"请求成功但结果显示不出来"
这两个是完全不同的方向。
bash
# ✗ 差的写法:失败了什么都看不到
curl -s http://192.168.1.123 | grep -o "RealServer: [a-z]*"
# ✓ 好的写法:明确区分成功/失败
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.123
# 200 = 成功,000 = 连接失败
本项目就吃过这个亏 :用 curl -s 加上一个错误的 grep 正则,导致"请求成功但匹配不到"被误读成"请求失败"。见第 9 章案例 4。
原则四:规律性 = 确定性原因,不规律 = 竞态/资源
200 000 200 200 200 000 200 200 200 000 ... ← 固定间隔,规律
200 200 000 200 000 200 200 000 200 200 ... ← 杂乱无章,不规律
规律性的失败,几乎不可能是"网络抖动"或"硬件不稳"。 它意味着系统在按某个确定的规则分流或丢包。
这个直觉一旦建立,就会直接去查"是不是有两套规则在打架",而不是去查网线、交换机。本项目 25% 规律性丢包的突破,就来自这个判断。
原则五:计数器为 0 不代表这条路不通
这是一个非常隐蔽的陷阱。
我们看到 K8s 的 FORWARD 链计数器全是 0,就判断"IPVS 转发不走 FORWARD 链"。这个判断本身可能对,但我们当时想用它证明丢包和 FORWARD 无关------
而"计数器为 0"同样可能意味着"这条链根本没被走到",这两种情况的含义完全不同。
结论:只能单向解释的证据,不算证据。 证据必须能双向排除。
7.2 分层排查决策树
VIP 访问不通
│
├─ 两台都 ping 不通 VIP
│ ├─ MASTER 的 enp1s0 上有 VIP 吗?
│ │ → 没有:看 keepalived 状态 / 日志
│ ├─ 两台都认为自己是 MASTER?
│ │ → virtual_router_id 或 auth_pass 不一致
│ └─ 两台都是 BACKUP?
│ → VRRP 组播被防火墙挡了(检查 224.0.0.18)
│
├─ 能通但部分失败(规律或不规律)
│ ├─ ★ 每台的 ipvsadm -Ln 都看了吗?BACKUP 是不是有表?
│ │ → 有:转发循环!ipvsadm -C 清掉,改用 notify 钩子
│ ├─ arp_ignore / arp_announce 生效值是 1 / 2 吗?
│ │ → 不是:后端在抢答 ARP
│ ├─ rp_filter 的 all 和接口级分别是多少?
│ │ → all 是最大值,别只看接口级
│ └─ 在 RS 上 tcpdump 能看到 SYN 吗?
│ → 看得到:转发是通的,问题在上层
│ → 看不到:转发这一跳就断了
│
└─ 完全不通
├─ 防火墙 / ufw 挡了?
├─ conntrack 连接跟踪表满了?(dmesg | grep conntrack)
└─ 客户端 ARP 缓存是旧 MAC?→ arp -d 清一下
7.3 工具详解
ping ------ 三层的"你在不在"
bash
ping -c 3 192.168.1.123
- 基于 ICMP 协议
- 能 ping 通只说明三层可达,不代表 80 端口开着
- 有些服务器会禁用 ICMP,所以"ping 不通"也不一定代表服务不可用
bash
ping -c 1 -W 1 192.168.1.6
# -c 1 发 1 个包就停
# -W 1 超时 1 秒
ip ------ 网络配置的万能工具
取代了老的 ifconfig / route / arp。
bash
ip a # 看所有接口和 IP(addr 的缩写)
ip -br a # 简洁格式(brief),一行一个接口
ip a show enp1s0 # 只看某块网卡
ip r # 看路由表(route)
ip link # 看接口状态(up/down、MAC)
ip neigh # 看 ARP 邻居表(替代 arp -a)
排错时最常用的两条:
bash
# VIP 在不在 lo 上(两台都该有)
ip a show lo | grep 192.168.1.123
# VIP 在不在 enp1s0 上(只有 MASTER 该有)
ip a show enp1s0 | grep 192.168.1.123
ss ------ 看端口监听和连接
取代了 netstat。
bash
ss -lntp
# -l 只看 LISTEN
# -n 不解析域名(快)
# -t 只看 TCP
# -p 显示进程(需 root)
判断"服务到底起没起",这是第一手证据。
curl ------ HTTP 客户端(也是最好的诊断工具)
bash
# 基本用法
curl http://192.168.1.123
# ★ 只看状态码,不输出内容(排错必备)
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.123
# 显示详细过程(看 DNS、连接、TLS 各阶段耗时)
curl -v http://192.168.1.123
# 只看时间
curl -s -o /dev/null -w "连接: %{time_connect}s 总计: %{time_total}s\n" http://192.168.1.123
# 限制超时
curl -s -m 3 http://192.168.1.123
# -m 3 最多等 3 秒
参数速查:
| 参数 | 作用 |
|---|---|
-s |
静默(不显示进度条和错误信息) |
-o /dev/null |
丢弃输出内容 |
-w "%{http_code}" |
自定义输出格式(写什么就输出什么) |
-m N |
超时 N 秒 |
-I |
只发 HEAD 请求(只要响应头) |
-L |
跟随重定向 |
-k |
忽略证书错误 |
★ HTTP 状态码速查
| 码 | 含义 | 排错含义 |
|---|---|---|
| 200 | OK | 成功 |
| 301 / 302 | 重定向 | 正常,但要注意重定向目标 |
| 403 | Forbidden | 权限问题(如 nginx 读不到文件) |
| 404 | Not Found | 文件不存在 / 路径写错 |
| 500 | Internal Server Error | 后端程序出错 |
| 502 | Bad Gateway | 后端连不上(后端挂了/端口不对) |
| 504 | Gateway Timeout | 后端响应超时 |
| 000 | ------ | curl 特有:连接都没建立起来(超时/拒绝/网络不通) |
000是关键 。它不是标准 HTTP 状态码,是 curl 在"连接阶段就失败"时的表示。看到000就去查网络层,看到4xx/5xx就去查应用层。
tcpdump ------ 抓包(最强的一招)
原理:让网卡进入"混杂模式",把所有经过的包复制一份给 tcpdump,它按过滤条件筛选后打印出来。
bash
# 抓 80 端口的所有包(最常用)
tcpdump -i enp1s0 -nn port 80
# -i enp1s0 指定网卡
# -nn 不解析主机名和端口名(显示数字,更快也更清晰)
# 只看 SYN 包(判断连接请求有没有到达)
tcpdump -i enp1s0 -nn 'tcp[tcpflags] & tcp-syn != 0'
# 抓某个来源的包
tcpdump -i enp1s0 -nn host 192.168.1.100
# 写到文件,用 Wireshark 慢慢分析
tcpdump -i enp1s0 -nn -w /tmp/cap.pcap port 80
# 限制抓包数量后自动退出(适合脚本)
tcpdump -i enp1s0 -nn -c 20 port 80
# -c 20 抓 20 个包就停
怎么读懂输出:
14:23:45.123456 IP 192.168.1.100.49378 > 192.168.1.123.80: Flags [S], seq 123456
↑源IP和端口 ↑目标IP和端口 ↑标志位
关键标志位:
| 标志 | 含义 |
|---|---|
[S] |
SYN,连接请求 |
[S.] |
SYN-ACK,同意连接 |
[.] |
ACK,确认 |
[P.] |
PSH-ACK,有数据 |
[F] |
FIN,断开请求 |
[R] |
RST,重置(拒绝) |
本项目的经典用法 :在 .6 上抓包,看到 30 个 SYN 包但零个 SYN-ACK ------ 说明包到了但没人回应,直接排除了"包没到"的方向。
ipvsadm ------ LVS 的专有工具
已在 3.6 详解。排错时的核心用法:
bash
ipvsadm -Ln # 看表(每台都要看!!)
ipvsadm -Ln --stats # 看累计流量(判断有没有真的分流)
ipvsadm -Lnc # 看具体连接
ipvsadm -C # 清空
namei ------ 逐级检查路径权限
这是在排查"文件访问不了"时的神器。
bash
namei -l /root/pipe-bird.html
输出:
f: /root/pipe-bird.html
drwxr-xr-x root root /
drwx------ root root root ← ★ 问题在这:700,其他用户进不去
-rwxrwxrwx root root pipe-bird.html
为什么这个工具这么有用?
因为 Linux 访问文件需要路径上的每一个目录都有 x(执行/穿越)权限 。文件本身是 777 也没用,只要有一个上级目录不让进,就访问不了。
名词:目录权限的 x 位
对普通文件,
x表示"可执行"。对目录 ,
x表示"可以穿越 / 可以进入"。
r= 可以列出目录内容(ls)w= 可以在目录里创建/删除文件x= 可以访问目录里的文件(哪怕你知道完整路径)所以
drwx------(700)的目录,只有属主能进。nginx 以www-data身份运行,进不去/root。
权限数字换算:
r = 4, w = 2, x = 1
755 = rwxr-xr-x 属主全权限,其他人读+执行
644 = rw-r--r-- 属主读写,其他人只读
700 = rwx------ 只有属主能进
600 = rw------- 只有属主能读写
⚠️ 常见误区:用
chmod 777解决问题。这确实能让问题"消失",但它让任何人都能读写这个文件。对于 Web 文件,这意味着任何被攻破的其他服务都能篡改你的网页。
正确做法是
chown给正确的用户 +chmod 644(文件)/755(目录)。
sysctl ------ 看内核参数
bash
sysctl net.ipv4.conf.all.rp_filter # 看一个
sysctl -n net.ipv4.ip_forward # 只看值
sysctl net.ipv4.conf.all.arp_ignore net.ipv4.conf.all.arp_announce # 一次看多个
记住:永远看生效值,不看配置文件。
journalctl ------ 看服务日志
bash
journalctl -u keepalived -n 50 --no-pager # 最近 50 行,不翻页
journalctl -u keepalived -f # 实时跟踪(像 tail -f)
journalctl -u keepalived --since "10 min ago" # 最近 10 分钟
journalctl -u mysql -n 100 --no-pager
# -u 指定服务单元
# -n 显示最后 N 行
# --no-pager 不要用 less 分页(方便复制输出)
自定义日志 :脚本里用 logger 写日志,会进 journal:
bash
logger -t lvs "IPVS rules ADDED"
# -t lvs 打上标签 "lvs",便于过滤
journalctl -t lvs # 只看这个标签的
其他常用
bash
uptime # 系统负载和运行时间
free -h # 内存
df -h # 磁盘
nproc # CPU 核数
tail -f /var/log/nginx/error.log # 实时看 nginx 错误日志
dmesg | tail -30 # 内核消息(硬件错误、OOM、conntrack 满)
7.4 症状 → 命令 对照表
| 症状 | 第一步看什么 | 命令 | |
|---|---|---|---|
| VIP 完全不通 | keepalived 角色和日志 | systemctl status keepalived |
|
| 部分请求失败 | 每台的 IPVS 表 | ipvsadm -Ln |
|
| 轮询不交替 | 累计流量分布 | ipvsadm -Ln --stats |
|
| 不确定包到没到 | 抓包 | tcpdump -i enp1s0 -nn port 80 |
|
| VIP 的 MAC 乱跳 | ARP 表和参数 | ip neigh / sysctl ...arp_ignore |
|
| 改了参数没效果 | 生效值(不是配置文件) | sysctl net.ipv4.xxx |
|
| 文件访问不了 | 路径逐级权限 | namei -l /path/to/file |
|
| 服务没起来 | 端口 + 日志 | ss -lntp + journalctl -u xxx |
|
| HTTP 报错 | 状态码 | curl -s -o /dev/null -w "%{http_code}" |
|
| 怀疑连接表满 | 内核消息 | `dmesg | grep -i conntrack` |
7.5 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| ICMP | Internet Control Message Protocol | ping 用的协议 |
| 状态码 | Status Code | HTTP 响应里的三位数字 |
| 抓包 | Packet Capture | 复制并分析网络流量 |
| 混杂模式 | Promiscuous Mode | 网卡接收所有帧(不只发给自己的) |
| pcap | - | 抓包文件格式,Wireshark 可读 |
| 权限位 | Permission Bits | 文件/目录的 rwx 权限 |
| 穿越权限 | Execute on Directory | 目录的 x 位,表示能否进入 |
| journal | systemd Journal | systemd 的日志系统 |
| 分层排查 | Layered Troubleshooting | 从应用层到内核层逐层验证的排错方法 |
第 8 章 网络边界:NAT 与公网映射
本章讲的是"内网服务怎么给外网访问",以及为什么 DR 模式的 VIP 不能做端口映射。这是本项目一个具体的疑问。
8.1 为什么需要 NAT
私有 IP 地址段(192.168.x.x、10.x.x.x、172.16-31.x.x)不能在公网上路由 。你家里的电脑是 192.168.1.100,但互联网上的服务器不可能把包发给这个地址。
所以需要一个"翻译"机制:内网机器要访问外网时,路由器把源地址换成自己的公网 IP;收到回复时再换回来。
名词:NAT(Network Address Translation,网络地址转换)
修改数据包中的 IP 地址(和端口)的技术,用于内外网地址转换。
8.2 SNAT / DNAT / MASQUERADE
| 类型 | 全称 | 改什么 | 方向 | 典型用途 |
|---|---|---|---|---|
| SNAT | Source NAT | 源 IP | 出 | 内网访问外网 |
| DNAT | Destination NAT | 目标 IP | 入 | 外网访问内网服务 |
| MASQUERADE | - | 源 IP(动态) | 出 | SNAT 的动态版,IP 会变时用 |
内网访问外网(SNAT):
① 你的电脑 192.168.1.100 访问 8.8.8.8
源: 192.168.1.100:50000
目标: 8.8.8.8:53
② 路由器做 SNAT(改源地址)
源: 203.0.113.5:50000 ← 改成路由器的公网 IP
目标: 8.8.8.8:53
同时在连接跟踪表里记一笔:
(192.168.1.100:50000) ←→ (203.0.113.5:50000)
③ 8.8.8.8 回复
源: 8.8.8.8:53
目标: 203.0.113.5:50000
④ 路由器查连接跟踪表,发现这个映射关系
改回去:
目标: 192.168.1.100:50000
→ 发给你
外网访问内网服务(DNAT):
① 外网用户 198.51.100.7 访问 203.0.113.5:80
② 路由器做 DNAT(改目标地址)
目标: 192.168.1.5:80 ← 改成内网服务器
③ 内网服务器回复,源 IP 是 192.168.1.5
④ 路由器再把源 IP 改回 203.0.113.5,发给外网用户
名词:连接跟踪表(conntrack)
内核记录所有 NAT 转换关系的表。NAT 靠它才能把回复正确地"改回来"。
这张表有容量上限(默认几万到几十万条)。如果被填满,新连接会被丢弃------这是高并发场景下常见的故障(
dmesg | grep conntrack能看到相关报错)。
8.3 端口映射(Port Forwarding)
「端口映射」本质上是 DNAT 的一个特例,只是把"目标端口"也一起改了。
在路由器管理界面里的典型配置:
外部端口: 8080
内部 IP: 192.168.1.5
内部端口: 80
协议: TCP
含义:外网访问 公网IP:8080 → 转发到 192.168.1.5:80。
这是最常用的"把内网服务暴露到公网"的方式。
8.4 NAT 回流(Hairpin NAT)
问题现象 :内网电脑用公网 IP 访问自己的服务,经常不通,或者通但不稳定。
原因:
内网电脑 192.168.1.100 访问 203.0.113.5:8080(自己的公网 IP)
① 包发给路由器(因为 203.0.113.5 不是本网段)
② 路由器做 DNAT,目标改成 192.168.1.5:80 ------ 又发回内网
③ 192.168.1.5 收到包,源 IP 是 192.168.1.100,目标 IP 是 192.168.1.5
④ 回复:源 192.168.1.5,目标 192.168.1.100
⑤ ★ 但 192.168.1.100 就在同一个网段,所以 .5 直接就把包发给了它,
不经过路由器!于是路由器没机会把源 IP 改回 203.0.113.5
⑥ 客户端的连接是发给 203.0.113.5 的,收到一个源 IP 是 192.168.1.5 的包
→ 不匹配,丢弃 → 连接失败
"U 型回转"(包出去到路由器,又被转回内网)在小型路由器上经常不被支持。
这个现象的实际影响:当你说"内网能用公网 IP 打开,外网打不开"时,可能是两个独立的问题叠在一起:
- 内网访问其实是走了某种回流路径成功了(不代表外网能通)
- 外网访问有另一个问题
所以:测试外网可达性,一定要用不在内网的网络来测------比如手机开 4G/5G 流量(关掉 WiFi)访问。
名词:Hairpin NAT / NAT Loopback / NAT 回流
内网设备通过公网 IP 访问同一个内网服务时,流量需要"出去再回来"的情形。
8.5 ★ 三角路由:为什么 DR 模式的 VIP 不能做端口映射
这是本项目遇到的最"反直觉"的问题。
现象 :把端口映射的目标填成 VIP 192.168.1.123,外网访问不通。
原因分析:
外网用户 198.51.100.7
│ http://203.0.113.5:80
▼
路由器(做 DNAT)
│ 目标改成 192.168.1.123:80
▼
.5(MASTER,有 IPVS 表)
│ 按 rr 转发给 .6
▼
.6(RealServer,处理请求)
│
│ 生成响应:
│ 源 IP = 192.168.1.123 ← ★ 问题在这里
│ 目标 IP = 198.51.100.7
│
│ 查路由表:198.51.100.7 不在本网段
│ → 走默认网关 192.168.1.1
▼
路由器
│
✗ 路由器收到一个「源 IP 是 192.168.1.123」的包,
但路由器的 NAT 表里,
「源 IP 192.168.1.123」没有任何对应的转换规则!
→ 这个包被丢弃
这就是「三角路由」:
请求路径: 外网 → 路由器 → 调度器(.5) → 后端(.6)
响应路径: 后端(.6) → 路由器 → 外网
★ 后端直接发包给路由器,没有经过调度器
为什么 NAT 设备处理不了?
因为 NAT 的本质是「对称的地址替换」:
- 请求进来时,路由器把"目标 203.0.113.5:80"改成"目标 192.168.1.123:80",并记下这个映射
- 它期待回复是"源 192.168.1.123:80"→ 这时才能改回"源 203.0.113.5:80"
理论上这个映射是有的。但问题是:
DR 模式下,响应包的源 IP 是 VIP,而 VIP 在物理上"属于".5 到 .6 这一组机器。路由器只知道"内网有个 192.168.1.123",它并不知道这个地址可能从 .6 的网卡上发出来。 更要紧的是,请求和响应走的是不同的物理链路(请求经过 .5,响应直接从 .6 出去)。
而且如果做端口映射的是家用路由器,它往往只对"从 WAN 口进来的新连接"做 DNAT 处理 ,对"从 LAN 口冒出来、源地址是内网地址的包"不会有任何转换动作------它只会尝试按路由转发,而目标 198.51.100.7 是公网地址,包会被发出去,但源地址是私网地址 192.168.1.123,外网用户收到一个私网源地址的包,直接丢弃。
正确做法:
| 方案 | 说明 |
|---|---|
| 映射目标填真实服务器 IP | 填 192.168.1.5 而不是 VIP。这样响应源 IP 是 192.168.1.5,路由器有对应的 NAT 映射,能正确转换。代价是失去了负载均衡(只映射到一台)。 |
| 不在这一层做公网暴露 | 让负载均衡只服务内网,公网入口交给专门的网关 / 云负载均衡 |
| 用 NAT 模式代替 DR 模式 | NAT 模式下响应经过调度器,路径是对称的。代价是性能下降 |
⚠️ 但在做任何映射之前,必须先确认一件事 ------ 见 8.6。
8.6 CGNAT:先确认你有没有公网 IP
大前提:如果你的宽带根本没有独立的公网 IP,那么上面所有关于端口映射的讨论都是白费功夫。
名词:CGNAT(Carrier-Grade NAT,运营商级 NAT)
运营商在它那一侧也做了 NAT。你的"公网 IP"实际上是运营商内网的地址,多个用户共享一个真正的公网 IP。
这种情况在家庭宽带、部分企业宽带里非常普遍(尤其是 IPv4 地址枯竭之后)。
怎么判断自己在不在 CGNAT 后面?
在服务器上执行:
bash
curl ifconfig.me
# 输出:例如 203.0.113.5
然后登录路由器,查看 WAN 口 IP。
| 对比结果 | 结论 |
|---|---|
| 两者一致 | 你有独立公网 IP,端口映射可以做 ✓ |
| 两者不一致 | 你在 CGNAT 后面 ✗ |
进一步确认 :如果"curl ifconfig.me 返回的 IP" 落在这些私有地址段里,那 100% 是 CGNAT:
100.64.0.0/10 ← RFC 6598,专门给运营商 NAT 用的
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
CGNAT 下怎么办?
| 方案 | 说明 |
|---|---|
| 找运营商要公网 IP | 有些可以申请,可能要收费 |
| 用内网穿透(frp / ngrok / Cloudflare Tunnel) | 服务器主动连到一台有公网 IP 的中转机 |
| 用云服务器做反向代理 | 云服务器有公网 IP,转发到你的内网 |
| 改用 IPv6 | IPv6 地址充足,通常没有 CGNAT |
8.7 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| NAT | Network Address Translation | 网络地址转换 |
| SNAT | Source NAT | 改源地址,用于内网访问外网 |
| DNAT | Destination NAT | 改目标地址,用于外网访问内网 |
| MASQUERADE | - | 动态 SNAT,源 IP 会变时使用 |
| 端口映射 | Port Forwarding | DNAT 的特例,连端口一起转 |
| 端口转发 | Port Forwarding | 同上 |
| 连接跟踪 | conntrack | 内核记录 NAT 映射关系的表 |
| NAT 回流 | Hairpin NAT / NAT Loopback | 内网设备通过公网 IP 访问内网服务 |
| 三角路由 | Triangle Routing | 请求和响应走不同路径,NAT 无法处理 |
| 私有地址 | Private IP | 192.168/10/172.16-31 等不可公网路由的地址 |
| CGNAT | Carrier-Grade NAT | 运营商级 NAT,你没有真正独立的公网 IP |
| 内网穿透 | Tunneling / Reverse Proxy | 内网主动连外网中转,绕过无公网 IP 的限制 |
第 9 章 实战排错:本项目 8 个真实案例
这一章把前面的知识串起来。每个案例都按「现象 → 假设 → 验证 → 结论 → 教训」展开。
案例 1 ★★★:规律性 25% 丢包(转发循环)
现象
VIP 能访问,但每 4 次请求失败 1 次,间隔极其规律:
200 000 200 200 200 000 200 200 200 000 200 200 200 000 ...
000 是 curl 的连接失败标记(连接阶段就失败,说明网络层有问题)。
排查过程(走过三条死路)
死路 1:怀疑 rp_filter
bash
sysctl net.ipv4.conf.all.rp_filter # 2 ← 看起来很可疑!
sysctl net.ipv4.conf.enp1s0.rp_filter # 0
改了 /etc/sysctl.d/10-network-security.conf,把 2 改成 0,重启验证:
bash
sysctl net.ipv4.conf.all.rp_filter # 0 ✓
现象毫无变化。 → 排除。
死路 2:怀疑 RS 的 lo:0 没配 VIP
bash
ip a show lo | grep 192.168.1.123
# inet 192.168.1.123/32 scope global lo:0 ← 配着呢
→ 排除。
死路 3:怀疑客户端的 ARP 缓存
bash
arp -a | findstr 192.168.1.123
# 192.168.1.123 94-c6-91-73-45-aa 动态 ← MAC 是正确的 .5
→ 排除。
死路 4:怀疑 K8s 的 FORWARD 链 DROP 挡了转发
.5 是 K8s 节点,iptables 里有一堆 KUBE-* 链,而且 FORWARD 策略是 DROP:
bash
iptables -L FORWARD
# Chain FORWARD (policy DROP)
# KUBE-FORWARD ... 0 packets, 0 bytes ← 计数器全是 0
这个怀疑最有说服力 ,因为 DR 的转发确实要"转发"包。但计数器全是 0 ------ 说明这条链根本没被走到。
(当时我据此下了"IPVS 转发不经过 FORWARD"的结论。这个结论本身可能是对的,但用"计数器为 0"来证明它是不可靠的------因为"计数器为 0"同样可能意味着"这条链根本没生效"。)
→ 排除,但留下了一个方法论的教训(见本节"教训")。
真正的突破
在 .6(BACKUP 角色)上执行:
bash
ipvsadm -Ln
输出:
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.1.123:80 rr
-> 192.168.1.5:80 Route 1 0 0
-> 192.168.1.6:80 Route 1 0 0
BACKUP 机器上居然有完整的 IPVS 转发表!
根因
部署脚本在 keepalived.conf 里静态写了 virtual_server 段,而这个配置文件被复制到了两台机器上。于是:
.5生成 IPVS 表(正确,它是 MASTER).6也生成了 IPVS 表(错误!)- 请求从
.5转发到.6时,包的目标 IP 仍然是192.168.1.123 .6的内核发现"这个目标地址匹配我的 IPVS 规则",于是又把它转出去- 包在两台之间打转 → TTL 耗尽 / 连接表打满 → 客户端超时
为什么是 25% 而不是 100%?
因为环里转几圈后会概率性地"碰巧选到自己"而落地。这种"部分成功率"最容易让人误判成硬件问题。
修复
bash
# 在 .6 上
ipvsadm -C # 立即清空
sed -i '/virtual_server/,$d' /etc/keepalived/keepalived.conf
systemctl restart keepalived
# 验证
ipvsadm -Ln # 应该只剩版本号那一行
结果:20/20 全部 200。
永久修复
把静态 virtual_server 改成 notify_* 钩子(见 4.6):
conf
notify_master "/etc/keepalived/lvs-ipvs-add.sh" # 只有 MASTER 建表
notify_backup "/etc/keepalived/lvs-ipvs-del.sh" # BACKUP 清表
notify_fault "/etc/keepalived/lvs-ipvs-del.sh"
notify_stop "/etc/keepalived/lvs-ipvs-del.sh"
用代码保证铁律二,而不是靠人记住手动清理。
教训
1. 铁律必须用代码强制,不能靠流程约束。
「BACKUP 不能有 IPVS 表」这件事,如果只写在文档里,早晚有人会犯错。写成钩子,就不可能错。
2. 规律性失败 = 确定性原因。
固定间隔的失败不可能是"网络抖动"。这个判断让我们最终找到了"两套规则打架"这个方向。
3. 排查时要把「所有相关机器的状态」都看一遍。
我们在
.5上看了很多次ipvsadm -Ln都是对的,直到在.6上看才发现问题。4. 最像凶手的未必是凶手。
FORWARD 链 DROP 看起来极其可疑,翻遍了很多证据,结果它不是原因。
案例 2:rp_filter 是个"完美嫌疑犯"
见 6.3,此处从简。
教训
判断一个因素是不是原因,唯一标准是"去掉它,现象是否消失"。
rp_filter=2 这件事具备了一个"完美嫌疑犯"的所有特征:
- 值看起来不正常(为什么是 2?)
- 和故障领域高度相关(它就是管网络包过滤的)
- 解释起来很有说服力("包被内核丢了")
- 改动起来很有成就感(改个文件重启就行)
但改完现象不变,就不是根因。
如果当时花 5 分钟先做这个验证,能省掉后面一大圈。
案例 3:误读 IPVS 的连接计数器
现象
ipvsadm -Lnc 输出里看到:
-> 192.168.1.5:80 Route 1 6 10
-> 192.168.1.6:80 Route 1 21 0
ActiveConn↑ InActConn↑
看到 InActConn=10,一度以为"有 10 个连接卡在 .5 上没响应"。
真相
| 字段 | 真实含义 |
|---|---|
ActiveConn |
当前正在传输数据的连接 |
InActConn |
连接还在表里,但没在传数据(TIME_WAIT / FIN_WAIT 之类) |
InActConn 是"还没超时回收的表项",不代表连接卡住了。
正确做法
判断"到底有没有真实流量",看累计值:
bash
ipvsadm -Ln --stats
# 输出里有 Conn 累计连接数、InBytes/OutBytes 累计字节数
教训
任何监控指标,先搞清楚它的定义,再拿它下结论。 凭字面意思猜指标含义,是排错时的常见陷阱。
案例 4:curl -s 掩盖了失败
现象
bash
for i in $(seq 1 10); do curl -s http://192.168.1.123 | grep -o "RealServer: [a-z]*"; done
输出是空的,或者只输出 RealServer: 就卡住了。看起来像是"循环没执行"。
两个独立的问题
问题 A:curl -s 把错误也静音了
-s 会抑制 curl 的错误提示。请求失败时什么都不输出,看起来像"没跑"。
问题 B:grep 正则写错了
bash
grep -o "RealServer: [a-z]*"
# ↑ 只匹配小写字母
但实际页面上的内容是:
html
<h1>RealServer: 1.5</h1> ← 主机名是 "1.5"
<h1>RealServer: 192.168.1.6</h1> ← 或者 "192.168.1.6"
主机名是数字,不是小写字母 ,所以 [a-z]* 永远匹配不到(只能匹配空字符串)。
正确做法
bash
# 明确区分成功/失败
for i in {1..10}; do
curl -s -o /dev/null -m 3 -w "%{http_code} " http://192.168.1.123
done
echo
# 要提取内容,就用宽松的模式
curl -s http://192.168.1.123 | grep -o "RealServer:.*" | head -1
教训
排错时必须能区分「请求失败」和「请求成功但结果显示不出来」。
这两个方向完全相反:前者要查网络,后者要查你的解析逻辑。
-s -o /dev/null -w "%{http_code}"是区分它们的最小代价方案。
案例 5:Windows CMD 里的 curl
现象
在 Windows CMD 里写循环测试,curl 命令报错或行为异常。
原因
CMD 对某些字符的解析和 bash 不同。在 .bat 文件里,@ 用于抑制命令回显,路径分隔符、引号规则都不同。
正确写法
bat
for /L %i in (1,1,20) do @curl -s -m 3 -w "%{http_code} " http://192.168.1.123 -o nul
要点:
@前缀抑制回显-o nul(Windows 的"黑洞"设备,对应 Linux 的/dev/null)- 在
.bat文件里用%%i,在命令行直接敲用%i
案例 6:.bat 文件必须用 CRLF 换行 ★
现象
写了一个 Windows 批处理脚本用来观察 LVS 轮询,运行后输出全是字面量:
LVS Watch ... !PAD! !CODE! [ *** FAIL *** ] | totals 5:!C5! 6:!C6! fail:!CF!
!CODE! 这样的变量没有被展开,原样打印出来了。
原因
脚本是在 Linux 侧生成的,用了 LF 换行。而 CMD 在 LF 换行下无法正确解析:
setlocal EnableDelayedExpansion失效if() else()括号块解析错误
结果就是延迟变量 !VAR! 完全不展开。
验证方式:
bash
cat -A lvs-watch.bat | head -3
# 如果行尾没有 ^M$ 而只有 $,说明是 LF 换行
修复(两步)
第一步:转成 CRLF
bash
sed -i 's/$/\r/' lvs-watch.bat
第二步(更根本):彻底不用延迟变量和括号块
改用 goto 流程 + %VAR% 普通展开:
bat
:loop
set CODE=
for /f "delims=" %%c in ('curl -s -m 2 -o "%TMPF%" -w "%%{http_code}" http://%VIP%/node.txt 2^>nul') do set CODE=%%c
if "%CODE%"=="200" goto parse
echo [ FAIL ] code=%CODE%
goto loop
为什么不只是转 CRLF? 因为延迟变量在复杂场景下还有其他坑。用最朴素的 goto + %VAR% 写法,从根本上免疫这类解析问题。
教训
跨平台生成脚本时,换行符是一等公民。
.sh→ LF(\n).bat/.cmd/ Windows 文本 → CRLF(\r\n)换行符搞错,会以"语法看起来正确、但行为诡异"的形式报错,非常难查。
案例 7:nginx 读不到 /root 下的文件
现象
把网页文件 pipe-bird.html 放在 /root/ 下,权限设成了 777,但 nginx 返回 403 Forbidden。
排查
bash
namei -l /root/pipe-bird.html
输出:
f: /root/pipe-bird.html
drwxr-xr-x root root /
drwx------ root root root ← ★ 问题在这里
-rwxrwxrwx root root pipe-bird.html
根因
/root 的权限是 700(drwx------)------只有 root 能进入。
nginx 的 worker 进程以 www-data 用户身份运行,连 /root 目录都进不去 ,所以文件权限设成 777 完全没用。
关键概念 :Linux 访问文件需要路径上每一级目录都有
x权限 。目录的x位表示"能否穿越"。文件权限是"门槛",目录权限是"门"------门锁着,门槛再低也没用。
正确修复
bash
# 把文件放到标准位置
mv /root/pipe-bird.html /var/www/html/
# 改成正确的属主和权限
chown www-data:www-data /var/www/html/pipe-bird.html
chmod 644 /var/www/html/pipe-bird.html
⚠️ 错误修复(一定要避免)
有人会想"给 nginx 提权,让它以 root 运行 ",在 nginx.conf 里改 user root;。
这是明确的反模式,绝对不能做:
- nginx 是暴露在公网的服务。以 root 运行意味着一旦有漏洞,攻击者直接获得 root 权限(这是"权限提升"漏洞的经典场景)。
- nginx 的设计本来就是 "master 以 root 启动、worker 降权运行" 。master 需要 root 是为了绑定特权端口(< 1024);worker 处理实际请求,必须降权。强行改
user root是破坏这套安全设计。 - 这是在拿整个服务器的安全,换省一次
chown的功夫。
教训
遇到权限问题,正确的方向是"把资源放到正确的用户能访问的地方",而不是"把用户提到最高权限"。
案例 8:端口映射到 VIP 打不通
见 8.5,此处浓缩。
现象
- 路由器端口映射目标填
192.168.1.123(VIP),端口 80 - 内网电脑用公网 IP 能打开页面
- 外网(手机 4G)打不开
三个独立的原因叠加
原因 1:NAT 回流假象
内网能通不代表外网能通,这两条路径完全不同(见 8.4)。
原因 2:DR 模式的三角路由(核心原因)
DR 模式下响应源 IP 是 VIP,且响应直接从 RealServer 出去,不经过调度器。NAT 设备处理不了这种"不对称路径"(见 8.5)。
原因 3:未排除 CGNAT
如果宽带本身在 CGNAT 后面,任何端口映射都不会生效。必须先验证:
bash
curl ifconfig.me # 和路由器 WAN 口 IP 对比
正确做法
| 优先级 | 做法 |
|---|---|
| 1 | 先确认有没有公网 IP(curl ifconfig.me 对比 WAN 口) |
| 2 | 端口映射的目标填真实服务器 IP,不要填 VIP |
| 3 | 或者干脆不给负载均衡入口做公网映射 |
第 10 章 从 LVS 到 MySQL 集群
前面九章讲的是"怎么把请求分给多台无状态服务器"。这一章讲:当后端是有状态的数据库时,一切都不一样了。
10.1 无状态 vs 有状态 ★
这是本章最重要的一节。
名词:无状态(Stateless)
服务不保存任何"上一次请求"的信息。同一请求发给任意一台服务器,结果都一样。
例子:静态网页、图片服务器、无 session 的 API。
名词:有状态(Stateful)服务保存着必须保持一致的数据,不同服务器之间的状态可能不同。
例子:数据库、文件存储、有 session 的会话服务。
对比:
| 维度 | 无状态(我们的网页集群) | 有状态(MySQL) |
|---|---|---|
| 请求分给谁 | 谁都行 | 写请求只能给主库 |
能否用 rr 轮询 |
能 | 绝对不能 |
| 加机器 | 直接加就行 | 要同步数据,慢 |
| 数据一致性 | 不涉及 | 核心问题 |
| 故障切换代价 | 秒级 | 秒级到分钟级 |
为什么 MySQL 不能轮询?
假设两台 MySQL,用户执行 INSERT INTO t VALUES (1),轮询到了从库:
- 从库默认是只读 的 → 直接报错
ERROR 1290: The MySQL server is running with the --read-only option - 即使从库可写(多主模式),两台各写各的,数据就分叉了
所以数据库的流量分发逻辑是:写永远只去主库,读可以去从库。 这需要一个"知道谁是主库"的中间件 ------ 这就是 MySQL Router 的作用。
10.2 MySQL 复制的原理
基础:binlog
名词:binlog(Binary Log,二进制日志)
MySQL 记录所有数据变更 的日志(
INSERT/UPDATE/DELETE/DDL),是主从复制的数据来源。注意:binlog 记录的是"做了什么变更",不是"变更后的数据快照"。所以从库重放 binlog 就能得到相同的数据。
记录格式(binlog_format):
| 格式 | 记录内容 | 说明 |
|---|---|---|
STATEMENT |
SQL 语句原文 | 省空间,但某些函数(如 NOW())会导致主从不一致 |
ROW |
每一行的前后值 | 可靠,是 MGR 的要求 |
MIXED |
自动选择 | 折中 |
本项目的关键参数:
binlog_format = ROW+binlog_row_image = FULL
ROW格式记录每行变更;FULL表示记录变更前后的完整行(而不只是被改的字段)。MGR 强制要求这两个值。
传统主从复制流程
主库 (Master) 从库 (Slave)
┌──────────────┐ ┌──────────────┐
│ 处理写请求 │ │ │
│ ↓ │ │ │
│ 写 binlog │ │ │
│ ↓ │ │ │
│ dump thread │ ──① 推送 binlog ──►│ IO thread │
└──────────────┘ │ ↓ │
│ 写 relay log │
│ ↓ │
│ SQL thread │
│ ↓ │
│ 重放变更 │
└──────────────┘
三个线程:
| 线程 | 位置 | 职责 |
|---|---|---|
| dump thread | 主库 | 把 binlog 推送给从库 |
| IO thread | 从库 | 接收 binlog,写入中继日志 |
| SQL thread | 从库 | 读中继日志,重放变更 |
名词:relay log(中继日志)
从库上暂存"从主库收到的 binlog"的文件。先落盘再重放,是为了防止网络中断导致丢数据。
复制是异步的 → 有「复制延迟」(Replication Lag):主库写了,从库可能几毫秒到几小时后才同步到。
GTID:让复制更可靠
名词:GTID(Global Transaction Identifier,全局事务标识符)
给每一个事务分配一个全局唯一的 ID,格式是
<source_uuid>:<事务序号>,例如3e11fa47-71ca-11e1-9e33-c80aa9429562:23。好处:从库记录"我已经执行了哪些 GTID",切换主库时不需要人工计算 binlog 位置,直接按 GTID 续传。
传统复制 vs GTID 复制:
| 维度 | 传统 | GTID |
|---|---|---|
| 定位方式 | 文件名 + 偏移量(mysql-bin.000003:4567) |
GTID 集合 |
| 主库切换 | 要人工算位置,容易错 | 自动续传 |
| 一致性校验 | 难 | 可以对比 GTID 集合 |
MGR 强制要求 GTID 开启:
ini
gtid_mode = ON
enforce_gtid_consistency = ON
10.3 组复制 MGR 与多数派
名词:MGR(MySQL Group Replication,MySQL 组复制)
MySQL 5.7.17 引入的官方高可用方案。多个 MySQL 实例组成一个"复制组",组内自动同步数据、自动选主、自动处理成员故障。
为什么两节点不行
MGR 用「多数派(Quorum)」来决定"集群是否还能工作":
3 节点:多数派 = 2。挂 1 台,还剩 2 台 ≥ 2 → 集群继续工作 ✓
挂 2 台,还剩 1 台 < 2 → 集群停止写入 ✗(保护数据)
2 节点:多数派 = 2?还是 1.5?
无论怎么算都有问题:
- 如果多数派 = 2,挂 1 台就停止服务,等于没有高可用
- 如果多数派 = 1,两台互相认为对方挂了,可能出现"双主"(脑裂)✗
名词:脑裂(Split-Brain)
集群分裂成两个部分,各自都认为自己是"主",都接受写入。结果两边的数据分叉,无法合并。这是分布式系统最可怕的故障之一。
防止脑裂的唯一办法是多数派仲裁 ------ 任何一方必须获得超过半数的票才能继续工作。两个部分不可能同时超过半数。
这就是为什么必须加第三台机器。 这不是"配置问题",是数学问题。
MGR 的工作机制
组通信(Group Communication)
所有成员通过一个专用端口(默认 33061)互相通信,使用的是 Paxos 协议的变体。
名词:Paxos
一个分布式一致性算法。核心思想是:一个提案必须获得多数派同意才能生效。MGR 用它来保证所有成员对"事务提交顺序"达成一致。
事务提交流程
① 客户端在主库执行 INSERT
② 主库把变更打包成一个事务,广播给组内所有成员
③ 各成员收到后,先写进自己的 relay log,然后"投票"表示收到
④ 当主库收到多数派的确认(3 节点时需要 2 个,包括自己)
⑤ 主库才真正 COMMIT,返回成功给客户端
⑥ 其他成员并行地重放这个事务
关键点 :第 ⑤ 步------必须多数派确认才提交。
这意味着:
- 不会丢数据(这点比传统异步复制强很多:异步复制下主库 COMMIT 后立刻断电,那笔数据可能还没传到从库,就永久丢了)
- 代价是写入延迟略微增加(要等网络往返)
成员状态机
| 状态 | 含义 |
|---|---|
ONLINE |
正常,参与组内投票 |
RECOVERING |
正在从其他成员同步数据(追赶中) |
ERROR |
出错,通常是数据不一致或配置问题 |
OFFLINE |
已停止 |
MISSING |
其他成员看不到它(可能网络断了或宕机了) |
单主模式(Single-Primary)
MGR 支持两种模式:
| 模式 | 说明 | 适用 |
|---|---|---|
| 单主(Single-Primary) | 只有一个 PRIMARY,接受写入;其他都是 SECONDARY,只读 | 绝大多数场景 |
| 多主(Multi-Primary) | 所有成员都可写 | 需要应用层避免冲突,慎用 |
本项目用单主模式。从库自动被设为只读:
sql
SET GLOBAL read_only = ON;
SET GLOBAL super_read_only = ON;
super_read_only比read_only更严格 :read_only只禁止普通用户写,有SUPER权限的用户仍可写;super_read_only连 SUPER 用户也禁止写。这带来一个部署上的关键顺序:必须先在三台都创建好复制用户,再引导集群。因为引导后成员会变为只读,那时再建用户会很麻烦。
MGR 的硬性要求
| 要求 | 说明 |
|---|---|
| 存储引擎 | 只能用 InnoDB(MyISAM 无法写入) |
| 主键 | 每张表必须有主键(没有主键的表插入会报错) |
binlog_format |
必须是 ROW |
binlog_row_image |
必须是 FULL |
gtid_mode |
必须是 ON |
log_slave_updates |
必须是 ON |
lower_case_table_names |
三台必须一致 |
| 节点数 | ≥ 3(多数派) |
| 网络 | 各节点双向可达,33061 端口互通 |
10.4 MySQL Router
名词:MySQL Router
MySQL 官方提供的轻量级中间件,放在应用和 MGR 之间。它知道"谁是主库",把写请求路由到主库、读请求路由到从库。
端口约定:
| 端口 | 含义 |
|---|---|
6446 |
读写(指向主库) |
6447 |
只读(指向从库) |
6448 / 6449 |
X Protocol(用于 MySQL Shell 等) |
应用连接方式:
bash
# 写
mysql -h 192.168.1.5 -P 6446 -u user -p
# 读
mysql -h 192.168.1.5 -P 6447 -u user -p
关键价值:主库切换时,应用不用改配置。
切换前: Router 6446 → .5 (PRIMARY)
.5 宕机
切换后: Router 6446 → .6 (新的 PRIMARY) ← 自动完成,应用无感
对比 LVS 的角色:
| LVS 网页集群 | MySQL + Router | |
|---|---|---|
| 分发依据 | IP + 端口 + 调度算法 | SQL 的读写语义 |
| 目标 | 平均分摊 | 永远指向唯一的可写节点 |
| 后端挂了 | 从转发表移除 | 重新选主,Router 自动跟随 |
这两个是不同层次的东西,不能互相替代。
10.5 两套经验的对应关系
不能照搬的:
| 试图照搬的想法 | 为什么不行 |
|---|---|
用 LVS rr 轮询分发 MySQL 请求 |
MySQL 有状态,写请求必须去主库 |
| 用 LVS 的 VIP 直接暴露 3306 | 主库切换后 VIP 上的服务还是旧的;且 DR 模式做公网映射不可行 |
| 沿用同一套 keepalived 配置 | VRID 冲突(都是 51),必须换 |
能完整复用的:
| LVS 的经验 | 在 MySQL 场景的用法 |
|---|---|
keepalived notify 钩子模式 |
写个 is_primary.sh 脚本,让 VIP 只跟随主库 |
| VRRP 心跳的思维模型 | 理解 MGR 的组通信心跳和 MISSING 状态 |
| 清理残留必须是第一步 | K8s 的 KUBE-* 规则会淹没 33061 的排错 |
tcpdump + 状态表排错法 |
tcpdump port 33061 + replication_group_members |
| 参数必须看"生效值" | MySQL 的参数也要 SHOW VARIABLES 确认,不能只看配置文件 |
| 状态一致比配置好看更重要 | 三台的 GTID / binlog 参数必须逐字节一致 |
| 做观察工具的思路 | 做一个脚本持续跑 03-check.sh 看集群状态变化 |
如果要用 keepalived 做 MySQL 的 VIP(替代 Router):
conf
# 在每台节点上
vrrp_script chk_primary {
script "/usr/local/bin/is_primary.sh"
# 内部逻辑:
# mysql -uroot -N -e "SELECT MEMBER_ROLE FROM
# performance_schema.replication_group_members
# WHERE MEMBER_ID = @@server_uuid" | grep -q PRIMARY
interval 2
weight 40 # 是主库就 +40, 抢占 VIP
fall 2
rise 2
}
vrrp_instance VI_MYSQL {
virtual_router_id 52 # ★ 必须不同于网页集群的 51
auth_pass MYSQL_vip_2026 # ★ 密码也要不同
# ...
}
10.6 一个容易忽略的隐患:IP 地址
本项目的具体问题 :.5 的 IPv4 是 DHCP 动态获取的。
在 LVS 时代这只是隐患(VIP 漂移的目标地址可能变)。但在 MySQL MGR 时代,这是直接翻车:
MGR 配置: group_replication_group_seeds = "192.168.1.5:33061, ..."
/etc/hosts: 192.168.1.5 mysql1
Router 引导: mysqlrouter --bootstrap mgr_admin@192.168.1.5:3306
三处都写死了 192.168.1.5。IP 一变,三处全断。
而且症状是"重启后集群起不来",很难第一时间想到是 DHCP 惹的祸。
解法 :在路由器上做 MAC-IP 绑定(静态 DHCP 分配),或在服务器上用 netplan 配静态 IP。
bash
# 查看本机 MAC(用于在路由器上做绑定)
ip link show enp1s0 | grep ether
10.7 本章名词表
| 名词 | 英文 | 含义 |
|---|---|---|
| 无状态 | Stateless | 不保存跨请求的数据 |
| 有状态 | Stateful | 保存必须一致的数据 |
| binlog | Binary Log | MySQL 的数据变更日志 |
| relay log | Relay Log | 从库暂存 binlog 的中继日志 |
| 复制延迟 | Replication Lag | 从库落后主库的时间 |
| GTID | Global Transaction Identifier | 全局事务唯一标识 |
| MGR | MySQL Group Replication | MySQL 官方组复制方案 |
| 多数派 | Quorum | 超过半数的节点 |
| 脑裂 | Split-Brain | 集群分裂成两个各自认为自己是主的组 |
| 组通信 | Group Communication | MGR 成员间的通信(端口 33061) |
| Paxos | - | 分布式一致性算法 |
| 单主模式 | Single-Primary | 一个主库接受写,其余只读 |
| 多主模式 | Multi-Primary | 所有成员可写 |
| read_only | - | 只读模式(SUPER 用户仍可写) |
| super_read_only | - | 超级只读(SUPER 用户也不能写) |
| MySQL Router | - | 官方中间件,自动路由读写请求 |
| 主键 | Primary Key | 唯一标识一行的列,MGR 强制要求 |
| InnoDB | - | MySQL 的事务型存储引擎 |
第 11 章 附录
附录 A:名词总表(术语字典)
按主题分类,按需回查。
基础网络
| 名词 | 英文 / 缩写 | 含义 |
|---|---|---|
| MAC 地址 | MAC Address | 网卡物理地址,二层寻址 |
| IP 地址 | IP Address | 三层逻辑地址 |
| 数据帧 | Frame | 二层数据单位 |
| 数据包 | Packet | 三层数据单位 |
| ARP | Address Resolution Protocol | IP → MAC 解析协议 |
| ARP 缓存 | ARP Cache | 本机 IP-MAC 映射表 |
| ARP 冲突 | ARP Conflict | 多设备抢答同一 IP |
| ARP 污染 | ARP Pollution | 用 VIP 当源地址发 ARP |
| 广播 | Broadcast | 发给局域网内所有设备 |
| 组播 | Multicast | 发给特定一组设备 |
| 单播 | Unicast | 发给一台设备 |
| 子网 | Subnet | 一段 IP 范围 |
| CIDR | - | /24 形式的网络位表示法 |
| 网关 | Gateway | 跨子网时的下一跳 |
| 路由表 | Routing Table | 去往各网段的路径 |
| 端口 | Port | 定位程序,0-65535 |
| 四元组 | 4-tuple | 唯一标识一条 TCP 连接 |
| ICMP | - | ping 使用的协议 |
| 三次握手 | Three-way Handshake | SYN / SYN-ACK / ACK |
| 四次挥手 | Four-way Handshake | FIN / ACK / FIN / ACK |
| TIME_WAIT | - | 连接关闭后的等待状态 |
负载均衡
| 名词 | 英文 / 缩写 | 含义 |
|---|---|---|
| 负载均衡 | Load Balancing / LB | 把请求分散到多台后端 |
| 四层 | L4 | 传输层,看 IP + 端口 |
| 七层 | L7 | 应用层,能读 HTTP |
| 终止连接 | Connection Termination | 分别和两端建连 |
| SSL 卸载 | SSL Offloading | 由 LB 负责加解密 |
| 后端 | Backend | 真正处理请求的服务器 |
| 调度算法 | Scheduling Algorithm | 决定请求分给谁 |
| 会话保持 | Session Persistence | 同一客户端固定分到同一后端 |
LVS / IPVS
| 名词 | 英文 / 缩写 | 含义 |
|---|---|---|
| LVS | Linux Virtual Server | 开源四层负载均衡 |
| IPVS | IP Virtual Server | 内核模块,LVS 的实现 |
| Netfilter | - | 内核包处理框架 |
| iptables | - | Netfilter 的命令行工具(防火墙) |
| ipvsadm | - | IPVS 的命令行工具 |
| Director | - | 调度器 / 负载均衡器 |
| RealServer / RS | - | 后端服务器 |
| VIP | Virtual IP | 虚拟 IP |
| DIP | Director IP | 调度器真实 IP |
| RIP | RealServer IP | 后端真实 IP |
| CIP | Client IP | 客户端 IP |
| NAT 模式 | - | 改 IP 转发 |
| TUN 模式 | - | IPIP 封装转发 |
| DR 模式 | Direct Routing | 只改 MAC 转发 |
| 转发循环 | Forwarding Loop | 后端有 IPVS 表导致包打转 |
| rr | Round Robin | 轮询 |
| wrr | Weighted Round Robin | 加权轮询 |
| lc | Least Connection | 最少连接 |
| wlc | Weighted Least Connection | 加权最少连接 |
| sh | Source Hashing | 源地址哈希 |
| ActiveConn | - | 当前活跃连接数 |
| InActConn | - | 非活跃连接数 |
keepalived / HA
| 名词 | 英文 / 缩写 | 含义 |
|---|---|---|
| keepalived | - | VRRP + 健康检查守护进程 |
| VRRP | Virtual Router Redundancy Protocol | 虚拟路由器冗余协议 |
| 虚拟路由器 | Virtual Router | 多机共享 VIP 的逻辑路由器 |
| VRID | Virtual Router ID | VRRP 组号,1-255 |
| MASTER | - | 持有 VIP 的节点 |
| BACKUP | - | 待命节点 |
| Advertisement | - | MASTER 的心跳通告 |
| Master_Down_Interval | - | 判定 MASTER 死亡的时间 |
| priority | - | 优先级 |
| 抢占 | Preempt | 恢复后抢回 MASTER |
| 非抢占 | nopreempt | 恢复后不抢回 |
| 震荡 | Flapping | 状态反复切换 |
| 健康检查 | Health Check | 周期检测服务是否正常 |
| track_script | - | 挂载健康检查的配置 |
| weight | - | 对优先级的加减值 |
| fall / rise | - | 防抖计数 |
| notify 钩子 | notify hook | 状态变化时调用的脚本 |
| 单点故障 | SPOF | 单组件故障导致整体不可用 |
| 故障转移 | Failover | 自动切换到备用节点 |
| 高可用 | HA | 部分故障仍可服务 |
内核参数
| 名词 | 含义 |
|---|---|
| sysctl | 内核参数的查看/修改工具 |
| arp_ignore | 控制何时回应 ARP 请求 |
| arp_announce | 控制发 ARP 时的源 IP |
| rp_filter | 反向路径过滤(防 IP 欺骗) |
| 严格模式 | rp_filter = 1 |
| 松散模式 | rp_filter = 2 |
| ip_forward | 内核是否转发非本机包 |
| ip_nonlocal_bind | 是否允许绑定非本机 IP |
| 生效值 | 内核实际使用的值 |
| max() 语义 | all 与接口级参数取最大值 |
网络边界
| 名词 | 英文 / 缩写 | 含义 |
|---|---|---|
| NAT | Network Address Translation | 网络地址转换 |
| SNAT | Source NAT | 改源地址 |
| DNAT | Destination NAT | 改目标地址 |
| MASQUERADE | - | 动态 SNAT |
| 端口映射 | Port Forwarding | DNAT 的特例 |
| conntrack | Connection Tracking | 连接跟踪表 |
| NAT 回流 | Hairpin NAT | 内网通过公网 IP 访问内网服务 |
| 三角路由 | Triangle Routing | 请求响应路径不对称 |
| 私有地址 | Private IP | 192.168/10/172.16-31 |
| CGNAT | Carrier-Grade NAT | 运营商级 NAT |
| 内网穿透 | Tunneling | 绕过无公网 IP 的转发方案 |
MySQL / 数据库集群
| 名词 | 英文 / 缩写 | 含义 |
|---|---|---|
| 无状态 | Stateless | 不保存跨请求数据 |
| 有状态 | Stateful | 保存必须一致的数据 |
| binlog | Binary Log | 数据变更日志 |
| relay log | Relay Log | 从库中继日志 |
| GTID | Global Transaction Identifier | 全局事务唯一标识 |
| 复制延迟 | Replication Lag | 从库落后主库的时间 |
| MGR | MySQL Group Replication | 官方组复制 |
| 多数派 | Quorum | 超过半数节点 |
| 脑裂 | Split-Brain | 集群分裂成两个主 |
| Paxos | - | 分布式一致性算法 |
| 组通信 | Group Communication | MGR 成员通信(33061) |
| 单主 / 多主 | Single / Multi-Primary | MGR 的两种模式 |
| read_only | - | 只读 |
| super_read_only | - | 超级只读 |
| MySQL Router | - | 读写分离中间件 |
运维工具与概念
| 名词 | 含义 |
|---|---|
| sysctl | 内核参数工具 |
| journalctl | systemd 日志工具 |
| ss | 查看 socket(取代 netstat) |
| tcpdump | 抓包工具 |
| namei | 逐级检查路径权限 |
| conntrack | 连接跟踪 |
| CRLF / LF | Windows / Unix 换行符 |
| 权限位 | rwx 权限数字 |
| 穿越权限 | 目录 x 位 |
| 延迟变量 | Windows bat 的 !VAR! 语法 |
| K8s / Kubernetes | 容器编排系统 |
| kube-proxy | K8s 的网络代理组件,会写 iptables/IPVS |
| CNI | 容器网络接口 |
附录 B:命令速查
网络诊断
bash
ip a # 看所有 IP
ip -br a # 简洁格式
ip r # 路由表
ip neigh # ARP 表
ss -lntp # 监听端口
ping -c 3 1.2.3.4 # 三层连通
tcpdump -i enp1s0 -nn port 80 # 抓包
curl -s -o /dev/null -w "%{http_code}" URL # 看 HTTP 状态码
namei -l /path/to/file # 路径权限
LVS / keepalived
bash
ipvsadm -Ln # ★ 看 IPVS 表(每台都要看)
ipvsadm -Ln --stats # 累计流量
ipvsadm -Lnc # 连接详情
ipvsadm -C # 清空(危险)
systemctl status keepalived
systemctl reload keepalived # 重载配置
journalctl -u keepalived -n 50 --no-pager
keepalived -t -f /etc/keepalived/keepalived.conf # 检查语法
内核参数
bash
sysctl net.ipv4.ip_forward # 看一个
sysctl -n net.ipv4.conf.all.rp_filter # 只看值
sysctl -p /etc/sysctl.d/90-lvs-dr.conf # 加载指定文件
sysctl --system # 加载全部
sysctl -w net.ipv4.ip_forward=1 # 临时改
MySQL MGR
sql
-- 看集群成员和角色
SELECT member_host, member_port, member_state, member_role
FROM performance_schema.replication_group_members\G
-- 看复制通道状态
SELECT * FROM performance_schema.replication_group_member_stats\G
-- 看主库是谁
SELECT * FROM performance_schema.global_status
WHERE variable_name = 'group_replication_primary_member';
-- 手动启动/停止组复制
START GROUP_REPLICATION;
STOP GROUP_REPLICATION;
附录 C:常见问题 FAQ
Q1:ipvsadm -Ln 在 BACKUP 上有表,怎么办?
立即执行 ipvsadm -C,然后检查 keepalived.conf 里是不是静态写了 virtual_server 段。如有,删掉并改用 notify_* 钩子(见 4.6)。
Q2:两台机器都认为自己是 MASTER?
检查这三项是否一致:virtual_router_id、auth_type+auth_pass、interface。另外确认 VRRP 组播(224.0.0.18)没被防火墙挡。
Q3:改了 sysctl 参数没生效?
三种可能:① 只改了接口级,没改 all(取最大值);② 被 99- 开头的文件覆盖了;③ 没有 sysctl --system 重新加载。用 sysctl <key> 看生效值,不信配置文件。
Q4:VIP 能 ping 通,但访问 80 不通?
分层查:ss -lntp | grep :80(nginx 起了吗)→ curl 127.0.0.1(本机能访问吗)→ ipvsadm -Ln(表对吗)→ tcpdump port 80(包到了吗)。
Q5:curl 返回 000?
000 表示连接阶段就失败(不是 HTTP 层的问题)。查:网络可达性 → 端口监听 → 防火墙 → IPVS 表。
Q6:轮询看起来没有交替?
用 ipvsadm -Ln --stats 看累计连接数是否均匀增长(不要看 InActConn)。另外注意:HTTP 短连接 + rr 理论上应该交替,但如果客户端用了 HTTP keep-alive,一条 TCP 连接会承载多个请求,看起来就不交替。
Q7:keepalived 的钩子脚本没执行?
① 检查 chmod +x;② 检查 global_defs 里有没有 enable_script_security 和 script_user root(ipvsadm 需要 root);③ 用 journalctl -u keepalived 看有没有报错。
Q8:内网能用公网 IP 打开,外网不行?
先排除 CGNAT(curl ifconfig.me 对比路由器 WAN 口 IP),再看是不是 NAT 回流假象,最后确认端口映射的目标不能填 VIP(见第 8 章)。
Q9:MGR 成员卡在 RECOVERING 或 ERROR?
依次检查:① tail -100 /var/log/mysql/error.log;② 三台的 gtid_mode / binlog_format / binlog_row_image / lower_case_table_names 是否完全一致;③ 33061 端口是否互通(tcpdump port 33061);④ 复制用户密码三台是否一致,恢复通道是否配了 GET_SOURCE_PUBLIC_KEY=1。
Q10:插入数据报 "table has no primary key"?
这是 MGR 的硬性要求。给表加主键:ALTER TABLE t ADD PRIMARY KEY (id);
Q11:写了一个 .bat,运行后变量不展开(显示 !VAR!)?
文件是 LF 换行。转成 CRLF(sed -i 's/$/\r/'),并改用 goto + %VAR% 写法,不用延迟变量。
Q12:nginx 返回 403,文件权限已经是 777 了?
用 namei -l <文件路径> 检查是否某一级目录没有 x 权限。目录权限是门,文件权限是门槛。
附录 D:配置文件模板
/etc/sysctl.d/90-lvs-dr.conf
ini
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.lo.arp_ignore = 1
net.ipv4.conf.lo.arp_announce = 2
net.ipv4.conf.enp1s0.arp_ignore = 1
net.ipv4.conf.enp1s0.arp_announce = 2
net.ipv4.ip_nonlocal_bind = 1
/etc/sysctl.d/91-lvs-forward.conf
ini
net.ipv4.ip_forward = 1
/etc/keepalived/keepalived.conf(LVS 版,node1)
conf
global_defs {
router_id LVS_NODE_1
enable_script_security
script_user root
}
vrrp_script chk_nginx {
script "/usr/bin/killall -0 nginx"
interval 2
weight -20
fall 2
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface enp1s0
virtual_router_id 51
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass HA_lvs_2026
}
virtual_ipaddress {
192.168.1.123/24 dev enp1s0
}
track_script {
chk_nginx
}
notify_master "/etc/keepalived/lvs-ipvs-add.sh"
notify_backup "/etc/keepalived/lvs-ipvs-del.sh"
notify_fault "/etc/keepalived/lvs-ipvs-del.sh"
notify_stop "/etc/keepalived/lvs-ipvs-del.sh"
}
/etc/keepalived/lvs-ipvs-add.sh
bash
#!/bin/bash
ipvsadm -C 2>/dev/null || true
ipvsadm -A -t 192.168.1.123:80 -s rr
ipvsadm -a -t 192.168.1.123:80 -r 192.168.1.5:80 -g -w 1
ipvsadm -a -t 192.168.1.123:80 -r 192.168.1.6:80 -g -w 1
logger -t lvs "IPVS rules ADDED (this node is MASTER)"
/etc/keepalived/lvs-ipvs-del.sh
bash
#!/bin/bash
ipvsadm -C 2>/dev/null || true
logger -t lvs "IPVS rules REMOVED (this node is BACKUP)"
附录 E:学习路径建议
如果你已经读完本文,下一步可以按这个顺序深入:
| 顺序 | 主题 | 为什么 |
|---|---|---|
| 1 | Wireshark 深入 | 把 tcpdump 升级成可视化分析,能看懂 TCP 重传、窗口、拥塞 |
| 2 | HTTP / HTTPS 协议 | 日常排错一半以上和 HTTP 有关;理解 keep-alive、Cookie、缓存 |
| 3 | nginx 配置 | 七层负载均衡 + 反向代理,是 LVS 的天然搭档 |
| 4 | DNS 与域名 | 从"记 IP"进化到"用域名",以及 DNS 层的负载均衡 |
| 5 | iptables / nftables | 深入理解 Netfilter,才能看懂 K8s 为什么干扰 |
| 6 | Ansible | 把手工脚本变成可重复的自动化部署 |
| 7 | Prometheus + Grafana | 从"出问题才排查"进化到"提前发现" |
| 8 | Docker / K8s | 现代部署方式;同时也能理解它们为什么会和 LVS 打架 |
| 9 | MySQL 深入 | binlog 格式、GTID、复制原理、性能调优 |
| 10 | 分布式系统理论 | CAP、Paxos、Raft------理解 MGR / etcd / Redis 集群的底层 |
一条重要的学习建议:
不要只学"怎么做",要学"为什么这么做"。
本教程里所有的配置项,都对应着一个具体的物理约束或失效场景。比如:
arp_ignore=1→ 因为两台机器都有 VIP,不能让非 MASTER 回应notify钩子 → 因为静态配置会让两台都建表,导致转发循环- 三节点 → 因为两节点无法形成多数派
如果你只背命令,遇到新场景就废了;如果你理解了约束,遇到新场景能自己推导出配置。
结语
这份教程覆盖的知识,是从零搭一套 keepalived + LVS 高可用集群、并把它迁移到 MySQL 集群的过程中,实际用到的全部内容。
回顾一下主线:
第 0 章 为什么要集群(问题定义)
第 1 章 网络基础(MAC / IP / ARP / VIP)
第 2 章 负载均衡概念(四层 vs 七层)
第 3 章 LVS 原理与三种模式
第 4 章 keepalived 与 VRRP
第 5 章 两者合体:完整架构与请求生命周期
第 6 章 支撑这一切的内核参数
第 7 章 出问题时用什么工具查
第 8 章 网络边界(NAT 与公网)
第 9 章 8 个真实案例(把前面所有知识用一遍)
第 10 章 从无状态到有状态(MySQL 集群)
如果这份文档只能记住三件事:
- MAC 管一段,IP 管全程 ------ DR 只改 MAC,所以要求同二层;响应源 IP 是 VIP,所以不能做端口映射。
- 状态必须由代码保证,不能靠流程约束 ------ BACKUP 不能有 IPVS 表,这件事写成
notify钩子才可靠。 - 排查时,唯一可靠的判据是"去掉这个因素,现象是否消失" ------ 看起来很可疑的参数不一定是原因;计数器为 0 也不能证明什么。
最后,第 9 章里那些"死路"其实比"正确答案"更值得记。排错能力不是"知道答案",而是"知道怎么在死路上及时回头"。