01-项目一_企业级高可用Web集群实施手册

项目一实施手册:高可用 Web 集群搭建与调优

网络:eth0 = NAT 172.25.254.0/24(网关 172.25.254.2);eth1 = 仅主机 192.168.0.0/24

域名:统一使用 timinglee.org

前置阅读:《00-项目总览与资源规划.md》

目录

  • 阅读前必看:主机清单与执行约定
  • 第一章 母盘制作与 VMware 网络准备
  • 第二章 批量克隆与主机初始化
  • 第三章 infra 节点:时间同步 / NFS / 集中日志 / DNS / Redis
  • 第四章 web 节点:Nginx + Tomcat + Memcached 集群
  • 第五章 db 节点:MySQL 主从(GTID + 半同步)与 VIP 高可用
  • 第六章 lb 节点:HAProxy 七层负载 + Keepalived 双主高可用
  • 第七章 进阶:用 LVS DR 模式替换 HAProxy
  • 第八章 全链路联调与性能压测
  • 第九章 故障演练剧本
  • 第十章 简历写法与面试问答
  • 附录 A MySQL 源码编译(可选)
  • 附录 B MHA 高可用集群(可选)

阅读前必看:主机清单与执行约定

本手册每一个章节标题下都标了「本章操作主机」,每一个小节标题下都标了「执行主机」。动手之前先看这个标注,确认自己登录的是哪台机器------多机集群最容易犯的错就是在错的机器上敲了对的命令。

所有命令默认以 root 用户执行。除特别说明外,命令在标注的那台(或那几台)机器上执行。

主机与角色对照

主机名 业务面(NAT) 数据面(仅主机) 角色 内存
lb1 172.25.254.50 192.168.0.50 HAProxy 主 + Keepalived(持有 Web VIP .200) 1 GB
lb2 172.25.254.60 192.168.0.60 HAProxy 备 + Keepalived(持有监控 VIP .202) 1 GB
web1 172.25.254.10 192.168.0.10 Nginx + Tomcat + Memcached 节点 1 1.5 GB
web2 172.25.254.20 192.168.0.20 Nginx + Tomcat + Memcached 节点 2 + Redis 从节点 1.5 GB
db1 172.25.254.30 192.168.0.30 MySQL 主库(持有 DB VIP .201) 1 GB
db2 172.25.254.40 192.168.0.40 MySQL 从库 + Sentinel 哨兵 1 GB
infra 172.25.254.100 192.168.0.100 chrony / NFS / rsyslog / DNS / Redis 主节点 1 GB
base-rhel9.8 --- --- 母盘,只做克隆源,不跑业务 ---
宿主机 Windows --- --- VMware 操作、ISO 镜像、快照管理 ---

VIP 一览 :172.25.254.200(Web 业务)、172.25.254.201(数据库)、172.25.254.202(监控/状态页)

两条阅读约定

  1. 成对节点只写一遍命令 。web1/web2、lb1/lb2、db1/db2 这类节点的命令完全相同,手册里只写一次,标注会写明"两台都执行"。如果两侧配置有差异(例如 Keepalived 的 priority、MySQL 的主从角色),会明确分开写。

  2. 标注里的箭头表示操作流向 。例如「infra(服务端)→ 其余 6 台(客户端)」表示先在 infra 上配服务端,再逐台去其他机器配客户端。


第一章 母盘制作与 VMware 网络准备

本章操作主机 :宿主机 Windows → base-rhel9.8 母盘

1.1 安装 VMware Workstation 与准备安装镜像

执行主机 :宿主机 Windows

两个项目一共 11 台虚拟机,全部跑在 VMware Workstation 上。这一节从零开始,把虚拟机软件装好、镜像备好、目录规划好,为后面的母盘制作打地基。

1.1.1 硬件条件自检

先确认三件事,缺任何一个后面都会卡住。

检查项 要求 怎么查
CPU 虚拟化 Intel VT-x 或 AMD-V 已开启 任务管理器 → 性能 → CPU → 看右下角「虚拟化:已启用」
内存 ≥ 8 GB(本手册按 15 GB 实测规划) 任务管理器 → 性能 → 内存
磁盘剩余 数据盘 ≥ 60 GB 这台电脑 → 看各盘剩余空间

如果「虚拟化」显示为已禁用 ,需要进 BIOS/UEFI 打开。AMD 平台(Ryzen)叫 SVM Mode,通常藏在 Advanced → CPU Configuration 里;Intel 平台叫 Intel Virtualization Technology / VT-x。改完按 F10 保存重启。

这一步没开的话,虚拟机一上电就会报 This host supports Intel VT-x, but Intel VT-x is disabled,或者能跑但慢得像卡死。这是新手最常见的第一个坑。

1.1.2 安装 VMware Workstation Pro

用 VMware Workstation 17.x(Pro 版,个人使用免费)。

  1. 双击安装包 VMware-workstation-full-17.x.x-xxxxx.exe,点「下一步」。
  2. 勾选「我接受许可协议中的条款」。
  3. 安装位置改到 D 盘 :D:\VMware\Workstation(C 盘只剩 40 多 GB,要给虚拟机文件留空间)。
  4. 「启动时检查产品更新」和「加入 VMware 客户体验提升计划」取消勾选,省事。
  5. 快捷方式两项都勾上。
  6. 点「安装」,等 2 ~ 3 分钟,完成后点「完成」。
  7. 重启一次系统(安装过程会装虚拟网卡驱动,不重启可能导致后面虚拟网络编辑器不可用)。

重启后检查:控制面板 → 网络和 Internet → 网络连接,应该能看到两块虚拟网卡:

复制代码
VMware Network Adapter VMnet1     ← 仅主机模式(Host-Only)
VMware Network Adapter VMnet8     ← NAT 模式

看到这两块网卡才说明装好了。如果没看到,卸载后重新安装一次。

1.1.3 准备 RHEL 9.8 安装镜像

两个项目统一使用 Red Hat Enterprise Linux 9.8 的 DVD ISO(约 8 ~ 10 GB)。

  1. 从红帽开发者站点(或培训机构/学校提供的镜像源)下载 rhel-9.8-x86_64-dvd.iso。
  2. 校验完整性------这一步别省,镜像不完整会在安装到 70% 时莫名报错,而且很难排查:
powershell 复制代码
# 在宿主机上打开 PowerShell,进到 ISO 所在目录
cd D:\iso
Get-FileHash .\rhel-9.8-x86_64-dvd.iso -Algorithm SHA256

把输出的哈希值和下载页公布的 SHA256 逐位比对,完全一致才能用。

  1. 把 ISO 放在固定路径,后面创建虚拟机会反复指向它:

    D:\iso\rhel-9.8-x86_64-dvd.iso

两个不要:不要放桌面(容易被清理),不要用中文路径。虚拟机配置文件里记录的是绝对路径,一移动就变成「无法连接虚拟设备 ide1:0」。

1.1.4 规划虚拟机存放目录

先在 D 盘把目录结构建好,后面所有虚拟机都按这个规则存放,找起来不乱:

复制代码
D:\vm\
├── base-rhel9.8\        ← 母盘(项目一、项目二共用)
├── proj1-lb1\           ← 项目一:负载均衡 1
├── proj1-lb2\           ← 项目一:负载均衡 2
├── proj1-web1\          ← 项目一:Web 节点 1
├── proj1-web2\          ← 项目一:Web 节点 2
├── proj1-db1\           ← 项目一:数据库主库
├── proj1-db2\           ← 项目一:数据库从库
├── proj1-infra\         ← 项目一:基础服务节点
├── proj2-master\        ← 项目二:K8s 控制面
├── proj2-node1\         ← 项目二:K8s 工作节点 1
├── proj2-node2\         ← 项目二:K8s 工作节点 2
└── proj2-harbor\        ← 项目二:私有镜像仓库

为什么必须放 D 盘:11 台虚拟机即便全部用链接克隆,母盘本体也要 20 ~ 25 GB。C 盘只剩 40 多 GB,既装不下,装满了还会拖慢整个 Windows。

1.2 配置 VMware 虚拟网络

执行主机 :宿主机 Windows

必须以管理员身份运行 VMware Workstation(否则虚拟网络编辑器改不了)。

打开 编辑 → 虚拟网络编辑器,点右下角「更改设置」获取写权限,然后配置两个网络。

VMnet8(NAT)

项目 值
类型 NAT 模式
子网 IP 172.25.254.0
子网掩码 255.255.255.0
网关 172.25.254.2
DHCP 可以关闭(我们用静态 IP)

VMnet1(仅主机)

项目 值
类型 仅主机模式
子网 IP 192.168.0.0
子网掩码 255.255.255.0
DHCP 关闭(我们用静态 IP)

关闭 DHCP 很重要。如果 DHCP 还在跑,它可能把 192.168.0.100 之类的地址分给你以后要手动指定的机器,造成 IP 冲突,而且这种冲突很难排查。

1.3 创建母盘虚拟机

执行主机 :宿主机 Windows(1.3.1 ~ 1.3.2)→ base-rhel9.8(1.3.3 起,在虚拟机内)

母盘(Base VM)是后面 11 台虚拟机的共同祖先。母盘只做一次,做得越规范,克隆出来的机器越省事。

1.3.1 先定好规格

项目 值 说明
客户机操作系统 Red Hat Enterprise Linux 9 64 位 与 RHEL 9.8 镜像对应
名称 base-rhel9.8 母盘,后续所有机器从它克隆
位置 D:\vm\base-rhel9.8 放 D 盘,C 盘只剩 42 GB
固件类型 UEFI RHEL 9 官方推荐;若镜像在 UEFI 下引导失败,改回 BIOS 也能用
安全引导 关闭(取消勾选) 开启会导致后面要装的部分内核模块加载失败
处理器 2 vCPU(处理器数量 1 × 内核数 2) 安装期给 2 个够快,克隆后按清单调整
内存 2048 MB 安装期用,克隆后按各机清单调整
网络适配器 两块:网卡1 = NAT,网卡2 = 仅主机 母盘就配好双网卡,克隆即继承,省掉逐台加网卡
磁盘 25 GB,精简置备,拆分成多个文件 25 GB 是为了同时满足项目二(容器镜像吃空间)
光驱 指向 RHEL 9.8 ISO 安装完不要卸载,后面当本地 yum 源用

磁盘为什么给 25 GB :项目一的 db/web 节点 20 GB 够用,但项目二的节点要存大量容器镜像,20 GB 会紧张。精简置备下给 25 GB 实际占用不到 5 GB,多给不吃亏。这样一份母盘两个项目通用,不用为项目二重建。

1.3.2 新建虚拟机向导(逐步对照)

打开 VMware Workstation,按 Ctrl + N 打开新建向导,选**「自定义(高级)」**,然后逐步对照下表。

向导步骤 选择 / 填写 说明
1. 欢迎 自定义(高级) 典型模式不能改固件类型,也不能加第二块网卡
2. 硬件兼容性 Workstation 17.x 与软件版本一致
3. 安装客户机操作系统 稍后安装操作系统 先建空壳,再手动挂 ISO。选「安装程序光盘映像文件」会走简易安装,把分区和软件选择都替你决定,反而容易出问题
4. 客户机操作系统 Linux → Red Hat Enterprise Linux 9 64 位
5. 命名虚拟机 名称 base-rhel9.8;位置 D:\vm\base-rhel9.8 位置必须手动改,默认在 C 盘
6. 固件类型 UEFI ,并取消勾选「安全引导」 见 1.3.1 表
7. 处理器配置 处理器数量 1,每个处理器的内核数量 2 合计 2 vCPU
8. 此虚拟机的内存 2048 MB
9. 网络类型 使用网络地址转换(NAT) 第一块网卡先走 NAT,第二块稍后加
10. I/O 控制器类型 LSI Logic(推荐) 默认即可
11. 磁盘类型 SCSI(推荐) 默认即可
12. 选择磁盘 创建新虚拟磁盘
13. 指定磁盘容量 最大磁盘大小 25 GB;勾选「将虚拟磁盘拆分成多个文件」 不要勾选「立即分配所有磁盘空间」------那会一次性吃满 25 GB 真实空间
14. 指定磁盘文件 D:\vm\base-rhel9.8\base-rhel9.8.vmdk 默认即可
15. 已准备好创建虚拟机 点**「自定义硬件」**,见下 这一步别急着点「完成」

在「自定义硬件」窗口里补三件事:

  1. 加第二块网卡 :选中左侧「网络适配器」→ 点「添加」→ 硬件类型选「网络适配器」→ 完成 → 右侧新网卡的「网络连接」选 仅主机模式。
  2. 挂载安装镜像 :选中「新 CD/DVD (SATA)」→ 右侧选「使用 ISO 映像文件」→ 浏览到 D:\iso\rhel-9.8-x86_64-dvd.iso → 勾选「启动时连接」。
  3. 删掉不需要的设备:若向导自动加了「USB 控制器」「声卡」「打印机」,可以选中后「移除」------虚拟机用不到,还能省点资源。

点「关闭」回到向导,再点「完成」。左侧列表里出现 base-rhel9.8。

1.3.3 安装 RHEL 9.8 操作系统

从这一步开始,操作都在虚拟机 base-rhel9.8 内进行,不再是宿主机。

选中虚拟机,点「开启此虚拟机」。以下是安装界面的关键选择。

第 1 步:引导菜单

UEFI 引导会出现菜单,选 Install Red Hat Enterprise Linux 9.8,回车。

第 2 步:语言

建议选英文 (English)。报错信息好搜索,也更贴合生产环境。

第 3 步:安装信息摘要(Installation Summary)

这是全部关键配置所在,逐个进去设置:

项目 设置 说明
键盘 English (US)
语言支持 英文 不用加中文,省空间
时间和日期 时区 Asia/Shanghai 有外网时可开启网络时间
安装源 默认 Local media(自动识别挂载的 ISO) 若显示红色感叹号,重新选一次 ISO 路径
软件选择 最小安装(Minimal Install) 关键:不要选带 GUI 的,能省 3 ~ 4 GB 磁盘和大量内存
安装目的地 自动分区(Automatic),勾选磁盘 自动分区会创建 /boot/efi、/boot、/、swap
Kdump 关闭(取消「启用 kdump」) 省内存。1 GB 内存的节点开启 kdump 会直接吃光
网络和主机名 打开两块网卡;主机名填 base-rhel9.8 网卡名此时可能是 ens160/ens192,后面基线会统一改回 eth0/eth1
安全策略 不选 实验环境不需要
root 密码 设为 lee 与课程笔记统一,方便记忆
创建用户 可以不建(全程 root 操作) 建了也不影响

手动分区的话要注意 :UEFI 下自动分区会切出一个约 600 MB 的 /boot/efi(EFI 系统分区)。如果你想手动分区,必须保留这个 EFI 分区,否则装完无法引导 。推荐方案:/boot/efi 600 MB ~ 1 GB + /boot 1 GB + / 20 GB + swap 2 GB(swap 后面会关掉,也可以只给 1 GB)。

第 4 步:开始安装

点「开始安装」,等待 5 ~ 15 分钟(取决于宿主机磁盘速度)。完成后点「重启系统」。

重启时若卡在 Please remove the installation medium, then press ENTER,直接回车即可(ISO 还挂在光驱上是正常的,后面要用)。

1.3.4 安装后的首次自检

第一次登录:用户名 root,密码 lee。

逐条执行,确认母盘基本可用:

bash 复制代码
# 1. 确认系统版本 ------ 写简历要用这个版本号
cat /etc/redhat-release
# Red Hat Enterprise Linux release 9.8 (Ootpa)

# 2. 确认内核版本
uname -r
# 5.14.0-687.x.el9_8.x86_64

# 3. 确认两块网卡都在(名字可能还是 ens160/ens192,正常,后面基线统一改)
ip -br link
# lo      UNKNOWN  00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
# ens160  UP       00:0c:29:xx:xx:xx <BROADCAST,MULTICAST,UP,LOWER_UP>
# ens192  UP       00:0c:29:xx:xx:xx <BROADCAST,MULTICAST,UP,LOWER_UP>

# 4. 确认光驱被识别(后面要当本地 yum 源用)
lsblk
# 应该能看到 sr0

# 5. 确认能解析域名(NAT 网卡通外网)
ping -c 2 www.baidu.com

排错方向:

  • 第 3 步只看到一块网卡 → 回 VMware「虚拟机设置」确认第二块网卡存在且为「仅主机模式」。
  • 第 5 步不通 → 检查 NAT 网卡网关(ip route)和 DNS(cat /etc/resolv.conf)。

安装虚拟机工具(推荐):

bash 复制代码
dnf install -y open-vm-tools
systemctl enable --now vmtoolsd

装完之后宿主机与虚拟机之间可以复制粘贴文本、鼠标无缝切换,操作舒服很多。

给刚装好的干净系统打第一个快照:

VMware 菜单 虚拟机 → 快照 → 拍摄快照,名称填 v1-installed-2026。

这个快照的作用:万一后面基线配置改废了,可以一键退回「刚装完系统」的状态,不用重装。做完基线配置、打上正式快照后要把它删掉------快照会持续占用磁盘,也拖慢虚拟机性能。

1.3.5 关机

bash 复制代码
poweroff

两个注意:不要用「挂起」代替关机;不要在开机状态给虚拟机改内存/CPU(改不了)。要改硬件配置必须先彻底关机。

1.4 母盘基线配置

执行主机 :base-rhel9.8(母盘)

以下 9 步是所有虚拟机的共同基线。做完之后打快照,之后所有克隆机继承这些配置,省掉大量重复工作。

1.4.1 把网卡名固定为 eth0 / eth1

RHEL9 默认用 ens160 / ens192 这种可预测命名,双网卡环境下很容易搞混。统一改回 eth0 / eth1。

bash 复制代码
# 查看当前网卡名
ip -br link

# 关闭可预测网卡命名
grubby --update-kernel ALL --args net.ifnames=0

# 重建 grub 配置并重启
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot

重启后验证:

bash 复制代码
ip -br link
# lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
# eth0             UP             00:0c:29:xx:xx:xx <BROADCAST,MULTICAST,UP,LOWER_UP>
# eth1             UP             00:0c:29:xx:xx:xx <BROADCAST,MULTICAST,UP,LOWER_UP>

关键点:eth0 一定是网卡1(NAT) ,eth1 一定是网卡2(仅主机) 。用 nmcli device show eth0 看连接配置,或者对比 VMware 里的 MAC 地址确认。如果顺序反了,在 VMware 里调整网卡顺序即可。

1.4.2 关闭 SELinux

bash 复制代码
vim /etc/sysconfig/selinux
# 修改为:
SELINUX=disabled

grubby --update-kernel ALL --args selinux=0
reboot

验证:

bash 复制代码
getenforce
# Disabled

面试注意:企业生产环境不建议 关闭 SELinux。这里的理由是实验环境反复调目录权限会大量消耗时间。面试如果被问到,正确答法是「实验环境关闭以便聚焦运维链路,生产环境会保留 SELinux 并用 restorecon / semanage 做策略放行」。这句话要背下来。

1.4.3 关闭并屏蔽 firewalld

本项目用数据面网络隔离 + Keepalived做访问控制。为了减少变量先关掉 firewalld,文档 3.7 节会专门做一次「用 firewalld 富规则放行业务端口」的补充实验,让你不至于丢掉这项技能。

bash 复制代码
systemctl disable --now firewalld
systemctl mask firewalld

1.4.4 配置本地 yum 源(离线可用)

母盘光驱里就是 RHEL9.8 的完整 RPM 仓库,用它做源最稳,不受网络影响。

bash 复制代码
mkdir -p /rhel9.8
mount /dev/cdrom /rhel9.8/
# mount: /rhel9.8: WARNING: source write-protected, mounted read-only.

# 开机自动挂载
echo "mount /dev/cdrom /rhel9.8/" >> /etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local

ls /rhel9.8/
# AppStream  BaseOS  EFI  EULA  GPL  images  isolinux  media.repo  RPM-GPG-KEY-*

写仓库文件:

bash 复制代码
cd /etc/yum.repos.d/
# 清掉可能存在的无效源
mkdir -p /root/repo.bak && mv *.repo /root/repo.bak/ 2>/dev/null

vim rhel9.repo
ini 复制代码
[AppStream]
name = AppStream
baseurl = file:///rhel9.8/AppStream
gpgcheck = 0

[BaseOS]
name = BaseOS
baseurl = file:///rhel9.8/BaseOS
gpgcheck = 0

验证:

bash 复制代码
dnf clean all
dnf repolist
dnf list gcc
# gcc.x86_64    11.5.0-14.el9    AppStream

1.4.5 配置 EPEL 源

EPEL 是网络源,需要能上网。母盘要测一次 NAT 是否通。

bash 复制代码
ping -c 2 172.25.254.2      # 通网关
ping -c 2 www.baidu.com     # 通公网

如果网关通但公网不通,检查 VMware 的 NAT 设置和「NAT 设置」里的网关地址。

bash 复制代码
vim /etc/yum.repos.d/epel.repo
ini 复制代码
[epel]
name = epel
baseurl = https://mirrors.aliyun.com/epel/9/Everything/x86_64/
gpgcheck = 0
bash 复制代码
dnf makecache
dnf repolist
dnf list ansible
# ansible.noarch   1:7.7.0-1.el9   epel

1.4.6 安装运维常用工具链

bash 复制代码
dnf install -y \
  vim wget curl lsof net-tools telnet bind-utils tree unzip zip \
  htop iotop sysstat tcpdump nmap-ncat bash-completion \
  httpd-tools socat rsync chrony

# 编译工具链(后面 Nginx / Redis 源码编译要用)
dnf install -y gcc gcc-c++ make openssl-devel pcre2-devel zlib-devel \
  pcre-devel automake autoconf libtool

1.4.7 部署 vmset.sh 网络配置脚本

这是你笔记里的脚本,非常好用,直接复用。它一次性完成:主机名、/etc/hosts、IP、网关、DNS。

bash 复制代码
cat > /bin/vmset.sh <<'EOF'
#!/bin/bash
[ "$#" -lt "3" ] && {
  echo "error: Please input device deviceip and hostname following $0"
  exit 1
}
ifconfig $1 &> /dev/null || {
  echo $1 is not exist
  exit 2
}
CONNECTION=`nmcli connection show | awk "/$1/"'{print $1}'`
[ -n "$CONNECTION" ] && {
   nmcli connection delete $CONNECTION
}
sleep 1

hostnamectl hostname $3
cat > /etc/hosts <<HOSTEOF
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
$2      $3
HOSTEOF

if [ "$4" != "noroute" ]
then
nmcli connection add type ethernet ifname $1 con-name $1 ipv4.method manual ipv4.addresses $2/24 ipv4.dns 8.8.8.8 ipv4.gateway 172.25.254.2
nmcli connection reload
nmcli connection up $1
else
nmcli connection add type ethernet ifname $1 con-name $1 ipv4.method manual ipv4.addresses $2/24
nmcli connection reload
nmcli connection up $1
fi

ip addr show $1
hostname
EOF

chmod +x /bin/vmset.sh

用法:

bash 复制代码
vmset.sh eth0 172.25.254.50 lb1          # 带网关(业务面用这个)
vmset.sh eth1 192.168.0.50 lb1 noroute   # 不带网关(数据面用这个)

为什么数据面不配网关?一台机器只能有一条默认路由。业务面(NAT)负责出公网,数据面(仅主机)只做内网互通,不需要默认路由。这是双网卡的标准做法,面试常问。

1.4.8 关闭 swap

MySQL 和后续 K8s 都要求关闭 swap。开着 swap 会导致数据库出现不可控的响应延迟抖动。

bash 复制代码
systemctl disable --now swap.target
systemctl mask swap.target
sed -i '/swap/s/^/#/' /etc/fstab
swapon -s        # 无输出即为关闭成功
free -h

1.4.9 实现所有克隆机互相免密

这是你笔记里的绝招,用来一次性解决全网免密。

bash 复制代码
# 1. 在母盘生成密钥对(一路回车)
ssh-keygen

# 2. 先把公钥装到本机的 root 授权
ssh-copy-id -i /root/.ssh/id_rsa.pub root@localhost

# 3. 关键一步:把 .ssh 目录放进用户骨架目录
cp -rp /root/.ssh/ /etc/skel/

原理:/etc/skel/ 是新用户家目录的模板。放进它之后,母盘克隆出去的每一台机器、每一个新建用户的 home 目录都会自动带上这对密钥和 authorized_keys,从而天然实现全网免密。

验证(母盘上):

bash 复制代码
ssh root@localhost hostname
# 不需要密码,直接返回主机名

安全提示:这会把私钥分发到所有克隆机的新用户家目录,实验环境很方便,生产环境绝不能这么做。面试可以主动提这个对比,是加分项。

1.5 母盘统计与打快照

执行主机 :base-rhel9.8(母盘)+ 宿主机 Windows(VMware 快照操作)

1.5.1 记录基线信息

bash 复制代码
# 记录基线版本信息,写简历要用
cat /etc/redhat-release
uname -r
df -h /

记录:Red Hat Enterprise Linux 9.8,内核 5.14.0-687.x.el9_8.x86_64。

1.5.2 拍摄正式快照

先关机------快照记录的是关机状态,后续克隆最稳定:

bash 复制代码
poweroff

在宿主机 VMware 菜单 虚拟机 → 快照 → 拍摄快照:

字段 填写
名称 base-clean-2026
描述 RHEL9.8 minimal + 基线9步 + 免密 + 本地源,克隆源

点「拍摄快照」。左侧虚拟机列表里这台的快照树会多出一个节点。

1.5.3 验证快照可用(别跳过)

拍完快照要实际回滚一次确认它真的能用------很多人做到第七章才发现克隆源快照是坏的,只能从头重来。

VMware 菜单 虚拟机 → 快照 → 快照管理器 → 选中 base-clean-2026 → 点「转到」→ 确认回滚。

回滚后开机确认基线还在:

bash 复制代码
cat /etc/redhat-release      # 版本正常
ip -br link                  # 应该是 eth0 / eth1,不是 ens160
sestatus                    # 应该是 disabled

1.5.4 清理中间快照

v1-installed-2026(1.3.4 拍的那个)已经不需要了------它记录的是「刚装完系统、基线还没配」的状态,而 base-clean-2026 才是真正的克隆源。

在快照管理器里选中 v1-installed-2026 → 点「删除」,提示合并磁盘时确认。

为什么不保留多个快照 :快照不是备份,它记录的是磁盘增量差异。每多一个快照,VMware 写入时就要多维护一层 delta 文件------虚拟机越跑越慢,磁盘占用也持续增长。

快照纪律:功能验证完立刻删,只保留一个确实要用的最新快照。这条纪律在后面做故障演练时会反复用到。

再次关机:

bash 复制代码
poweroff

母盘到此完成。今后不要直接启动母盘做实验,它只作为克隆源。母盘越干净,克隆出来的机器越可靠。


第二章 批量克隆与主机初始化

