【无标题】

从零开始:keepalived + LVS 负载均衡与高可用完全教程

适用对象 :完全没有接触过负载均衡/高可用的读者。

配套环境 :三台 Ubuntu 服务器(192.168.1.5 / .6 / .7),虚拟 IP 192.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:0

Linux 的环回接口别名。lo 是 lo:0 的简写形式。给它挂 VIP 是 LVS-DR 的标准做法,因为它比物理网卡"低调"------不会主动参与物理网络的 ARP 竞争。

为什么要挂 /32 而不是 /24?

/32 表示"就这一个地址,没有同子网的邻居"。这样内核不会认为 192.168.1.0/24 整个网段都在本地(否则会引发奇怪的路由行为)。在 DR 模式里这是必须的写法。

1.7 本章小结

如果这一章只记三句话:

  1. MAC 管一段路,IP 管全程。 DR 只改 MAC 不改 IP,所以要求同二层。
  2. ARP 决定"这个 IP 的 MAC 是谁"。 VIP 出现在多台机器上时,必须让多余的机器闭嘴。
  3. 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),它不知道后面有三台服务器。

所以必须在中间放一个"接待员":用户只管找接待员,接待员负责把活分给后面的工人。

复制代码
       用户们
         │
         ▼
   ┌───────────┐
   │  接待员    │ ← 负载均衡器
   └───────────┘
      │  │  │
      ▼  ▼  ▼
    工人 工人 工人   ← 后端服务器

这个"接待员"就是负载均衡器。它需要做到:

  1. 分发:把请求按某种规则分给后端(轮询、按连接数、按权重......)
  2. 健康检查:发现某个后端挂了,就不再往它那儿分
  3. 透明:用户完全感觉不到后面有多台机器

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 快这么多? 三个原因:

  1. 在内核里干活 。数据包不用从内核态拷贝到用户态再拷回去(这叫「零拷贝」层面的优化)。
  2. 不终止连接。nginx 要维护两倍的连接(客户端→nginx,nginx→后端),每连接都要占用内存和文件描述符;LVS 只"骗"一下 MAC 地址就转走了。
  3. 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 年发起的开源项目。

它由两部分组成:

  1. IPVS (IP Virtual Server)------内核模块,真正干活的。从 Linux 2.4 内核开始就内置了。
  2. ipvsadm ------用户态命令行工具,用来查看和修改 IPVS 的转发表。

名词:IPVS

Linux 内核中的一个模块,实现四层负载均衡的转发逻辑。它挂在 Netfilter(内核的包处理框架)的钩子上,包一进内核就被它截获处理。
名词:Netfilter / iptables

Linux 内核的包过滤框架。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 详细讲):

  1. 后端必须在 lo:0 上有 VIP(否则收不下这个"目标不是自己"的包)
  2. 后端绝对不能响应 VIP 的 ARP 请求(否则抢走流量)
  3. 调度器和后端必须在同一个二层网络(因为只靠 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 就是干这个的。

它做两件事:

  1. 管 VIP 的归属------通过 VRRP 协议,决定"现在谁持有 VIP"
  2. 做健康检查------发现本机服务挂了,主动让出 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。

几个实战注意事项:

  1. 钩子脚本必须有执行权限 :chmod +x /etc/keepalived/*.sh
  2. 脚本里不要用 set -e 后再让某条命令失败 ,否则脚本会中途退出(本项目用的 || true 就是这个作用)
  3. enable_script_security + script_user root :keepalived 默认降权执行脚本。如果脚本需要 root 权限(比如 ipvsadm),必须显式配置:
conf 复制代码
global_defs {
    enable_script_security
    script_user root
}
  1. 排查钩子有没有跑,看系统日志:
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 本章小结

三句话总结整个方案:

  1. keepalived 决定"谁是入口"------通过 VRRP 选举出 MASTER,VIP 只在它身上。
  2. LVS 决定"请求给谁"------MASTER 上的 IPVS 表做轮询分发。
  3. 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 ------ 一致,不会被丢。

但实践中经常出问题,原因是:

  1. 回程可能走不同路径(多网卡、多路由的场景)
  2. 非对称路由的存在
  3. 上面说的 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 打开,外网打不开"时,可能是两个独立的问题叠在一起:

  1. 内网访问其实是走了某种回流路径成功了(不代表外网能通)
  2. 外网访问有另一个问题

所以:测试外网可达性,一定要用不在内网的网络来测------比如手机开 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 段,而这个配置文件被复制到了两台机器上。于是:

  1. .5 生成 IPVS 表(正确,它是 MASTER)
  2. .6 也生成了 IPVS 表(错误!)
  3. 请求从 .5 转发到 .6 时,包的目标 IP 仍然是 192.168.1.123
  4. .6 的内核发现"这个目标地址匹配我的 IPVS 规则",于是又把它转出去
  5. 包在两台之间打转 → 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;。

这是明确的反模式,绝对不能做:

  1. nginx 是暴露在公网的服务。以 root 运行意味着一旦有漏洞,攻击者直接获得 root 权限(这是"权限提升"漏洞的经典场景)。
  2. nginx 的设计本来就是 "master 以 root 启动、worker 降权运行" 。master 需要 root 是为了绑定特权端口(< 1024);worker 处理实际请求,必须降权。强行改 user root 是破坏这套安全设计。
  3. 这是在拿整个服务器的安全,换省一次 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 集群)

如果这份文档只能记住三件事:

  1. MAC 管一段,IP 管全程 ------ DR 只改 MAC,所以要求同二层;响应源 IP 是 VIP,所以不能做端口映射。
  2. 状态必须由代码保证,不能靠流程约束 ------ BACKUP 不能有 IPVS 表,这件事写成 notify 钩子才可靠。
  3. 排查时,唯一可靠的判据是"去掉这个因素,现象是否消失" ------ 看起来很可疑的参数不一定是原因;计数器为 0 也不能证明什么。

最后,第 9 章里那些"死路"其实比"正确答案"更值得记。排错能力不是"知道答案",而是"知道怎么在死路上及时回头"。

相关推荐
weixin_416667961 小时前
linux ping命令详解
linux·运维·php
硅基手札1 小时前
【Linux 内核专栏 17】进程间通信 IPC(上)— 管道·信号·System V IPC·mmap·文件锁
linux·运维·服务器·stm32·单片机·mcu
小能喵1 小时前
kismet -h 帮助信息 中文版
linux·服务器·网络
seuzhouchenglong1 小时前
服务器通过本地电脑连接本地开发板
运维·服务器
AR-26710-1 小时前
Linux Day21——systemctl操作
linux·运维
臻选云栗子1 小时前
PCDN晚高峰掉量原因分析及优化建议
服务器·网络·安全·网络安全·阿里云·云计算·信息与通信
YumiProxy1 小时前
流媒体区域内容访问:代理IP的技术原理、检测机制与稳定性优化
服务器·网络·网络协议·tcp/ip·ip
额额额对了1 小时前
Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动
linux·c语言·开发语言·驱动开发·嵌入式硬件
讯捷蓝达2 小时前
台山曙光I620-G20服务器RAID5离线数据恢复全过程
运维·服务器