本章操作主机 :宿主机 Windows(克隆)→ 7 台克隆机(初始化)

2.1 克隆 7 台虚拟机

执行主机 :宿主机 Windows(VMware 克隆操作)

母盘已经打好快照 base-clean-2026,现在用链接克隆批量生出 7 台。

为什么必须用链接克隆 :7 台机器如果都用「完整克隆」,每台独占 20 ~ 25 GB,合计 140 ~ 175 GB,D 盘装不下。链接克隆共享母盘的只读磁盘块,每台增量只有几百 MB,7 台合计约 5 ~ 8 GB。

克隆向导(每台走一遍,共 7 次):

向导步骤 选择 / 填写 说明
0. 准备 先关闭母盘电源 克隆要求源虚拟机处于关机或快照状态
1. 入口 右键 base-rhel9.8 → 管理 → 克隆
2. 克隆来源 虚拟机中的当前快照 → 选 base-clean-2026 关键:从快照克隆,MAC 地址才会重新生成
3. 克隆类型 创建链接克隆 界面会提示"链接克隆依赖于父虚拟机"
4. 名称和位置 见下表,位置都在 D:\vm\ 下
5. 完成 点「完成」,等 10 ~ 30 秒 链接克隆很快,不复制磁盘数据

必须记住的一件事 :链接克隆依赖母盘文件。母盘 D:\vm\base-rhel9.8 和整个 D:\vm 目录以后都不能移动、重命名或删除,否则 7 台克隆机全部无法开机。要迁移就把整个目录一起搬。

依次克隆 7 台,名称和位置如下。

序号 名称 位置
1 lb1 D:\vm\proj1-lb1
2 lb2 D:\vm\proj1-lb2
3 web1 D:\vm\proj1-web1
4 web2 D:\vm\proj1-web2
5 db1 D:\vm\proj1-db1
6 db2 D:\vm\proj1-db2
7 infra D:\vm\proj1-infra

克隆完成后,逐台按总览表格调整 vCPU 与内存(在虚拟机关机状态下改)。

主机 vCPU 内存
lb1 / lb2 1 768 MB
web1 / web2 2 1536 MB
db1 / db2 1 1024 MB
infra 1 1024 MB

如果 VMware 不允许设置 768 MB,用 1024 MB 也行,只是总内存会到 8.5 GB。内存实在紧张时把 lb2 平时挂起。

2.2 逐台配置网络与主机名

执行主机 :lb1、lb2、web1、web2、db1、db2、infra(7 台依次执行)

每台机器都要做 。以 lb1 为例(开机后用 root 登录):

bash 复制代码
# 业务面:NAT,带网关
vmset.sh eth0 172.25.254.50 lb1

# 数据面:仅主机,不带网关
vmset.sh eth1 192.168.0.50 lb1 noroute

其余 6 台对照下表执行。

主机 业务面命令 数据面命令
lb1 vmset.sh eth0 172.25.254.50 lb1 vmset.sh eth1 192.168.0.50 lb1 noroute
lb2 vmset.sh eth0 172.25.254.60 lb2 vmset.sh eth1 192.168.0.60 lb2 noroute
web1 vmset.sh eth0 172.25.254.10 web1 vmset.sh eth1 192.168.0.10 web1 noroute
web2 vmset.sh eth0 172.25.254.20 web2 vmset.sh eth1 192.168.0.20 web2 noroute
db1 vmset.sh eth0 172.25.254.30 db1 vmset.sh eth1 192.168.0.30 db1 noroute
db2 vmset.sh eth0 172.25.254.40 db2 vmset.sh eth1 192.168.0.40 db2 noroute
infra vmset.sh eth0 172.25.254.100 infra vmset.sh eth1 192.168.0.100 infra noroute

确认 IP 生效:

bash 复制代码
# 在每台上执行
ip -br a
# eth0  UP  172.25.254.50/24
# eth1  UP  192.168.0.50/24

vmset.sh 会把 /etc/hosts 重置为只包含自己。所以下一步要统一写全局 hosts。

2.3 统一的 hosts 文件

执行主机 :lb1(生成并分发)→ 其余 6 台(接收)

在 lb1 上生成一份完整 hosts,然后全网分发:

bash 复制代码
cat > /etc/hosts <<'EOF'
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

# ===== 项目一:业务面 NAT 172.25.254.0/24 =====
172.25.254.50    lb1        lb1.timinglee.org
172.25.254.60    lb2        lb2.timinglee.org
172.25.254.10    web1       web1.timinglee.org
172.25.254.20    web2       web2.timinglee.org
172.25.254.30    db1        db1.timinglee.org
172.25.254.40    db2        db2.timinglee.org
172.25.254.100   infra      infra.timinglee.org dns.timinglee.org

# ===== 项目一:数据面 仅主机 192.168.0.0/24 =====
192.168.0.50     lb1-lan
192.168.0.60     lb2-lan
192.168.0.10     web1-lan
192.168.0.20     web2-lan
192.168.0.30     db1-lan
192.168.0.40     db2-lan
192.168.0.100    infra-lan

# ===== VIP =====
172.25.254.200   www.timinglee.org app.timinglee.org api.timinglee.org
172.25.254.201   db-vip.timinglee.org
172.25.254.202   status.timinglee.org
EOF

分发:

bash 复制代码
for i in 60 10 20 30 40 100
do
  scp /etc/hosts root@172.25.254.$i:/etc/hosts
done

验证免密与连通性(在 lb1 上):

bash 复制代码
for i in 10 20 30 40 50 60 100; do
  echo -n "$i: "; ssh -o StrictHostKeyChecking=no root@172.25.254.$i hostname
done

期望输出:

复制代码
10: web1
20: web2
30: db1
40: db2
50: lb1
60: lb2
100: infra

如果某一台要求输密码,说明该机的 /etc/skel 继承没生效。补救方法:在母盘上重新 ssh-keygen + cp -rp /root/.ssh/ /etc/skel/,或者直接 ssh-copy-id 一次到那台机器。

2.4 数据面连通性验证

执行主机 :lb1(发起验证)

bash 复制代码
# 从 lb1 逐台 ping 数据面
for i in 10 20 30 40 50 60 100; do
  ping -c1 -W1 192.168.0.$i > /dev/null && echo "192.168.0.$i OK" || echo "192.168.0.$i FAIL"
done

期望全部 OK。数据面不通的常见原因:

  • 仅主机网络的 DHCP 还在跑,分了一个同网段地址导致冲突 → 关掉 VMnet1 的 DHCP。
  • Windows 上的 VMware Network Adapter VMnet1 网卡被禁用 → 在网络连接里启用。
  • vmset.sh 的第四参数写错了(noroute 敲成了 norouter)→ 检查 ip route,会发现多了一条默认路由。

打快照 :给 7 台都打一个 stage1-network-ok 快照。后面任何一步搞坏,回滚到这个点比重新配置快得多。


第三章 infra 节点:基础服务

本章操作主机 :infra(服务端)为主,web1 / web2 / db2 作为客户端参与

infra(172.25.254.100 / 192.168.0.100)承担五件事:时间同步、NFS 共享存储、集中日志、DNS、Redis 缓存主节点。

为什么把这些放一台?它们在真实企业里通常也是独立的「基础服务机」,而且共同特征是负载极低,合并到一台虚拟机不浪费内存。

3.1 时间同步服务器(chrony)

执行主机 :infra(时间服务器)→ 其余 6 台(客户端)

VRRP、MySQL 主从、K8s 证书全都对时间一致性敏感。必须先把时间统一。

bash 复制代码
dnf install -y chrony

vim /etc/chrony.conf

服务端配置(关键两行):

ini 复制代码
# 注释掉默认的公网源: pool 2.rhel.pool.ntp.org iburst

# 允许所有客户端同步
allow 0.0.0.0/0

# 即便本机未同步到上游,也对外提供时间(内网无外网时的关键)
local stratum 10

# 上游时间源(有外网时保留)
pool ntp.aliyun.com iburst
bash 复制代码
systemctl enable --now chronyd
systemctl restart chronyd

# 验证
chronyc sources -v
chronyc tracking

客户端节点(除 infra 外的 6 台)修改:

bash 复制代码
vim /etc/chrony.conf
# 注释 pool 行,改成:
pool 192.168.0.100 iburst

批量执行(在 infra 上一次性推送):

bash 复制代码
for i in 50 60 10 20 30 40
do
ssh root@172.25.254.$i "sed -i 's|^pool .*|# &|' /etc/chrony.conf; \
  grep -q '^pool 192.168.0.100' /etc/chrony.conf || echo 'pool 192.168.0.100 iburst' >> /etc/chrony.conf; \
  systemctl restart chronyd; sleep 1; chronyc sources -v | tail -3"
done

验证:

bash 复制代码
ssh root@172.25.254.10 "chronyc sources -v | grep '\^*'"
# ^* infra-lan    2   6   377   12   +1234ns[  +56us] +/-   ..ms

时间同步没做好,后面 Keepalived 会出现「两台都在 MASTER」的脑裂现象,MySQL 主从会报错。这一步别跳过。

3.2 NFS 共享存储

执行主机 :infra(NFS 服务端)→ web1、web2(客户端)

要解决的问题:web1 / web2 的静态资源、用户上传目录必须是同一份。否则用户在 web1 上传了文件,请求被负载均衡分到 web2 时就读不到。这是 Web 集群必须解决的经典问题。

3.2.1 服务端(infra)

bash 复制代码
dnf install -y nfs-utils rpcbind
systemctl enable --now nfs-server

# 创建共享目录结构
mkdir -p /nfs/web/static
mkdir -p /nfs/web/upload
mkdir -p /nfs/web/app

# 权限:让 web 节点上的 nginx / tomcat 用户能写
chmod 2777 /nfs/web/static /nfs/web/upload /nfs/web/app

vim /etc/exports
ini 复制代码
/nfs/web/static   192.168.0.0/24(rw,sync,no_root_squash,no_subtree_check)
/nfs/web/upload   192.168.0.0/24(rw,sync,no_root_squash,no_subtree_check)
/nfs/web/app      192.168.0.0/24(rw,sync,no_root_squash,no_subtree_check)
bash 复制代码
exportfs -rv
# exporting 192.168.0.0/24:/nfs/web/app
# exporting 192.168.0.0/24:/nfs/web/static
# exporting 192.168.0.0/24:/nfs/web/upload

# 验证
showmount -e 192.168.0.100

参数解释(面试会问):

  • rw 读写;sync 同步写(数据安全,性能略低);async 异步写(快,宕机可能丢数据)。
  • no_root_squash 允许客户端 root 保持 root 权限,方便运维,但生产环境有安全风险 ,更严谨的做法是 root_squash + 匹配 UID。
  • no_subtree_check 关闭子树检查,避免文件重命名时出现 stale file handle 报错。
  • 只导出给 192.168.0.0/24,不导出给业务网段------这就是双网卡隔离的价值。

3.2.2 客户端(web1 / web2)

bash 复制代码
dnf install -y nfs-utils
showmount -e 192.168.0.100

mkdir -p /data/nfs/{static,upload,app}
mount 192.168.0.100:/nfs/web/static /data/nfs/static
mount 192.168.0.100:/nfs/web/upload /data/nfs/upload
mount 192.168.0.100:/nfs/web/app    /data/nfs/app

写入 /etc/fstab 实现开机自动挂载:

bash 复制代码
cat >> /etc/fstab <<'EOF'
192.168.0.100:/nfs/web/static  /data/nfs/static  nfs  defaults,_netdev 0 0
192.168.0.100:/nfs/web/upload  /data/nfs/upload  nfs  defaults,_netdev 0 0
192.168.0.100:/nfs/web/app     /data/nfs/app     nfs  defaults,_netdev 0 0
EOF

mount -a
df -h | grep nfs

验证跨机共享:

bash 复制代码
# 在 web1 写
echo "hello from web1 $(date)" > /data/nfs/upload/test.txt

# 在 web2 读
cat /data/nfs/upload/test.txt
# hello from web1 2026-09-13 ...

这就是共享存储生效的证明,截图保存。

3.2.3 进阶:用 autofs 实现按需自动挂载(加分项)

/etc/fstab 挂载的问题:NFS 服务端宕机时客户端会卡在启动阶段,_netdev 只能部分缓解。autofs 解决得更彻底。

bash 复制代码
dnf install -y autofs

vim /etc/auto.master
# 追加一行
/-    /etc/auto.nfs

vim /etc/auto.nfs
/nfs/static  -rw,soft,intr  192.168.0.100:/nfs/web/static
/nfs/upload  -rw,soft,intr  192.168.0.100:/nfs/web/upload
/nfs/app     -rw,soft,intr  192.168.0.100:/nfs/web/app

systemctl enable --now autofs
ls /nfs/static      # 第一次访问时自动挂载
mount | grep nfs
# 闲置 300 秒后自动卸载

autofs 的价值:按需挂载、空闲卸载,NFS 服务端故障时不会阻塞客户端。-soft 参数让 IO 超时后返回错误而不是无限等待,避免应用线程被卡死。

3.3 集中日志服务器(rsyslog)

执行主机 :infra(日志服务器)→ 其余 6 台(客户端)

作用:所有节点的日志统一汇总到 infra,一台机器就能排查整个集群。这是真实运维最常用的手段。

3.3.1 服务端(infra)

bash 复制代码
vim /etc/rsyslog.conf

添加/取消注释:

复制代码
# 加载 UDP 接收模块
module(load="imudp")
input(type="imudp" port="514")

# 也支持 TCP(更可靠,不丢日志)
module(load="imtcp")
input(type="imtcp" port="514")

# 按来源 IP 分目录存放
$template RemoteLogs,"/var/log/remote/%FROMHOST-IP%/%programname%.log"
*.* ?RemoteLogs
& stop
bash 复制代码
systemctl enable --now rsyslog
systemctl restart rsyslog

# 验证监听
ss -antlupe | grep 514
# udp  UNCONN 0 0 0.0.0.0:514
# tcp  LISTEN 0 25 0.0.0.0:514

3.3.2 客户端(其余 6 台)

bash 复制代码
cat >> /etc/rsyslog.conf <<'EOF'

# 发送所有日志到 infra(@@ 表示 TCP,@ 表示 UDP)
*.* @@192.168.0.100:514
EOF

批量下发(在 infra 上):

bash 复制代码
for i in 50 60 10 20 30 40
do
ssh root@172.25.254.$i "grep -q '192.168.0.100:514' /etc/rsyslog.conf || echo '*.* @@192.168.0.100:514' >> /etc/rsyslog.conf; systemctl restart rsyslog; echo done $i"
done

@ = UDP(快但可能丢),@@ = TCP(可靠)。集中日志用 TCP 更合适,这是常见面试题。

3.3.3 验证

bash 复制代码
# 在 web1 上造一条日志
ssh root@172.25.254.10 "logger 'TEST-MSG-FROM-WEB1'"

# 在 infra 上查
ls /var/log/remote/
# 192.168.0.10  192.168.0.20 ...

grep -r "TEST-MSG-FROM-WEB1" /var/log/remote/
# /var/log/remote/192.168.0.10/root.log:... TEST-MSG-FROM-WEB1

目录名会是 192.168.0.10(客户端用数据面网卡发过去的源 IP)。这正是我们希望看到的------日志走数据面,不占业务面带宽。

3.3.4 日志轮转(防止撑爆磁盘)

bash 复制代码
cat > /etc/logrotate.d/timinglee-remote <<'EOF'
/var/log/remote/*/*.log {
    daily
    rotate 7
    missingok
    notifempty
    compress
    delaycompress
    copytruncate
}
EOF

copytruncate 是应对 rsyslog 持有文件句柄的关键参数,不加会轮转失败。

3.4 DNS 服务器(bind)

执行主机 :infra

作用 :让集群内部可以用域名访问而不是记 IP,也让 www.timinglee.org 这类域名在集群内正式可解析。

3.4.1 安装与主配置

bash 复制代码
dnf install -y bind bind-utils
systemctl enable --now named

vim /etc/named.conf

修改 options 段的关键项:

复制代码
options {
        listen-on port 53 { any; };
        listen-on-v6 port 53 { ::1; };
        directory       "/var/named";
        allow-query     { any; };
        recursion yes;
        forwarders      { 114.114.114.114; 8.8.8.8; };
        dnssec-validation no;
};

3.4.2 正向解析区域

bash 复制代码
vim /etc/named.rfc1912.zones
# 追加到文件末尾
zone "timinglee.org" IN {
        type master;
        file "timinglee.org.zone";
        allow-update { none; };
};
bash 复制代码
cd /var/named/
cp -p named.localhost timinglee.org.zone
vim timinglee.org.zone
复制代码
$TTL 1D
@       IN SOA  dns.timinglee.org.  admin.timinglee.org. (
                                        2026091301   ; serial
                                        1D           ; refresh
                                        1H           ; retry
                                        1W           ; expire
                                        3H )         ; minimum
        NS      dns.timinglee.org.
dns     A       172.25.254.100
www     A       172.25.254.200
app     A       172.25.254.200
api     A       172.25.254.200
status  A       172.25.254.202
db-vip  A       172.25.254.201
web1    A       172.25.254.10
web2    A       172.25.254.20
db1     A       172.25.254.30
db2     A       172.25.254.40
lb1     A       172.25.254.50
lb2     A       172.25.254.60

serial 每次改完 zone 文件都要递增(这里用了日期 + 序号 2026091301),否则从库不会同步。这是最经典的踩坑点。

bash 复制代码
chown root:named /var/named/timinglee.org.zone
named-checkzone timinglee.org /var/named/timinglee.org.zone
# zone timinglee.org/IN: loaded serial 2026091301
# OK

systemctl restart named

3.4.3 验证

bash 复制代码
dig www.timinglee.org @172.25.254.100 +short
# 172.25.254.200

dig app.timinglee.org @192.168.0.100 +short
# 172.25.254.200

# 反向验证:查一个不存在的域名
dig notexist.timinglee.org @172.25.254.100 +short
# (空)

3.4.4 让所有节点使用这个 DNS

注意 :vmset.sh 把 DNS 直接写进了 NetworkManager 连接配置(ipv4.dns 8.8.8.8),所以直接改 /etc/resolv.conf 会被 NetworkManager 覆盖。正确做法是改连接配置:

bash 复制代码
# 在 infra 上批量推送
for i in 50 60 10 20 30 40
do
ssh root@172.25.254.$i "nmcli connection modify eth0 ipv4.dns '192.168.0.100 8.8.8.8'; nmcli connection up eth0; sleep 1; cat /etc/resolv.conf | grep nameserver"
done

验证:

bash 复制代码
ssh root@172.25.254.10 "dig www.timinglee.org +short"
# 172.25.254.200

3.4.5 多向解析(view,进阶)

企业内网中,「内网客户端解析到内网地址,外网客户端解析到公网地址」是一个常见需求。bind 用 view 实现。

bash 复制代码
vim /etc/named.conf

把原有的 zone "." 和 include "/etc/named.rfc1912.zones"; 注释掉,改成:

复制代码
view "lan" {
    match-clients { 192.168.0.0/24; };
    recursion yes;
    zone "." IN {
        type hint;
        file "named.ca";
    };
    include "/etc/named.rfc1912.lan";
};

view "wan" {
    match-clients { any; };
    recursion yes;
    zone "." IN {
        type hint;
        file "named.ca";
    };
    include "/etc/named.rfc1912.zones";
};

include "/etc/named.root.key";
bash 复制代码
cp -p /etc/named.rfc1912.zones /etc/named.rfc1912.lan
# 在 lan 版本的 zone 文件里把 www 指向内网地址
vim /var/named/timinglee.org.lan.zone
# www   A   192.168.0.50

view 必须定义在最外层,两个 view 的 match-clients 不能重叠(否则第一条匹配生效),并且 zone "." 的 hint 必须在每个 view 里都定义一份。这是踩坑点。

3.5 Redis 缓存主节点

执行主机 :infra(Redis 主)+ web2(Redis 从)+ infra / web2 / db2(Sentinel 哨兵)

作用:作为业务数据缓存(首页热门数据、商品详情等),减轻 MySQL 压力。

Redis 7.x 源码编译,编译约 1 分钟,资源占用低。

3.5.1 源码编译安装

bash 复制代码
dnf install -y make gcc initscripts tcl
cd /root
wget https://download.redis.io/releases/redis-7.4.8.tar.gz
tar zxf redis-7.4.8.tar.gz
cd redis-7.4.8/

make -j2
make install
redis-server --version
# Redis server v=7.4.8

3.5.2 安装成系统服务

bash 复制代码
cd utils/
vim install_server.sh

必须注释掉 systemd 检测块 (否则脚本直接 exit 1):

bash 复制代码
#bail if this system is managed by systemd
#_pid_1_exe="$(readlink -f /proc/1/exe)"
#if [ "${_pid_1_exe##*/}" = systemd ]
#then
#       echo "This systems seems to use systemd."
#       echo "Please take a look at the provided example service unit files in this directory, and adapt them. Sorry."
#       exit 1
#fi
bash 复制代码
./install_server.sh
# Please select the redis port for this instance: [6379]            → 回车
# Please select the redis config file name: [/etc/redis/6379.conf]  → 回车
# Please select the redis log file name: [/var/log/redis_6379.log]  → 回车
# Please select the data directory: [/var/lib/redis/6379]           → 回车
# Please select the redis executable path: [/usr/local/bin/redis-server] → 回车

systemctl daemon-reload
systemctl start redis_6379
systemctl enable redis_6379

3.5.3 生产化配置

bash 复制代码
vim /etc/redis/6379.conf
ini 复制代码
# 注释掉默认只监听本地
#bind 127.0.0.1 -::1
bind 0.0.0.0

# 关闭保护模式(因为已改 bind)
protected-mode no

# 设置密码
requirepass lee

# 主从认证密码(从节点需要)
masterauth lee

# 最大内存限制,配合淘汰策略(小内存环境必须设)
maxmemory 128mb
maxmemory-policy allkeys-lru

# 降低持久化频率(实验环境减少 IO)
save 900 1
appendonly no

# 管理命令重命名(安全加固,防止误操作)
rename-command FLUSHALL ""
rename-command FLUSHDB  ""
bash 复制代码
systemctl restart redis_6379

# 验证
redis-cli -a lee ping
# PONG     (会有一条密码明文警告,正常)

redis-cli -a lee info replication
# role:master
# connected_slaves:0

内存警告处理:Redis 会提示 overcommit_memory 建议设为 1。执行:

bash 复制代码
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf
sysctl -p

rename-command 的意义:把 FLUSHALL、CONFIG 这类高危命令改名或禁用,防止误操作或被入侵后一键清库。这是 Redis 安全加固的标准动作,面试会问。

3.5.4 Redis 主从复制

在 web2 上用同样方式编译安装 Redis,然后配置为从节点。

bash 复制代码
vim /etc/redis/6379.conf
ini 复制代码
bind 0.0.0.0
protected-mode no
requirepass lee
masterauth lee
maxmemory 128mb
maxmemory-policy allkeys-lru

# 关键:指定主节点(走数据面)
replicaof 192.168.0.100 6379

# 从节点只读(默认就是,显式声明更清晰)
replica-read-only yes
bash 复制代码
systemctl restart redis_6379
redis-cli -a lee info replication | head -5
# role:slave
# master_host:192.168.0.100
# master_port:6379
# master_link_status:up       ← 关键

在 infra 上确认:

bash 复制代码
redis-cli -a lee info replication
# role:master
# connected_slaves:1
# slave0:ip=192.168.0.20,port=6379,state=online,offset=...,lag=0     ← 关键

数据同步验证:

bash 复制代码
# infra 上写
redis-cli -a lee set site timinglee
# OK

# web2 上读
ssh root@172.25.254.20 "redis-cli -a lee get site"
# "timinglee"

# web2 上写(应该被拒绝)
ssh root@172.25.254.20 "redis-cli -a lee set x 1"
# (error) READONLY You can't write against a read only replica.

3.5.5 Sentinel 哨兵(三节点自动故障切换)

哨兵需要 3 个 节点才能形成仲裁(quorum = 2)。部署在 infra、web2、db2 上。

准备配置文件(在 infra 上):

bash 复制代码
cp /root/redis-7.4.8/sentinel.conf /etc/redis/sentinel.conf
vim /etc/redis/sentinel.conf
ini 复制代码
bind 0.0.0.0

port 26379

daemonize yes

pidfile "/var/run/redis-sentinel.pid"

logfile "/var/log/redis-sentinel.log"

dir "/var/lib/redis/sentinel"

# 哨兵自身访问密码(生产环境建议开启,和下面ExecStop对应)
requirepass lee

# 监控主节点:mymaster 是自定义名字,2 = quorum(需要 2 个哨兵同意才判定客观下线)
sentinel monitor mymaster 192.168.0.100 6379 2

# 主节点认证密码
sentinel auth-pass mymaster lee

# 10 秒连不上判定主观下线
sentinel down-after-milliseconds mymaster 10000

# 故障转移后同时同步新主节点的从节点数
sentinel parallel-syncs mymaster 1

# 整个故障转移的超时时间(毫秒)
sentinel failover-timeout mymaster 180000

# 关闭保护模式(允许外网/跨机访问)
protected-mode no

quorum 必须是 (节点数/2) + 1。3 个哨兵 → quorum = 2。2 个哨兵 → quorum = 2,但此时任何一个挂了就无法完成切换,所以生产环境至少要 3 个哨兵。这是必问点。

分发配置:

bash 复制代码
scp /etc/redis/sentinel.conf root@192.168.0.20:/etc/redis/sentinel.conf
scp /etc/redis/sentinel.conf root@192.168.0.40:/etc/redis/sentinel.conf

三台都创建 systemd 服务:

bash 复制代码
for i in 100 20 40
do
ssh root@192.168.0.$i "cat > /lib/systemd/system/redis-sentinel.service <<'SF'
[Unit]
Description=Redis Sentinel
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/redis-sentinel /etc/redis/sentinel.conf
ExecStop=/usr/local/bin/redis-cli -p 26379 -a lee shutdown
PIDFile=/var/run/redis-sentinel.pid
TimeoutStopSec=30
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
SF
systemctl daemon-reload
systemctl enable --now redis-sentinel
sleep 1
ss -antlupe | grep 26379"
done

验证:

bash 复制代码
# 主节点信息
redis-cli -p 26379 sentinel master mymaster
# num-slaves  1
# num-other-sentinels  2
# quorum  2

# 其他哨兵
redis-cli -p 26379 sentinel sentinels mymaster

# 当前主节点
redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
# 192.168.0.100
# 6379

故障演练:

bash 复制代码
# 停掉主节点 Redis(在 infra 上)
systemctl stop redis_6379

# 观察任意一台哨兵的日志
tail -f /var/log/redis-sentinel.log
# +sdown master mymaster 192.168.0.100 6379
# +odown master mymaster 192.168.0.100 6379 #quorum 2/2
# +try-failover master mymaster 192.168.0.100 6379
# +elected-leader master mymaster 192.168.0.100 6379
# +failover-state-select-slave master mymaster 192.168.0.100 6379
# +selected-slave slave 192.168.0.20:6379
# +promoted-slave slave 192.168.0.20:6379
# +switch-master mymaster 192.168.0.100 6379 192.168.0.20 6379   ← 主节点已切换

# 确认
redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
# 192.168.0.20
# 6379

# 恢复原主(它会以从节点身份自动加入)
systemctl start redis_6379
redis-cli -a lee info replication | head -3
# role:slave

重点结论(面试必答) :Sentinel 会自动改写被降级节点的 redis.conf,把 replicaof 指向新的主节点。所以你不需要手工改配置。这个自动改写是 Sentinel 最有价值的特性之一。

3.6 用 firewalld 富规则放行(补充技能)

执行主机 :infra(示例,其余节点同理)

前面关了 firewalld 是因为实验要减少变量。但 firewalld 是简历上的技能点,这里补一份规则清单,让你面试能讲。

bash 复制代码
# 放行服务
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=mysql

# 放行端口(NFS 用)
firewall-cmd --permanent --add-port=2049/tcp
firewall-cmd --permanent --add-port=111/tcp
firewall-cmd --permanent --add-port=20048/tcp
firewall-cmd --permanent --add-port=26379/tcp
firewall-cmd --permanent --add-port=11211/tcp
firewall-cmd --permanent --add-port=514/tcp
firewall-cmd --permanent --add-port=514/udp

# 富规则:只允许数据面访问 3306,并记录日志
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.0.0/24" port port="3306" protocol="tcp" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.0.0/24" port port="3306" protocol="tcp" log prefix="MYSQL-ACCESS " level="info" accept'

# 富规则:拒绝业务面访问 3306(数据库不能从业务面进来)
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.25.254.0/24" port port="3306" protocol="tcp" reject'

# 生效
firewall-cmd --reload
firewall-cmd --list-all
firewall-cmd --list-rich-rules

# 移除某条规则
firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="172.25.254.0/24" port port="3306" protocol="tcp" reject'
firewall-cmd --reload

富规则(rich rule)四要素速记:rule family source/destination port protocol accept/reject/drop log。

不要在本次项目里真的开启 firewalld------7 台机器逐一配规则会让后续调试极其痛苦。企业里这些规则由 Ansible 统一推送或云安全组管理。这个认知本身就是面试答案。

打快照 :stage2-infra-ok。


第四章 web 节点:Nginx + Tomcat + Memcached 集群

本章操作主机 :web1、web2(两台命令完全一致)

web1(172.25.254.10 / 192.168.0.10)和 web2(172.25.254.20 / 192.168.0.20)配置完全一致,只有 Tomcat 的 MSM 配置有一处差异(4.5.3 节)。

4.1 架构说明

执行主机:本节为架构说明,无需执行命令

复制代码
客户端 → lb1/lb2 (HAProxy 七层)
              ↓ 轮询
         web1 / web2
              ├── Nginx :80 ── 静态资源直接返回(读 NFS)
              └── Nginx :80 ──→ proxy_pass → Tomcat :8080(动态 JSP)
                                     ↓
                              Memcached (web1+web2) 共享 Session

动静分离的价值(面试必答):静态请求(图片/CSS/JS)由 Nginx 直接返回,不经过 Tomcat;Tomcat 只处理动态请求。Tomcat 的并发压力下降一个数量级,因为它不用再为每个图片请求分配一个 Java 线程。

面试官问「为什么要在 Tomcat 前面放 Nginx」,不要只说「反向代理」,要说这三条:

  1. 动静分离:静态资源由 Nginx 的 epoll 模型处理,并发能力比 Tomcat 的线程模型高一个数量级。
  2. 负载均衡:Nginx 把请求分发到 Tomcat 集群。
  3. 安全隔离:Tomcat 不直接暴露给客户端,可以隐藏内部端口和版本信息。

4.2 创建运行用户

执行主机 :web1、web2

bash 复制代码
useradd -s /sbin/nologin -M nginx
useradd -s /sbin/nologin -M tomcat

4.3 Nginx 源码编译

执行主机 :web1、web2

bash 复制代码
dnf install -y gcc openssl-devel pcre2-devel zlib-devel make

cd /root
wget https://nginx.org/download/nginx-1.28.1.tar.gz
tar zxf nginx-1.28.1.tar.gz
cd nginx-1.28.1/

先隐藏版本号再编译(比编译完再改更省一步):

bash 复制代码
sed -i 's|#define NGINX_VERSION .*|#define NGINX_VERSION      ""|' src/core/nginx.h
sed -i 's|#define NGINX_VER .*|#define NGINX_VER          "TIMINGLEE/" NGINX_VERSION|' src/core/nginx.h
grep -n "NGINX_VER\|NGINX_VERSION" src/core/nginx.h | head -5

编译:

bash 复制代码
./configure \
  --prefix=/usr/local/nginx \
  --user=nginx \
  --group=nginx \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-http_realip_module \
  --with-http_stub_status_module \
  --with-http_gzip_static_module \
  --with-pcre \
  --with-stream \
  --with-stream_ssl_module \
  --with-stream_realip_module

make -j2
make install

注意每个 --with-* 后面都要有反斜杠换行,最后一行不能有反斜杠。这是最常见的语法错误。

配置环境变量:

bash 复制代码
echo 'export PATH=$PATH:/usr/local/nginx/sbin' >> ~/.bash_profile
source ~/.bash_profile
nginx -V 2>&1 | head -1
# nginx version: TIMINGLEE/         ← 版本号已隐藏

隐藏版本号的价值:默认响应头会暴露 Server: nginx/1.28.1,攻击者可以据此查 CVE 匹配攻击工具。隐藏后无法判断版本。这是「你做过的安全加固」的实打实证据。

4.3.1 systemd 托管

bash 复制代码
cat > /lib/systemd/system/nginx.service <<'EOF'
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
LimitNOFILE=100000

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now nginx

# 验证
ss -antlp | grep ':80'
curl -I 127.0.0.1

4.3.2 Nginx 主配置

bash 复制代码
vim /usr/local/nginx/conf/nginx.conf
nginx 复制代码
user  nginx;
worker_processes  auto;
# 2 核 CPU,绑到 0、1 号核
worker_cpu_affinity 01 10;

error_log  logs/error.log  warn;
pid        logs/nginx.pid;

events {
    worker_connections  10240;
    use epoll;
    accept_mutex on;
    multi_accept on;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" '
                      'upstream_time=$upstream_response_time request_time=$request_time';

    access_log  logs/access.log  main;

    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
    keepalive_timeout   65;
    keepalive_requests  1000;

    # 隐藏版本号(编译期已改,这里是双保险)
    server_tokens off;

    # gzip 压缩
    gzip  on;
    gzip_comp_level 4;
    gzip_min_length 1k;
    gzip_buffers 32 4k;
    gzip_types text/plain text/css text/xml application/xml application/json application/javascript application/x-javascript image/svg+xml;
    gzip_vary on;
    gzip_static on;
    gzip_disable "MSIE [1-6]\.";

    # 客户端 body 大小(配合上传功能)
    client_max_body_size 50m;

    # 后端 Tomcat 集群
    upstream tomcat_cluster {
        # 用 cookie 做会话粘滞(配了 MSM 后其实可以去掉这一行)
        hash $cookie_JSESSIONID consistent;
        server 192.168.0.10:8080 max_fails=3 fail_timeout=15s;
        server 192.168.0.20:8080 max_fails=3 fail_timeout=15s;
        keepalive 32;
    }

    # 站点子配置
    include /usr/local/nginx/conf/conf.d/*.conf;
}

关键参数:

  • worker_processes auto 让 Nginx 按 CPU 核数自动起 worker。
  • worker_cpu_affinity 01 10 是把 2 个 worker 分别绑到 CPU0 和 CPU1,减少上下文切换。用 ps axo pid,cmd,psr | grep worker 可以验证绑核生效。
  • use epoll 是 Linux 下的最高效事件模型(默认就是,显式声明更清楚)。
  • keepalive 32 让 Nginx 与后端 Tomcat 之间复用长连接,减少 TCP 三次握手开销。必须配合 proxy_http_version 1.1 和 proxy_set_header Connection "",否则不生效。

4.3.3 站点配置

bash 复制代码
mkdir -p /usr/local/nginx/conf/conf.d/
mkdir -p /webdir/timinglee.org/{www,app,error}/{html,logs}

vim /usr/local/nginx/conf/conf.d/www.conf
nginx 复制代码
server {
    listen 80;
    server_name www.timinglee.org;

    root /webdir/timinglee.org/www/html;
    index index.html index.htm;

    access_log /webdir/timinglee.org/www/logs/access.log main;
    error_log  /webdir/timinglee.org/www/logs/error.log warn;

    # ========== 静态资源:Nginx 直接返回,走 NFS ==========
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|mp4)$ {
        root /data/nfs/static;
        expires 7d;
        add_header Cache-Control "public, max-age=604800";
        access_log off;
    }

    # ========== 上传文件:走 NFS 共享目录 ==========
    location /upload/ {
        alias /data/nfs/upload/;
        autoindex on;
        autoindex_exact_size off;
        autoindex_localtime on;
    }

    # ========== 动态请求:转发到 Tomcat 集群 ==========
    location ~* \.(jsp|do|action)$ {
        proxy_pass http://tomcat_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 5s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;

        proxy_next_upstream error timeout http_502 http_503 http_504;
    }

    # ========== 健康检查接口(给 HAProxy / Keepalived 用) ==========
    location = /health {
        access_log off;
        default_type text/plain;
        return 200 "web-ok\n";
    }

    # ========== Nginx 状态页(监控用) ==========
    location = /nginx_status {
        stub_status;
        allow 172.25.254.0/24;
        allow 192.168.0.0/24;
        deny all;
    }

    # ========== 错误页 ==========
    error_page 404 500 502 503 504 /error.html;
    location = /error.html {
        root /webdir/timinglee.org/error/html;
        internal;
    }

    # ========== 防盗链 ==========
    location ~* \.(jpg|png|gif|jpeg)$ {
        valid_referers none blocked server_names *.timinglee.org;
        if ($invalid_referer) {
            return 403;
        }
    }
}

proxy_next_upstream 的意义:当后端返回 502/503/504 或超时时,Nginx 自动把请求转给下一台后端。这是「后端有机器挂掉时用户无感知」的关键配置。注意不要加 http_500------500 是应用逻辑错误,换一台机器也是 500,重试没意义。

4.3.4 创建页面与静态资源

bash 复制代码
cat > /webdir/timinglee.org/www/html/index.html <<'EOF'
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>timinglee 企业级集群</title></head>
<body>
<h1>企业级高可用 Web 集群运行中</h1>
<p>本页面由 Nginx 直接返回(静态资源层)</p>
<p><a href="/info.jsp">查看 Tomcat 集群节点信息</a></p>
<p><a href="/css/main.css">测试静态资源缓存</a></p>
</body>
</html>
EOF

cat > /webdir/timinglee.org/error/html/error.html <<'EOF'
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>服务暂时不可用</title></head>
<body style="text-align:center;padding-top:100px;font-family:sans-serif">
<h1>页面暂时无法访问</h1>
<p>请稍后重试</p>
</body>
</html>
EOF

# 页面里带上主机名,方便验证负载均衡
hostname > /webdir/timinglee.org/www/html/hostname.html

# 静态资源目录
mkdir -p /data/nfs/static/{images,css,js}
echo "body{font-family:sans-serif;margin:40px}" > /data/nfs/static/css/main.css
dd if=/dev/zero of=/data/nfs/static/bigfile.txt bs=1M count=10
bash 复制代码
nginx -t
systemctl reload nginx

4.3.5 验证

bash 复制代码
curl http://192.168.0.10/hostname.html
# web1
curl http://192.168.0.20/hostname.html
# web2

curl -I http://192.168.0.10/css/main.css
# HTTP/1.1 200 OK
# Server: TIMINGLEE/                   ← 版本号已隐藏
# Cache-Control: public, max-age=604800

curl http://192.168.0.10/nginx_status
# Active connections: 1
# server accepts handled requests
#  5 5 5
# Reading: 0 Writing: 1 Waiting: 0

# 确认 worker 绑核生效
ps axo pid,cmd,psr | grep "nginx: worker"
# 1701 nginx: worker process   0
# 1702 nginx: worker process   1

4.3.6 系统级并发调优

这一节是面试高频考点,因为「Too many open files」是你笔记里真实踩过的坑。

bash 复制代码
vim /etc/security/limits.conf
# 追加
*               soft    nofile          100000
*               hard    nofile          100000
*               soft    noproc          100000
*               hard    noproc          100000
root            soft    nofile          100000
root            hard    nofile          100000
bash 复制代码
cat > /etc/security/limits.d/nginx.conf <<'EOF'
nginx           soft    nofile          100000
nginx           hard    nofile          100000
EOF

vim /etc/sysctl.conf
# 追加
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 65535
fs.file-max = 1000000
bash 复制代码
sysctl -p
systemctl restart nginx

验证:

bash 复制代码
cat /proc/$(cat /usr/local/nginx/logs/nginx.pid)/limits | grep "open files"
# Max open files    100000   100000   files

参数解释:

  • somaxconn 是 accept 队列长度,默认 128,高并发下会成为瓶颈。
  • tcp_tw_reuse 允许复用 TIME_WAIT 状态的连接,缓解端口耗尽。
  • ip_local_port_range 扩大可用本地端口范围(默认只有 32768 ~ 60999,约 2.8 万个,高并发下不够)。
  • fs.file-max 是系统级的总文件句柄上限。
  • limits.conf 对 systemd 管理的服务无效 ,所以必须额外写 /etc/security/limits.d/nginx.conf,或者用 LimitNOFILE 写在 service 文件里。这是最容易踩的坑。

压测验证(可选):

bash 复制代码
dnf install -y httpd-tools

# 低并发基线
ab -n 10000 -c 100 http://192.168.0.10/hostname.html

# 高并发(测试调优效果)
ab -n 100000 -c 5000 http://192.168.0.10/hostname.html

# 如果出现 socket: Too many open files (24),说明 limits 没生效
ulimit -n
cat /proc/$(cat /usr/local/nginx/logs/nginx.pid)/limits | grep "open files"

4.4 Tomcat 部署

执行主机 :web1、web2

bash 复制代码
dnf install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel
java -version
# openjdk version "1.8.0_xxx"

cd /root
wget https://mirrors.huaweicloud.com/apache/tomcat/tomcat-9/v9.0.117/bin/apache-tomcat-9.0.117.tar.gz
tar zxf apache-tomcat-9.0.117.tar.gz -C /usr/local/
cd /usr/local/
mv apache-tomcat-9.0.117 tomcat
chown -R tomcat:tomcat /usr/local/tomcat

Tomcat 用 JDK 8 是因为你的课程笔记用的就是 JDK 8,而且 JDK 8 在小内存下占用更低。生产环境现在多用 JDK 11 或 17,面试可以提这个差异。

4.4.1 JVM 内存参数(1.5 GB 内存节点的关键设置)

小内存环境的重点是限制 JVM 堆大小,否则 Tomcat 会吃掉全部内存导致 OOM 甚至拖死整台虚拟机。

bash 复制代码
vim /usr/local/tomcat/bin/setenv.sh
bash 复制代码
#!/bin/bash
export JAVA_HOME=/etc/alternatives/jre
export CATALINA_HOME=/usr/local/tomcat

# 堆内存:初始 256m,最大 512m(1.5GB 内存节点的合理值)
export JAVA_OPTS="-Xms256m -Xmx512m \
  -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m \
  -XX:+UseG1GC \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/usr/local/tomcat/logs/ \
  -Djava.security.egd=file:/dev/./urandom \
  -Dfile.encoding=UTF-8 \
  -Duser.timezone=Asia/Shanghai"
bash 复制代码
chmod +x /usr/local/tomcat/bin/setenv.sh
chown tomcat:tomcat /usr/local/tomcat/bin/setenv.sh

参数解读(面试会问):

  • -Xms 与 -Xmx 相等是最佳实践,避免堆动态扩缩带来的停顿。这里资源紧张所以留了扩缩空间(256m → 512m)。
  • MaxMetaspaceSize 必须限制,否则类加载器泄漏会把内存吃光(Metaspace 默认无上限,用的是本地内存)。
  • UseG1GC 在 4 GB 以下堆表现最好,低延迟。JDK 8 默认是 ParallelGC。
  • HeapDumpOnOutOfMemoryError 是排障救命的,OOM 时自动导出堆快照便于分析。
  • /dev/./urandom 加速 Session ID 生成,绕开 JDK 早期版本的熵池阻塞问题(/dev/random 在熵不足时会阻塞)。

4.4.2 systemd 托管

bash 复制代码
cat > /usr/local/tomcat/conf/tomcat.conf <<'EOF'
JAVA_HOME=/etc/alternatives/jre
CATALINA_HOME=/usr/local/tomcat
EOF

cat > /lib/systemd/system/tomcat.service <<'EOF'
[Unit]
Description=Apache Tomcat Web Application Container
After=syslog.target network.target
Wants=network.target

[Service]
Type=forking
EnvironmentFile=/usr/local/tomcat/conf/tomcat.conf
ExecStart=/usr/local/tomcat/bin/startup.sh
ExecStop=/usr/local/tomcat/bin/shutdown.sh
User=tomcat
Group=tomcat
PrivateTmp=true
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now tomcat

# 验证
ss -antlp | grep -E ':(8080|8005)'
curl -I 127.0.0.1:8080

4.4.3 部署测试应用

写一个能打印主机名和 SessionID 的 JSP,用来验证负载均衡和会话共享。

bash 复制代码
cat > /usr/local/tomcat/webapps/ROOT/info.jsp <<'EOF'
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.net.InetAddress" %>
<%
    String hostname = InetAddress.getLocalHost().getHostName();
    String ip = InetAddress.getLocalHost().getHostAddress();
    String sessionId = session.getId();
    Integer count = (Integer) session.getAttribute("visitCount");
    if (count == null) { count = 0; }
    count++;
    session.setAttribute("visitCount", count);
    if (session.getAttribute("node") == null) {
        session.setAttribute("node", hostname);
    }
%>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Tomcat 集群节点信息</title></head>
<body style="font-family:sans-serif;margin:40px">
<h2>Tomcat 节点信息(用于验证集群与会话共享)</h2>
<table border="1" cellpadding="8" cellspacing="0">
  <tr><td>后端主机名</td><td><b><%= hostname %></b></td></tr>
  <tr><td>后端 IP</td><td><%= ip %></td></tr>
  <tr><td>SessionID</td><td><%= sessionId %></td></tr>
  <tr><td>会话访问次数</td><td><b><%= count %></b></td></tr>
  <tr><td>会话首次创建于</td><td><%= session.getAttribute("node") %></td></tr>
</table>
</body>
</html>
EOF

chown tomcat:tomcat /usr/local/tomcat/webapps/ROOT/info.jsp

在两台机器上都放一份:

bash 复制代码
scp /usr/local/tomcat/webapps/ROOT/info.jsp root@192.168.0.20:/usr/local/tomcat/webapps/ROOT/
ssh root@192.168.0.20 "chown tomcat:tomcat /usr/local/tomcat/webapps/ROOT/info.jsp"

验证(通过 Nginx 访问):

bash 复制代码
curl http://192.168.0.10/info.jsp | grep -E "后端主机名|SessionID|会话访问次数"

打快照 :stage3-web-base-ok。

4.5 Memcached 会话共享(MSM 方案)

执行主机 :web1、web2

要解决的问题 :用户登录后 Session 存在 Tomcat 的本地 JVM 内存里。如果下一个请求被负载均衡分到另一台 Tomcat,就找不到 Session,用户被迫重新登录。这在企业里是不能接受的故障。

解决方案:用 Memcached 集中存放 Session,两台 Tomcat 都从 Memcached 读写 Session。这就是 MSM(memcached-session-manager)。

4.5.1 部署 Memcached(两台都装)

bash 复制代码
dnf install -y memcached

vim /etc/sysconfig/memcached
bash 复制代码
PORT="11211"
USER="memcached"
MAXCONN="4096"
CACHESIZE="128"
OPTIONS="-l 0.0.0.0 -m 128 -c 4096"

CACHESIZE 在小内存环境不要设太大,128 MB 足够存几十万个 Session(一个 Session 通常 1 ~ 5 KB)。

bash 复制代码
systemctl enable --now memcached
ss -antlupe | grep memcached

# 验证
echo stats | nc 127.0.0.1 11211 | head -10

两台互相验证:

bash 复制代码
# 在 web1 上验证能连到 web2
echo stats | nc 192.168.0.20 11211 | grep curr_items
# curr_items 0

4.5.2 部署 MSM 依赖的 JAR 包

MSM 需要一组 jar 包(memcached-session-manager 及其序列化器依赖)。你课程资料里的 jar.zip 就是它。

bash 复制代码
# 假设 jar.zip 已通过 scp 上传到 /root
unzip /root/jar.zip -d /root/msm/
ls /root/msm/jar/

需要的核心包:

JAR 作用
memcached-session-manager-2.3.2.jar MSM 核心
memcached-session-manager-tc9-2.3.2.jar Tomcat 9 适配器
spymemcached-2.12.3.jar Memcached 客户端
kryo-3.0.3.jar Kryo 序列化器(性能最好)
kryo-serializers-0.45.jar Kryo 扩展
minlog-1.3.0.jar 日志依赖
reflectasm-1.11.9.jar 反射加速
asm-9.7.1.jar 字节码操作
msm-kryo-serializer-2.3.2.jar MSM Kryo 集成
objenesis-3.3.jar 对象实例化
bash 复制代码
cp /root/msm/jar/*.jar /usr/local/tomcat/lib/
chown tomcat:tomcat /usr/local/tomcat/lib/*.jar

# 同步到 web2
scp /root/msm/jar/*.jar root@192.168.0.20:/usr/local/tomcat/lib/
ssh root@192.168.0.20 "chown tomcat:tomcat /usr/local/tomcat/lib/*.jar"

如果 jar.zip 找不到,可以从 Maven 中央仓库逐个下载,或者从你课程实验包里再取一份。MSM 版本必须与 Tomcat 主版本匹配(tc9 对应 Tomcat 9),否则启动时会报 ClassNotFound。

4.5.3 配置 Tomcat 使用 MSM

bash 复制代码
vim /usr/local/tomcat/conf/context.xml
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<Context>

    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
        memcachedNodes="n1:192.168.0.10:11211,n2:192.168.0.20:11211"
        failoverNodes="n1"
        requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
        transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
        sessionBackupAsync="false"
        sessionBackupTimeout="1000"
        copyCollectionsForSerialization="false"
        />
</Context>

关键的差异:

主机 memcachedNodes failoverNodes
web1 n1:192.168.0.10:11211,n2:192.168.0.20:11211 n1
web2 n1:192.168.0.10:11211,n2:192.168.0.20:11211 n2

failoverNodes 表示「本机不把 Session 备份到哪个节点」。web1 不备份到 n1(也就是它自己),所以它备份到 n2;web2 不备份到 n2,备份到 n1。这样两台互为备份,任何一台挂掉,Session 都还在对端。

在 web1 上执行:

bash 复制代码
scp /usr/local/tomcat/conf/context.xml root@192.168.0.20:/usr/local/tomcat/conf/context.xml
ssh root@192.168.0.20 "sed -i 's/failoverNodes=\"n1\"/failoverNodes=\"n2\"/' /usr/local/tomcat/conf/context.xml; grep failoverNodes /usr/local/tomcat/conf/context.xml"
# failoverNodes="n2"

重启两台 Tomcat:

bash 复制代码
# 两台都执行
systemctl restart tomcat

# 看启动日志确认 MSM 加载
grep -i "memcached" /usr/local/tomcat/logs/catalina.out | tail -20
# 期望包含:MemcachedSessionManager ... started

如果启动失败,先看 catalina.out 里的 ClassNotFoundException,多半是 jar 包版本不匹配或漏拷贝。

4.5.4 验证会话零丢失(核心演示)

第 1 步:从 web1 拿一个 SessionID

bash 复制代码
curl -c /tmp/cookie.txt http://192.168.0.10/info.jsp | grep -E "后端主机名|SessionID|会话访问次数"
# 后端主机名    web1
# SessionID     3A8F2C1D...
# 会话访问次数  1

第 2 步:带着同一个 Cookie 请求 web2(模拟负载均衡切到另一台)

bash 复制代码
curl -b /tmp/cookie.txt http://192.168.0.20/info.jsp | grep -E "后端主机名|SessionID|会话访问次数|会话首次创建于"
# 后端主机名      web2                  ← 请求确实换到了另一台!
# SessionID       3A8F2C1D...           ← SessionID 没变!
# 会话访问次数    2                     ← 计数从 1 变 2,Session 状态被保留
# 会话首次创建于  web1                  ← 证明它在 web1 创建,被 web2 读到了

这一组输出就是「会话零丢失」的直接证据。截图保存,简历和面试都要用。

第 3 步:验证 Memcached 里确实有数据

bash 复制代码
echo "stats" | nc 127.0.0.1 11211 | grep -E "curr_items|get_hits|cmd_set|bytes"
# curr_items  1
# get_hits    ...
# cmd_set     ...

第 4 步(最强验证):直接停掉 web1 的 Tomcat

bash 复制代码
# 先在 web1 创建会话
curl -c /tmp/c2.txt http://192.168.0.10/info.jsp | grep SessionID
# SessionID  ABC123...

# 停掉 web1 的 Tomcat
systemctl stop tomcat

# 用同一个 Cookie 访问 web2
curl -b /tmp/c2.txt http://192.168.0.20/info.jsp | grep -E "SessionID|会话访问次数"
# SessionID       ABC123...      ← 依然不变
# 会话访问次数    2              ← 会话状态继续累加

结论:后端 Tomcat 宕机,用户完全无感知,不需要重新登录。 这就是「会话零丢失」的完整证明链,也是简历上最有说服力的一条。

4.6 通过 Nginx 验证动静分离

执行主机 :web1、web2(验证可从 lb1 发起)

bash 复制代码
# 静态:Nginx 直接返回
curl -I http://192.168.0.10/css/main.css
# HTTP/1.1 200 OK
# Server: TIMINGLEE/
# Cache-Control: public, max-age=604800

# 动态:经过 proxy_pass 到 Tomcat
curl -I http://192.168.0.10/info.jsp
# HTTP/1.1 200 OK
# Server: TIMINGLEE/

对比 Tomcat 访问日志更直观:

bash 复制代码
# 清空日志后触发两种请求
> /usr/local/tomcat/logs/localhost_access_log.$(date +%Y-%m-%d).txt

curl -s http://192.168.0.10/css/main.css > /dev/null
curl -s http://192.168.0.10/info.jsp > /dev/null

cat /usr/local/tomcat/logs/localhost_access_log.$(date +%Y-%m-%d).txt
# 只有 info.jsp 出现了,main.css 没有 → 静态请求没进 Tomcat

4.7 Nginx 平滑升级与回滚(可选进阶)

执行主机 :web1、web2

这个技能面试含金量很高,因为你课程笔记里完整做过。

4.7.1 编译新版本

bash 复制代码
cd /root
wget https://nginx.org/download/nginx-1.29.4.tar.gz
tar zxf nginx-1.29.4.tar.gz
cd nginx-1.29.4/

# 同样隐藏版本号
sed -i 's|#define NGINX_VERSION .*|#define NGINX_VERSION      ""|' src/core/nginx.h
sed -i 's|#define NGINX_VER .*|#define NGINX_VER          "TIMINGLEE/" NGINX_VERSION|' src/core/nginx.h

./configure --prefix=/usr/local/nginx --user=nginx --group=nginx \
  --with-http_ssl_module --with-http_v2_module --with-http_realip_module \
  --with-http_stub_status_module --with-http_gzip_static_module \
  --with-pcre --with-stream --with-stream_ssl_module --with-stream_realip_module

make -j2

注意:只 make,不要 make install。 make install 会覆盖配置文件和站点目录。平滑升级只需要替换二进制文件。

4.7.2 平滑升级

bash 复制代码
# 备份旧二进制
cd /usr/local/nginx/sbin/
cp -p nginx nginx.old

# 替换(\cp 绕过可能的 alias)
\cp -f /root/nginx-1.29.4/objs/nginx /usr/local/nginx/sbin/nginx

# 先验证新二进制可用
/usr/local/nginx/sbin/nginx -t

# 记录旧 master PID
OLD_PID=$(cat /usr/local/nginx/logs/nginx.pid)
echo $OLD_PID

# 向旧 master 发 USR2,拉起新 master
kill -USR2 $OLD_PID

# 观察:新旧 master 并存
ps aux | grep "nginx: master" | grep -v grep
ls /usr/local/nginx/logs/
# nginx.pid  nginx.pid.oldbin

# 验证服务正常(此时新版本在服务)
curl -s http://192.168.0.10/hostname.html

# 优雅关闭旧 worker(旧 master 还在,随时可以回滚)
kill -WINCH $OLD_PID

# 观察一段时间确认无误后,完整关闭旧 master
kill -QUIT $OLD_PID

4.7.3 回滚演练

bash 复制代码
#场景 A:旧 master 仅 WINCH、未 QUIT(标准回滚)
# 前提:升级后已执行 kill -WINCH $OLD_PID,旧master还在(nginx.pid.oldbin存在)
# 1. 确认双master PID
OLD_MASTER_PID=$(cat /usr/local/nginx/logs/nginx.pid.oldbin)
NEW_MASTER_PID=$(cat /usr/local/nginx/logs/nginx.pid)
echo "旧master: $OLD_MASTER_PID | 新master: $NEW_MASTER_PID"

# 2. 给旧master发HUP,重新拉起旧版本worker
kill -HUP $OLD_MASTER_PID
sleep 1

# 3. 验证旧worker已启动、业务正常
ps aux | grep "nginx: worker process" | grep -v grep
curl -s http://192.168.0.10/hostname.html

# 4. 给新master发WINCH,优雅关闭新版本worker
kill -WINCH $NEW_MASTER_PID
sleep 1

# 5. 确认流量全部切回旧版本后,彻底退出新master
kill -QUIT $NEW_MASTER_PID

# 6. 验证回滚结果
ps aux | grep "nginx: master" | grep -v grep
curl -I http://192.168.0.10/hostname.html | grep Server

# 7. 清理残留文件
rm -f /usr/local/nginx/logs/nginx.pid.oldbin

#场景 B:旧 master 已经被 QUIT
# 1. 备份当前新二进制
cd /usr/local/nginx/sbin/
cp -p nginx nginx.new

# 2. 换回旧版本二进制
\cp -f nginx.old nginx

# 3. 给当前运行的新master发USR2,启动旧版本master
CUR_PID=$(cat /usr/local/nginx/logs/nginx.pid)
kill -USR2 $CUR_PID
sleep 1

# 4. 确认旧版本master启动成功
ps aux | grep "nginx: master" | grep -v grep

# 5. 关闭旧(新版本)master的worker
OLD_NEW_PID=$(cat /usr/local/nginx/logs/nginx.pid.oldbin)
kill -WINCH $OLD_NEW_PID

# 6. 确认无误后退出新版本master
kill -QUIT $OLD_NEW_PID

# 7. 验证+清理
curl -I http://192.168.0.10/hostname.html | grep Server
rm -f /usr/local/nginx/logs/nginx.pid.oldbin

信号速记(面试必答):

  • USR2 → 拉起新 master(平滑升级第一步)
  • WINCH → 优雅关闭 worker(不再接受新连接,处理完现有请求后退出)
  • HUP → 重新加载配置 / 重新拉起 worker
  • QUIT → 优雅退出 master
  • USR1 → 重新打开日志文件(配合 logrotate)
  • TERM / INT → 立即退出

打快照 :stage4-web-final-ok。


第五章 db 节点:MySQL 主从与 VIP 高可用

本章操作主机 :db1(主库)、db2(从库)

db1(172.25.254.30 / 192.168.0.30)为主库,db2(172.25.254.40 / 192.168.0.40)为从库,且 db2 同时持有数据库 VIP 172.25.254.201。

5.1 为什么用 RPM 装 MySQL

执行主机:本节为选型说明,无需执行命令

你之前用源码编译 MySQL 8.3.0。那套流程在小内存虚拟机上有个硬问题:编译过程需要 3 ~ 4 GB 内存和约 15 GB 磁盘,你的 db 节点只有 1 GB 内存,编译必然失败(会被 OOM Killer 杀掉)。

所以本项目用 AppStream 里的 MySQL 8.0 官方包,安装只要 1 分钟,占用约 400 MB。源码编译的完整步骤放在 附录 A,你可以在需要展示「会编译」时单独做一次(临时把内存调到 3 GB)。

5.2 安装与安全初始化(db1 / db2 都做)

执行主机 :db1、db2

bash 复制代码
dnf install -y mysql-server mysql
systemctl enable --now mysqld

# 查看初始临时密码
grep "temporary password" /var/log/mysqld.log

# 安全初始化
mysql_secure_installation
# 输入临时密码后依次回答:
#   是否启用密码强度校验插件?  n(实验环境)
#   修改 root 密码?            y  → 设为 lee
#   移除匿名用户?              y
#   禁止 root 远程登录?        y
#   删除 test 库?              y
#   重载权限表?                y

# 验证
mysql -uroot -plee -e "select version();"
# 8.0.x

生产环境做法:validate_password 插件应该开启,密码要满足长度 + 大小写 + 数字 + 符号。面试被问到就说「实验环境为了简化流程关闭了,生产环境会保留并设置 validate_password.policy=MEDIUM」。

5.3 主库配置(db1)

执行主机 :db1(主库)

bash 复制代码
vim /etc/my.cnf.d/mysql-server.cnf
ini 复制代码
[mysqld]
# ===== 基础 =====
server-id=30
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
symbolic-links=0

# ===== 二进制日志(复制的核心) =====
log-bin=mysql-bin
binlog_format=ROW
binlog_row_image=FULL
expire_logs_days=7
max_binlog_size=256M
sync_binlog=1

# ===== GTID =====
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON

# ===== 字符集 =====
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

# ===== 小内存调优(1GB 内存节点的关键!) =====
innodb_buffer_pool_size=256M
innodb_log_file_size=64M
innodb_flush_log_at_trx_commit=1
max_connections=200
table_open_cache=400
performance_schema=OFF
skip-name-resolve

# ===== 认证插件(兼容旧客户端) =====
default_authentication_plugin=mysql_native_password

# ===== 慢查询日志 =====
slow_query_log=ON
slow_query_log_file=/var/log/mysql-slow.log
long_query_time=1
log_queries_not_using_indexes=OFF
bash 复制代码
systemctl restart mysqld

关键参数解释(面试必问):

  • innodb_buffer_pool_size 一般是物理内存的 50% ~ 70%。1 GB 内存的机器给 256 MB 是安全的(再大就可能被 OOM Killer 杀)。
  • performance_schema=OFF 能省约 150 MB 内存,代价是失去细粒度性能诊断。小内存环境值得。
  • sync_binlog=1 + innodb_flush_log_at_trx_commit=1 是「双 1 配置」,保证崩溃不丢数据,是金融级的标准。代价是每次提交都要刷盘,性能下降。可以这样答:「本项目为保证数据可靠性选了双 1;纯日志类的写入场景可以调成 2 和 0 换取吞吐」。
  • binlog_format=ROW 是主从复制最安全的方式,避免了 STATEMENT 格式下 now()、uuid()、limit 语句在从库执行结果不一致的问题。
  • skip-name-resolve 禁止 MySQL 做反向 DNS 解析,避免每次连接都查一次 DNS,高并发下这是明显的性能收益。

未使用索引的慢查询 :log_queries_not_using_indexes=OFF 是因为开了会让慢日志爆炸。生产环境按需开启。

5.4 从库配置(db2)

执行主机 :db2(从库)

ini 复制代码
[mysqld]
server-id=40

# ===== 基础 =====
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
symbolic-links=0

# ===== 二进制日志(GTID复制必备,从库也要开启) =====
log-bin=mysql-bin
binlog_format=ROW
binlog_row_image=FULL
binlog_expire_logs_seconds=604800
max_binlog_size=256M
sync_binlog=1

# ===== GTID 核心参数(必须显式开启) =====
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON

# ===== 字符集 =====
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

# ===== 小内存调优 =====
innodb_buffer_pool_size=256M
innodb_log_file_size=64M
innodb_flush_log_at_trx_commit=1
max_connections=200
table_open_cache=400
performance_schema=OFF
skip-name-resolve

# ===== 认证插件 =====
default_authentication_plugin=mysql_native_password

# ===== 慢查询日志 =====
slow_query_log=ON
slow_query_log_file=/var/log/mysql-slow.log
long_query_time=1
log_queries_not_using_indexes=OFF

# ===== 从库专属:多线程并行回放 =====
replica_parallel_type=LOGICAL_CLOCK
replica_parallel_workers=4
replica_preserve_commit_order=ON

# ===== 从库专属:中继日志自动恢复 =====
relay_log_recovery=ON

# ===== 从库专属:只读防误写 =====
read_only=ON
super_read_only=ON

server-id 是复制的身份证,全网必须唯一 。本项目用「IP 最后一段」作为 server-id(.30 → 30,.40 → 40),这是你笔记里的习惯,也是最佳实践。

replica_preserve_commit_order=ON 很重要:并行回放会导致从库提交顺序与主库不一致,开启这个参数能保证最终顺序一致,避免基于顺序的逻辑出错。

bash 复制代码
systemctl restart mysqld

5.5 主库创建复制账号与业务库

执行主机 :db1(主库)

在 db1 上:

bash 复制代码
mysql -uroot -plee
sql 复制代码
-- 复制专用账号(只给复制权限,最小权限原则)
CREATE USER 'repl'@'192.168.0.%' IDENTIFIED WITH mysql_native_password BY 'lee';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.0.%';

-- 业务账号(给应用用,权限要收敛到具体库)
CREATE USER 'appuser'@'192.168.0.%' IDENTIFIED WITH mysql_native_password BY 'lee';
GRANT SELECT, INSERT, UPDATE, DELETE ON timinglee.* TO 'appuser'@'192.168.0.%';

-- 监控账号(MHA / 半同步检查用)
CREATE USER 'monitor'@'192.168.0.%' IDENTIFIED WITH mysql_native_password BY 'lee';
GRANT REPLICATION CLIENT, PROCESS ON *.* TO 'monitor'@'192.168.0.%';

-- 备份账号
CREATE USER 'backup'@'192.168.0.%' IDENTIFIED WITH mysql_native_password BY 'lee';
GRANT SELECT, RELOAD, LOCK TABLES, REPLICATION CLIENT, SHOW VIEW, EVENT, TRIGGER ON *.* TO 'backup'@'192.168.0.%';

FLUSH PRIVILEGES;

-- 业务库
CREATE DATABASE timinglee DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE timinglee;

CREATE TABLE users (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(64) NOT NULL UNIQUE,
  email VARCHAR(128),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TABLE orders (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
  status TINYINT NOT NULL DEFAULT 0,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  KEY idx_user (user_id),
  KEY idx_created (created_at)
) ENGINE=InnoDB;

INSERT INTO users (username, email) VALUES ('timinglee','admin@timinglee.org'),('lee','lee@timinglee.org');
INSERT INTO orders (user_id, amount, status) VALUES (1, 199.00, 1),(2, 299.00, 0);

SELECT * FROM users;

-- 记录当前 GTID 位置
SHOW MASTER STATUS\G

为什么必须 IDENTIFIED WITH mysql_native_password?MySQL 8 默认认证插件是 caching_sha2_password,老版本客户端和部分复制组件不支持,会报认证失败。这是你笔记里真实踩过的坑,面试很值得讲。

5.6 从库启动复制

执行主机 :db2(从库)

在 db2 上:

bash 复制代码
mysql -uroot -plee
sql 复制代码
-- GTID 方式启动复制(不需要指定 binlog file 和 pos)
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='192.168.0.30',
  SOURCE_PORT=3306,
  SOURCE_USER='repl',
  SOURCE_PASSWORD='lee',
  SOURCE_AUTO_POSITION=1,
  GET_SOURCE_PUBLIC_KEY=1;

START REPLICA;

SHOW REPLICA STATUS\G

必须确认这两行都是 Yes:

复制代码
Replica_IO_Running: Yes
Replica_SQL_Running: Yes

同时确认:

复制代码
Retrieved_Gtid_Set: <与主库一致>
Executed_Gtid_Set:  <与主库一致>
Seconds_Behind_Source: 0
Last_IO_Error:
Last_SQL_Error:

排错顺序(面试常考):

  1. Replica_IO_Running: No → 网络 / 账号 / 防火墙问题。看 Last_IO_Error。
  2. Replica_SQL_Running: No → 数据冲突或权限问题。看 Last_SQL_Error。
  3. Seconds_Behind_Source 不断增大 → 从库性能不足,考虑开并行回放或升级硬件。
  4. 认证报错 → 检查是否用了 caching_sha2_password,以及 GET_SOURCE_PUBLIC_KEY=1 是否加上。
  5. GTID 模式下 AUTO_POSITION=1,不需要手工指定 binlog 位置,这是 GTID 相对传统复制最大的优势:主库故障切换后不用重新找位点。

5.7 启用半同步复制

执行主机 :db1、db2

半同步 的意义:主库提交事务后,至少等一个从库确认收到 binlog才向客户端返回成功。这样主库宕机时数据不会丢。性能略有下降,可靠性大幅提升。

动态加载插件(db1 和 db2 都做):

sql 复制代码
-- db1(主库)
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 3000;

-- db2(从库)
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

永久生效(写进配置文件,重启不丢):

bash 复制代码
# db1
cat >> /etc/my.cnf.d/mysql-server.cnf <<'EOF'

plugin-load-add=rpl_semi_sync_master=semisync_master.so
rpl_semi_sync_master_enabled=ON
rpl_semi_sync_master_timeout=3000
EOF
bash 复制代码
# db2
cat >> /etc/my.cnf.d/mysql-server.cnf <<'EOF'

plugin-load-add=rpl_semi_sync_slave=semisync_slave.so
rpl_semi_sync_slave_enabled=ON
EOF

从库必须重启 IO 线程,半同步才真正生效(这是最容易漏的一步):

sql 复制代码
-- db2
STOP REPLICA IO_THREAD;
START REPLICA IO_THREAD;

验证:

sql 复制代码
-- db1
SHOW VARIABLES LIKE 'rpl_semi_sync%';
SHOW STATUS LIKE 'Rpl_semi_sync%';
-- Rpl_semi_sync_master_status   ON        ← 关键
-- Rpl_semi_sync_master_clients  1         ← 有一个从库在响应
-- Rpl_semi_sync_master_yes_tx   >0        ← 有事务走了半同步

半同步降级演练:

sql 复制代码
-- 在 db2 上停掉 IO 线程,模拟从库故障
STOP REPLICA IO_THREAD;
bash 复制代码
# 在 db1 上写入,会明显变慢(等待 3 秒超时)
time mysql -uroot -plee -e "INSERT INTO timinglee.users(username) VALUES('semisync_test');"
# real  0m3.0xxs        ← 等了 3 秒超时

# 观察状态自动降级
mysql -uroot -plee -e "SHOW STATUS LIKE 'Rpl_semi_sync_master_status';"
# Rpl_semi_sync_master_status   OFF       ← 已自动降级为异步复制
sql 复制代码
-- 恢复
-- db2 上:
START REPLICA IO_THREAD;
bash 复制代码
# db1 上观察自动恢复
mysql -uroot -plee -e "SHOW STATUS LIKE 'Rpl_semi_sync_master_status';"
# Rpl_semi_sync_master_status   ON

这是半同步最值得讲的特性 :从库无响应时,主库会在 rpl_semi_sync_master_timeout 超时后自动降级为异步复制,保证业务不被阻塞;从库恢复后又自动升回半同步。这体现了「可用性优先、降级可用」的工程思维。

5.8 复制验证与延迟测试

执行主机 :db1、db2

bash 复制代码
# 主库写入
mysql -uroot -plee -e "
INSERT INTO timinglee.users (username,email) VALUES ('test_$(date +%s)','t@timinglee.org');
SELECT COUNT(*) AS master_count FROM timinglee.users;"

# 从库读取
mysql -uroot -plee -e "
SELECT COUNT(*) AS slave_count FROM timinglee.users;
SHOW REPLICA STATUS\G" | grep -E "slave_count|Seconds_Behind_Source|Running"

期望:master_count == slave_count,Seconds_Behind_Source: 0。

从库只读验证:

bash 复制代码
mysql -uroot -plee -e "INSERT INTO timinglee.users(username) VALUES('x');"
# ERROR 1290 (HY000): The MySQL server is running with the --read-only option
# so it cannot execute this statement

主从延迟压测:

bash 复制代码
cat > /root/repl_lag_test.sh <<'EOF'
#!/bin/bash
# 持续写入并观测主从延迟
for i in $(seq 1 200); do
  mysql -uroot -plee -e "INSERT INTO timinglee.orders(user_id,amount) VALUES($((RANDOM%2+1)),$((RANDOM%100)).00);" 2>/dev/null
done

echo "=== 主库条数 ==="
mysql -uroot -plee -e "SELECT COUNT(*) FROM timinglee.orders;" 2>/dev/null

echo "=== 从库条数 & 延迟 ==="
mysql -umonitor -plee -h192.168.0.40 -e "SELECT COUNT(*) FROM timinglee.orders; SHOW REPLICA STATUS\G" 2>/dev/null \
  | grep -E "COUNT\(\*\)|Seconds_Behind_Source|^[[:space:]]*[0-9]+[[:space:]]*$"
EOF

chmod +x /root/repl_lag_test.sh
/root/repl_lag_test.sh

5.9 延迟复制(实战价值很高的技能)

执行主机 :db1、db2

延迟复制常用于防误删 :从库故意慢 5 分钟,如果主库被误 DROP TABLE / DELETE,你有 5 分钟时间在从库上把数据捞出来。

sql 复制代码
-- db2
STOP REPLICA;
CHANGE REPLICATION SOURCE TO SOURCE_DELAY = 300;
START REPLICA;

SHOW REPLICA STATUS\G
-- SQL_Delay: 300
-- SQL_Remaining_Delay: 299

演练:

bash 复制代码
# db1 上误删数据
mysql -uroot -plee -e "DELETE FROM timinglee.users WHERE username='lee';"

# db2 上:数据还在(延迟 300 秒还没回放)
mysql -uroot -plee -e "SELECT * FROM timinglee.users WHERE username='lee';"
# 数据还在! ← 抢救成功

# 立刻在 db2 上导出
mysqldump -uroot -plee --single-transaction --set-gtid-purged=OFF timinglee > /root/rescue.sql
ls -lh /root/rescue.sql
scp /root/rescue.sql root@192.168.0.30:/root/

# db1上:用备份恢复 db1
mysql -uroot -plee timinglee < /root/rescue.sql
mysql -uroot -plee -e "SELECT * FROM timinglee.users WHERE username='lee';"

#db2上:取消延迟,恢复正常复制
STOP REPLICA;
CHANGE REPLICATION SOURCE TO SOURCE_DELAY = 0;
START REPLICA;

面试表达:「我用延迟复制做了一个误删数据的兜底方案,从库故意延迟 5 分钟,主库误操作时可以从延迟从库快速恢复数据。」这个答案会明显比只讲主从复制的候选人强。

5.10 备份策略(XtraBackup 或 mysqldump)

执行主机 :db1(备份源;恢复验证也在 db1 上做)

5.10.1 mysqldump 逻辑备份(轻量,适合小数据量)

bash 复制代码
cat > /root/mysql_backup.sh <<'EOF'
#!/bin/bash
# MySQL 全量逻辑备份脚本
BACKUP_DIR=/backup/mysql
DATE=$(date +%F_%H%M)
KEEP_DAYS=7

mkdir -p ${BACKUP_DIR}

mysql -uroot -plee \
  -e "SHOW MASTER STATUS\G" > ${BACKUP_DIR}/master_status_${DATE}.txt

mysqldump -uroot -plee \
  --single-transaction \
  --master-data=2 \
  --flush-logs \
  --routines --triggers --events \
  --all-databases \
  --set-gtid-purged=ON \
  2>/dev/null | gzip > ${BACKUP_DIR}/all_${DATE}.sql.gz

# 清理旧备份
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +${KEEP_DAYS} -delete

echo "[$(date '+%F %T')] backup done: all_${DATE}.sql.gz ($(du -h ${BACKUP_DIR}/all_${DATE}.sql.gz | cut -f1))"
EOF

chmod +x /root/mysql_backup.sh
/root/mysql_backup.sh

# 定时任务:每天凌晨 2 点
cat > /etc/cron.d/mysql-backup <<'EOF'
0 2 * * * root /root/mysql_backup.sh >> /var/log/mysql-backup.log 2>&1
EOF

关键参数:

  • --single-transaction:InnoDB 下不锁表的一致性快照(这是最优选,不会阻塞业务)。
  • --master-data=2:在备份里记录 binlog 位置(注释形式),便于搭建新从库。GTID 模式下配合 --set-gtid-purged=ON。
  • --flush-logs:备份前切一次 binlog,便于后续做增量恢复。

5.10.2 恢复演练(必须真的做一次)

bash 复制代码
# 模拟故障:删掉一个库
mysql -uroot -plee -e "DROP DATABASE timinglee;"
mysql -uroot -plee -e "SHOW DATABASES;"
# timinglee 不见了

# 恢复
zcat /backup/mysql/all_*.sql.gz \
  | sed -e '/GTID_PURGED/d' -e '/GTID_EXECUTED/d' \
  | mysql -uroot -plee

# 验证
mysql -uroot -plee -e "SHOW DATABASES; SELECT COUNT(*) FROM timinglee.users;"

面试金句 :「备份的价值不在于备份本身,而在于能恢复。我做过完整的恢复演练,确认备份文件可用。」这句话能立刻把你和只会敲 mysqldump 的候选人区分开。

5.11 数据库 VIP 高可用(Keepalived)

执行主机 :db1、db2

数据库需要一个漂浮的入口地址,主库宕机时自动把流量引导到从库。

注意:真正的 MySQL 主从自动切换需要 MHA 或 MGR。这里先做 VIP 漂移(5.11 节),MHA 的完整方案见附录 B(因为 MHA 0.58 在 RHEL9 上需要打 Perl 补丁,且需要额外一台管理机)。

5.11.1 安装 Keepalived(db1 / db2)

bash 复制代码
dnf install -y keepalived

5.11.2 db1(MASTER)配置

bash 复制代码
vim /etc/keepalived/keepalived.conf
conf 复制代码
global_defs {
   router_id db1
   vrrp_skip_check_adv_addr
   vrrp_garp_interval 1
   vrrp_gna_interval 1
   vrrp_mcast_group4 224.0.0.45
}

# 健康检查:MySQL 实例是否存活
vrrp_script chk_mysql {
    script "/etc/keepalived/scripts/check_mysql.sh"
    interval 2
    weight -40
    fall 3
    rise 2
    timeout 5
    user root
}

vrrp_instance DB_VIP {
    state MASTER
    interface eth0
    virtual_router_id 52
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 1111
    }

    virtual_ipaddress {
        172.25.254.201/24 dev eth0 label eth0:1
    }

    track_script {
        chk_mysql
    }

    notify_master "/etc/keepalived/scripts/notify.sh master"
    notify_backup "/etc/keepalived/scripts/notify.sh backup"
    notify_fault  "/etc/keepalived/scripts/notify.sh fault"
}

5.11.3 db2(BACKUP)配置

conf 复制代码
global_defs {
   router_id db2
   vrrp_skip_check_adv_addr
   vrrp_garp_interval 1
   vrrp_gna_interval 1
   vrrp_mcast_group4 224.0.0.45
}

vrrp_script chk_mysql {
    script "/etc/keepalived/scripts/check_mysql.sh"
    interval 2
    weight -40
    fall 3
    rise 2
    timeout 5
    user root
}

vrrp_instance DB_VIP {
    state BACKUP
    interface eth0
    virtual_router_id 52
    priority 90
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 1111
    }

    virtual_ipaddress {
        172.25.254.201/24 dev eth0 label eth0:1
    }

    track_script {
        chk_mysql
    }

    notify_master "/etc/keepalived/scripts/notify.sh master"
    notify_backup "/etc/keepalived/scripts/notify.sh backup"
    notify_fault  "/etc/keepalived/scripts/notify.sh fault"
}

注意 :virtual_router_id 必须相同(52),priority 必须不同。第一层 lb 用的是 51 和 53,所以这里用 52 避免冲突。VIP 也不同(.201 vs lb 的 .200/.202)。

5.11.4 健康检查脚本

bash 复制代码
mkdir -p /etc/keepalived/scripts/

cat > /etc/keepalived/scripts/check_mysql.sh <<'EOF'
#!/bin/bash
# MySQL 实例存活检查:能读到 server-id 就算健康
mysql -uroot -plee -e "SELECT @@server_id;" > /dev/null 2>&1
exit $?
EOF

cat > /etc/keepalived/scripts/notify.sh <<'EOF'
#!/bin/bash
STATE=$1
LOG=/var/log/keepalived-notify.log
echo "$(date '+%F %T') $(hostname) state changed to $STATE" >> ${LOG}
logger -t keepalived "DB VIP state -> ${STATE} on $(hostname)"
exit 0
EOF

chmod +x /etc/keepalived/scripts/*.sh

5.11.5 启动与验证

bash 复制代码
# 语法校验(非常重要,写错一个字符 keepalived 直接起不来)
keepalived -t -f /etc/keepalived/keepalived.conf

systemctl enable --now keepalived

# 日志分离
sed -i 's|^KEEPALIVED_OPTIONS.*|KEEPALIVED_OPTIONS="-D -S 6"|' /etc/sysconfig/keepalived
grep -q "local6" /etc/rsyslog.conf || echo "local6.*    /var/log/keepalived.log" >> /etc/rsyslog.conf
systemctl restart rsyslog keepalived

验证:

bash 复制代码
# db1 上应该看到 VIP
ssh root@172.25.254.30 "ip a show eth0 | grep 172.25.254.201"
# inet 172.25.254.201/24 scope global secondary eth0:1

# 从别的机器(不是 db1/db2)通过 VIP 访问
ssh root@172.25.254.10 "mysql -uappuser -plee -h172.25.254.201 -e 'SELECT @@server_id;'"
# 30        ← 当前是 db1

重要陷阱 :不要在 db1 或 db2 本机访问 VIP 来测漂移。本机访问自己持有的 VIP 走本地回环,看不出效果。必须从第三方机器测。

5.11.6 故障漂移验证

bash 复制代码
# 在 db1 上停掉 MySQL(模拟数据库故障)
ssh root@172.25.254.30 "systemctl stop mysqld"

# keepalived 在健康检查失败 3 次后降权,VIP 漂到 db2
# 大约 6 ~ 8 秒

# 在 web1 上验证
ssh root@172.25.254.10 "mysql -uappuser -plee -h172.25.254.201 -e 'SELECT @@server_id;'"
# 40        ← 已经切到 db2!
bash 复制代码
# 查看 db2 上的 VIP
ssh root@172.25.254.40 "ip a show eth0 | grep 172.25.254.201"

# 查看告警日志
ssh root@172.25.254.40 "tail -5 /var/log/keepalived-notify.log"
# 2026-09-13 ... db2 state changed to master

恢复:

bash 复制代码
ssh root@172.25.254.30 "systemctl start mysqld"
sleep 5

# 因为 db1 priority 更高(100 > 90),它会抢回 VIP
ssh root@172.25.254.30 "ip a show eth0 | grep 172.25.254.201"

但要注意 :VIP 回来了,MySQL 的角色没有变 。db1 还是主库、db2 还是从库,架构是一致的,所以这里能正常工作。

如果真实场景中主库硬件损坏、需要把从库提升为主库,靠 VIP 漂移是不够的------从库仍然是只读的,写请求会失败。这就是为什么需要 MHA / MGR。 这句话一定要能讲出来,它证明你真正理解了这个方案的边界。

打快照 :stage5-db-ok。

第六章 lb 节点:HAProxy + Keepalived 高可用

本章操作主机 :lb1、lb2(两台命令完全一致)

lb1(172.25.254.50 / 192.168.0.50)和 lb2(172.25.254.60 / 192.168.0.60)构成对外入口。

6.1 架构与思路

执行主机:本节为架构说明,无需执行命令

复制代码
        客户端
          │  访问 172.25.254.200(VIP)
          ▼
   ┌──────────────┐
   │  Keepalived  │  ← VRRP 协议选主,持有 VIP
   ├──────────────┤
   │   HAProxy    │  ← 七层负载,健康检查,ACL
   └──────┬───────┘
          │  轮询 / 一致性哈希
    ┌─────┴─────┐
    ▼           ▼
  web1        web2

为什么要 Keepalived + HAProxy 两层?(面试必问,务必答全)

  1. HAProxy 只做负载均衡,它没有解决「自己挂了怎么办」的问题------它是单点。
  2. Keepalived 通过 VRRP 协议让两台机器共享一个 VIP,谁活着谁持有 VIP,从而解决 HAProxy 本身的单点。
  3. 也可以直接把 Nginx 装在 lb 上,用 Nginx 做七层负载 + Keepalived 做 VIP。换成 HAProxy 的理由 :健康检查更细(inter/fall/rise 三维度可调)、有原生状态页、支持 socat 热更新权重、nbthread 多线程模型在高并发下更稳,而且错误页定制更灵活。

双主模式的意义:lb1 持有 Web VIP(.200),lb2 持有监控 VIP(.202)。两台都在干活,不是一主一闲。这是真实企业里常见的做法,资源利用率 100%。

6.2 HAProxy 安装与配置

执行主机 :lb1、lb2

bash 复制代码
dnf install -y haproxy
haproxy -v
# HAProxy version 2.4.x

6.2.1 haproxy.cfg 完整配置

bash 复制代码
vim /etc/haproxy/haproxy.cfg
cfg 复制代码
# ======================== 全局配置 ========================
global
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

    stats socket /var/lib/haproxy/stats mode 600 level admin
    stats timeout 2m

    # 多线程(2 核用 2 线程;注意不能同时使用 nbproc)
    nbthread    2
    cpu-map     1 0

# ======================== 默认配置 ========================
defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option                  http-server-close
    option                  forwardfor except 127.0.0.0/8
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000

    # 自定义错误页
    errorfile 503           /etc/haproxy/errors/503.http
    errorfile 502           /etc/haproxy/errors/502.http

# ======================== 状态页 ========================
listen stats
    mode        http
    bind        0.0.0.0:4321
    stats       enable
    log         global
    stats       refresh 5
    stats       uri     /status
    stats       realm   HAProxy\ Statistics
    stats       auth    admin:lee
    stats       admin if TRUE

# ======================== Web 业务入口 ========================
frontend web_front
    bind        *:80
    mode        http

    # ---- ACL 规则 ----
    # 1) 恶意扫描器拦截
    acl bad_agent hdr_sub(User-Agent) -i -m reg (scanner|crawler|nikto|sqlmap|masscan|nmap)
    http-request deny if bad_agent

    # 2) 黑名单 IP
    acl black_ip src 172.25.254.222
    http-request deny if black_ip

    # 3) 白名单模式(只允许内网访问管理路径)
    acl admin_path path_beg -i /admin
    acl intranet   src 192.168.0.0/24 172.25.254.0/24
    http-request deny if admin_path !intranet

    # 4) 限流:单 IP 每秒最多 100 请求(防爬虫/防刷)
    stick-table type ip size 100k expire 30s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 1000 }

    # 5) 按域名分流
    acl host_www hdr_dom(host) -i www.timinglee.org
    acl host_app hdr_dom(host) -i app.timinglee.org
    acl host_api hdr_dom(host) -i api.timinglee.org
    use_backend be_webcluster if host_www
    use_backend be_webcluster if host_app
    use_backend be_api        if host_api

    # 6) 按文件后缀做动静分离
    acl url_static path_end -i .css .js .png .jpg .jpeg .gif .ico .woff .woff2 .svg .mp4
    use_backend be_static if url_static

    default_backend be_webcluster

# ======================== 后端:Web 集群 ========================
backend be_webcluster
    mode        http
    balance     roundrobin
    option      httpchk GET /health
    http-check expect status 200

    # 会话保持:插入自定义 Cookie
    cookie SERVERID insert nocache indirect

    # 透传真实客户端 IP
    option forwardfor

    server web1 192.168.0.10:80 cookie w1 check inter 2s fall 3 rise 2 weight 2
    server web2 192.168.0.20:80 cookie w2 check inter 2s fall 3 rise 2 weight 2

    # 兜底页面(所有后端都挂时启用)
    server sorry 192.168.0.50:8080 backup

# ======================== 后端:纯静态资源 ========================
backend be_static
    mode        http
    balance     roundrobin
    cookie      SERVERID insert nocache indirect
    option      httpchk GET /health
    server web1 192.168.0.10:80 cookie w1 check inter 2s fall 3 rise 2
    server web2 192.168.0.20:80 cookie w2 check inter 2s fall 3 rise 2

# ======================== 后端:API(接口服务) ========================
backend be_api
    mode        http
    balance     leastconn
    option      httpchk GET /health
    server web1 192.168.0.10:80 check inter 2s fall 3 rise 2
    server web2 192.168.0.20:80 check inter 2s fall 3 rise 2

# ======================== 四层:数据库读写入口 ========================
listen mysql_read
    bind        *:3307
    mode        tcp
    balance     roundrobin
    option      tcpka
    server db1 192.168.0.30:3306 check inter 3s fall 3 rise 2
    server db2 192.168.0.40:3306 check inter 3s fall 3 rise 2

配置详解(面试重点):

  • balance roundrobin 是动态算法 ,支持 socat 热改权重。static-rr 和 first 是静态算法 ,不支持 热改权重(会报 Backend is using a static LB algorithm and only accepts weights '0%' and '100%')。
  • check inter 2s fall 3 rise 2:每 2 秒检查一次,连续失败 3 次标记下线,连续成功 2 次标记上线。
  • cookie SERVERID insert nocache indirect:HAProxy 自己插入一个名为 SERVERID 的 Cookie 来记录后端选择。nocache 让客户端不缓存这个 Cookie,indirect 让后端看不到它。
  • option forwardfor:插入 X-Forwarded-For 头,让后端能拿到真实客户端 IP。
  • httpchk GET /health:用 HTTP 健康检查而不是 TCP 检查,能识别「端口在但应用返回 500」的情况。这是 HAProxy 比 LVS 强的地方之一。
  • leastconn 适合长连接场景(如 API 的耗时请求),roundrobin 适合短连接。
  • redispatch:后端连接失败时重新分发到另一台。但要注意,如果请求已经发出去了,redispatch 会导致重复请求,所以只建议对幂等请求使用。

6.2.2 自定义错误页

bash 复制代码
mkdir -p /etc/haproxy/errors

cat > /etc/haproxy/errors/503.http <<'EOF'
HTTP/1.0 503 Service Unavailable
Cache-Control: no-cache
Connection: close
Content-Type: text/html;charset=UTF-8

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>服务暂时不可用</title></head>
<body style="text-align:center;padding-top:100px;font-family:sans-serif">
<h1>服务暂时不可用</h1>
<p>正在紧急恢复中,请稍后重试</p>
</body></html>
EOF

cat > /etc/haproxy/errors/502.http <<'EOF'
HTTP/1.0 502 Bad Gateway
Cache-Control: no-cache
Connection: close
Content-Type: text/html;charset=UTF-8

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>网关错误</title></head>
<body style="text-align:center;padding-top:100px;font-family:sans-serif">
<h1>网关错误</h1>
<p>后端服务响应异常,请稍后重试</p>
</body></html>
EOF

格式极其严格 :第一行必须是 HTTP/1.0 <状态码> <原因短语>,然后是响应头,空一行之后才是 HTML 内容。少一个空行或状态码写错,HAProxy 会拒绝启动。

6.2.3 配置日志

bash 复制代码
# 让 rsyslog 接收 local2 的日志
vim /etc/rsyslog.conf

取消注释或添加:

复制代码
module(load="imudp")
input(type="imudp" port="514")

local2.*    /var/log/haproxy.log
bash 复制代码
systemctl restart rsyslog

mkdir -p /var/lib/haproxy
chown haproxy:haproxy /var/lib/haproxy

haproxy -c -f /etc/haproxy/haproxy.cfg
# Configuration file is valid

systemctl enable --now haproxy
ss -antlp | grep -E ':(80|4321|3307)'

6.2.4 部署 sorryserver(兜底页面)

在 lb1 上:

bash 复制代码
dnf install -y httpd
sed -i 's/^Listen 80/Listen 8080/' /etc/httpd/conf/httpd.conf
cat > /var/www/html/index.html <<'EOF'
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>系统维护中</title></head>
<body style="text-align:center;padding-top:100px;font-family:sans-serif">
<h1>服务正在维护中</h1>
<p>请稍后重试</p>
</body></html>
EOF
systemctl enable --now httpd
ss -antlp | grep 8080

sorryserver 的价值:当所有后端都不可用时,用户看到的是一个友好的提示页,而不是浏览器默认的 502/503 错误页。这是「用户体验意识」的体现,面试可以主动提。

6.2.5 验证 HAProxy 负载

bash 复制代码
# 轮询验证
for i in {1..6}; do curl -s http://192.168.0.50/hostname.html; done
# web1 web2 web1 web2 web1 web2

# 通过 lb1 的 IP 访问
for i in {1..4}; do curl -s http://172.25.254.50/hostname.html; done

# 通过域名访问
curl -s -H "Host: www.timinglee.org" http://172.25.254.50/hostname.html

# 会话保持验证:带 Cookie 的两次请求落到同一台
curl -s -c /tmp/hp.txt http://172.25.254.50/hostname.html
curl -s -b /tmp/hp.txt http://172.25.254.50/hostname.html
# 两次应该是同一个主机名
grep SERVERID /tmp/hp.txt

权重热更新(socat):

bash 复制代码
dnf install -y socat

# 查看当前权重
echo "get weight be_webcluster/web1" | socat stdio /var/lib/haproxy/stats
# 2 (initial 2)

# 动态改权重(不需要重启!)
echo "set weight be_webcluster/web1 5" | socat stdio /var/lib/haproxy/stats
echo "set weight be_webcluster/web2 1" | socat stdio /var/lib/haproxy/stats

# 验证流量分配变了
for i in {1..12}; do curl -s http://172.25.254.50/hostname.html; done
# 会明显偏向 web1

# 下线/上线后端(做灰度发布的基础)
echo "disable server be_webcluster/web2" | socat stdio /var/lib/haproxy/stats
echo "enable  server be_webcluster/web2" | socat stdio /var/lib/haproxy/stats

# 查看完整状态
echo "show stat" | socat stdio /var/lib/haproxy/stats | cut -d, -f1,2,18 | column -s, -t

如果没有加 level admin,会报 Permission denied。这是最常见的坑,你笔记里也记录了。另外注意 stats socket 的用户权限------systemctl restart haproxy 后 /var/lib/haproxy/ 里的 socket 文件会被重建,权限跟着配置走。

状态页访问 :浏览器打开 http://172.25.254.50:4321/status,账号 admin / 密码 lee。

6.2.6 IP 透传验证

bash 复制代码
# 在后端 web1 上确认收到了真实客户端 IP
curl -s http://172.25.254.50/hostname.html > /dev/null
grep -E "X-Forwarded-For|X-Real-IP" /webdir/timinglee.org/www/logs/access.log | tail -3

日志格式中的 $http_x_forwarded_for 会显示真实客户端 IP(比如 172.25.254.1,那是你的 Windows 宿主机上的 VMnet8 地址)。

面试延伸:七层透传用 X-Forwarded-For + option forwardfor;四层透传必须用 proxy_protocol(HAProxy 的 send-proxy 配合后端的 proxy_protocol)。四层透传时如果 HAProxy 还是 mode http,后端会报 502------这是你笔记里记录过的坑。

6.3 Keepalived 双主高可用

执行主机 :lb1、lb2

6.3.1 关键内核参数

HAProxy 需要绑定一个当前主机可能没有的 VIP(备份机平时不持有 VIP)。必须开启地址非本地绑定:

bash 复制代码
# lb1 和 lb2 都执行
echo "net.ipv4.ip_nonlocal_bind = 1" >> /etc/sysctl.conf
sysctl -p
sysctl net.ipv4.ip_nonlocal_bind
# net.ipv4.ip_nonlocal_bind = 1

不开这个参数,HAProxy 启动时会报 Cannot bind socket (Cannot assign requested address) [0.0.0.0:80],因为备份机上并没有 VIP 这个地址。

更优雅的替代方案是 HAProxy 配置里用 bind *:80(监听所有地址),配合 Keepalived。本项目用 ip_nonlocal_bind 是因为要明确绑定 VIP,便于理解。两种做法都要能讲。

6.3.2 lb1 配置(Web VIP 主 / 监控 VIP 备)

bash 复制代码
dnf install -y keepalived

vim /etc/keepalived/keepalived.conf
conf 复制代码
global_defs {
   router_id lb1
   vrrp_skip_check_adv_addr
   vrrp_garp_interval 1
   vrrp_gna_interval 1
   vrrp_mcast_group4 224.0.0.44
   # 新增:启用脚本安全机制
   enable_script_security
   # 指定脚本以 root 身份运行(匹配你脚本里 killall、端口检测的权限需求)
   script_user root
}

# 进程级健康检查:HAProxy 是否存活
vrrp_script chk_haproxy {
    script "/etc/keepalived/scripts/check_haproxy.sh"
    interval 2
    weight -30
    fall 2
    rise 2
    timeout 3
    user root
}

# ===== 实例一:Web VIP(lb1 为主) =====
vrrp_instance WEB_VIP {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 1111
    }

    virtual_ipaddress {
        172.25.254.200/24 dev eth0 label eth0:0
    }

    track_script {
        chk_haproxy
    }

    notify_master "/etc/keepalived/scripts/notify.sh master WEB_VIP"
    notify_backup "/etc/keepalived/scripts/notify.sh backup WEB_VIP"
    notify_fault  "/etc/keepalived/scripts/notify.sh fault  WEB_VIP"
}

# ===== 实例二:监控/状态页 VIP(lb1 为备) =====
vrrp_instance STATS_VIP {
    state BACKUP
    interface eth0
    virtual_router_id 53
    priority 90
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 1111
    }

    virtual_ipaddress {
        172.25.254.202/24 dev eth0 label eth0:2
    }

    track_script {
        chk_haproxy
    }
}

6.3.3 lb2 配置(与 lb1 镜像对称)

conf 复制代码
global_defs {
   router_id lb2
   vrrp_skip_check_adv_addr
   vrrp_garp_interval 1
   vrrp_gna_interval 1
   vrrp_mcast_group4 224.0.0.44
   # 新增:启用脚本安全机制
   enable_script_security
   # 指定脚本以 root 身份运行(匹配你脚本里 killall、端口检测的权限需求)
   script_user root
}

vrrp_script chk_haproxy {
    script "/etc/keepalived/scripts/check_haproxy.sh"
    interval 2
    weight -30
    fall 2
    rise 2
    timeout 3
    user root
}

# ===== 实例一:Web VIP(lb2 为备) =====
vrrp_instance WEB_VIP {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 90
    advert_int 1
    preempt_delay 10

    authentication {
        auth_type PASS
        auth_pass 1111
    }

    virtual_ipaddress {
        172.25.254.200/24 dev eth0 label eth0:0
    }

    track_script {
        chk_haproxy
    }

    notify_master "/etc/keepalived/scripts/notify.sh master WEB_VIP"
    notify_backup "/etc/keepalived/scripts/notify.sh backup WEB_VIP"
    notify_fault  "/etc/keepalived/scripts/notify.sh fault  WEB_VIP"
}

# ===== 实例二:监控/状态页 VIP(lb2 为主) =====
vrrp_instance STATS_VIP {
    state MASTER
    interface eth0
    virtual_router_id 53
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 1111
    }

    virtual_ipaddress {
        172.25.254.202/24 dev eth0 label eth0:2
    }

    track_script {
        chk_haproxy
    }
}

preempt_delay 10 的作用:BACKUP 变 MASTER 后,如果原 MASTER 恢复,延迟 10 秒再让出 VIP。这是为了防抖动------避免原 MASTER 因为短暂的网络闪断恢复后立刻抢回 VIP,造成来回切换、连接反复中断。这个参数很能体现经验。

6.3.4 健康检查脚本

bash 复制代码
mkdir -p /etc/keepalived/scripts/

cat > /etc/keepalived/scripts/check_haproxy.sh <<'EOF'
#!/bin/bash
# 方式一:检查进程是否存在(最轻量)
killall -0 haproxy 2>/dev/null || exit 1

# 方式二:检查状态页端口是否响应(更严格,推荐生产用)
# curl -s -o /dev/null -m 2 http://127.0.0.1:4321/status || exit 2

exit 0
EOF

cat > /etc/keepalived/scripts/notify.sh <<'EOF'
#!/bin/bash
STATE=$1
INSTANCE=$2
LOG=/var/log/keepalived-notify.log

echo "$(date '+%F %T') $(hostname) [${INSTANCE}] -> ${STATE}" >> ${LOG}
logger -t keepalived "[${INSTANCE}] state -> ${STATE}"

# 可扩展为邮件告警
# echo "VIP 状态变化: ${INSTANCE} -> ${STATE}" | mail -s "Keepalived Alert" admin@timinglee.org

exit 0
EOF

chmod +x /etc/keepalived/scripts/*.sh

两台都放好后:

bash 复制代码
# 语法检查(非常重要,写错一个字符 keepalived 直接不起来)
keepalived -t -f /etc/keepalived/keepalived.conf
# 没有输出 = 语法正确

systemctl enable --now keepalived

# 日志分离
sed -i 's|^KEEPALIVED_OPTIONS.*|KEEPALIVED_OPTIONS="-D -S 6"|' /etc/sysconfig/keepalived
grep -q "local6" /etc/rsyslog.conf || echo "local6.*    /var/log/keepalived.log" >> /etc/rsyslog.conf
systemctl restart rsyslog keepalived

6.3.5 验证双主状态

bash 复制代码
# lb1 上
ssh root@172.25.254.50 "ip -br a show eth0"
# eth0  UP  172.25.254.50/24 172.25.254.200/24

# lb2 上
ssh root@172.25.254.60 "ip -br a show eth0"
# eth0  UP  172.25.254.60/24 172.25.254.202/24

双主模式下两台各自持有不同的 VIP 。lb1 持有业务 VIP(.200),lb2 持有监控 VIP(.202)。这样两台机器都在提供服务,资源利用率 100%。这正是你笔记里 Keepalived双主模式.md 的做法。

验证 VRRP 通告:

bash 复制代码
tcpdump -i eth0 -nn host 224.0.0.44 -c 6
# IP 172.25.254.50 > 224.0.0.44: VRRPv2, Advertisement, vrid 51, prio 100, intvl 1s
# IP 172.25.254.60 > 224.0.0.44: VRRPv2, Advertisement, vrid 51, prio 90,  intvl 1s
# IP 172.25.254.60 > 224.0.0.44: VRRPv2, Advertisement, vrid 53, prio 100, intvl 1s
# IP 172.25.254.50 > 224.0.0.44: VRRPv2, Advertisement, vrid 53, prio 90,  intvl 1s

这就是双主的直接证据:vrid 51 上 lb1 优先级 100、lb2 是 90;vrid 53 上正好相反。截图保存。

6.3.6 业务 VIP 故障漂移验证

bash 复制代码
# 第 1 步:确认当前 VIP 在 lb1
ssh root@172.25.254.50 "ip a show eth0 | grep 172.25.254.200"
# inet 172.25.254.200/24 scope global secondary eth0:0

# 第 2 步:从 web1 通过 VIP 访问,确认业务正常
ssh root@172.25.254.10 "curl -s http://172.25.254.200/hostname.html"
# web1 或 web2

# 第 3 步:模拟 lb1 故障
ssh root@172.25.254.50 "systemctl stop keepalived"

# 第 4 步:立刻在 lb2 上观察(漂移应该在 3 秒内完成)
ssh root@172.25.254.60 "ip a show eth0 | grep 172.25.254.200"
# inet 172.25.254.200/24 scope global secondary eth0:0   ← 已经漂到 lb2

# 第 5 步:业务不中断
ssh root@172.25.254.10 "curl -s http://172.25.254.200/hostname.html"
# 依然正常返回

# 第 6 步:查看告警记录
ssh root@172.25.254.60 "tail -5 /var/log/keepalived-notify.log"
# ... lb2 [WEB_VIP] -> master

用量化数据证明切换时间:

bash 复制代码
# 在 web1 上持续探测(每 0.2 秒一次)
cat > /root/vip_watch.sh <<'EOF'
#!/bin/bash
FAIL=0
for i in $(seq 1 150); do
  if curl -s -m 1 -o /dev/null http://172.25.254.200/health; then
    echo "$(date +%T.%3N) OK"
    [ $FAIL -gt 0 ] && { echo ">>> 恢复,共失败 $FAIL 次(约 $((FAIL*2))00 ms)"; FAIL=0; }
  else
    echo "$(date +%T.%3N) FAIL"
    FAIL=$((FAIL+1))
  fi
  sleep 0.2
done
EOF
chmod +x /root/vip_watch.sh
bash 复制代码
# 一个终端跑监控
ssh root@172.25.254.10 "/root/vip_watch.sh"

# 另一个终端制造故障
ssh root@172.25.254.50 "systemctl stop keepalived"

正常情况下应该只有 2 ~ 5 次失败 (约 400 ms ~ 1 s)。这就是「高可用切换时间」的量化指标,可以写进简历。

恢复:

bash 复制代码
ssh root@172.25.254.50 "systemctl start keepalived"
sleep 3
# 因为 lb1 priority 更高(100 > 90),它会抢回 VIP
ssh root@172.25.254.50 "ip a show eth0 | grep 172.25.254.200"

6.3.7 HAProxy 进程级故障漂移(vrrp_script 的价值)

这一节证明 vrrp_script 的意义:Keepalived 活得好好的,但 HAProxy 挂了。这种情况靠 VRRP 自身是发现不了的,必须靠脚本。

bash 复制代码
# 在 lb1 上只停 HAProxy,keepalived 继续运行
ssh root@172.25.254.50 "systemctl stop haproxy"

# 脚本每 2 秒检查一次,连续失败 2 次后权重 100 - 30 = 70,低于 lb2 的 90
sleep 8

# 验证 VIP 已漂到 lb2
ssh root@172.25.254.60 "ip a show eth0 | grep 172.25.254.200"
# 172.25.254.200 出现了

ssh root@172.25.254.10 "curl -s http://172.25.254.200/hostname.html"
# 业务依然正常

这是本项目最值得讲的一个设计点。 面试时这样表达:

「Keepalived 的 VRRP 协议只能检测主机是否存活,检测不了上面跑的应用是否正常。所以我用 vrrp_script 每 2 秒执行一次 killall -0 haproxy,连续失败 2 次就把本机优先级降 30,让它低于备份节点,从而触发 VIP 漂移。这样就实现了应用级的健康检查,而不只是主机级。」

恢复:

bash 复制代码
ssh root@172.25.254.50 "systemctl start haproxy"
sleep 10
ssh root@172.25.254.50 "ip a show eth0 | grep 172.25.254.200"

6.3.8 脑裂防护

脑裂:两台机器都认为自己是 MASTER,同时持有 VIP。会导致流量随机乱飘、数据不一致。

产生原因:

  1. 心跳网络中断(VRRP 组播包过不去),但两台机器本身都还活着。
  2. 优先级配置错误(两台 priority 相同)。
  3. 时间不同步导致判定异常。

防护手段:

bash 复制代码
# 1) 时间同步(前面已做,验证一下)
for i in 50 60; do echo "=== lb$i ==="; ssh root@172.25.254.$i "chronyc tracking | grep -E 'Reference ID|System time|Leap'"; done

# 2) 检查优先级确实不同
for i in 50 60; do echo "=== lb$i ==="; ssh root@172.25.254.$i "grep -E 'state|priority|virtual_router_id' /etc/keepalived/keepalived.conf"; done

增加一个「双活检测」脚本,检测到对端也有 VIP 时主动让出:

bash 复制代码
cat > /etc/keepalived/scripts/check_split_brain.sh <<'EOF'
#!/bin/bash
# 如果本机持有 VIP,且对端也持有 VIP,则判定为脑裂,本机主动让出
MY_VIP=$(ip a show eth0 | grep -c "172.25.254.200")

if [ "$MY_VIP" -gt 0 ]; then
    case "$(hostname)" in
        lb1) PEER="172.25.254.60" ;;
        lb2) PEER="172.25.254.50" ;;
        *)   exit 0 ;;
    esac

    if timeout 2 ssh -o ConnectTimeout=1 -o BatchMode=yes root@$PEER \
         "ip a show eth0 | grep -q 172.25.254.200" 2>/dev/null; then
        logger -t keepalived "检测到脑裂!本机($(hostname))主动让出 VIP"
        exit 1        # 非 0 → 降权 → 让出 VIP
    fi
fi
exit 0
EOF

chmod +x /etc/keepalived/scripts/check_split_brain.sh
conf 复制代码
# 挂到 vrrp_instance 里
vrrp_script chk_splitbrain {
    script "/etc/keepalived/scripts/check_split_brain.sh"
    interval 5
    weight -60
    fall 2
    rise 2
    timeout 5
    user root
}

vrrp_instance WEB_VIP {
    # ... 原有配置 ...
    track_script {
        chk_haproxy
        chk_splitbrain
    }
}

生产环境更常用的脑裂防护是第三方仲裁(fencing) :加一台「仲裁机」,两台 lb 都向它注册,只有拿到锁的才是 MASTER。也可以用共享存储或带外管理口(IPMI)做 fencing。

这个认知层面的回答,比只会说「配好优先级」要强得多。

6.3.9 日志分离与轮转

bash 复制代码
# lb1 / lb2 都做
# 1) HAProxy 日志到独立文件
grep -q "haproxy.log" /etc/rsyslog.conf || echo "local2.*    /var/log/haproxy.log" >> /etc/rsyslog.conf

# 2) Keepalived 日志到独立文件
sed -i 's|^KEEPALIVED_OPTIONS.*|KEEPALIVED_OPTIONS="-D -S 6"|' /etc/sysconfig/keepalived
grep -q "local6" /etc/rsyslog.conf || echo "local6.*    /var/log/keepalived.log" >> /etc/rsyslog.conf

systemctl restart rsyslog keepalived haproxy

# 3) logrotate 防止日志撑爆磁盘
cat > /etc/logrotate.d/timinglee <<'EOF'
/var/log/haproxy.log
/var/log/keepalived.log
/var/log/keepalived-notify.log {
    daily
    rotate 7
    missingok
    notifempty
    compress
    copytruncate
}
EOF

# 4) 验证日志分流生效
logger -t haproxy-test "log test"
tail -3 /var/log/haproxy.log
tail -3 /var/log/keepalived.log

6.3.10 全站 HTTPS(SSL 卸载)

SSL 卸载的意义:HTTPS 加解密很消耗 CPU。在 HAProxy 上集中做一次加解密,后端 Web 节点继续用 HTTP,节省 Web 层算力。这是企业标准做法。

bash 复制代码
mkdir -p /etc/haproxy/certs
cd /etc/haproxy/certs/

openssl req -newkey rsa:2048 -nodes -sha256 -days 365 \
  -keyout timinglee.org.key \
  -x509 \
  -subj "/C=CN/ST=Shaanxi/L=Xian/O=timinglee/OU=linux/CN=www.timinglee.org" \
  -out timinglee.org.crt

# 合成 pem(HAProxy 要求 key + crt 合并成一个文件)
cat timinglee.org.key timinglee.org.crt > timinglee.pem
chmod 600 timinglee.pem
ls -l

在 haproxy.cfg 中把原来的 frontend web_front 替换为下面两个:

cfg 复制代码
# ======================== HTTP:全部跳转到 HTTPS ========================
frontend web_front_http
    bind        *:80
    mode        http
    redirect scheme https code 301 if !{ ssl_fc }

# ======================== HTTPS:真正对外服务 ========================
frontend web_front_https
    bind        *:443 ssl crt /etc/haproxy/certs/timinglee.pem alpn h2,http/1.1

    # 只启用安全的 TLS 版本与加密套件(安全加固)
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384

    mode        http
    option      httplog
    option      forwardfor

    http-request set-header X-Forwarded-Proto https

    acl host_status hdr_dom(host) -i status.timinglee.org
    use_backend be_stats if host_status

    acl bad_agent hdr_sub(User-Agent) -i -m reg (scanner|nikto|sqlmap|masscan|nmap)
    http-request deny if bad_agent

    acl host_www hdr_dom(host) -i www.timinglee.org
    acl host_app hdr_dom(host) -i app.timinglee.org
    use_backend be_webcluster if host_www
    use_backend be_webcluster if host_app

    acl url_static path_end -i .css .js .png .jpg .jpeg .gif .ico .woff .woff2 .svg .mp4
    use_backend be_static if url_static

    default_backend be_webcluster
bash 复制代码
haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl restart haproxy

验证:

bash 复制代码
# HTTP 应该返回 301
curl -I http://172.25.254.50/
# HTTP/1.1 301 Moved Permanently
# location: https://172.25.254.50/

# HTTPS 应该正常(-k 跳过自签证书校验)
curl -kI https://172.25.254.50/
# HTTP/2 200
# server: TIMINGLEE/

# 查看协议版本与加密套件
curl -kI --tlsv1.2 https://172.25.254.50/ -v 2>&1 | grep -E "SSL connection|ALPN|TLS"

# 验证弱协议被拒绝
curl -k --tlsv1.0 https://172.25.254.50/ 2>&1 | head -2
# 应该握手失败(因为 ssl-min-ver TLSv1.2)

面试延伸:

  • SSL 卸载(SSL Termination):代理层解密,后端明文。优点:后端省 CPU;缺点:代理到后端这段是明文,需要内网可信。
  • SSL 透传(SSL Pass-through):四层转发,不解密。后端自己处理证书。代理看不到内容,做不了七层路由。
  • SSL 桥接(SSL Bridging):代理解密后再重新加密到后端。最安全但最耗 CPU。

打快照 :stage6-ha-final-ok。


第七章 进阶:用 LVS DR 模式替换 HAProxy

本章操作主机 :lb1、lb2(调度器);web1、web2(真实服务器 RS)

这一章是可选加分项。做完之后你的技术栈里就有了 LVS,简历上能写「掌握四层负载均衡 LVS(NAT / DR / TUN / FULLNAT)」。

7.1 为什么加这一层

执行主机:本节为说明,无需执行命令

  • HAProxy 是七层负载,能理解 HTTP,可以做基于 URL、Cookie、Header 的精细调度,但七层必须完整解析请求,吞吐上限受 CPU 限制。
  • LVS 是四层负载,工作在传输层,只改包头不改内容,性能高一个数量级(单台 LVS 能扛几十万并发连接),但做不了七层精细控制。

企业标准架构 = LVS(四层)+ Nginx/HAProxy(七层)+ 应用。四层先做粗粒度分流,七层再做精细路由。这是面试里能明显拉开差距的一段。

7.2 LVS DR 模式原理

执行主机:本节为原理说明,无需执行命令

DR(Direct Routing,直接路由)模式的精髓:

复制代码
                        ┌──────────────────────────┐
客户端 ──请求──→ VIP ──→│  LVS 调度器(lb1/lb2)    │
                        │  只改目标 MAC,不改 IP     │
                        └───────────┬──────────────┘
                                    │ 转发给某台 RS 的 MAC
                          ┌─────────┴─────────┐
                          ▼                   ▼
                       web1 (lo:VIP)       web2 (lo:VIP)
                          │                   │
                          └───── 直接回包给客户端 ─────┘
                                (响应不经过调度器)

三个关键点(面试必答):

  1. 请求经过调度器,响应不经过调度器。调度器只做「改 MAC 转发」,回包由 RS 直接发给客户端。所以调度器不会成为带宽瓶颈------这是 DR 模式性能最高的原因。
  2. VIP 必须同时配置在调度器(对外)和每台 RS 的 lo 网卡上(/32)。因为回包时源 IP 必须是 VIP,客户端才认。
  3. RS 必须做 ARP 抑制。否则 RS 会响应 VIP 的 ARP 请求(它 lo 上有这个 IP),导致客户端的 ARP 表把 VIP 指向 RS 而不是调度器,调度就失效了。

7.3 环境准备

执行主机 :lb1、lb2

先在 lb1 / lb2 上停掉 HAProxy(避免抢 80 端口):

bash 复制代码
# lb1 和 lb2
systemctl stop haproxy
systemctl disable haproxy

7.4 在 RS 上配置 VIP 与 ARP 抑制

执行主机 :web1、web2(真实服务器 RS)

在 web1 和 web2 上都执行。

7.4.1 lo 网卡上添加 VIP

bash 复制代码
cd /etc/NetworkManager/system-connections/
cp -p eth0.nmconnection lo.nmconnection
vim lo.nmconnection
ini 复制代码
[connection]
id=lo
type=loopback
interface-name=lo

[ethernet]

[ipv4]
method=manual
address1=127.0.0.1/8
address2=172.25.254.200/32

[ipv6]
addr-gen-mode=eui64
method=ignore

[proxy]
bash 复制代码
nmcli connection reload
nmcli connection up lo

# 验证
ip a show lo
# inet 127.0.0.1/8 scope host lo
# inet 172.25.254.200/32 scope global lo        ← VIP 已配置

7.4.2 ARP 抑制(DR 模式最关键的一步)

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
EOF

sysctl -p /etc/sysctl.d/90-lvs-dr.conf

参数含义(面试必答):

  • arp_ignore = 1:只回答「目标 IP 是本接口 IP」的 ARP 请求。这样当别人问「谁是 172.25.254.200」时,RS 不会用 lo 上的地址去应答(因为请求是从 eth0 进来的,而 VIP 在 lo 上)。
  • arp_announce = 2:发送 ARP 通告时,只使用与本接口地址在同一子网的源地址。防止 RS 用 lo 上的 VIP 作为源去通告。

不做的后果:VIP 会被多台 RS 同时宣称持有,客户端的 ARP 表混乱,请求时通时不通------这是最经典的 DR 模式故障。

7.4.3 验证 ARP 抑制生效

bash 复制代码
# 在 web1 上看 ARP 通告行为
tcpdump -i eth0 -nn arp -c 10

# 在别处 ping VIP 时,应该只有调度器响应 ARP(从客户端 arp -n 验证)

7.5 配置 LVS 调度器

执行主机 :lb1、lb2(调度器)

在 lb1 上(lb2 稍后由 Keepalived 自动同步规则):

bash 复制代码
dnf install -y ipvsadm

7.5.1 DR 模式规则

bash 复制代码
# 清空旧规则
ipvsadm -C

# 添加虚拟服务(VIP:80,调度算法 wrr)
ipvsadm -A -t 172.25.254.200:80 -s wrr

# 添加真实服务器,-g 表示 DR(直接路由)模式
ipvsadm -a -t 172.25.254.200:80 -r 192.168.0.10:80 -g -w 1
ipvsadm -a -t 172.25.254.200:80 -r 192.168.0.20:80 -g -w 1

# 查看
ipvsadm -Ln

期望输出:

复制代码
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  172.25.254.200:80 wrr
  -> 192.168.0.10:80              Route   1      0          0
  -> 192.168.0.20:80              Route   1      0          0

Route 就是 DR 模式的标识。NAT 模式显示 Masq,TUN 模式显示 Tunnel。

7.5.2 测试

bash 复制代码
# 从 web1 或任意第三方机器上测试(注意不要在 lb1/lb2 本机测)
for i in {1..8}; do curl -s http://172.25.254.200/hostname.html; done
# web1 web2 web1 web2 ...

注意端口问题:RS 上现在有 Nginx 监听 80,同时也可能被 HAProxy 占用过。确保 lb1/lb2 上的 HAProxy 已停止。

7.5.3 权重调整与持久连接

bash 复制代码
# 改权重(临时调整流量分配)
ipvsadm -e -t 172.25.254.200:80 -r 192.168.0.10:80 -g -w 3

# 验证流量倾斜
for i in {1..12}; do curl -s http://172.25.254.200/hostname.html; done

# 持久连接(同一客户端 N 秒内固定到同一台 RS)
ipvsadm -E -t 172.25.254.200:80 -s rr -p 30

# 查看持久连接表
ipvsadm -Lnc
# pro expire state       source             virtual            destination
# TCP 00:29  ESTABLISHED 172.23.254.1:54321 172.25.254.200:80  192.168.0.10:80

-p 30 表示 30 秒内同一源 IP 的连接都固定到同一台 RS,实现会话粘滞。这对没有做 Session 共享的老系统是必要的。项目里我们做了 MSM 会话共享,所以其实不需要持久连接------但面试会问,你要能讲。

7.5.4 规则持久化

bash 复制代码
# 保存规则
ipvsadm-save -n > /etc/sysconfig/ipvsadm
cat /etc/sysconfig/ipvsadm

# 让 ipvsadm 服务开机自动恢复规则
systemctl enable --now ipvsadm.service

# 验证:清空后重启服务
ipvsadm -C
ipvsadm -Ln        # 空
systemctl restart ipvsadm
ipvsadm -Ln        # 规则回来了

7.5.5 用防火墙标记统一多端口调度(进阶)

当 RS 同时提供 80 和 443 时,两个虚拟服务各自独立轮询,会导致同一客户端访问 80 和 443 落到不同 RS,会话不一致。用 iptables mangle 打标记 + ipvsadm -f 解决。

bash 复制代码
# 给 80 和 443 的包打上同一个标记 6666
iptables -t mangle -A PREROUTING -d 172.25.254.200 -p tcp -m multiport --dports 80,443 -j MARK --set-mark 6666

# 用防火墙标记创建虚拟服务
ipvsadm -A -f 6666 -s rr -p 30
ipvsadm -a -f 6666 -r 192.168.0.10:0 -g
ipvsadm -a -f 6666 -r 192.168.0.20:0 -g

ipvsadm -Ln
# FWM  6666 rr persistent 30
#   -> 192.168.0.10:0    Route   1      0          0
#   -> 192.168.0.20:0    Route   1      0          0

FWM(FireWall Mark)是 LVS 的一个高级用法。端口写 0 表示不限制端口(因为标记已经限定了)。这也是你笔记里 利用火墙标记解决轮询错误.md 的内容。

7.6 用 Keepalived 管理 LVS(企业级做法)

执行主机 :lb1、lb2

手工敲 ipvsadm 命令不适合生产------规则不会自动同步,也没法做 RS 健康检查。企业做法是用 Keepalived 直接管理 IPVS,它会:

  1. 自动在两台调度器间同步 LVS 规则(通过 VRRP 通告携带)。
  2. 对每台 RS 做健康检查,挂掉的自动从 IPVS 表里摘除。

7.6.1 前端虚拟服务也要配 VIP

前端(lb1/lb2)的 keepalived.conf 里已经有 WEB_VIP 实例(6.3.2 节),VIP 172.25.254.200 已经配好。RS 上的 lo 也配了同一个 VIP(7.4.1 节)。这是 DR 模式的完整形态。

7.6.2 在 Keepalived 里定义 virtual_server

在 lb1 和 lb2 的 keepalived.conf 末尾 (或写到 /etc/keepalived/conf.d/webipvs.conf)追加:

conf 复制代码
virtual_server 172.25.254.200 80 {
    delay_loop 6
    lb_algo wrr
    lb_kind DR
    persistence_timeout 30
    protocol TCP

    real_server 192.168.0.10 80 {
        weight 1
        HTTP_GET {
            url {
              path /health
              status_code 200
            }
            connect_timeout 2
            retry 3
            delay_before_retry 1
        }
    }

    real_server 192.168.0.20 80 {
        weight 1
        HTTP_GET {
            url {
              path /health
              status_code 200
            }
            connect_timeout 2
            retry 3
            delay_before_retry 1
        }
    }
}

用 include 拆分更利于维护:

bash 复制代码
mkdir -p /etc/keepalived/conf.d
# 把上面的 virtual_server 块写到 /etc/keepalived/conf.d/webipvs.conf
# 然后在 keepalived.conf 的 global_defs 后面加一行:
# include /etc/keepalived/conf.d/*.conf
bash 复制代码
keepalived -t -f /etc/keepalived/keepalived.conf
systemctl restart keepalived

7.6.3 验证 Keepalived 管理的 IPVS

bash 复制代码
# 自动生成了 IPVS 规则
ipvsadm -Ln
# TCP  172.25.254.200:80 wrr persistent 30
#   -> 192.168.0.10:80     Route   1      0          0
#   -> 192.168.0.20:80     Route   1      0          0

# 规则自动同步到了备份节点
ssh root@172.25.254.60 "ipvsadm -Ln"
# 一样的内容

# 健康检查生效:停掉 web2 的 Nginx
ssh root@172.25.254.20 "systemctl stop nginx"
sleep 10

# web2 被自动摘除
ipvsadm -Ln
# 只剩 192.168.0.10 了!

ssh root@172.25.254.10 "curl -s http://172.25.254.200/hostname.html"
# web1        ← 流量全部落到 web1,业务不中断

# 恢复
ssh root@172.25.254.20 "systemctl start nginx"
sleep 10
ipvsadm -Ln
# web2 自动回来了

这是 LVS + Keepalived 最有价值的特性 :调度器自己会做 RS 的健康检查,并动态增删 IPVS 表项。对比 HAProxy 的表现形式不同,但目的相同。

注意:不要在 lb1 或 lb2 本机用 curl 访问 VIP,会走本地回环导致检测不出效果。这是你笔记里明确记录过的坑。

7.6.4 四层 vs 七层对比实验(面试杀手锏)

这是本项目的终极实验 :在同一个 VIP 上,先跑 LVS 四层,再跑 HAProxy 七层,用 ab 压测对比。

bash 复制代码
dnf install -y httpd-tools

# ===== 测 LVS DR(四层)=====
# 确认当前是 LVS 在服务
ipvsadm -Ln | head -5
systemctl stop haproxy

ab -n 50000 -c 500 -k http://172.25.254.200/hostname.html
# 记录 Requests per second 和 Time per request

# ===== 测 HAProxy(七层)=====
# 停掉 LVS 规则,启动 HAProxy
ipvsadm -C
systemctl start haproxy
ss -antlp | grep ':80'

ab -n 50000 -c 500 -k http://172.25.254.200/hostname.html
# 记录数据

预期结论:LVS 因为只改 MAC 不做 HTTP 解析,静态短连接场景下 QPS 通常比 HAProxy 高 20% ~ 50%;但 HAProxy 能做基于 URL/Cookie/Header 的七层路由,功能强得多。

面试黄金答法 :

「我在项目里同时部署了 LVS 和 HAProxy 做对比压测。LVS 四层在纯转发场景 QPS 更高,因为只改 MAC 不解析报文;HAProxy 七层虽然略慢,但能做基于域名、URL、Cookie 的精细调度,还有原生状态页和热更新能力。所以企业架构通常是 LVS 做四层入口扛量、HAProxy/Nginx 做七层做业务路由,两者配合,而不是二选一。」

打快照 :stage7-lvs-ok。

第八章 全链路联调与性能压测

本章操作主机:按顺序涉及全部 7 台,每个小节已单独标注

8.1 完整启动顺序

执行主机 :按顺序:infra → db1/db2 → web1/web2 → lb1/lb2

必须按这个顺序启动,否则会出现「应用起了但连不上数据库」之类的伪故障。

bash 复制代码
# 1. infra(基础服务最先)
ssh root@172.25.254.100 "systemctl start chronyd nfs-server rsyslog named redis_6379 redis-sentinel"

# 2. db(数据层)
ssh root@172.25.254.30 "systemctl start mysqld keepalived"
ssh root@172.25.254.40 "systemctl start mysqld keepalived redis-sentinel"

# 3. web(应用层)
for i in 10 20; do
  ssh root@172.25.254.$i "systemctl start memcached tomcat nginx"
done
ssh root@172.25.254.20 "systemctl start redis-sentinel"

# 4. lb(接入层最后)
for i in 50 60; do
  ssh root@172.25.254.$i "systemctl start keepalived"
done

一键健康检查脚本:

bash 复制代码
cat > /root/cluster_check.sh <<'SH'
#!/bin/bash
GREEN='\033[0;32m'; RED='\033[0;31m'; YEL='\033[0;33m'; NC='\033[0m'
ok(){ echo -e "${GREEN}[ OK ]${NC} $1"; }
bad(){ echo -e "${RED}[FAIL]${NC} $1"; }
warn(){ echo -e "${YEL}[WARN]${NC} $1"; }

echo "=========== 项目一 集群健康检查 $(date '+%F %T') ==========="

echo "--- 1. 网络连通性 ---"
for h in 172.25.254.100 172.25.254.30 172.25.254.40 172.25.254.10 172.25.254.20 172.25.254.50 172.25.254.60; do
  ping -c1 -W1 $h >/dev/null 2>&1 && ok "ping $h" || bad "ping $h"
done

echo "--- 2. 基础服务 ---"
ssh -o ConnectTimeout=2 root@172.25.254.100 "systemctl is-active --quiet chronyd" && ok "chrony" || bad "chrony"
ssh -o ConnectTimeout=2 root@172.25.254.100 "systemctl is-active --quiet nfs-server" && ok "nfs" || bad "nfs"
ssh -o ConnectTimeout=2 root@172.25.254.100 "systemctl is-active --quiet named" && ok "dns" || bad "dns"
ssh -o ConnectTimeout=2 root@172.25.254.100 "systemctl is-active --quiet rsyslog" && ok "rsyslog" || bad "rsyslog"

echo "--- 3. 数据库 ---"
mysql -uroot -plee -h192.168.0.30 -e "SELECT 1" >/dev/null 2>&1 && ok "MySQL db1" || bad "MySQL db1"
mysql -uroot -plee -h192.168.0.40 -e "SELECT 1" >/dev/null 2>&1 && ok "MySQL db2" || bad "MySQL db2"
mysql -uroot -plee -h192.168.0.40 -e "SHOW REPLICA STATUS\G" 2>/dev/null | grep -q "Replica_IO_Running: Yes" && ok "主从 IO 线程" || bad "主从 IO 线程"
mysql -uroot -plee -h192.168.0.40 -e "SHOW REPLICA STATUS\G" 2>/dev/null | grep -q "Replica_SQL_Running: Yes" && ok "主从 SQL 线程" || bad "主从 SQL 线程"
mysql -uroot -plee -h192.168.0.30 -e "SHOW STATUS LIKE 'Rpl_semi_sync_master_status'\G" 2>/dev/null | grep -q "ON" && ok "半同步复制" || warn "半同步复制(可能为 OFF)"

echo "--- 4. Redis ---"
redis-cli -a lee -h192.168.0.100 ping 2>/dev/null | grep -q PONG && ok "Redis master" || bad "Redis master"
redis-cli -a lee -h192.168.0.20 ping 2>/dev/null | grep -q PONG && ok "Redis slave" || bad "Redis slave"
redis-cli -p 26379 -h192.168.0.100 sentinel get-master-addr-by-name mymaster >/dev/null 2>&1 && ok "Redis Sentinel" || bad "Redis Sentinel"

echo "--- 5. Web 层 ---"
for i in 10 20; do
  curl -s -m 2 http://192.168.0.$i/health >/dev/null && ok "Nginx web$i" || bad "Nginx web$i"
  curl -s -m 2 http://192.168.0.$i:8080/ >/dev/null && ok "Tomcat web$i" || bad "Tomcat web$i"
done

echo "--- 6. 接入层 ---"
for i in 50 60; do
  ssh -o ConnectTimeout=2 root@172.25.254.$i "systemctl is-active --quiet haproxy" && ok "HAProxy lb$i" || bad "HAProxy lb$i"
  ssh -o ConnectTimeout=2 root@172.25.254.$i "systemctl is-active --quiet keepalived" && ok "Keepalived lb$i" || bad "Keepalived lb$i"
done

echo "--- 7. VIP 归属 ---"
ssh -o ConnectTimeout=2 root@172.25.254.50 "ip a show eth0 | grep -q 172.25.254.200" && ok "Web VIP 在 lb1" || warn "Web VIP 不在 lb1"
ssh -o ConnectTimeout=2 root@172.25.254.60 "ip a show eth0 | grep -q 172.25.254.202" && ok "Stats VIP 在 lb2" || warn "Stats VIP 不在 lb2"
ssh -o ConnectTimeout=2 root@172.25.254.30 "ip a show eth0 | grep -q 172.25.254.201" && ok "DB VIP 在 db1" || warn "DB VIP 不在 db1"

echo "--- 8. 端到端业务 ---"
for i in 1 2 3 4; do
  R=$(curl -s -m 2 http://172.25.254.200/hostname.html)
  [ -n "$R" ] && ok "VIP 业务请求 -> $R" || bad "VIP 业务请求无响应"
done

echo "--- 9. DNS 解析 ---"
dig www.timinglee.org @192.168.0.100 +short | grep -q 172.25.254.200 && ok "DNS 解析 www" || bad "DNS 解析 www"

echo "--- 10. 集中日志 ---"
ssh -o ConnectTimeout=2 root@172.25.254.100 "ls /var/log/remote/ | wc -l" | awk '{if($1>=5) print "\033[0;32m[ OK ]\033[0m 已收集 "$1" 个节点的日志"; else print "\033[0;31m[FAIL]\033[0m 只收集到 "$1" 个节点"}'

echo "=========== 检查完成 ==========="
SH

chmod +x /root/cluster_check.sh
/root/cluster_check.sh

8.2 端到端业务链路验证

执行主机 :任意客户端节点发起(推荐 web1),链路经过全部 7 台

8.2.1 完整链路打点

bash 复制代码
cat > /root/e2e_test.sh <<'SH'
#!/bin/bash
# 端到端链路:客户端 -> VIP -> HAProxy -> Nginx -> Tomcat -> Memcached/MySQL/NFS
VIP=172.25.254.200

echo "=== 1. DNS 解析 ==="
echo "www.timinglee.org -> $(dig www.timinglee.org @172.25.254.100 +short)"

echo "=== 2. HTTPS 跳转 ==="
curl -sI http://$VIP/ | grep -E "^HTTP|^location"

echo "=== 3. 负载均衡(静态页面,10 次)==="
for i in $(seq 1 10); do curl -s -m 2 http://$VIP/hostname.html; done

echo ""
echo "=== 4. 动静分离验证 ==="
echo "--- 静态资源(Nginx 直出)---"
curl -sI http://$VIP/css/main.css | grep -E "^HTTP|^Server|^Cache-Control"
echo "--- 动态页面(Tomcat 处理)---"
curl -s http://$VIP/info.jsp | grep -E "后端主机名|SessionID"

echo "=== 5. 会话共享(跨节点)==="
curl -s -c /tmp/e2e.txt http://$VIP/info.jsp | grep -E "后端主机名|SessionID|会话访问次数"
sleep 1
curl -s -b /tmp/e2e.txt http://$VIP/info.jsp | grep -E "后端主机名|SessionID|会话访问次数"

echo "=== 6. NFS 共享存储 ==="
echo "nfs-test-$(date +%s)" > /data/nfs/upload/e2e.txt
[ -f /data/nfs/upload/e2e.txt ] && echo "NFS 写入成功: $(cat /data/nfs/upload/e2e.txt)"

echo "=== 7. 数据库读写(经 VIP)==="
mysql -uappuser -plee -h172.25.254.201 -e "SELECT @@server_id AS backend_server_id, COUNT(*) AS user_count FROM timinglee.users;" 2>/dev/null

echo "=== 8. 反向代理到后端的 IP 透传 ==="
ssh root@172.25.254.10 "tail -1 /webdir/timinglee.org/www/logs/access.log"
SH

chmod +x /root/e2e_test.sh
/root/e2e_test.sh

8.2.2 预期输出示例

复制代码
=== 1. DNS 解析 ===
www.timinglee.org -> 172.25.254.200

=== 2. HTTPS 跳转 ===
HTTP/1.1 301 Moved Permanently
location: https://172.25.254.200/

=== 3. 负载均衡(静态页面,10 次)===
web2
web1
web2
web1
...

=== 4. 动静分离验证 ===
--- 静态资源(Nginx 直出)---
HTTP/1.1 200 OK
Server: TIMINGLEE/
Cache-Control: public, max-age=604800
--- 动态页面(Tomcat 处理)---
后端主机名    web1
SessionID     C3F9A2B1D4E5...

=== 5. 会话共享(跨节点)===
后端主机名      web1
SessionID       C3F9A2B1D4E5...
会话访问次数    1
后端主机名      web2        ← 换节点了
SessionID       C3F9A2B1D4E5...   ← SessionID 不变!
会话访问次数    2

=== 6. NFS 共享存储 ===
NFS 写入成功: nfs-test-1757745000

=== 7. 数据库读写(经 VIP)===
backend_server_id   user_count
30                  2

=== 8. 反向代理到后端的 IP 透传 ===
172.25.254.1 - - [13/Sep/2026:14:30:00 +0800] "GET /hostname.html HTTP/1.1" 200 5 "-" "curl/7.61.1" "172.25.254.1" upstream_time=0.001 request_time=0.001

这一份输出就是完整的项目证明,务必截图保存。

8.3 性能压测与优化对比

执行主机 :web1(压测机);被压对象是 lb1 上的 Web VIP

8.3.1 压测基线(不加缓存的动态请求)

bash 复制代码
# 在 web1 上安装压测工具
dnf install -y httpd-tools

# 造一个有一定计算量的 PHP/JSP 页面作为压测目标
# 这里用一个简单页面,重点是测吞吐
ssh root@172.25.254.10 "curl -s -o /dev/null -w '%{http_code}' http://192.168.0.10/info.jsp"
bash 复制代码
# 基线压测(直接访问单台 Nginx + Tomcat)
ab -n 10000 -c 200 http://192.168.0.10/info.jsp

记录关键指标:

复制代码
Requests per second:    xxxx.xx [#/sec] (mean)
Time per request:       xxx.xxx [ms] (mean)
Failed requests:        x

8.3.2 Nginx proxy_cache 缓存加速对比(核心数据)

这是本项目最有说服力的量化数据。

第 1 步:在 web1 上配置 proxy_cache

bash 复制代码
mkdir -p /usr/local/nginx/proxy_cache
chown nginx:nginx /usr/local/nginx/proxy_cache

vim /usr/local/nginx/conf/nginx.conf

在 http 块中添加:

nginx 复制代码
# 代理缓存配置
proxy_cache_path /usr/local/nginx/proxy_cache levels=1:2:2 keys_zone=proxycache:20m inactive=120s max_size=1g;
proxy_temp_path /usr/local/nginx/proxy_cache/temp;

修改站点配置里的动态 location:

nginx 复制代码
    location ~* \.(jsp|do|action)$ {
        proxy_pass http://tomcat_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # ===== 缓存配置 =====
        proxy_cache proxycache;
        proxy_cache_key $request_uri;
        proxy_cache_valid 200 302 301 10m;
        proxy_cache_valid any 1m;
        proxy_cache_min_uses 1;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;

        # 加一个响应头便于判断是否命中缓存
        add_header X-Cache-Status $upstream_cache_status;
    }
bash 复制代码
nginx -t && systemctl reload nginx

# 验证缓存生效
curl -sI http://192.168.0.10/info.jsp | grep X-Cache-Status
# X-Cache-Status: MISS       ← 第一次
curl -sI http://192.168.0.10/info.jsp | grep X-Cache-Status
# X-Cache-Status: HIT        ← 第二次命中缓存!

这里为了演示缓存效果先对 JSP 做缓存。生产环境不要缓存带用户信息的动态页面 ,否则会把 A 用户的页面返回给 B 用户,这是严重的数据泄露事故。正确做法是缓存不区分用户 的数据接口(如商品列表、排行榜),或者在 proxy_cache_key 里带上用户标识。

第 2 步:压测对比

bash 复制代码
# ===== 未开缓存(先把上面缓存配置注释掉、reload)=====
nginx -t && systemctl reload nginx
ab -n 10000 -c 50 http://192.168.0.10/info.jsp
# Requests per second:   731.10 [#/sec] (mean)      ← 你的笔记里的实测数据
# Failed requests:        9963

# ===== 开启缓存 =====
nginx -t && systemctl reload nginx
ab -n 10000 -c 50 http://192.168.0.10/info.jsp
# Requests per second:   2290.76 [#/sec] (mean)     ← 提升约 3.1 倍
# Failed requests:        0

这组数据(QPS 731 → 2290,提升 3.1 倍;失败请求 9963 → 0)就是简历上最有价值的一行。 它是你自己压测出来的,面试官问细节你答得上。

第 3 步:观察缓存目录结构

bash 复制代码
tree /usr/local/nginx/proxy_cache/
# /usr/local/nginx/proxy_cache/
# └── 1
#     └── af
#         └── 15
#             └── e251273eb74a8ee3f661a7af00915af1

8.3.3 各层压测对比汇总表

把下面这张表填满,就是你的项目性能报告。

测试场景 并发 QPS 平均响应时间 失败数
直连 Nginx 静态页(hostname.html) 500
直连 Nginx + Tomcat 动态页(info.jsp) 50 731 9963
Nginx + Tomcat + proxy_cache 50 2290 0
经 HAProxy 七层 → Nginx 静态 500
经 LVS DR 四层 → Nginx 静态 500
经 VIP 全链路 → 动态页 200

测试脚本:

bash 复制代码
cat > /root/bench.sh <<'SH'
#!/bin/bash
TARGET=${1:-http://192.168.0.10/hostname.html}
N=${2:-50000}
C=${3:-500}

echo "=== 压测目标: $TARGET  请求数: $N  并发: $C ==="
ab -n $N -c $C -k $TARGET 2>/dev/null | grep -E \
  "Complete requests|Failed requests|Requests per second|Time per request|Transfer rate|Non-2xx"
SH
chmod +x /root/bench.sh

# 用法
/root/bench.sh http://192.168.0.10/hostname.html 50000 500          # Nginx 静态
/root/bench.sh http://172.25.254.200/hostname.html 50000 500          # 经 VIP 全链路

8.4 数据一致性验证

执行主机 :db1(写入)、db2(读取校验)

bash 复制代码
cat > /root/consistency_test.sh <<'SH'
#!/bin/bash
# 数据一致性验证:主库写 -> 从库读 -> 校验一致
echo "=== 数据一致性验证 ==="

for i in $(seq 1 20); do
  mysql -uroot -plee -h192.168.0.30 -e \
    "INSERT INTO timinglee.orders(user_id,amount,status) VALUES($((RANDOM%2+1)),$RANDOM.00,1);" 2>/dev/null
done

sleep 2

M=$(mysql -uroot -plee -N -h192.168.0.30 -e "SELECT COUNT(*) FROM timinglee.orders;" 2>/dev/null)
S=$(mysql -uroot -plee -N -h192.168.0.40 -e "SELECT COUNT(*) FROM timinglee.orders;" 2>/dev/null)

echo "主库订单数: $M"
echo "从库订单数: $S"

if [ "$M" = "$S" ]; then
  echo "结果: 数据一致 [通过]"
else
  echo "结果: 数据不一致 [失败]"
  exit 1
fi

echo ""
echo "=== 主从延迟 ==="
mysql -uroot -plee -h192.168.0.40 -e "SHOW REPLICA STATUS\G" 2>/dev/null | \
  grep -E "Seconds_Behind_Source|Replica_IO_Running|Replica_SQL_Running"

echo ""
echo "=== 半同步状态 ==="
mysql -uroot -plee -h192.168.0.30 -e "SHOW STATUS LIKE 'Rpl_semi_sync%';" 2>/dev/null
SH

chmod +x /root/consistency_test.sh
/root/consistency_test.sh

8.5 压力下的高可用验证(综合演练)

执行主机 :web1(压测)+ lb1 / lb2(制造故障)

目标:在持续压测的情况下,依次干掉各个组件,验证业务不中断。

bash 复制代码
# 终端 1:持续压测
ssh root@172.25.254.10 "ab -n 200000 -c 100 -k http://172.25.254.200/hostname.html"

在压测进行中,依次在另一个终端 执行下面的动作,观察 ab 输出中的 Failed requests 数量:

步骤 动作 期望结果
1 ssh root@172.25.254.20 "systemctl stop tomcat" 失败请求数极少(HAProxy 健康检查摘除后有短暂窗口)
2 ssh root@172.25.254.20 "systemctl start tomcat" 恢复正常
3 ssh root@172.25.254.50 "systemctl stop haproxy" 失败 2 ~ 5 个请求(VIP 漂移窗口),之后恢复
4 ssh root@172.25.254.50 "systemctl start haproxy" 恢复正常
5 ssh root@172.25.254.30 "systemctl stop mysqld" 静态页不受影响;动态页可能报错(因为 Tomcat 连不上库)
6 ssh root@172.25.254.30 "systemctl start mysqld" 恢复正常

把每一步的 Failed requests 数字记下来,这就是「高可用能力」的量化证据。

步骤 5 会暴露一个真实问题:当主库宕机、VIP 漂到从库后,从库是只读的,写操作会失败。这正是需要 MHA/MGR 的原因。面试时主动指出这个局限,比强行说「我的架构万无一失」要可信得多。


第九章 故障演练剧本

本章操作主机:按剧本涉及不同节点,每个演练已单独标注

把这一章当成面试前的实战彩排 。每个故障你都要能:制造 → 观察 → 定位 → 修复 → 复述。

9.1 演练一:Web 节点整体宕机

执行主机 :在 web1 上制造故障;在 lb1、web2 上观察

制造:

bash 复制代码
ssh root@172.25.254.10 "systemctl stop nginx tomcat"

观察:

bash 复制代码
# 业务不中断,流量全部落到 web2
for i in {1..10}; do curl -s http://172.25.254.200/hostname.html; done
# 全是 web2

# HAProxy 状态页显示 web1 为 DOWN
ssh root@172.25.254.50 "echo 'show stat' | socat stdio /var/lib/haproxy/stats | grep web1 | cut -d',' -f1,2,18,41"

定位思路 :查看 HAProxy 日志确认健康检查失败 → 查看 web1 的 systemctl status nginx → 查看 /usr/local/nginx/logs/error.log。

修复:

bash 复制代码
ssh root@172.25.254.10 "systemctl start nginx tomcat"
sleep 5
ssh root@172.25.254.50 "echo 'show stat' | socat stdio /var/lib/haproxy/stats | grep web1 | cut -d',' -f1,2,18,41"
# 状态恢复为 UP

9.2 演练二:Tomcat 进程假死(端口在但应用不响应)

执行主机 :在 web1 上制造故障;在 lb1 上观察

这是最阴险的故障:进程活着、端口监听着,但应用不响应请求。纯 TCP 健康检查发现不了。

制造:

bash 复制代码
# 让 Tomcat 线程池打满(或者简单点:用 iptables 丢弃 8080 的包,模拟应用无响应)
ssh root@172.25.254.20 "iptables -I INPUT -p tcp --dport 8080 -j DROP"

观察:

bash 复制代码
# TCP 连接测试还是通的
ssh root@172.25.254.20 "ss -antlp | grep 8080"
# LISTEN

# 但 HTTP 请求超时
ssh root@172.25.254.20 "curl -m 3 -sI http://127.0.0.1:8080/ ; echo exit=\$?"

# Nginx 层面会报 502/504
curl -sI http://172.25.254.200/info.jsp
# HTTP/1.1 504 Gateway Time-out

修复与预防:

bash 复制代码
ssh root@172.25.254.20 "iptables -D INPUT -p tcp --dport 8080 -j DROP"

关键教训 :Nginx 的 upstream 健康检查是被动 的(max_fails / fail_timeout),只有请求失败才会标记下线。要主动检测,需要:

  1. Nginx Plus 的 health_check 指令(商业版)。
  2. 用 nginx_upstream_check_module 第三方模块(开源方案)。
  3. 换成 HAProxy 或 LVS+Keepalived 做主动健康检查。
    这个知识点非常能体现深度,面试主动讲出来会加分。

9.3 演练三:数据库主库宕机

执行主机 :在 db1 上制造故障;在 db2、web1 上观察

制造:

bash 复制代码
ssh root@172.25.254.30 "systemctl stop mysqld"

观察:

bash 复制代码
# 数据库 VIP 漂移到 db2(约 6 ~ 8 秒)
ssh root@172.25.254.40 "ip a show eth0 | grep 172.25.254.201"

# 读操作正常
ssh root@172.25.254.10 "mysql -uappuser -plee -h172.25.254.201 -e 'SELECT COUNT(*) FROM timinglee.users;'"

# 写操作失败(因为 db2 是只读从库)
ssh root@172.25.254.10 "mysql -uappuser -plee -h172.25.254.201 -e \"INSERT INTO timinglee.users(username) VALUES('fail_test');\""
# ERROR 1290 (HY000): The MySQL server is running with the --read-only option ...

修复 :ssh root@172.25.254.30 "systemctl start mysqld"(VIP 会自动抢回)。

真正的解决:需要 MHA 或 MGR 把从库提升为主库。见附录 B。

9.4 演练四:脑裂(VIP 在两台上同时存在)

执行主机 :在 lb1、lb2 上制造并观察

制造:

bash 复制代码
# 用 iptables 阻断 VRRP 组播(模拟心跳网络中断)
ssh root@172.25.254.50 "iptables -I INPUT -p vrrp -j DROP; iptables -I OUTPUT -p vrrp -j DROP"
ssh root@172.25.254.60 "iptables -I INPUT -p vrrp -j DROP; iptables -I OUTPUT -p vrrp -j DROP"

sleep 5
# 现在两台都认为对方挂了,都会成为 MASTER
ssh root@172.25.254.50 "ip a show eth0 | grep 172.25.254.200"
ssh root@172.25.254.60 "ip a show eth0 | grep 172.25.254.200"
# 两台都有!← 脑裂现象

观察现象:

bash 复制代码
# 反复请求 VIP,会出现不可预测的响应(有时走 lb1 的 HAProxy,有时走 lb2)
for i in {1..10}; do curl -s -m 1 http://172.25.254.200/hostname.html; done

修复:

bash 复制代码
for i in 50 60; do
  ssh root@172.25.254.$i "iptables -D INPUT -p vrrp -j DROP; iptables -D OUTPUT -p vrrp -j DROP"
done
sleep 5
# 恢复单主
ssh root@172.25.254.50 "ip a show eth0 | grep 172.25.254.200"

预防手段回顾 :时间同步、优先级差异化、preempt_delay、双活检测脚本、第三方仲裁(fencing)。

9.5 演练五:NFS 服务端宕机

执行主机 :在 infra 上制造故障;在 web1 上观察

制造:

bash 复制代码
ssh root@172.25.254.100 "systemctl stop nfs-server"

观察:

bash 复制代码
# 客户端访问 NFS 目录会卡住(用了硬挂载)
timeout 5 ls /data/nfs/upload/ ; echo "exit=$?"
# exit=124   ← 超时

# 静态资源请求失败
curl -sI http://172.25.254.200/css/main.css
# 502 或 504

定位与修复:

bash 复制代码
# 查看客户端挂载状态
ssh root@172.25.254.10 "mount | grep nfs"
ssh root@172.25.254.10 "showmount -e 192.168.0.100"
# clnt_create: RPC: Unable to receive

# 恢复服务端
ssh root@172.25.254.100 "systemctl start nfs-server"
sleep 3

# 客户端可能需要重新挂载
ssh root@172.25.254.10 "umount -f /data/nfs/static 2>/dev/null; mount -a"

教训 :NFS 硬挂载(hard)会在服务端故障时无限等待,导致应用线程被卡死。应该:

  1. 用 soft + intr 挂载,超时后返回错误而不是阻塞。
  2. 或者用 autofs 按需挂载。
  3. 生产环境用 DRBD + Keepalived 做高可用 NFS,或者直接用分布式存储(CephFS / GlusterFS)。
    这个延伸回答是很强的加分项。

9.6 演练六:磁盘写满

执行主机 :在 web1 上制造故障

制造:

bash 复制代码
# 在 web1 上写满一个小分区(危险操作,仅在测试机做)
ssh root@172.25.254.10 "df -h / "
# 造大文件占满(用一个循环写到报错)
ssh root@172.25.254.10 "fallocate -l 5G /tmp/bigfile 2>/dev/null || dd if=/dev/zero of=/tmp/bigfile bs=1M count=3000"

观察:

bash 复制代码
# 系统日志报错
ssh root@172.25.254.10 "dmesg | tail -5"

# Nginx 写入 access.log 失败
ssh root@172.25.254.10 "tail -5 /usr/local/nginx/logs/error.log"
# [crit] ... write() failed (28: No space left on device)

# 服务可能拒绝新连接
curl -sI http://192.168.0.10/

修复:

bash 复制代码
ssh root@172.25.254.10 "rm -f /tmp/bigfile; df -h /"
ssh root@172.25.254.10 "systemctl restart nginx"

预防:logrotate(前面已配)+ 磁盘使用率告警 + 日志级别不要设成 debug。

9.7 演练七:MySQL 主键冲突导致复制中断

执行主机 :在 db2、db1 上制造故障并修复

制造:

bash 复制代码
# 在从库直接插入一条与主库将来会冲突的数据
ssh root@172.25.254.40 "mysql -uroot -plee -e \"SET GLOBAL super_read_only=0; INSERT INTO timinglee.users(id,username) VALUES(100,'conflict'); SET GLOBAL super_read_only=1;\""

# 主库也插入相同 ID
ssh root@172.25.254.30 "mysql -uroot -plee -e \"INSERT INTO timinglee.users(id,username) VALUES(100,'conflict_master');\""

观察:

bash 复制代码
ssh root@172.25.254.40 "mysql -uroot -plee -e 'SHOW REPLICA STATUS\G' | grep -E 'Replica_SQL_Running:|Last_SQL_Error:'"
# Replica_SQL_Running: No
# Last_SQL_Error: Could not execute Write_rows event on table timinglee.users;
#                 Duplicate entry '100' for key 'users.PRIMARY'

修复(GTID 模式下的标准做法):

bash 复制代码
mysql -uroot -plee   # 在 db2 上
sql 复制代码
-- 1. 停掉复制
STOP REPLICA;

-- 2. 查看出错的 GTID
SHOW REPLICA STATUS\G
-- Retrieved_Gtid_Set: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1-15
-- Executed_Gtid_Set:  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1-14

-- 3. 跳过出错的那个事务(空事务方式)
SET GTID_NEXT='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:15';
BEGIN;
COMMIT;
SET GTID_NEXT='AUTOMATIC';

-- 4. 重新启动复制
START REPLICA;

-- 5. 验证
SHOW REPLICA STATUS\G
-- Replica_SQL_Running: Yes

这是 DBA 面试的经典题目。要点:

  • 传统复制用 SET GLOBAL sql_slave_skip_counter = 1 跳过,但 GTID 模式下这个参数是禁止的。
  • GTID 模式必须用「空事务」方式:SET GTID_NEXT → BEGIN → COMMIT → SET GTID_NEXT='AUTOMATIC'。
  • 跳过事务会导致主从数据永久不一致,必须事后用 pt-table-checksum 校验并 pt-table-sync 修复。

第十章 简历写法与面试问答

本章操作主机:无需在虚拟机内操作

10.1 项目描述模板(可直接粘进简历)

执行主机:文档编写,无需在虚拟机内操作

要点:STAR 结构 + 量化数据 + 技术关键词密度 + 个人贡献明确。

复制代码
企业级高可用 Web 应用集群与数据库高可用平台                        2026.07 -- 2026.09
技术栈:RHEL9 / Nginx / Tomcat / Memcached / MySQL / Redis / NFS / LVS / HAProxy / Keepalived / rsyslog

项目背景:针对单体 Web 应用存在的单点故障、静态资源与动态请求争抢 Tomcat 线程、
         会话无法跨节点共享、数据库单点等四类问题,设计并落地一套七层 + 四层的高可用架构。

架构设计:客户端 → Keepalived VIP(172.25.254.200) 双主高可用 → HAProxy 七层负载
         → Nginx 动静分离 + Tomcat 集群 → Memcached 会话共享 → MySQL 主从(GTID+半同步)
         → Redis 主从 + Sentinel 缓存高可用 → NFS 共享存储 → rsyslog 集中日志

核心工作:
• 基于 RHEL9 母盘 + 链接克隆构建 7 节点双网卡环境(业务面 172.25.254.0/24 / 数据面 192.168.0.0/24),
  通过数据面隔离 MySQL 复制与 NFS 流量,业务面只承载客户端请求,降低互相干扰。
• Nginx 源码编译部署并隐藏版本号(Server 头由 nginx/1.28.1 改为自定义标识),
  配置动静态分离、gzip、proxy_cache、防盗链、stub_status 监控;
  通过 proxy_cache 将动态接口 QPS 从 731 提升至 2290(提升 3.1 倍),压测失败请求从 9963 降至 0。
• 部署 Tomcat 集群并基于 MSM(memcached-session-manager)实现会话集中存储,
  web1/web2 的 failoverNodes 配置互为备份;实测携带同一 SessionID 跨节点访问时
  会话计数持续累加、后端 Tomcat 宕机用户无需重新登录,实现会话零丢失。
• 搭建 MySQL 8.0 主从复制(GTID + ROW 格式),启用半同步复制插件,
  主库超时后自动降级为异步复制、从库恢复后自动升回,兼顾可靠性与可用性;
  配置延迟复制(SOURCE_DELAY=300)作为误删数据的兜底恢复方案。
• 基于 Keepalived VRRP 实现 HAProxy 与数据库 VIP 双主高可用;
  使用 vrrp_script 对 HAProxy 进程做应用级健康检查(2s 间隔 / 失败 2 次降权 30),
  实测主机级故障 VIP 切换丢包 2 ~ 5 个、耗时 < 1s;进程级故障同样可触发漂移。
• 部署 LVS DR 模式四层负载并与 HAProxy 做对比压测,验证 LVS 在纯转发场景的吞吐优势,
  通过 arp_ignore/arp_announce 抑制 ARP 冲突,用 Keepalived 托管 virtual_server 实现
  RS 健康检查与规则的跨节点自动同步。
• 搭建 NFS 共享存储 + autofs 按需挂载,解决集群多节点静态资源与上传文件的一致性问题;
  部署 rsyslog 集中日志(TCP 514,按来源 IP 分目录)与 chrony 全网时间同步;
  编写一键集群健康检查脚本(覆盖 10 类 40+ 检查项)与端到端链路验证脚本。

10.2 面试高频问题与答法

执行主机:文档编写,无需在虚拟机内操作

10.2.1 架构设计类

Q1:为什么要 Keepalived + HAProxy 两层,直接用 Keepalived + Nginx 不行吗?

可以,但各有取舍。保留两层的原因:

  1. 分工清晰。Keepalived 只负责「谁是主」,解决单点;HAProxy 只负责「怎么分发」。
  2. HAProxy 的健康检查维度更细(inter / fall / rise 独立可调),还有原生状态页和 socat 热更新。
  3. 换负载均衡器时只动 HAProxy,不动 Keepalived,变更风险小。

如果只要一个 80 端口做简单轮询,Keepalived + Nginx 完全够用。

Q2:为什么用双主而不是主备?

主备模式下备份机平时闲置,资源利用率 50%。双主模式下 lb1 持有业务 VIP、lb2 持有监控 VIP,两台都在干活,资源利用率 100%。代价是配置稍复杂,且两个 VRRP 实例的 virtual_router_id 必须不同。

Q3:VIP 漂移时正在传输的连接会怎样?

会断。VRRP 只负责把 VIP 从一台机器挪到另一台,不存在连接状态迁移。所以:

  • 短连接(HTTP 类)影响很小,用户重试即可。
  • 长连接(WebSocket、数据库连接池)会全部断开,需要客户端重连。
  • 要减少影响:设置较短的 advert_int(1s)加快检测、用 preempt_delay 防抖动、业务层做重试。

要做到连接不丢,需要会话同步方案(如 LVS 的 sync daemon + ipvsadm --start-daemon,或 HAProxy 的 peers 配置)。这个可以做延伸回答。

Q4:数据面为什么单独用一个网段?

三个理由:

  1. 带宽隔离。MySQL 主从复制、NFS 读写、日志上报都是大流量,和客户端请求抢带宽会导致业务抖动。
  2. 安全隔离。数据库和 NFS 只监听内网网卡,从业务面根本访问不到,等于天然的一道防火墙。
  3. 故障隔离。VMware 的 NAT 网络出问题不影响数据面复制,反之亦然。

10.2.2 Nginx 类

Q5:location 的匹配优先级?

复制代码
=     精确匹配                 优先级最高,匹配上直接返回
^~    前缀匹配                 匹配上后不再尝试正则
~     正则匹配(区分大小写)    按配置文件中的出现顺序
~*    正则匹配(不区分大小写)  按配置文件中的出现顺序
(空)普通前缀匹配            记住最长前缀匹配,然后继续尝试正则

完整优先级:= > ^~ > ~ / ~* > 普通前缀。

注意一个反直觉点 :普通前缀匹配会先记住最长匹配,但如果后面有正则能匹配上,正则优先 。只有 ^~ 才能阻止正则的检查。

Q6:root 和 alias 的区别?

  • root:会用「root 值 + location 值」拼出最终路径。

    nginx 复制代码
    location /images/ { root /data/www; }
    # 访问 /images/a.png → /data/www/images/a.png
  • alias:直接用 alias 值替换 location 部分。

    nginx 复制代码
    location /images/ { alias /data/pics/; }
    # 访问 /images/a.png → /data/pics/a.png

关键坑:alias 匹配目录时,location 和 alias 都必须以 / 结尾,否则路径拼接会出错。

Q7:rewrite 的 break 和 last 区别?

  • break:重写后不再重新匹配 location,停留在当前 location 继续执行剩余指令。
  • last:重写后重新走一遍 location 匹配,可能进入另一个 location。

你的笔记里有一个绝佳的验证例子:/break/index.html 用 break 时返回 test1(停在当前 location),用 last 时返回 test1 end page(重新匹配到了 /test1 这个 location)。这个例子直接讲给面试官,很有说服力。

Q8:proxy_cache 怎么配,怎么验证命中?

nginx 复制代码
proxy_cache_path /path levels=1:2:2 keys_zone=mycache:20m inactive=120s max_size=1g;

location / {
    proxy_cache mycache;
    proxy_cache_key $request_uri;
    proxy_cache_valid 200 302 10m;
    proxy_cache_valid any 1m;
    add_header X-Cache-Status $upstream_cache_status;   # 关键:加这个头才能看到命中状态
}

$upstream_cache_status 的取值:MISS / HIT / EXPIRED / STALE / UPDATING / REVALIDATED / BYPASS。

生产注意 :不要缓存带用户身份的动态页面,否则会把 A 用户的数据返回给 B 用户。要缓存的话,proxy_cache_key 里必须带上用户标识或干脆只缓存公共数据接口。

Q9:Nginx 平滑升级的原理?

Nginx 的信号机制:

  1. 备份旧二进制 → 替换新二进制。
  2. kill -USR2 <旧master PID>:旧 master 用新二进制拉起一个新 master,旧 master 把自己的 PID 文件改名为 nginx.pid.oldbin。
  3. 此时新旧两组 master + worker 并存,新 worker 开始接管新连接。
  4. kill -WINCH <旧master PID>:旧 worker 处理完现有请求后优雅退出,旧 master 保留(便于回滚)。
  5. 观察一段时间没问题,kill -QUIT <旧master PID> 彻底关闭旧 master。
  6. 要回滚就在第 4 步之后 kill -HUP <旧master PID>,旧 master 重新拉起 worker。

10.2.3 负载均衡类

Q10:LVS 的四种模式区别?

模式 转发方式 请求路径 响应路径 是否需改 RS 是否支持跨网段 性能
NAT 改目标 IP 经 VS 经 VS 网关指向 VS 是 最低(VS 是瓶颈)
DR 改目标 MAC 经 VS 不经 VS lo 加 VIP + ARP 抑制 否(同二层) 最高
TUN 加 IP 隧道头 经 VS 不经 VS 需支持隧道 是 高
FULLNAT 同时改源和目标 IP 经 VS 经 VS 无需改 是 中

生产最常用 DR(性能最好,但要求 RS 与 VS 在同二层),跨机房或 RS 异构时用 FULLNAT。

Q11:DR 模式为什么必须做 ARP 抑制?

DR 模式下 VIP 同时存在于调度器和所有 RS 的 lo 上。如果 RS 不做抑制,当有人 ARP 询问「谁是 172.25.254.200」时,RS 也会用自己的 lo 地址去应答,导致客户端的 ARP 表把 VIP 映射到某一台 RS 的 MAC。这样请求就绕过调度器直达 RS,负载均衡彻底失效。

参数:

  • arp_ignore=1:只回答目标 IP 是本接口 IP 的 ARP 请求(拒绝用 lo 上的 VIP 应答)。
  • arp_announce=2:发送 ARP 通告时只用与本接口同子网的源地址(不用 lo 上的 VIP 作源)。

Q12:HAProxy 的 8 种负载算法,静态和动态的区别?

算法 类型 说明
static-rr 静态 加权轮询,不支持热改权重,不支持慢启动
first 静态 优先第一台,达到 maxconn 才用第二台
roundrobin 动态 平滑加权轮询,支持热改权重
leastconn 动态 最少连接数,适合长连接
source 哈希 按源 IP 哈希,同一客户端固定同一台
uri 哈希 按 URI 哈希(缓存友好)
url_param 哈希 按 URL 参数哈希
hdr 哈希 按请求头哈希(如 User-Agent)

静态算法的权重只能在配置文件里改并 reload;动态算法可以用 socat 实时改,这就是为什么生产用 roundrobin 而不是 static-rr。

四种哈希算法都要加 hash-type consistent 才是一致性哈希,否则节点增减会导致大量缓存失效。

10.2.4 数据库类

Q13:GTID 复制和传统复制的区别?

  • 传统复制 :从库记录 MASTER_LOG_FILE + MASTER_LOG_POS(二进制日志文件名 + 偏移量)。
  • GTID 复制 :每个事务有一个全局唯一 ID(server_uuid:transaction_id),从库只需 MASTER_AUTO_POSITION=1,由 MySQL 自动找到缺失的事务。

GTID 的优势:

  1. 主从切换后不用重新找位点,从库自动补齐差异 ------ 这是最大的价值。
  2. 从库可以多源复制,GTID 自动去重。
  3. failover 后不会出现「重复或遗漏事务」的经典问题。

启用的前提:gtid_mode=ON、enforce_gtid_consistency=ON、log_slave_updates=ON(级联复制需要)。且不能使用 CREATE TABLE ... SELECT 这类非确定性语句。

Q14:半同步和异步复制的区别?

维度 异步 半同步
主库返回时机 写 binlog 后立即返回 至少一个从库 ack 收到 binlog 后才返回
数据安全性 主库宕机可能丢最后几个事务 不丢(前提是从库 ack 过)
性能 高 略低(多一次网络往返)
从库故障时 无影响 超时后自动降级为异步

关键点:半同步是「至少一个从库收到」,不是「从库执行完毕」 。所以它保证的是 binlog 不丢,不是数据不丢(从库可能还没 apply)。要做到「至少一个从库执行完毕」,需要 rpl_semi_sync_master_wait_for_slave_count=N 配合 AFTER_SYNC 模式。

Q15:MySQL 主从延迟怎么解决?

  1. 开并行回放 :replica_parallel_type=LOGICAL_CLOCK + replica_parallel_workers=4~16。
  2. 一主多从:把读压力分散到多个从库,每个从库的复制量不变但服务能力提升。
  3. 业务层容错:写后立刻读走主库(缓存路由),或者关键业务强制读主库。
  4. 半同步 + 延迟监控 :Seconds_Behind_Source 超阈值告警。
  5. 根因治理:主库大事务拆分(一次删 100 万行改成批量删)、避免全表更新。
  6. 硬件:从库用 SSD、binlog 与数据分盘。

注意:Seconds_Behind_Source 在多线程回放时可能不准(它只反映 SQL 线程最后执行事务的时间差),更准确的做法是 pt-heartbeat 或对比 GTID 差值。

Q16:Redis 主从、Sentinel、Cluster 分别解决什么问题?

方案 解决的问题 不解决
主从复制 数据冗余、读扩展 主节点单点,不会自动切换
Sentinel 主节点单点、自动故障切换 单机容量上限(数据量还是受一台内存限制)
Cluster 水平扩展(数据分片)、高并发写入 跨 slot 的多 key 操作受限

Sentinel 的三条关键配置:quorum(判定客观下线需要的票数)、down-after-milliseconds(主观下线阈值)、failover-timeout(切换超时)。

Cluster 的 16384 个槽位是固定的,三主默认各分 5461 / 5462 / 5461。扩容新节点后必须 reshard 才有槽,否则不存数据。

10.2.5 系统与排障类

Q17:Too many open files 怎么解决?

bash 复制代码
# 1. 看当前限制
ulimit -n
cat /proc/<pid>/limits | grep "open files"

# 2. 改用户级限制(对通过 shell 启动的进程有效)
vim /etc/security/limits.conf
* soft nofile 100000
* hard nofile 100000

# 3. 对 systemd 管理的服务,limits.conf 无效!必须加在 service 文件里
vim /lib/systemd/system/nginx.service
[Service]
LimitNOFILE=100000

# 或者写一份单独的 limits.d
cat > /etc/security/limits.d/nginx.conf <<EOF
nginx soft nofile 100000
nginx hard nofile 100000
EOF

# 4. 改系统级上限
sysctl -w fs.file-max=1000000
vim /etc/sysctl.conf
fs.file-max = 1000000

# 5. 重启服务并验证
systemctl daemon-reload && systemctl restart nginx
cat /proc/$(cat /usr/local/nginx/logs/nginx.pid)/limits | grep "open files"

关键知识点 :limits.conf 对 systemd 服务不生效。这是最常见的踩坑点,一定要能说出来。

Q18:Linux 上服务起不来,怎么排查?

标准排查顺序:

  1. 看服务状态 :systemctl status xxx(看退出码和最近日志)。
  2. 看日志 :journalctl -u xxx -n 100 --no-pager,以及服务自己的日志文件。
  3. 看端口占用 :ss -antlp | grep :PORT,lsof -i:PORT。
  4. 看配置文件语法 :nginx -t、haproxy -c -f、keepalived -t -f。
  5. 手工前台启动 :绕过 systemd,比如 /usr/local/nginx/sbin/nginx -g 'daemon off;',能直接看到报错。
  6. 看权限 :文件属主、目录权限、SELinux(getenforce / ausearch -m avc -ts recent)。
  7. 看资源 :free -h(内存)、df -h(磁盘)、ulimit -n(句柄)。

Q19:系统负载高怎么定位?

bash 复制代码
# 1. 先看整体
uptime              # load average
top -bn1 | head -20
vmstat 1 5          # r 队列长度、si/so 换页、wa IO 等待

# 2. 判断瓶颈类型
#   r 大、us/sy 高  → CPU 瓶颈
#   wa 高、b 大     → IO 瓶颈
#   si/so 非 0      → 内存不足在换页
#   sy 特别高       → 上下文切换过多或系统调用频繁

# 3. 定位到进程
pidstat -u -d -r 1 5          # 按进程看 CPU/IO/内存
pidstat -w 1 5                # 上下文切换
iostat -x 1 5                 # 磁盘指标(await、%util)

# 4. 定位到线程
top -H -p <pid>
# 把线程 ID 转 16 进制
printf "%x\n" <tid>
jstack <pid> | grep -A 30 <hex_tid>     # Java 应用
pstack <pid>                             # C/C++ 应用

# 5. 定位到系统调用
strace -f -p <pid> -T -tt 2>&1 | head -50

区分 load average 和 CPU 使用率:load 包含「运行中」和「不可中断等待(IO)」两类。所以 load 高但 CPU 使用率低,通常说明卡在 IO 上。

Q20:一个请求从浏览器到数据库,中间经历了什么?

这是综合能力题,答好能显著加分。结合本项目答:

  1. 浏览器 DNS 解析 www.timinglee.org → 得到 VIP 172.25.254.200。
  2. 请求到达 VIP。VIP 由 Keepalived 通过 VRRP 持有,当前在 lb1 上。
  3. HAProxy 监听 80/443,做 SSL 卸载(如果是 https)、ACL 匹配、健康检查确认后端可用。
  4. HAProxy 按 roundrobin 算法选一台后端(或用 SERVERID cookie 做会话保持),转发到 192.168.0.10:80,并加上 X-Forwarded-For 头。
  5. Nginx 收到请求:如果是静态资源(.css/.js/.png),直接从 NFS 挂载的 /data/nfs/static 读取返回,并加上 expires 缓存头;如果是 .jsp,proxy_pass 到 tomcat_cluster upstream。
  6. Tomcat 处理 JSP:从 Memcached 读取 Session(MSM 机制);需要数据时通过数据面连接到 MySQL VIP 172.25.254.201:3306 或 HAProxy 的 3307 端口。
  7. MySQL VIP 由 Keepalived 持有,当前在 db1(主库)上,主库处理读写并通过 binlog 半同步复制到 db2。
  8. 响应沿原路返回:Tomcat → Nginx → HAProxy → 客户端。注意 LVS DR 模式下响应不经过调度器,但本项目用的是 HAProxy 七层,响应要经过 HAProxy。
  9. 全程日志:各节点通过 rsyslog 把日志发到 infra 的 /var/log/remote/<源IP>/ 下。

面试时讲这条链路,每经过一层就主动带出你知道的优化点(Nginx 缓存、HAProxy 健康检查、MSM 会话共享、MySQL 半同步、数据面隔离)。这是展示技术深度最好的机会。


附录 A MySQL 源码编译(可选)

本章操作主机 :db1(建议临时把内存调到 3 GB)

如果面试需要展示「会源码编译 MySQL」,可以单独做一次。前提:把 db1 的内存临时调到 3 GB,磁盘扩到 30 GB。

bash 复制代码
# 1. 安装依赖
dnf install -y cmake3 gcc gcc-c++ git bison openssl-devel ncurses-devel \
  systemd-devel rpcgen libtirpc-devel make

# 如果 AppStream 里没有 gcc-toolset-12,用默认 gcc 即可
dnf install -y gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ 2>/dev/null || true

# 2. 下载(约 448 MB)
cd /root
wget https://downloads.mysql.com/archives/get/p/23/file/mysql-boost-8.3.0.tar.gz
tar zxf mysql-boost-8.3.0.tar.gz
cd mysql-8.3.0/
mkdir build && cd build/

# 3. 编译(-j2 不要用 -j8,1~3 GB 内存下并行度太高会被 OOM Killer 杀掉)
cmake3 .. \
  -DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
  -DMYSQL_DATADIR=/data/mysql \
  -DMYSQL_UNIX_ADDR=/data/mysql/mysql.sock \
  -DWITH_INNOBASE_STORAGE_ENGINE=1 \
  -DWITH_EXTRA_CHARSETS=all \
  -DDEFAULT_CHARSET=utf8mb4 \
  -DDEFAULT_COLLATION=utf8mb4_unicode_ci \
  -DWITH_BOOST=bundled \
  -DWITH_SSL=system \
  -DWITH_DEBUG=OFF \
  -DSYSTEMD=ON \
  -DSYSTEMD_SERVICE_DIR=/usr/lib/systemd/system

make -j2          # 约 30 ~ 60 分钟
make install
bash 复制代码
# 4. 环境变量与用户
echo 'export PATH=$PATH:/usr/local/mysql/bin' >> ~/.bash_profile
source ~/.bash_profile

useradd -r -s /sbin/nologin -M mysql
mkdir -p /data/mysql
chown mysql:mysql /data/mysql

# 5. 初始化
mysqld --initialize --user=mysql
# 记录输出的临时密码

# 6. 配置文件
cat > /etc/my.cnf <<'EOF'
[mysqld]
datadir=/data/mysql
socket=/data/mysql/mysql.sock
symbolic-links=0
server-id=30
log-bin=mysql-bin
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON
EOF

# 7. systemd 服务(注意 --defaults-file 必须是第一个参数!)
cat > /lib/systemd/system/mysqld.service <<'EOF'
[Unit]
Description=MySQL 8.3 Database Server
After=network.target syslog.target

[Service]
Type=notify
User=mysql
Group=mysql
ExecStart=/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf
ExecReload=/usr/local/mysql/bin/mysqladmin --defaults-file=/etc/my.cnf shutdown
TimeoutSec=300
PrivateTmp=true
LimitNOFILE=65535
LimitNPROC=65535
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now mysqld

# 8. 安全初始化
mysql_secure_installation

# 9. 验证
mysql -uroot -plee -e "SELECT VERSION();"
# 8.3.0

几个必知的坑:

  1. --defaults-file=/etc/my.cnf 必须是 ExecStart 的第一个参数,否则 systemd 下会报参数顺序错误。
  2. make -j2 不要调高。编译 Mysql 单个编译单元内存占用很大,-j8 在 3 GB 内存下必然被 OOM Killer 杀掉。
  3. RHEL9 需要 libtirpc-devel 和 rpcgen,否则 cmake 阶段会报 Could NOT find RPC。
  4. 编译完要 make install,产物在 /usr/local/mysql(不是 build 目录里直接跑)。

附录 B MHA 高可用集群(可选)

本章操作主机 :infra(管理机);db1、db2(数据节点)

MHA(Master High Availability)实现 MySQL 主从的自动故障切换 + 从库提升。这是 VIP 漂移方案的必要补充。

前提:需要额外的管理机(可以复用 infra,但内存要多给 512 MB)。且在 RHEL9 上 MHA 0.58 需要打 Perl 补丁。

B.1 准备主从(GTID 模式)

执行主机 :db1、db2

MHA 要求:

ini 复制代码
# 所有 MySQL 节点
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON
log-bin=mysql-bin
relay_log_purge=ON

主库和从库都创建管理员账号:

sql 复制代码
CREATE USER 'mha'@'192.168.0.%' IDENTIFIED WITH mysql_native_password BY 'lee';
GRANT ALL PRIVILEGES ON *.* TO 'mha'@'192.168.0.%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

B.2 安装 MHA(在管理机 infra 上)

执行主机 :infra(管理机)

bash 复制代码
dnf install -y perl perl-DBD-MySQL perl-CPAN

# 用 cpan 装依赖模块
cpan
cpan[1]> install Config::Tiny
cpan[2]> install Log::Dispatch
cpan[3]> install Mail::Sender
cpan[4]> install Parallel::ForkManager
cpan[5]> exit
bash 复制代码
# 安装 MHA(el7 的包在 RHEL9 上需要 --nodeps)
rpm -ivh mha4mysql-manager-0.58-0.el7.centos.noarch.rpm \
         mha4mysql-node-0.58-0.el7.centos.noarch.rpm --nodeps

# 所有 MySQL 节点装 node 包
for i in 30 40
do
  scp mha4mysql-node-0.58-0.el7.centos.noarch.rpm root@172.25.254.$i:/mnt/
  ssh root@172.25.254.$i "rpm -ivh /mnt/mha4mysql-node-0.58-0.el7.centos.noarch.rpm --nodeps"
done

B.3 RHEL9 兼容补丁(必须打,否则报版本号解析错误)

执行主机 :infra、db1、db2

bash 复制代码
vim /usr/share/perl5/vendor_perl/MHA/NodeUtil.pm

把 parse_mysql_major_version 函数改为:

perl 复制代码
sub parse_mysql_major_version($) {
  my $str = shift;
  my @nums = $str =~ m/(\d+)/g;
  my $result = sprintf( '%03d%03d', $nums[0]//0, $nums[1]//0);
  return $result;
}

原因:原函数用 sprintf('%03d%03d', $str =~ m/(\d+)/g) 一次性取所有数字,MySQL 8.3.0 会取出 3 个数字导致参数不匹配。这是 MHA 在 RHEL9 上的经典问题。

B.4 MHA 配置文件

执行主机 :infra(管理机)

bash 复制代码
mkdir -p /etc/masterha /etc/masterha/scripts
vim /etc/masterha/app1.cnf
ini 复制代码
[server default]
manager_workdir=/etc/masterha
manager_log=/etc/masterha/mha.log

user=mha
password=lee
ssh_user=root

repl_user=repl
repl_password=lee

master_binlog_dir=/var/lib/mysql
remote_workdir=/tmp

ping_interval=2
# 二次检查(防止网络抖动误判),-s 后面是其他存活节点的 IP
secondary_check_script = masterha_secondary_check -s 172.25.254.40 -s 172.25.254.100

# VIP 漂移脚本
master_ip_failover_script=/etc/masterha/scripts/master_ip_failover
master_ip_online_change_script=/etc/masterha/scripts/master_ip_online_change

# 故障告警脚本
report_script=/etc/masterha/scripts/send_report

[server1]
hostname=172.25.254.30
port=3306
candidate_master=1
check_repl_delay=0

[server2]
hostname=172.25.254.40
port=3306
candidate_master=1
check_repl_delay=0
  • candidate_master=1:允许这台被提升为主库。
  • check_repl_delay=0:提升时不检查复制延迟(否则延迟大的从库不会被选为候选)。生产环境应谨慎使用,因为可能提升一个数据落后的从库。
  • secondary_check_script:从多个节点二次确认主库真的挂了,避免「网络分区导致误切换」。

B.5 VIP 漂移脚本

执行主机 :infra(管理机)

bash 复制代码
cat > /etc/masterha/scripts/master_ip_failover <<'EOF'
#!/usr/bin/env perl
use strict;
use warnings FATAL => 'all';

my (
    $command, $ssh_user, $orig_master_host, $orig_master_ip,
    $orig_master_port, $new_master_host, $new_master_ip,
    $new_master_port
);

my $vip = '172.25.254.201/24';
my $key = '1';
my $ssh_start_vip = "/sbin/ip addr add $vip dev eth0 label eth0:1";
my $ssh_stop_vip  = "/sbin/ip addr del $vip dev eth0";

if ($command eq "stop") {
    print "Disabling the VIP on old master: $orig_master_host \n";
    &stop_vip();
    exit 0;
}
elsif ($command eq "start") {
    print "Enabling the VIP - $vip on the new master: $new_master_host \n";
    &start_vip();
    exit 0;
}
elsif ($command eq "status") {
    print "Checking the Status of the script.. OK \n";
    exit 0;
}
else {
    &usage();
    exit 1;
}

sub start_vip() {
    `ssh $ssh_user\@$new_master_host \" $ssh_start_vip \"`;
}
sub stop_vip() {
    return 0 unless ($ssh_user);
    `ssh $ssh_user\@$orig_master_host \" $ssh_stop_vip \"`;
}

sub usage {
    print "Usage: master_ip_failover --command=start|stop|status "
        . "--ssh_user=root --orig_master_host=hostname "
        . "--orig_master_ip=ip --orig_master_port=port "
        . "--new_master_host=hostname --new_master_ip=ip "
        . "--new_master_port=port\n";
}
EOF

chmod +x /etc/masterha/scripts/master_ip_failover

B.6 检查与启动

执行主机 :infra(管理机)

bash 复制代码
# SSH 免密检查
masterha_check_ssh --conf=/etc/masterha/app1.cnf

# 复制健康检查
masterha_check_repl --conf=/etc/masterha/app1.cnf
# 期望看到:GTID failover mode = 1
#           MySQL Replication Health is OK.

# 查看当前状态
masterha_check_status --conf=/etc/masterha/app1.cnf
# app1 (pid:xxxx) is running(0:PING_OK), master:172.25.254.30

# 手动给主库加上 VIP
ssh root@172.25.254.30 "ip a a 172.25.254.201/24 dev eth0 label eth0:1"

# 启动 MHA 管理进程(生产用 systemd 托管)
nohup masterha_manager --conf=/etc/masterha/app1.cnf \
  --remove_dead_master_conf --ignore_last_failover \
  > /var/log/mha-manager.log 2>&1 &

B.7 故障切换演练

执行主机 :infra(观察)+ db1(制造故障)

bash 复制代码
# 制造主库故障
ssh root@172.25.254.30 "systemctl stop mysqld"

# 观察 MHA 日志
tail -f /etc/masterha/mha.log
# ----- Failover Report -----
# app1: MySQL Master failover 172.25.254.30 to 172.25.254.40 succeeded
# Master 172.25.254.30 is down!
# Check MHA Manager logs at /etc/masterha/mha.log for details.
# Started automated(non-interactive) failover.
# Selected 172.25.254.40 as a new master.
# 172.25.254.40: OK: Applying all logs succeeded.
# 172.25.254.40: RESET SLAVE ALL; issued
# Generating relay diff files from the latest slave succeeded.
# 172.25.254.40: Resetting slave info succeeded.
# Master failover to 172.25.254.40(172.25.254.40:3306) completed successfully.

# 验证新主库
ssh root@172.25.254.40 "mysql -uroot -plee -e 'SELECT @@server_id; SHOW REPLICA STATUS\G' | head -5"
# 40
# role 从 slave 变成 master

# 验证 VIP 已漂移
ssh root@172.25.254.40 "ip a show eth0 | grep 172.25.254.201"

# 验证可写(这是 VIP 漂移方案做不到的)
ssh root@172.25.254.10 "mysql -uappuser -plee -h172.25.254.201 -e \"INSERT INTO timinglee.users(username) VALUES('after_failover');\""
# 成功!因为新主库不是只读的

B.8 故障恢复(原主库重新加入)

执行主机 :db1、db2、infra

bash 复制代码
# 1. 启动原主库
ssh root@172.25.254.30 "systemctl start mysqld"

# 2. 清掉 MHA 的锁文件(不删无法再次切换!)
rm -f /etc/masterha/app1.failover.complete

# 3. 把原主库变成新主库的从库
ssh root@172.25.254.30 "mysql -uroot -plee -e \"
STOP REPLICA;
RESET REPLICA ALL;
SET GLOBAL gtid_purged='';
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='192.168.0.40',
  SOURCE_USER='repl',
  SOURCE_PASSWORD='lee',
  SOURCE_AUTO_POSITION=1;
START REPLICA;\""

# 4. 验证
ssh root@172.25.254.30 "mysql -uroot -plee -e 'SHOW REPLICA STATUS\G' | grep -E 'Running|Behind'"

必知的坑 :/etc/masterha/app1.failover.complete 这个锁文件在每次故障切换后生成,不删除就无法再次切换(MHA 会直接退出)。这是 MHA 最经典的踩坑点。

B.9 MHA vs VIP 高可用对比

执行主机:本节为对比说明,无需执行命令

维度 Keepalived VIP 漂移 MHA
能做的 把入口地址挪到从库 把从库提升为主库,再挪 VIP
写操作 切换后仍失败(从库只读) 切换后可正常写
数据一致性 不保证 会尝试从最新从库补齐差异日志
复杂度 低(两台机器即可) 高(需要管理机 + Perl 依赖 + 补丁)
现代替代 --- MGR(组复制)、Orchestrator、云 RDS 自动切换

面试结论:「VIP 漂移解决的是入口问题,MHA 解决的是角色切换问题。只做 VIP 漂移的话,主库宕机后虽然入口切了,但从库是只读的,写请求依然失败。所以完整方案需要 MHA 或 MGR。我两个都做了,所以清楚它们的边界。」

相关推荐
随性而行3601 小时前
企业微信API调用案例:如何通过接口实现多账号统一管理与业务接入?
运维·微信·机器人·自动化·企业微信
guo_wen_qiang1 小时前
云服务器redis主从复制-哨兵模式
运维·服务器·redis
运维小文2 小时前
apache nifi 2.90部署+运维手册
大数据·运维·数据分析·apache·nifi
2603_965148112 小时前
终极清单:构建自动化选品比价系统的所有组件与资源
大数据·运维·数据库·数据挖掘·自动化
北城笑笑2 小时前
Server 18 ,Nginx 双服务器 HTTP转HTTPS 代理实战( HTTP 地址换 HTTPS 域名 )
运维·服务器·前端·后端·nginx·http·https
程序员-Benothing2 小时前
Linux磁盘IO性能分析:iostat、iotop、fio实战
linux·运维·服务器
LiaoWL1232 小时前
从零开始学 Linux(一):目录结构、常用命令与 VIM 编辑器上手
linux·运维·服务器
分布式存储与RustFS2 小时前
RustFS 上生产第一步:从单机到 4 节点多盘集群(MNMD)部署实操
运维·云原生·开源·对象存储·分布式存储·s3·性能基准