生产环境监控告警与可观测性体系建设部署笔记
部署线路图
主线:固定参数与审批 → 服务器初始化 → API 高可用 → Kubernetes 与 Cilium → NFS、TLS、镜像凭据 → Prometheus、Grafana、Alertmanager → MySQL/JMX 采集 → 告警规则 → Cloud Alert(睿象云)→ 企业微信、短信、电话语音 → 网页验收 → 回滚、备份与恢复。
0. 固定参数与验收边界
| 项目 | 固定值 |
|---|---|
| 堡垒机与账号 | ops-bastion=10.20.10.5,账号 opsadmin;需要 root 的操作通过已审批 sudo 执行 |
| 运维访问网段 | 10.20.10.0/24;仅该网段可访问三套监控网页和节点 SSH,集群节点网段为 192.168.88.0/24 |
| 服务器 | master01=192.168.88.170、master02=192.168.88.171、master03=192.168.88.172、worker01=192.168.88.173、worker02=192.168.88.174、worker03=192.168.88.175、nfs01=192.168.88.180 |
| 集群 | Red Hat 系 Linux(知识库使用 dnf/yum、chronyd、firewalld、SELinux;课程未标明具体发行版版本)、containerd、Kubernetes 1.30.14、API VIP 192.168.88.169、API 入口 lb.heimak8s.com:6443 |
| 网络与入口 | Pod 10.100.0.0/16、Service 10.96.0.0/12、Cilium 1.18.2、IngressClass traefik |
| 存储与证书 | NFS /srv/nfs/k8s、StorageClass nfs-storage、TLS Secret monitoring-ops-tls、证书目录 /etc/ops-pki/monitoring/;Grafana 凭据目录 /etc/ops-secrets/monitoring/ |
| 监控栈 | namespace monitor、release prometheus-monitor、Helm 3.17.0、kube-prometheus-stack 76.4.1、Prometheus 保留 90 天、每副本 200Gi |
| 业务采集 | namespace zzyl、MySQL Service mysql、Java 应用 zzyl-admin、镜像仓库 harbor.lanqicheng.top/zzyl |
| 告警出口 | Cloud Alert(睿象云)Prometheus Webhook;项目接口配置由密管文件 /etc/ops-secrets/monitoring/cloudalert-alertmanager.yaml 提供,Kubernetes Secret 固定名为 cloudalert-alertmanager-config,告警恢复时同步发送恢复事件 |
| 备份 | ops-bastion 独立备份盘 /srv/backup/monitoring;恢复演练固定恢复点 2026-08-02,生产恢复须由批准变更单将该日期改为指定备份点 |
重要程度:必看=高风险/易中断;核心=主线必须完成;推荐=生产最佳实践。
1. 全新服务器初始化
1.1 主机名、时间与静态解析
执行机器:全部 7 台,root。每台只执行与本机相符的主机名行,其余命令全部执行。
# 在 master01 执行:写入控制面一号的固定主机名。
hostnamectl set-hostname master01
# 在 master02 执行:写入控制面二号的固定主机名。
hostnamectl set-hostname master02
# 在 master03 执行:写入控制面三号的固定主机名。
hostnamectl set-hostname master03
# 在 worker01 执行:写入工作节点一号主机名。
hostnamectl set-hostname worker01
# 在 worker02 执行:写入工作节点二号主机名。
hostnamectl set-hostname worker02
# 在 worker03 执行:写入工作节点三号主机名。
hostnamectl set-hostname worker03
# 在 nfs01 执行:写入 NFS 存储节点主机名。
hostnamectl set-hostname nfs01
# 安装知识库同类 Red Hat 系环境使用的时间、网络、审计与防火墙工具。
dnf install -y chrony curl jq wget vim net-tools firewalld policycoreutils-python-utils rsync openssl nmap-ncat
# 将企业 NTP 写入 Red Hat 系 chrony 配置。
sed -i 's|^pool .*|server ntp.ops.example.internal iburst|' /etc/chrony.conf
# 启动 chronyd 并设置开机自启。
systemctl enable --now chronyd
# 追加三个控制面、三个工作节点、NFS 与统一 API 名称的固定解析。
printf '%s\n' '192.168.88.169 lb.heimak8s.com' '192.168.88.170 master01' '192.168.88.171 master02' '192.168.88.172 master03' '192.168.88.173 worker01' '192.168.88.174 worker02' '192.168.88.175 worker03' '192.168.88.180 nfs01' | tee -a /etc/hosts
# 校验主机名、时间同步和 API 名称解析。
hostnamectl --static && chronyc tracking && getent hosts lb.heimak8s.com
执行结果:
master01
System clock synchronized: yes
192.168.88.169 lb.heimak8s.com
注意事项:必看:若已有错误或重复 hosts 记录,先在变更单确认后精确修复;不可关闭防火墙绕过网络问题。
通过条件:各机器名称与第 0 节一致,时钟已同步,API 入口 lb.heimak8s.com 解析为 192.168.88.169。
失败排查:
# 在时间异常的节点执行:检查 chronyd 服务、同步状态和 NTP 来源。
systemctl is-active chronyd && chronyc tracking && chronyc sources -v
# 在时间异常的节点执行:检查到固定 NTP 服务器的 UDP/123 可达性。
nc -uvz ntp.ops.example.internal 123
# 在解析异常的节点执行:列出受管主机名在 hosts 中的全部记录,用于人工确认重复项。
grep -nE 'lb\.heimak8s\.com|master0[1-3]|worker0[1-3]|nfs01' /etc/hosts
# 在解析异常的节点执行:验证最终解析结果必须指向固定 API VIP。
getent hosts lb.heimak8s.com
1.2 内核、containerd 与 Kubernetes 1.30
执行机器:6 台 Kubernetes 节点,root;不在 nfs01 执行。
# 加载 containerd 所需 overlay 内核模块。
modprobe overlay
# 加载 Kubernetes 桥接流量检查模块。
modprobe br_netfilter
# 持久化两个模块,保证重启后自动加载。
printf '%s\n' 'overlay # containerd overlayfs 依赖。' 'br_netfilter # Kubernetes 网络策略依赖。' | tee /etc/modules-load.d/k8s.conf
# 写入 IPv4 转发和桥接流量的 Kubernetes sysctl 参数。
printf '%s\n' 'net.bridge.bridge-nf-call-iptables = 1 # 让 iptables 检查桥接 IPv4。' 'net.bridge.bridge-nf-call-ip6tables = 1 # 让 iptables 检查桥接 IPv6。' 'net.ipv4.ip_forward = 1 # 允许节点转发 Pod 流量。' | tee /etc/sysctl.d/99-kubernetes-cri.conf
# 立即加载 sysctl 配置。
sysctl --system
# 在企业 RPM 镜像中预检四个固定版本均可获得;任一项缺失即停止,不允许安装最新版本替代。
dnf list --showduplicates containerd.io kubelet kubeadm kubectl | grep -E 'containerd.io.*1\.7\.18|kubelet.*1\.30\.14|kubeadm.*1\.30\.14|kubectl.*1\.30\.14'
# 安装与本文固定版本一致的 containerd 和 Kubernetes 客户端/节点组件。
dnf install -y containerd.io-1.7.18 kubelet-1.30.14 kubeadm-1.30.14 kubectl-1.30.14
# 在全部六个 Kubernetes 节点关闭当前 swap。
swapoff -a
# 在全部六个 Kubernetes 节点注释 fstab 中的 swap,避免重启后恢复。
sed -ri '/\sswap\s/s/^#?/#/' /etc/fstab
# 生成完整 containerd 默认配置。
containerd config default | tee /etc/containerd/config.toml
# 将 runc cgroup 驱动设为 systemd,与 kubelet 保持一致。
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# 重启 containerd 并设置开机自启。
systemctl enable --now containerd
# 设置 kubelet 开机自启;kubeadm 初始化时会完成 kubelet 配置。
systemctl enable kubelet
# 输出 containerd、Kubernetes、IPv4 转发、服务状态和 cgroup 驱动状态。
containerd --version && kubeadm version -o short && sysctl net.ipv4.ip_forward && systemctl is-active containerd && grep -m1 'SystemdCgroup = true' /etc/containerd/config.toml
执行结果:
containerd github.com/containerd/containerd 1.7.18
kubeadm version: v1.30.14
net.ipv4.ip_forward = 1
active
SystemdCgroup = true
注意事项:核心:6 个节点必须输出同一 kubeadm 1.30.14 版本;不能再安装第二个 CRI。
通过条件:containerd 服务为 active,配置中存在 SystemdCgroup = true。
失败排查:
# 在异常 Kubernetes 节点执行:查看 containerd 当前状态和最近八十行服务日志。
systemctl status containerd --no-pager && journalctl -u containerd -n 80 --no-pager
# 在异常 Kubernetes 节点执行:确认配置文件仍使用 systemd cgroup 驱动。
grep -n 'SystemdCgroup' /etc/containerd/config.toml
# 在企业 RPM 镜像可访问的节点执行:确认四个固定版本均可查询。
dnf list --showduplicates containerd.io kubelet kubeadm kubectl | grep -E 'containerd.io.*1\.7\.18|kubelet.*1\.30\.14|kubeadm.*1\.30\.14|kubectl.*1\.30\.14'
1.2.1 ops-bastion 管理客户端
执行机器:ops-bastion,opsadmin 使用已审批 sudo。此机是后续 Helm、kubectl、备份与网页 API 校验的唯一运维执行机;此时只安装客户端,不复制尚未生成的 Kubernetes kubeconfig。
# 在 ops-bastion 执行:安装固定 kubectl 与后续部署、验收、备份实际使用的客户端工具。
sudo dnf install -y kubectl-1.30.14 curl jq openssl rsync nmap-ncat
# 在 ops-bastion 执行:下载固定 Helm 3.17.0 安装包。
curl -fLO https://get.helm.sh/helm-v3.17.0-linux-amd64.tar.gz
# 在 ops-bastion 执行:解压 Helm 安装包。
tar -xzf helm-v3.17.0-linux-amd64.tar.gz
# 在 ops-bastion 执行:将 Helm 安装到全局可执行路径。
sudo install -m 0755 linux-amd64/helm /usr/local/bin/helm
# 在 ops-bastion 执行:验证管理、检索与备份客户端均可执行。
kubectl version --client --output=yaml && helm version --short && jq --version && rsync --version | head -n 1 && nc -h | head -n 1
执行结果:
gitVersion: v1.30.14
v3.17.0+g301108e
jq-1.6
rsync version 3.2.7 protocol version 32
Ncat: Version 7.92
注意事项:必看:此时集群尚未初始化,kubectl 只能验证客户端版本;第 2.2 节复制 kubeconfig 后,才允许在 ops-bastion 执行集群管理命令。
通过条件:ops-bastion 的 kubectl 为 v1.30.14,Helm 为 v3.17.0,curl、jq、openssl、rsync 与 nmap-ncat 均可执行。
失败排查:
# 在 ops-bastion 执行:确认企业 RPM 源存在固定 kubectl 版本。
dnf list --showduplicates kubectl | grep '1\.30\.14'
# 在 ops-bastion 执行:检查 Helm 下载文件完整且客户端可执行。
tar -tzf helm-v3.17.0-linux-amd64.tar.gz | head && /usr/local/bin/helm version --short
1.3 防火墙最小放通
执行机器:6 台 Kubernetes 节点,root;nfs01 按第 3.1 节单独放通 NFSv4。此处不关闭 firewalld,也不把 Kubernetes 端口暴露到公网。
# 在六个 Kubernetes 节点启用 firewalld,后续规则重启后仍生效。
systemctl enable --now firewalld
# 将集群节点与 Pod 网段加入仅供 Kubernetes 数据面使用的 trusted 区域;运维网段不加入 trusted。
for cidr in 192.168.88.0/24 10.100.0.0/16; do firewall-cmd --permanent --zone=trusted --add-source="$cidr"; done
# 仅当本机为工作节点时,向批准运维网段开放 Traefik HTTP 和 HTTPS。
if [[ "$(hostname -s)" =~ ^worker0[1-3]$ ]]; then firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.20.10.0/24 port port=80 protocol=tcp accept'; firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.20.10.0/24 port port=443 protocol=tcp accept'; fi
# 仅当本机为控制面时,允许同一集群网段访问 Kubernetes API、etcd、kubelet、控制器与调度器端口。
if [[ "$(hostname -s)" =~ ^master0[1-3]$ ]]; then for port in 6443 2379-2380 10250 10257 10259; do firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=192.168.88.0/24 port port=$port protocol=tcp accept"; done; fi
# 重新加载永久规则并显示服务、可信来源与批准的 HTTPS 规则。
firewall-cmd --reload && systemctl is-active firewalld && firewall-cmd --zone=trusted --list-sources && firewall-cmd --list-rich-rules
执行结果:
success
active
192.168.88.0/24 10.100.0.0/16
rule family="ipv4" source address="10.20.10.0/24" port port="443" protocol="tcp" accept
注意事项:必看:在远程执行前先确认当前 SSH 来源属于 10.20.10.0/24,且由控制台或第二会话保留回退入口;不允许将 6443、2379-2380、10250、9100 直接对公网开放。
通过条件:firewalld 为 active,两个集群 CIDR 出现在 trusted 区域;仅 worker01—worker03 对运维网段开放 80/443。
失败排查:
# 在通信两端分别执行:显示 trusted 来源和所有 rich rule。
firewall-cmd --zone=trusted --list-sources && firewall-cmd --list-rich-rules
# 在控制面执行:确认 API 端口的永久规则已加载。
firewall-cmd --list-all --zone=public | grep -E '6443|2379|2380|10250|10257|10259'
# 在 ops-bastion 执行:检查 Grafana HTTPS 入口的状态码与证书连接。
curl -kI --connect-timeout 5 https://grafana.ops.example.internal
# 在目标工作节点执行:确认批准运维网段的 TCP/443 rich rule 存在。
firewall-cmd --list-rich-rules | grep 'port="443"'
2. 高可用 Kubernetes
2.1 Keepalived 与 HAProxy API VIP
执行机器:master01、master02、master03,root。三台都安装 API 四层负载均衡组件;下列基线先在 master01 创建,复制给另外两台后仅改优先级。
# 安装 Kubernetes API VIP 所需组件。
dnf install -y haproxy keepalived
# 写入只转发 Kubernetes API 的 HAProxy 配置。
printf '%s\n' 'global # 全局日志设置。' ' log /dev/log local0 # 输出系统日志。' 'defaults # 默认代理参数。' ' mode tcp # API Server 使用 TCP。' ' timeout connect 10s # 后端连接超时。' ' timeout client 30s # 客户端超时。' ' timeout server 30s # 后端超时。' 'frontend kubernetes-api # 定义 API 入口。' ' bind *:6443 # 监听 API 端口。' ' default_backend kubernetes-control-plane # 转发到控制面池。' 'backend kubernetes-control-plane # 定义控制面后端。' ' option tcp-check # 用 TCP 健康检查。' ' server master01 192.168.88.170:6443 check # 控制面一号。' ' server master02 192.168.88.171:6443 check # 控制面二号。' ' server master03 192.168.88.172:6443 check # 控制面三号。' | tee /etc/haproxy/haproxy.cfg
# 写入 master01 优先级 120 的 Keepalived 配置。
printf '%s\n' 'vrrp_script chk_haproxy { # 定义 HAProxy 进程检查。' ' script "/usr/bin/pgrep haproxy" # 进程不存在即失败。' ' interval 2 # 每两秒检查。' ' weight -20 # 失败时降低优先级。' '}' 'vrrp_instance VI_K8S { # 定义 API VIP 组。' ' state BACKUP # 由优先级决定持有者。' ' interface ens192 # 固定生产网卡。' ' virtual_router_id 51 # 三台必须相同。' ' priority 120 # master01 最高。' ' advert_int 1 # 每秒发送通告。' ' authentication { # 定义 VRRP 认证。' ' auth_type PASS # 使用 Keepalived 认证类型。' ' auth_pass K8sVpc51 # 固定 VRRP 密码。' ' }' ' virtual_ipaddress { # 定义漂移地址。' ' 192.168.88.169/24 dev ens192 # 固定 API VIP。' ' }' ' track_script { # 绑定健康检查。' ' chk_haproxy # HAProxy 失败会触发漂移。' ' }' '}' | tee /etc/keepalived/keepalived.conf
# 将同一份 HAProxy API 转发配置复制到 master02 和 master03。
for host in master02 master03; do scp /etc/haproxy/haproxy.cfg root@"$host":/etc/haproxy/haproxy.cfg; done
# 将 Keepalived 基线配置复制到 master02 和 master03。
for host in master02 master03; do scp /etc/keepalived/keepalived.conf root@"$host":/etc/keepalived/keepalived.conf; done
# 将 master02 优先级改为 110。
ssh root@master02 "sed -i 's/priority 120/priority 110/' /etc/keepalived/keepalived.conf"
# 将 master03 优先级改为 100。
ssh root@master03 "sed -i 's/priority 120/priority 100/' /etc/keepalived/keepalived.conf"
# 在三台控制面逐台启动并设置 HAProxy、Keepalived 开机自启。
for host in master01 master02 master03; do ssh root@"$host" 'systemctl enable --now haproxy keepalived; systemctl is-active haproxy keepalived'; done
# 在 master01 查看 API 入口、三台 6443 监听状态和 VIP 仅由一台控制面持有的实际状态。
getent hosts lb.heimak8s.com && for host in master01 master02 master03; do ssh root@"$host" 'hostname; ss -lntp | grep ":6443"; ip -4 addr show dev ens192 | awk "/192.168.88.169/{print \$2}" || true'; done
配置作用:HAProxy 提供统一 API TCP 入口;Keepalived 在控制面失效时漂移 VIP。
生效结果:
active
active
active
active
active
active
192.168.88.169 lb.heimak8s.com
master01
LISTEN 0 4096 0.0.0.0:6443 0.0.0.0:* users:(("haproxy",pid=1298,fd=4))
192.168.88.169/24
master02
LISTEN 0 4096 0.0.0.0:6443 0.0.0.0:* users:(("haproxy",pid=1307,fd=4))
master03
LISTEN 0 4096 0.0.0.0:6443 0.0.0.0:* users:(("haproxy",pid=1314,fd=4))
配置详解:所有客户端最终连接 VIP;HAProxy 后端只含三个 API Server;track_script 防止 VIP 停在 HAProxy 已失效的节点。
注意事项:必看:先用 ip route get 192.168.88.1 核实生产网卡是否为 ens192;VRID 51 与密码不得和同网段既有组冲突。
通过条件:每个控制面上的 HAProxy、Keepalived 均为 active,三台均监听 6443,lb.heimak8s.com 可解析到 API 负载均衡入口,且 192.168.88.169/24 仅出现在一台控制面。
失败排查:
# 在三台控制面分别执行:检查 Keepalived、HAProxy 服务状态和 API VIP 当前持有者。
systemctl is-active keepalived haproxy && ip -4 addr show dev ens192 | grep '192.168.88.169'
# 在三台控制面分别执行:抓取五秒 VRRP 协议 112 通告,确认控制面之间能收发通告。
timeout 5 tcpdump -ni ens192 ip proto 112
# 在三台控制面分别执行:查看 Keepalived 最近日志,定位优先级、认证或网卡问题。
journalctl -u keepalived -n 80 --no-pager
# 在初始化 Kubernetes 前执行:验证 HAProxy 已监听 6443;此时没有 API 后端属于预期状态。
ss -lntp | grep ':6443'
2.2 初始化、加入节点与安装 Cilium
执行机器:master01 root 初始化;master02、master03 加控制面;3 个 worker 加工作节点;master01 的 opsadmin 安装 Cilium;集群就绪后将受控管理 kubeconfig 复制给 ops-bastion 的 opsadmin。
# 在 master01 执行:通过知识库 API 入口初始化 Kubernetes 1.30 控制面。
kubeadm init --kubernetes-version=v1.30.14 --apiserver-advertise-address=192.168.88.170 --image-repository registry.aliyuncs.com/google_containers --control-plane-endpoint=lb.heimak8s.com:6443 --service-cidr=10.96.0.0/12 --upload-certs
# 在 master01 执行:创建 opsadmin 的 kubeconfig 目录。
install -d -m 0700 /home/opsadmin/.kube
# 在 master01 执行:复制管理员 kubeconfig 并限制权限。
install -m 0600 -o opsadmin -g opsadmin /etc/kubernetes/admin.conf /home/opsadmin/.kube/config
# 在 master01 执行:生成两小时有效的 worker 加入脚本。
kubeadm token create --ttl 2h --print-join-command > /root/k8s-worker-join.sh
# 在 master01 执行:生成控制面证书上传密钥。
CERT_KEY=$(kubeadm init phase upload-certs --upload-certs | tail -n 1)
# 在 master01 执行:读取 worker 加入命令。
JOIN_CMD=$(cat /root/k8s-worker-join.sh)
# 在 master01 执行:生成包含证书密钥的控制面加入脚本。
printf '%s --control-plane --certificate-key %s\n' "$JOIN_CMD" "$CERT_KEY" > /root/k8s-control-plane-join.sh
# 在 master01 执行:仅允许 root 读取临时脚本。
chmod 0700 /root/k8s-worker-join.sh /root/k8s-control-plane-join.sh
# 在 master02 执行:读取并执行控制面加入脚本。
ssh root@master01 'cat /root/k8s-control-plane-join.sh' | bash
# 在 master03 执行:读取并执行控制面加入脚本。
ssh root@master01 'cat /root/k8s-control-plane-join.sh' | bash
# 在 worker01、worker02、worker03 分别执行:读取并执行 worker 加入脚本。
ssh root@master01 'cat /root/k8s-worker-join.sh' | bash
# 在 master01 执行:解压知识库离线 Cilium CLI 安装包。
tar -xzvf /opt/packages/cilium-linux-amd64.tar.gz
# 在 master01 执行:安装 Cilium CLI 到系统可执行路径。
install -m 0755 cilium /usr/local/bin/cilium
# 在 master01 执行:验证 Cilium CLI 可执行。
cilium version
# 在 master01、master02、master03、worker01、worker02、worker03 分别执行:导入离线 Cilium 主镜像。
ctr -n k8s.io images import /opt/packages/cilium.tar
# 在 master01、master02、master03、worker01、worker02、worker03 分别执行:导入离线 Cilium Envoy 镜像。
ctr -n k8s.io images import /opt/packages/cilium-envoy.tar
# 在 master01、master02、master03、worker01、worker02、worker03 分别执行:导入离线 Cilium Operator 镜像。
ctr -n k8s.io images import /opt/packages/cilium-operator.tar
# 在 master01 的 opsadmin 执行:安装知识库固定版本 Cilium。
cilium install --version 1.18.2 --set kubeProxyReplacement=false --set ipam.mode=cluster-pool --set routingMode=native --set autoDirectNodeRoutes=true --set ipam.operator.clusterPoolIPv4PodCIDRList=10.100.0.0/16 --set ipam.operator.clusterPoolIPv4MaskSize=24 --set ipv4NativeRoutingCIDR=10.100.0.0/16
# 在 master01 的 opsadmin 执行:检查 Cilium、API Server 就绪状态和节点。
cilium status && kubectl get --raw='/readyz' && kubectl get nodes -o wide
# 在 master01 的 root 执行:在 ops-bastion 创建受限 kubeconfig 目录。
ssh opsadmin@10.20.10.5 'install -d -m 0700 /home/opsadmin/.kube'
# 在 master01 的 root 执行:复制管理员 kubeconfig 到唯一运维执行机并限制权限。
scp /etc/kubernetes/admin.conf opsadmin@10.20.10.5:/home/opsadmin/.kube/config && ssh opsadmin@10.20.10.5 'chmod 0600 /home/opsadmin/.kube/config'
# 在 ops-bastion 的 opsadmin 执行:验证 kubeconfig 可连接 API 并读取六个节点。
kubectl get nodes --kubeconfig=/home/opsadmin/.kube/config
执行结果:
Your Kubernetes control-plane has initialized successfully!
DaemonSet cilium Desired: 6, Ready: 6, Available: 6
ok
NAME STATUS ROLES VERSION INTERNAL-IP
master01 Ready control-plane v1.30.14 192.168.88.170
master02 Ready control-plane v1.30.14 192.168.88.171
master03 Ready control-plane v1.30.14 192.168.88.172
worker01 Ready <none> v1.30.14 192.168.88.173
worker02 Ready <none> v1.30.14 192.168.88.174
worker03 Ready <none> v1.30.14 192.168.88.175
master01 Ready control-plane v1.30.14
master02 Ready control-plane v1.30.14
master03 Ready control-plane v1.30.14
worker01 Ready <none> v1.30.14
worker02 Ready <none> v1.30.14
worker03 Ready <none> v1.30.14
注意事项:必看:加入脚本只在两小时内有效,成功后执行 shred -u /root/k8s-*-join.sh;不得把脚本复制到知识库、Git 或聊天记录。
通过条件:6 节点均 Ready,API readyz 返回 ok,3 控制面都有 control-plane 角色,ops-bastion 的 kubeconfig 可读取全部 6 节点。
失败排查:
# 在无法加入的节点执行:检查名称解析和到 API VIP 的 TCP/6443 连接。
getent hosts lb.heimak8s.com && nc -vz lb.heimak8s.com 6443
# 在 master01 的 opsadmin 执行:查看 Cilium 组件、节点和健康检查详情。
cilium status --wait --wait-duration 60s && cilium connectivity test
# 在任一 Kubernetes 节点执行:列出与 Pod 网段相关的现有路由,确认没有冲突路由。
ip route show | grep '10.100.'
# 在 master01 的 opsadmin 执行:保留失败节点事件,不删除节点或重置集群。
kubectl get nodes -o wide && kubectl get events -A --sort-by=.lastTimestamp | tail -n 40
3. 存储、证书与镜像
3.1 NFS 动态制备
执行机器:nfs01 root 配置服务端;ops-bastion 的 opsadmin 部署制备器。
# 在 nfs01 执行:安装 Red Hat 系 NFS 服务端。
dnf install -y nfs-utils
# 在 nfs01 执行:创建 Red Hat 系匿名 NFS 用户和组可写的受控导出目录。
install -d -m 0770 -o nobody -g nobody /srv/nfs/k8s
# 在 nfs01 执行:首次新增或后续修改自定义 NFS 的 SELinux 标签,并允许受限读写导出。
semanage fcontext -a -t nfs_t '/srv/nfs/k8s(/.*)?' || semanage fcontext -m -t nfs_t '/srv/nfs/k8s(/.*)?'; restorecon -Rv /srv/nfs/k8s; setsebool -P nfs_export_all_rw 1
# 在 nfs01 执行:仅启用 NFSv4,避免额外暴露 NFSv3 的 RPC 服务端口。
printf '%s\n' '[nfsd] # 配置内核 NFS 服务。' 'vers3=n # 禁用 NFSv3。' 'vers4=y # 仅启用 NFSv4。' | tee /etc/nfs.conf.d/monitoring-nfsv4.conf
# 在 nfs01 执行:仅允许课程固定集群网段读写 NFS。
printf '%s\n' '/srv/nfs/k8s 192.168.88.0/24(rw,sync,no_subtree_check,root_squash) # 限制集群网段并保留 root_squash。' | tee /etc/exports.d/k8s.exports
# 在 nfs01 执行:加载导出并启动 Red Hat 系 NFS 服务。
exportfs -ra && systemctl enable --now nfs-server
# 在 nfs01 执行:启用防火墙并仅向 Kubernetes 节点网段放通 NFSv4 TCP/2049。
systemctl enable --now firewalld && firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=192.168.88.0/24 port port=2049 protocol=tcp accept' && firewall-cmd --reload
# 在 ops-bastion 执行:添加 NFS provisioner Helm 仓库。
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
# 在 ops-bastion 执行:部署固定版本制备器和存储类。
helm upgrade --install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --version 4.0.18 --namespace nfs-provisioner --create-namespace --set nfs.server=192.168.88.180 --set nfs.path=/srv/nfs/k8s --set storageClass.name=nfs-storage --set storageClass.defaultClass=true --set storageClass.reclaimPolicy=Retain
# 在 ops-bastion 执行:创建一次性 PVC 冒烟验证,等待绑定后清理该测试对象。
printf '%s\n' 'apiVersion: v1 # 使用核心 API。' 'kind: PersistentVolumeClaim # 创建临时 PVC。' 'metadata: # 定义对象元数据。' ' name: nfs-storage-smoke # 固定冒烟对象名。' ' namespace: nfs-provisioner # 放入制备器命名空间。' 'spec: # 定义 PVC 规格。' ' accessModes: # 定义访问模式。' ' - ReadWriteOnce # 单节点写入验证。' ' storageClassName: nfs-storage # 使用本节创建的存储类。' ' resources: # 定义资源请求。' ' requests: # 定义存储请求。' ' storage: 1Gi # 申请最小验证容量。' | kubectl apply -f - && kubectl -n nfs-provisioner wait --for=jsonpath='{.status.phase}'=Bound pvc/nfs-storage-smoke --timeout=120s && kubectl get storageclass nfs-storage && kubectl -n nfs-provisioner get pods,pvc nfs-storage-smoke && kubectl -n nfs-provisioner delete pvc nfs-storage-smoke
执行结果:
NAME PROVISIONER RECLAIMPOLICY
nfs-storage cluster.local/nfs-subdir-external-provisioner Retain
nfs-subdir-external-provisioner-7cbbd96749-r6m2k 1/1 Running
nfs-storage-smoke Bound pvc-6b7f... 1Gi RWO nfs-storage
注意事项:必看:NFS 必须位于独立数据盘并具有快照和备份;不能导出操作系统根分区。
通过条件:nfs-storage 存在,制备器为 Running,后续 PVC 进入 Bound。
失败排查:
# 在 ops-bastion 执行:查看制备器日志和等待绑定的 PVC 事件。
kubectl -n nfs-provisioner logs deploy/nfs-subdir-external-provisioner --tail=100 && kubectl -n nfs-provisioner describe pvc nfs-storage-smoke
# 在挂载超时的工作节点执行:检查 NFSv4 TCP/2049 连通性。
nc -vz 192.168.88.180 2049
# 在 nfs01 执行:检查 SELinux 模式、导出目录标签、实际导出和防火墙规则。
getenforce && ls -Zd /srv/nfs/k8s && exportfs -v && firewall-cmd --list-rich-rules | grep 'port="2049"'
3.2 Traefik、TLS 与镜像凭据
执行机器:企业权威 DNS 完成已批准的记录变更;ops-bastion 的 opsadmin 验证并部署。证书和 Docker config 已由密管下发到第 0 节固定受限目录。
DNS 固定记录:grafana.ops.example.internal、prometheus.ops.example.internal、alertmanager.ops.example.internal 分别各有三条 A 记录,依次为 192.168.88.173、192.168.88.174、192.168.88.175。DNS 系统不在本文服务器范围内,变更单必须附三条记录的权威 DNS 变更号;缺少变更号即停止本节。
# 添加 Traefik 官方 Helm 仓库。
helm repo add traefik https://traefik.github.io/charts
# 刷新 Helm 索引。
helm repo update
# 在部署前确认三个固定域名均解析到三个工作节点。
for name in grafana.ops.example.internal prometheus.ops.example.internal alertmanager.ops.example.internal; do getent ahostsv4 "$name" | awk '{print $1}'; done | sort -u
# 在部署前确认 TLS 证书含三个固定域名的 SAN,不显示私钥。
openssl x509 -in /etc/ops-pki/monitoring/tls.crt -noout -ext subjectAltName
# 使用 DaemonSet 与 hostPort 在每个工作节点直接监听 80/443,不依赖未部署的 LoadBalancer。
helm upgrade --install traefik traefik/traefik --version 28.2.0 --namespace traefik --create-namespace --set deployment.kind=DaemonSet --set service.enabled=false --set ports.web.hostPort=80 --set ports.websecure.hostPort=443
# 验证三个工作节点各有 Traefik Pod,且每台都监听 TCP/80、TCP/443。
kubectl -n traefik get pods -o wide && for host in worker01 worker02 worker03; do ssh root@"$host" 'hostname; ss -lntp | grep -E ":80|:443"'; done
# 可重复创建监控命名空间。
kubectl create namespace monitor --dry-run=client -o yaml | kubectl apply -f -
# 从受限 PKI 文件创建 TLS Secret。
kubectl -n monitor create secret tls monitoring-ops-tls --cert=/etc/ops-pki/monitoring/tls.crt --key=/etc/ops-pki/monitoring/tls.key --dry-run=client -o yaml | kubectl apply -f -
# 从密管下发的 Cloud Alert 配置文件创建 Alertmanager 配置 Secret,文件内的项目接口不写入本文。
kubectl -n monitor create secret generic cloudalert-alertmanager-config --from-file=alertmanager.yaml=/etc/ops-secrets/monitoring/cloudalert-alertmanager.yaml --dry-run=client -o yaml | kubectl apply -f -
# 从密管下发的单独文件创建 Grafana 管理员 Secret,命令不显示账号或密码。
kubectl -n monitor create secret generic grafana-admin --from-file=admin-user=/etc/ops-secrets/monitoring/grafana-admin-user --from-file=admin-password=/etc/ops-secrets/monitoring/grafana-admin-password --dry-run=client -o yaml | kubectl apply -f -
# 可重复创建业务命名空间。
kubectl create namespace zzyl --dry-run=client -o yaml | kubectl apply -f -
# 从受限 Docker config 文件创建拉取 Secret。
kubectl -n zzyl create secret generic registry-ops-pull --from-file=.dockerconfigjson=/etc/ops-secrets/zzyl/registry-ops-pull.json --type=kubernetes.io/dockerconfigjson --dry-run=client -o yaml | kubectl apply -f -
# 仅显示四个 Secret 的类型,不输出敏感内容。
kubectl -n monitor get secret monitoring-ops-tls -o jsonpath='{.type}{"\n"}' && kubectl -n monitor get secret cloudalert-alertmanager-config -o jsonpath='{.type}{"\n"}' && kubectl -n monitor get secret grafana-admin -o jsonpath='{.type}{"\n"}' && kubectl -n zzyl get secret registry-ops-pull -o jsonpath='{.type}{"\n"}'
执行结果:
192.168.88.173
192.168.88.174
192.168.88.175
X509v3 Subject Alternative Name:
DNS:grafana.ops.example.internal, DNS:prometheus.ops.example.internal, DNS:alertmanager.ops.example.internal
NAME READY STATUS NODE
traefik-6f984d7c47-a1b2c 1/1 Running worker01
traefik-6f984d7c47-d3e4f 1/1 Running worker02
traefik-6f984d7c47-g5h6i 1/1 Running worker03
worker01
LISTEN 0 4096 *:80 *:* users:(("traefik",pid=2181,fd=7))
LISTEN 0 4096 *:443 *:* users:(("traefik",pid=2181,fd=8))
worker02
LISTEN 0 4096 *:80 *:* users:(("traefik",pid=2147,fd=7))
LISTEN 0 4096 *:443 *:* users:(("traefik",pid=2147,fd=8))
worker03
LISTEN 0 4096 *:80 *:* users:(("traefik",pid=2116,fd=7))
LISTEN 0 4096 *:443 *:* users:(("traefik",pid=2116,fd=8))
kubernetes.io/tls
Opaque
Opaque
kubernetes.io/dockerconfigjson
注意事项:推荐:私钥文件必须为 0600;入口 80/443 只向批准运维网段开放;证书 SAN 不含三个域名、DNS 未返回三个工作节点或权威 DNS 变更号缺失时,不得继续安装。
通过条件:三个工作节点各有一个 Traefik Pod 为 Running,三台工作节点的 TCP/80、TCP/443 都被 Traefik 监听,四个 Secret 类型正确。
失败排查:
# 在 ops-bastion 执行:显示线上证书链、有效期和 SAN,不输出私钥。
openssl s_client -connect grafana.ops.example.internal:443 -servername grafana.ops.example.internal -showcerts </dev/null | openssl x509 -noout -dates -ext subjectAltName
# 在 worker01、worker02、worker03 分别执行:检查 80/443 的实际占用进程。
ss -lntp | grep -E ':(80|443)'
# 在 ops-bastion 执行:检查 Traefik DaemonSet、调度事件和工作节点污点。
kubectl -n traefik get daemonset,pods -o wide && kubectl -n traefik get events --sort-by=.lastTimestamp | tail -n 40 && kubectl get nodes -l node-role.kubernetes.io/worker -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
4. 部署监控栈
4.1 唯一 Helm values
执行机器:ops-bastion,opsadmin。密管任务必须预建 monitor/grafana-admin,键为 admin-user 和 admin-password;values 不保存密码。
# /home/opsadmin/monitoring-values.yaml
grafana: # 定义 Grafana 子组件。
admin: # 定义管理员凭据来源。
existingSecret: grafana-admin # 引用密管预建 Secret。
persistence: # 启用持久化。
enabled: true # 保存仪表盘数据。
storageClassName: nfs-storage # 使用固定存储类。
size: 20Gi # 申请容量。
ingress: # 创建 HTTPS 入口。
enabled: true # 启用 Ingress。
ingressClassName: traefik # 使用 Traefik。
hosts: # 固定域名列表。
- grafana.ops.example.internal # Grafana 域名。
tls: # 绑定 TLS。
- secretName: monitoring-ops-tls # 使用固定 Secret。
hosts: # 列出证书域名。
- grafana.ops.example.internal # 与入口一致。
prometheus: # 定义 Prometheus 子组件。
prometheusSpec: # 定义 Prometheus CR。
replicas: 2 # 两副本提高采集可用性。
podAntiAffinity: hard # 强制两个 Prometheus 副本不落在同一节点。
retention: 90d # 保留九十天。
retentionSize: 180GiB # 限制单副本数据大小。
serviceMonitorSelectorNilUsesHelmValues: true # 只选择本 release 的 ServiceMonitor。
ruleSelectorNilUsesHelmValues: true # 只选择本 release 的规则。
storageSpec: # 定义持久卷模板。
volumeClaimTemplate: # 每副本独立 PVC。
spec: # PVC 规格。
storageClassName: nfs-storage # 固定存储类。
accessModes: # 访问模式。
- ReadWriteOnce # 每副本独占卷。
resources: # 资源请求。
requests: # 存储请求。
storage: 200Gi # 每副本容量。
ingress: # Prometheus HTTPS 入口。
enabled: true # 启用 Ingress。
ingressClassName: traefik # 使用 Traefik。
hosts: # 固定域名。
- prometheus.ops.example.internal # Prometheus 域名。
tls: # 绑定 TLS。
- secretName: monitoring-ops-tls # 使用证书。
hosts: # 证书域名。
- prometheus.ops.example.internal # 与入口一致。
alertmanager: # 定义 Alertmanager 子组件。
alertmanagerSpec: # 定义 Alertmanager CR。
replicas: 3 # 三副本高可用。
useExistingSecret: true # 指定使用密管生成的完整 Alertmanager 配置 Secret。
configSecret: cloudalert-alertmanager-config # 固定引用 Cloud Alert 路由配置。
storage: # 持久化静默和告警状态。
volumeClaimTemplate: # 每副本独立 PVC。
spec: # PVC 规格。
storageClassName: nfs-storage # 固定存储类。
accessModes: # 访问模式。
- ReadWriteOnce # 每副本独占卷。
resources: # 资源请求。
requests: # 存储请求。
storage: 20Gi # 每副本容量。
ingress: # 定义 Alertmanager HTTPS 运维入口。
enabled: true # 创建 Ingress。
ingressClassName: traefik # 使用 Traefik。
hosts: # 固定域名。
- alertmanager.ops.example.internal # Alertmanager 域名。
tls: # 绑定 TLS。
- secretName: monitoring-ops-tls # 使用固定证书。
hosts: # 证书域名。
- alertmanager.ops.example.internal # 与入口一致。
配置作用:将 Operator、Prometheus、Grafana、Alertmanager、node-exporter、kube-state-metrics 部署成一套固定监控栈。
生效结果:Prometheus 两副本、Alertmanager 三副本、Grafana 各有 NFS PVC;三项服务通过 HTTPS 访问。
配置详解:两个 selector 参数强制自定义对象带 release: prometheus-monitor 标签,避免误采集其他团队资源;Alertmanager 只读取 cloudalert-alertmanager-config,路由、Cloud Alert 项目接口与通知凭据不进入 Helm values、Git 或网页截图。
注意事项:必看:若 grafana-admin Secret 缺失必须停止发布,不能临时在 YAML 写明文密码;DNS 必须将 grafana.ops.example.internal、prometheus.ops.example.internal、alertmanager.ops.example.internal 解析到三个工作节点的 192.168.88.173、192.168.88.174、192.168.88.175。
# 添加 Prometheus Community Helm 仓库。
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# 刷新 Helm 索引。
helm repo update
# 先预渲染配置,不改变集群。
helm upgrade --install prometheus-monitor prometheus-community/kube-prometheus-stack --version 76.4.1 --namespace monitor --create-namespace --values /home/opsadmin/monitoring-values.yaml --dry-run
# 安装固定 Chart 并等待资源就绪。
helm upgrade --install prometheus-monitor prometheus-community/kube-prometheus-stack --version 76.4.1 --namespace monitor --create-namespace --values /home/opsadmin/monitoring-values.yaml --wait --timeout 15m
# 检查 Helm revision、Pod 与 PVC。
helm -n monitor status prometheus-monitor && kubectl -n monitor get pods,pvc
执行结果:
NAME READY STATUS RESTARTS AGE
STATUS: deployed
prometheus-monitor-kube-prometheus-prometheus-0 2/2 Running 0 4m
prometheus-monitor-kube-prometheus-prometheus-1 2/2 Running 0 4m
alertmanager-prometheus-monitor-kube-prometheus-alertmanager-0 2/2 Running 0 4m
prometheus-monitor-grafana-7d7cbddbd8-pzq4s 3/3 Running 0 4m
prometheus-prometheus-monitor-kube-prometheus-prometheus-db-prometheus-monitor-kube-prometheus-prometheus-0 Bound pvc-6b7f... 200Gi RWO nfs-storage
注意事项:核心:不混用课程离线包和在线 Chart;本文唯一采用 76.4.1。
通过条件:Helm 状态为 deployed,监控 PVC 全部 Bound,关键 Pod Running。
失败排查:
# 在 ops-bastion 执行:确认 Prometheus Operator 的 CRD 已注册。
kubectl get crd | grep 'monitoring.coreos.com'
# 在 ops-bastion 执行:查看 Helm release、失败 Pod 事件与 monitor PVC 状态。
helm -n monitor status prometheus-monitor && kubectl -n monitor get events --sort-by=.lastTimestamp | tail -n 60 && kubectl -n monitor get pvc
# 在 ops-bastion 执行:若 PVC 仍为 Pending,回查 NFS 制备器日志而不删除 PVC。
kubectl -n nfs-provisioner logs deploy/nfs-subdir-external-provisioner --tail=100
4.2 Prometheus、Grafana 网页验收
执行机器:运维工作站浏览器和 ops-bastion 的 opsadmin。

该真实课程截图对应 Prometheus 的 Status → Targets,核心 Job 必须显示 UP。

该真实课程截图对应 Grafana 的 Connections → Data sources → Prometheus,数据源必须成功。

该真实课程截图对应 Grafana 新建面板时选择 Prometheus 数据源;它与上一张数据源页面共同证明数据源已可被仪表盘查询使用。节点与 Pod 时序数据必须按本节 PromQL 查询结果验收。
# 通过 HTTPS 检查 Prometheus 健康接口。
curl --fail --silent --show-error https://prometheus.ops.example.internal/-/healthy
# 查询所有健康 Targets 总数。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(up%3D%3D1)' | jq '.data.result[0].value[1]'
# 查询 Kubernetes 节点指标数量。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(kube_node_info)' | jq '.data.result[0].value[1]'
执行结果:
Prometheus Server is Healthy.
"18"
"6"
注意事项:推荐:Grafana 登录后立即纳入企业 SSO,不以 NodePort 或任意公网源地址暴露管理入口。
通过条件:基础 Targets 为 UP,kube_node_info 返回 6,Grafana 数据源成功。
失败排查:
# 在 ops-bastion 执行:读取 Prometheus Targets API,定位 Down Target 的 lastError 和 lastScrape。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/targets?state=any' | jq '.data.activeTargets[] | select(.health != "up") | {labels:.labels,lastError:.lastError,lastScrape:.lastScrape}'
# 在 ops-bastion 执行:检查被采集服务的 Service、EndpointSlice 与 NetworkPolicy。
kubectl get svc,endpointslice,networkpolicy -A
# 在 ops-bastion 执行:检查 Ingress 引用的 Secret 和线上证书 SAN。
kubectl -n monitor get ingress -o yaml | grep -E 'secretName|host:' && openssl s_client -connect prometheus.ops.example.internal:443 -servername prometheus.ops.example.internal </dev/null | openssl x509 -noout -ext subjectAltName
5. 业务指标采集与告警
5.0 业务准入检查
执行机器:ops-bastion,opsadmin。仅在 zzyl 业务系统已按其独立变更单上线后执行;本节是第 5.1、5.2 节的硬前置,不通过时不得执行 set image、创建 Exporter 或告警规则。
# 确认业务命名空间、Java Deployment 与 MySQL Service 已存在。
kubectl -n zzyl get namespace zzyl && kubectl -n zzyl get deployment zzyl-admin && kubectl -n zzyl get service mysql
# 确认业务 Deployment 具有本文固定的 app 标签,且包含名称为 zzyl-admin 的业务容器。
kubectl -n zzyl get deployment zzyl-admin -o jsonpath='{.spec.template.metadata.labels.app}{"\n"}{.spec.template.spec.containers[?(@.name=="zzyl-admin")].name}{"\n"}'
# 确认私有仓库拉取 Secret 已绑定到业务 Pod 模板,避免 JavaAgent 镜像发布后 ImagePullBackOff。
kubectl -n zzyl get deployment zzyl-admin -o jsonpath='{range .spec.template.spec.imagePullSecrets[*]}{.name}{"\n"}{end}' | grep -Fx registry-ops-pull
# 确认 MySQL Service 已有可用后端,不显示数据库账号或密码。
kubectl -n zzyl get endpointslice -l kubernetes.io/service-name=mysql -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\n"}{end}'
执行结果:
NAME STATUS AGE
zzyl Active 12d
NAME READY UP-TO-DATE AVAILABLE AGE
zzyl-admin 2/2 2 2 12d
NAME TYPE CLUSTER-IP PORT(S) AGE
mysql ClusterIP 10.96.2.88 3306/TCP 12d
zzyl-admin
zzyl-admin
registry-ops-pull
10.100.2.41
注意事项:必看:若业务 Deployment 使用不同的容器名、标签、服务名或命名空间,必须先在业务系统变更单中统一,不得在监控变更中盲目修改业务 Deployment。
通过条件:四项命令均退出 0,zzyl-admin 为 2/2 Available,app 标签和容器名均为 zzyl-admin,registry-ops-pull 已绑定,MySQL EndpointSlice 至少返回一个地址。
失败排查:
# 在 ops-bastion 执行:确认业务 Deployment、可用副本与 Pod 模板标签。
kubectl -n zzyl get deployment zzyl-admin -o wide && kubectl -n zzyl get deployment zzyl-admin -o jsonpath='{.spec.template.metadata.labels}{"\n"}'
# 在 ops-bastion 执行:只验证镜像拉取 Secret 名称已绑定,不输出 Secret 内容。
kubectl -n zzyl get deployment zzyl-admin -o jsonpath='{.spec.template.spec.imagePullSecrets[*].name}{"\n"}' && kubectl -n zzyl get secret registry-ops-pull -o jsonpath='{.type}{"\n"}'
# 在 ops-bastion 执行:检查 MySQL Service 选择器、EndpointSlice 地址和数据库 Pod 就绪状态。
kubectl -n zzyl get svc mysql -o yaml && kubectl -n zzyl get endpointslice -l kubernetes.io/service-name=mysql -o wide && kubectl -n zzyl get pods -l app=mysql
5.1 MySQL Exporter 与 ServiceMonitor
执行机器:ops-bastion,opsadmin。密管已将最低权限 MySQL 帐号写入 /etc/ops-secrets/zzyl/mysqld-exporter.cnf;帐户仅有 PROCESS、REPLICATION CLIENT、SELECT。
# 从受限 MySQL 配置文件创建凭据 Secret。
kubectl -n zzyl create secret generic mysqld-exporter-auth --from-file=.my.cnf=/etc/ops-secrets/zzyl/mysqld-exporter.cnf --dry-run=client -o yaml | kubectl apply -f -
# 写入 MySQL Exporter、内部 Service 与 ServiceMonitor 清单。
cat > /home/opsadmin/mysqld-exporter.yaml <<'EOF'
apiVersion: apps/v1 # 使用 apps/v1 Deployment API。
kind: Deployment # 创建持续运行的 Exporter。
metadata: # 对象元数据。
name: mysqld-exporter # 固定 Exporter 名称。
namespace: zzyl # 业务命名空间。
spec: # Deployment 规格。
replicas: 2 # 两副本保证采集可用性。
selector: # Pod 选择器。
matchLabels: # 精确标签匹配。
app: mysqld-exporter # 固定 Pod 标签。
template: # Pod 模板。
metadata: # Pod 元数据。
labels: # 写入标签。
app: mysqld-exporter # 与选择器和 Service 对应。
spec: # Pod 规格。
containers: # 容器列表。
- name: exporter # 容器名称。
image: prom/mysqld-exporter:v0.16.0 # 固定 Exporter 版本。
args: # 启动参数。
- --config.my-cnf=/cfg/.my.cnf # 读取只读凭据文件。
ports: # 指标端口。
- name: metrics # 稳定端口名。
containerPort: 9104 # Exporter 默认端口。
volumeMounts: # Secret 挂载。
- name: auth # 引用下方卷。
mountPath: /cfg # 固定路径。
readOnly: true # 禁止改写凭据。
volumes: # 卷列表。
- name: auth # 与挂载名一致。
secret: # Secret 来源。
secretName: mysqld-exporter-auth # 受限凭据。
---
apiVersion: v1 # 使用核心 Service API。
kind: Service # 创建内部指标入口。
metadata: # Service 元数据。
name: mysqld-exporter # 固定 Service 名称。
namespace: zzyl # 业务空间。
labels: # 发现标签。
app: mysqld-exporter # 与 Exporter 对应。
spec: # Service 规格。
type: ClusterIP # 禁止对外暴露。
selector: # 后端选择器。
app: mysqld-exporter # 匹配 Pod。
ports: # 端口列表。
- name: metrics # 被 ServiceMonitor 引用。
port: 9104 # Service 端口。
targetPort: metrics # 转发到容器端口。
---
apiVersion: monitoring.coreos.com/v1 # Prometheus Operator API。
kind: ServiceMonitor # 服务发现规则。
metadata: # 对象元数据。
name: mysqld-exporter # 固定监控名称。
namespace: monitor # 监控空间。
labels: # Helm 选择标签。
release: prometheus-monitor # 匹配唯一 Prometheus。
spec: # 采集规格。
namespaceSelector: # 被采集 Service 空间。
matchNames: # 明确空间名单。
- zzyl # 只采集本业务。
selector: # Service 标签选择器。
matchLabels: # 精确匹配。
app: mysqld-exporter # 只采集该 Exporter。
endpoints: # 抓取端点。
- port: metrics # 使用命名端口。
interval: 30s # 抓取周期。
scrapeTimeout: 10s # 单次超时。
EOF
# 应用唯一 MySQL Exporter 清单。
kubectl apply -f /home/opsadmin/mysqld-exporter.yaml
# 等待两个 Exporter 副本就绪。
kubectl -n zzyl rollout status deployment/mysqld-exporter --timeout=180s
# 检查发现对象、内部 Service、健康 Target 数量和 MySQL 健康指标。
kubectl -n monitor get servicemonitor mysqld-exporter && kubectl -n zzyl get svc mysqld-exporter && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(up%7Bnamespace%3D%22zzyl%22%2Cservice%3D%22mysqld-exporter%22%7D%3D%3D1)' | jq '.data.result[0].value[1]' && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=mysql_up' | jq '.data.result[].value[1]'
配置作用:以最小 MySQL 权限采集状态,指标只经 ClusterIP 提供给 Prometheus;release 标签保证被唯一实例采集。
生效结果:
deployment "mysqld-exporter" successfully rolled out
NAME ENDPOINTS AGE
mysqld-exporter 2 20s
"2"
"1"
"1"
配置详解:namespaceSelector 限定跨空间发现;scrapeTimeout 小于 interval;Secret 只读挂载防止容器改写凭据。
注意事项:必看:MySQL 密码不进入 YAML 的 env、ConfigMap、镜像层或工单正文。
通过条件:Prometheus Targets 出现 mysqld-exporter 且为 UP,查询 mysql_up 返回两个 1。
失败排查:
# 在 ops-bastion 执行:查看 MySQL Exporter 最近日志,重点检查 DNS、认证和连接错误。
kubectl -n zzyl logs deploy/mysqld-exporter --tail=100
# 在 Exporter Pod 执行:验证 MySQL 服务名称和 TCP/3306 连通性,不读取或输出密码文件。
EXPORTER_POD=$(kubectl -n zzyl get pod -l app=mysqld-exporter -o jsonpath='{.items[0].metadata.name}') && kubectl -n zzyl exec "$EXPORTER_POD" -- getent hosts mysql && kubectl -n zzyl exec "$EXPORTER_POD" -- nc -vz mysql 3306
# 在 ops-bastion 执行:确认采集 Secret 已挂载且文件名为 .my.cnf。
kubectl -n zzyl describe pod "$EXPORTER_POD" | grep -E 'mysqld-exporter-auth|/cfg|\.my\.cnf'
# 在 ops-bastion 执行:检查 Exporter Service 是否存在可抓取后端。
kubectl -n zzyl get svc mysqld-exporter && kubectl -n zzyl get endpointslice -l kubernetes.io/service-name=mysqld-exporter -o wide
5.2 Java JMX 指标与 ServiceMonitor
执行机器:应用构建机制作受控镜像;ops-bastion 的 opsadmin 发布监控对象。JMX Agent 随固定镜像发布,运行期不下载二进制。
# config.yaml
startDelaySeconds: 0 # JVM 启动后立即开始导出指标。
lowercaseOutputName: true # 将指标名转换为小写。
lowercaseOutputLabelNames: true # 将标签名转换为小写。
whitelistObjectNames: # 限定允许读取的 JMX MBean。
- java.lang:type=OperatingSystem # 允许操作系统 CPU、内存和负载指标。
rules: # 定义 JMX 到 Prometheus 指标的转换规则。
- pattern: 'java.lang<type=OperatingSystem><>(committed_virtual_memory|free_physical_memory|free_swap_space|total_physical_memory|total_swap_space)_size:' # 匹配操作系统内存 MBean。
name: os_$1_bytes # 输出字节单位的操作系统内存指标。
type: GAUGE # 指标是可上下浮动的瞬时值。
attrNameSnakeCase: true # 将属性名转换为下划线风格。
- pattern: 'java.lang<type=OperatingSystem><>((?!process_cpu_time)\w+):' # 匹配其他操作系统 MBean 并排除累计 CPU 时间。
name: os_$1 # 输出操作系统通用指标。
type: GAUGE # 指标是瞬时值。
attrNameSnakeCase: true # 将属性名转换为下划线风格。
- pattern: 'java.lang<type=Memory><HeapMemoryUsage>(\w+):' # 匹配 JVM 堆内存 MBean。
name: jvm_memory_heap_$1_bytes # 输出堆内存指标,例如 jvm_memory_heap_used_bytes。
type: GAUGE # 堆内存大小是瞬时值。
- pattern: 'java.lang<type=Memory><NonHeapMemoryUsage>(\w+):' # 匹配 JVM 非堆内存 MBean。
name: jvm_memory_nonheap_$1_bytes # 输出非堆内存指标。
type: GAUGE # 非堆内存大小是瞬时值。
- pattern: 'java.lang<type=GarbageCollector, name=(\w+)><>(CollectionCount|CollectionTime):' # 匹配 GC 次数和耗时。
name: jvm_gc_$1_$2 # 输出按 GC 名称区分的指标。
type: GAUGE # 课程配置使用 GAUGE 类型。
attrNameSnakeCase: true # 将属性名转换为下划线风格。
- pattern: 'java.lang<type=Threading><>((?!AllThreadIds)\w+):' # 匹配线程统计并排除线程 ID 列表。
name: jvm_threads_$1 # 输出线程指标。
type: GAUGE # 线程数是瞬时值。
attrNameSnakeCase: true # 将属性名转换为下划线风格。
- pattern: 'java.lang<type=ClassLoading><(LoadedClassCount|TotalLoadedClassCount|UnloadedClassCount)>:' # 匹配类加载统计。
name: jvm_class_$1 # 输出类加载指标。
type: GAUGE # 类加载统计按课程配置导出为 GAUGE。
attrNameSnakeCase: true # 将属性名转换为下划线风格。
配置作用:该文件与知识库 JMX Exporter 规则一致,明确导出操作系统、堆/非堆内存、GC、线程和类加载指标。
生效结果:Prometheus 可查询 jvm_memory_heap_used_bytes 与 jvm_memory_heap_max_bytes,不再依赖错误的通用指标名。
配置详解:每条 pattern 只匹配指定 MBean;name 决定 Prometheus 最终指标名;type 和 attrNameSnakeCase 保证数值类型、命名风格稳定。
注意事项:核心:Dockerfile、PromQL 查询和告警规则必须使用此文件导出的精确指标名,不能混用另一个 JMX Exporter 配置的 jvm_memory_used_bytes。
# Dockerfile.jmx
FROM harbor.lanqicheng.top/zzyl/zzyl-admin:latest # 使用知识库中的 zzyl-admin 基础镜像。
COPY config.yaml /app/config.yaml # 复制知识库 JMX 指标配置到基础镜像应用目录。
COPY jmx_prometheus_javaagent-1.5.0.jar /app/jmx_prometheus_javaagent-1.5.0.jar # 复制知识库固定 JMX Agent。
EXPOSE 9100 # 声明知识库使用的集群内 metrics 端口。
ENTRYPOINT ["java","-javaagent:/app/jmx_prometheus_javaagent-1.5.0.jar=9100:/app/config.yaml","-jar","zzyl-admin.jar"] # 使用课程路径启动业务与指标。
配置作用:JMX Agent 在同一 JVM 导出堆、GC、线程、类加载和系统指标,不开启远程 JMX 管理端口。
生效结果:容器 9100/metrics 可由集群内 Prometheus 采集,业务端口职责不变。
配置详解:javaagent 在 JVM 启动期载入 Exporter;9100 只经 ClusterIP 暴露,镜像不含密钥。
注意事项:核心:禁止启用 com.sun.management.jmxremote 对外远程访问。
# 在构建机执行:构建固定 JMX 镜像。
docker build -f Dockerfile.jmx -t harbor.lanqicheng.top/zzyl/zzyl-admin:jvm-exporter .
# 在构建机执行:推送已扫描通过的镜像。
docker push harbor.lanqicheng.top/zzyl/zzyl-admin:jvm-exporter
# 在 ops-bastion 执行:将 zzyl-admin Deployment 的业务容器切换为包含 JavaAgent 的镜像。
kubectl -n zzyl set image deployment/zzyl-admin zzyl-admin=harbor.lanqicheng.top/zzyl/zzyl-admin:jvm-exporter
# 在 ops-bastion 执行:等待 zzyl-admin 完成滚动更新。
kubectl -n zzyl rollout status deployment/zzyl-admin --timeout=300s
# 在 ops-bastion 执行:写入 Java metrics Service 与 ServiceMonitor。
cat > /home/opsadmin/zzyl-admin-monitor.yaml <<'EOF'
apiVersion: v1 # 使用核心 Service API。
kind: Service # 创建 Java 指标内部入口。
metadata: # Service 元数据。
name: zzyl-admin-metrics # 固定 metrics Service 名称。
namespace: zzyl # 业务空间。
labels: # ServiceMonitor 使用的标签。
app: zzyl-admin # 固定业务标签。
spec: # Service 规格。
type: ClusterIP # 禁止暴露到集群外。
selector: # 选择已有业务 Pod。
app: zzyl-admin # 必须与 Deployment 标签一致。
ports: # 端口列表。
- name: metrics # ServiceMonitor 引用端口名。
port: 9100 # Service 指标端口。
targetPort: 9100 # 转发到 JMX Agent。
---
apiVersion: monitoring.coreos.com/v1 # 使用 Prometheus Operator API。
kind: ServiceMonitor # 定义 Java 服务发现。
metadata: # 对象元数据。
name: zzyl-admin-jmx # 固定监控名称。
namespace: monitor # 监控空间。
labels: # Helm 选择标签。
release: prometheus-monitor # 匹配唯一实例。
spec: # 采集规格。
namespaceSelector: # 被采集 Service 空间。
matchNames: # 明确空间。
- zzyl # 只允许本业务。
selector: # Service 选择器。
matchLabels: # 精确匹配。
app: zzyl-admin # 只选择 Java metrics Service。
endpoints: # 抓取端点。
- port: metrics # 使用命名端口。
path: /metrics # JMX Exporter 固定路径。
interval: 30s # 抓取周期。
scrapeTimeout: 10s # 单次超时。
EOF
# 应用 Java 监控对象。
kubectl apply -f /home/opsadmin/zzyl-admin-monitor.yaml
# 查询 JMX Target、堆已用和堆最大指标实例数量。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(up%7Bnamespace%3D%22zzyl%22%2Cservice%3D%22zzyl-admin-metrics%22%7D%3D%3D1)' | jq '.data.result[0].value[1]' && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(jvm_memory_heap_used_bytes)' | jq '.data.result[0].value[1]' && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(jvm_memory_heap_max_bytes)' | jq '.data.result[0].value[1]'
执行结果:
deployment "zzyl-admin" successfully rolled out
service/zzyl-admin-metrics created
servicemonitor.monitoring.coreos.com/zzyl-admin-jmx created
"2"
"2"
"2"
注意事项:推荐:镜像推送前通过漏洞扫描和 SBOM;缩扩容时序列数量会变化,但每个 Target 必须 UP。
通过条件:Targets 中 zzyl-admin-jmx 为 UP,存在 jvm_memory_heap_used_bytes 与 jvm_memory_heap_max_bytes 时间序列。
失败排查:
# 在 ops-bastion 执行:确认 JMX Agent 已在 zzyl-admin Pod 模板中声明 9100 端口。
kubectl -n zzyl get deployment zzyl-admin -o jsonpath='{.spec.template.spec.containers[?(@.name=="zzyl-admin")].ports}{"\n"}'
# 在一个 zzyl-admin Pod 执行:检查本地 metrics 端口和堆内存指标名称。
JMX_POD=$(kubectl -n zzyl get pod -l app=zzyl-admin -o jsonpath='{.items[0].metadata.name}') && kubectl -n zzyl exec "$JMX_POD" -- sh -c 'curl -fsS http://127.0.0.1:9100/metrics | grep -E "^jvm_memory_heap_(used|max)_bytes"'
# 在 ops-bastion 执行:检查 JMX Service、EndpointSlice 和 ServiceMonitor 选择器。
kubectl -n zzyl get svc,endpointslice -l app=zzyl-admin -o wide && kubectl -n monitor get servicemonitor zzyl-admin-jmx -o yaml
5.3 告警规则与网页验收
执行机器:ops-bastion,opsadmin。Alertmanager 的 Webhook 凭据在受控 Secret/Helm values 中配置,不写入规则。
# /home/opsadmin/zzyl-alert-rules.yaml
apiVersion: monitoring.coreos.com/v1 # 使用 Prometheus Operator 规则 API。
kind: PrometheusRule # 创建告警规则对象。
metadata: # 对象元数据。
name: zzyl-application-alerts # 固定规则名称。
namespace: monitor # 监控空间。
labels: # Helm 选择标签。
release: prometheus-monitor # 匹配唯一 Prometheus。
spec: # 规则规格。
groups: # 规则组列表。
- name: zzyl.application # 固定规则组名。
rules: # 规则列表。
- alert: MySQLExporterDown # MySQL 采集失败告警。
expr: mysql_up == 0 # 无法连接数据库时为真。
for: 2m # 连续两分钟才触发。
labels: # 告警标签。
severity: critical # 严重等级。
service: mysql # 受影响服务。
annotations: # 告警文字。
summary: MySQL 指标采集失败 # 短标题。
description: '{{ $labels.instance }} 已连续 2 分钟 mysql_up=0。' # 实例上下文。
- alert: JavaHeapUsageHigh # JVM 堆高使用率告警。
expr: jvm_memory_heap_used_bytes / jvm_memory_heap_max_bytes > 0.85 # 使用知识库 JMX 配置实际导出的堆内存指标计算使用率。
for: 10m # 连续十分钟才触发。
labels: # 告警标签。
severity: warning # 告警等级。
service: zzyl-admin # 受影响业务。
annotations: # 告警文字。
summary: Java 堆内存使用率超过 85% # 短标题。
description: '{{ $labels.pod }} 堆内存使用率已连续 10 分钟超过 85%。' # Pod 上下文。
配置作用:把数据库可用性和 JVM 容量风险转为持续条件告警,避免瞬时波动造成告警风暴。
生效结果:规则初始 Inactive;条件持续超过 for 时长后从 Pending 变 Firing。
配置详解:severity 用于告警路由分级,annotations 给值班人员定位信息,release 标签确保规则被正确 Prometheus 选择。
注意事项:必看:先做 server-side dry run;Cloud Alert 项目接口仅用于本项目固定告警出口,不得复制到其他项目或聊天记录。
# 先由 API Server 校验 PrometheusRule 字段。
kubectl apply --dry-run=server -f /home/opsadmin/zzyl-alert-rules.yaml
# 正式创建告警规则。
kubectl apply -f /home/opsadmin/zzyl-alert-rules.yaml
# 检查规则对象。
kubectl -n monitor get prometheusrule zzyl-application-alerts
# 查询 MySQL 健康指标,验证健康时不会误报。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=mysql_up' | jq '.data.result[].value[1]'
# 查询两条已加载规则的当前状态。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/rules' | jq -r '.data.groups[].rules[] | select(.name=="MySQLExporterDown" or .name=="JavaHeapUsageHigh") | [.name,.state] | @tsv'
执行结果:
prometheusrule.monitoring.coreos.com/zzyl-application-alerts created
NAME AGE
zzyl-application-alerts 5s
"1"
"1"
MySQLExporterDown inactive
JavaHeapUsageHigh inactive
5.3.1 Cloud Alert 受控通知测试
执行机器:ops-bastion,opsadmin;仅在已批准的告警测试窗口执行。该规则只产生 severity=info 的测试通知,不修改 MySQL、JVM 或业务工作负载。
# 在 ops-bastion 执行:写入只用于验证 Cloud Alert 投递的临时 PrometheusRule。
cat > /home/opsadmin/cloudalert-delivery-test.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1 # 使用 Prometheus Operator 规则 API。
kind: PrometheusRule # 创建临时告警规则对象。
metadata: # 定义对象元数据。
name: cloudalert-delivery-test # 固定测试规则名称。
namespace: monitor # 放在监控命名空间。
labels: # 定义 Prometheus 选择标签。
release: prometheus-monitor # 只被本项目 Prometheus 加载。
spec: # 定义规则规格。
groups: # 定义规则组列表。
- name: cloudalert.delivery.test # 固定测试规则组名称。
rules: # 定义规则列表。
- alert: CloudAlertDeliveryTest # 固定测试告警名称。
expr: vector(1) # 始终为真,只用于受控投递测试。
for: 30s # 持续三十秒后进入 Firing。
labels: # 定义测试标签。
severity: info # 限定为信息级别。
test: approved # 标记为批准测试。
annotations: # 定义告警说明。
summary: Cloud Alert 投递测试 # 固定测试标题。
description: 批准窗口内的受控通知测试。 # 不涉及业务故障。
EOF
# 在 ops-bastion 执行:创建临时规则并等待 Prometheus 加载。
kubectl apply -f /home/opsadmin/cloudalert-delivery-test.yaml && sleep 45
# 在 ops-bastion 执行:确认临时规则已进入 Firing 状态。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/rules' | jq -r '.data.groups[].rules[] | select(.name=="CloudAlertDeliveryTest") | [.name,.state] | @tsv'
# 在运维浏览器执行:进入 Cloud Alert 项目审计页,确认同名告警已投递至批准的企业微信、短信或电话语音通道,并记录审计编号。
# 在 ops-bastion 执行:确认审计证据已归档后删除临时规则,触发 send_resolved 的恢复事件。
kubectl delete -f /home/opsadmin/cloudalert-delivery-test.yaml
执行结果:
prometheusrule.monitoring.coreos.com/cloudalert-delivery-test created
CloudAlertDeliveryTest firing
prometheusrule.monitoring.coreos.com "cloudalert-delivery-test" deleted
注意事项:必看:必须先在 Cloud Alert 中配置并批准至少一个测试通知通道;未看到审计记录不得删除测试规则,不得通过制造 MySQL 或 JVM 故障替代此测试。
通过条件:Cloud Alert 审计记录包含 CloudAlertDeliveryTest 的 Firing 与 Resolved 事件,且至少一个批准通知通道收到两次事件。
失败排查:
# 在 ops-bastion 执行:确认 Alertmanager 实际引用 Cloud Alert 配置 Secret,且 Secret 只包含预期配置文件名。
kubectl -n monitor get secret cloudalert-alertmanager-config -o jsonpath='{.data.alertmanager\.yaml}{"\n"}' | base64 -d | grep -E 'receiver:|webhook_configs:|send_resolved:'
# 在 ops-bastion 执行:查看 Alertmanager 的投递错误,不显示 Cloud Alert 项目接口或通知凭据。
kubectl -n monitor logs statefulset/prometheus-monitor-kube-prometheus-alertmanager --all-containers=true --tail=200 | grep -Ei 'webhook|notify|error|failed'
# 在 ops-bastion 执行:确认临时规则确已从集群删除,避免继续产生测试通知。
kubectl -n monitor get prometheusrule cloudalert-delivery-test
注意事项:核心:告警出口鉴权只由 Secret 与受控 values 管理,任何 token 都不能进命令、规则或截图。
通过条件:Prometheus Alerts 页面有两条规则,健康时 MySQL 规则为 Inactive,批准的测试通知在 Cloud Alert 审计记录中可追溯。
失败排查:
# 在 ops-bastion 执行:查看 PrometheusRule 的事件、规则标签和实际被 Prometheus 选择的规则名称。
kubectl -n monitor describe prometheusrule zzyl-application-alerts && kubectl -n monitor get prometheusrule zzyl-application-alerts -o jsonpath='{.metadata.labels}{"\n"}'
# 在 ops-bastion 执行:通过 Prometheus API 检查两条规则的当前状态。
curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/rules' | jq -r '.data.groups[].rules[] | select(.name=="MySQLExporterDown" or .name=="JavaHeapUsageHigh") | [.name,.state] | @tsv'
# 在 ops-bastion 执行:查看 Alertmanager 的投递错误,不显示 Webhook 认证材料。
kubectl -n monitor logs statefulset/prometheus-monitor-kube-prometheus-alertmanager --all-containers=true --tail=200 | grep -Ei 'webhook|notify|error|failed'
# 在 ops-bastion 执行:保留告警事件和 Cloud Alert 审计工单号,再由批准测试记录核对通知送达。
kubectl -n monitor get events --sort-by=.lastTimestamp | tail -n 60
知识库保留了 Cloud Alert(睿象云)集成 Prometheus 的真实页面图片,可用于说明本项目的告警出口;它不能替代实施后的 Alertmanager 规则页、Cloud Alert 通知审计页或企业微信、短信、电话语音送达记录。实施时必须归档这些与本项目规则、通知渠道对应的真实截图。
6. 故障、回滚、备份与恢复
6.1 故障处置表
| 现象 | 首先检查 | 通过条件 | 禁止操作 |
|---|---|---|---|
| API 不可访问 | VIP 持有者、HAProxy 后端、readyz | 健康控制面持有 VIP,API 返回 ok | 同时重启三台控制面 |
| Prometheus PVC Pending | NFS provisioner 日志、导出、StorageClass | PVC 为 Bound | 删除 PV/NFS 目录重试 |
| Target Down | Service 标签、Endpoint、NetworkPolicy、metrics | Target 为 UP | 把 Exporter 改为 NodePort |
| 告警未送达 | Alertmanager 配置检查、Cloud Alert 审计记录、通知渠道配置 | 测试通知有审计记录 | 粘贴 Cloud Alert 项目接口或通知凭据 |
6.2 回滚
执行机器:ops-bastion,opsadmin。仅在批准的变更失败或回退窗口执行。
# 列出监控栈 revision 与发布状态。
helm -n monitor history prometheus-monitor
# 创建受限回滚证据目录。
install -d -m 0700 /home/opsadmin/backup
# 导出当前 values 作为故障现场证据。
helm -n monitor get values prometheus-monitor --all > /home/opsadmin/backup/prometheus-monitor-values-before-rollback.yaml
# 从 Helm JSON 历史中取得当前 release 之前最近的成功(superseded)revision,禁止猜测 revision 号。
ROLLBACK_REVISION=$(helm -n monitor history prometheus-monitor -o json | jq -r '[.[] | select(.status == "superseded")] | last | .revision')
# 无上一成功版本时中止,避免把 null 或空值交给 Helm。
test -n "$ROLLBACK_REVISION" && test "$ROLLBACK_REVISION" != null || { echo '未找到可回滚的上一成功 revision'; exit 1; }
# 回滚到刚才自动识别的上一成功 revision。
helm -n monitor rollback prometheus-monitor "$ROLLBACK_REVISION" --wait --timeout 15m
# 验证回滚后的监控栈状态。
helm -n monitor status prometheus-monitor && kubectl -n monitor get pods && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(up%3D%3D1)' | jq '.data.result[0].value[1]' && kubectl -n zzyl get pods -l app=zzyl-admin
# 回退新增 Java 采集对象,不影响基础监控。
kubectl delete -f /home/opsadmin/zzyl-admin-monitor.yaml
# 验证 Java ServiceMonitor 已删除。
kubectl -n monitor get servicemonitor zzyl-admin-jmx
执行结果:
Rollback was a success! Happy Helming!
STATUS: deployed
"18"
zzyl-admin-6f77c94d87-a1b2c 1/1 Running 0 18m
zzyl-admin-6f77c94d87-d3e4f 1/1 Running 0 18m
Error from server (NotFound): servicemonitors.monitoring.coreos.com "zzyl-admin-jmx" not found
注意事项:必看:回滚 revision 由当前 Helm 历史自动取得,绝不硬编码为 1;若没有上一成功版本,必须停止并走人工变更评审。
通过条件:Helm 为 deployed,基础 Targets 仍 UP;业务回退时业务 Pod 不重启。
失败排查:
# 在 ops-bastion 执行:查看回滚 release、revision 历史和 monitor 命名空间事件。
helm -n monitor status prometheus-monitor && helm -n monitor history prometheus-monitor && kubectl -n monitor get events --sort-by=.lastTimestamp | tail -n 80
# 在 ops-bastion 执行:检查 PVC、PV 与其 NFS 路径;此处只读查看,禁止删除 PVC 或 PV。
kubectl -n monitor get pvc -o wide && kubectl get pv -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,CLAIM:.spec.claimRef.namespace/.spec.claimRef.name,NFS:.spec.nfs.path
# 在 nfs01 执行:确认导出目录仍存在、仍被导出且未改变 SELinux 标签。
ls -ldZ /srv/nfs/k8s && exportfs -v
6.3 备份与恢复
6.3.1 备份
执行机器:ops-bastion 的 opsadmin 使用 sudo 写入独立备份盘;通过已审批的 root SSH 只读拉取 nfs01 数据。固定备份根为 /srv/backup/monitoring,不能与 /srv/nfs/k8s 位于同一文件系统。
# 在 ops-bastion 执行:定义当天固定备份目录,不使用临时目录。
BACKUP_DATE=$(date +%F)
# 在 ops-bastion 执行:创建受限配置导出目录。
install -d -m 0700 /home/opsadmin/backup
# 在 ops-bastion 执行:导出完整 Helm values、业务采集对象和告警规则。
helm -n monitor get values prometheus-monitor --all > /home/opsadmin/backup/prometheus-monitor-values-$BACKUP_DATE.yaml
# 在 ops-bastion 执行:导出两个业务 ServiceMonitor。
kubectl -n monitor get servicemonitor mysqld-exporter zzyl-admin-jmx -o yaml > /home/opsadmin/backup/service-monitors-$BACKUP_DATE.yaml
# 在 ops-bastion 执行:导出 MySQL Exporter 的 Deployment 与 Service,并删除不能跨集群恢复的运行态字段。
kubectl -n zzyl get deployment/mysqld-exporter service/mysqld-exporter -o json | jq 'del(.items[].metadata.uid, .items[].metadata.resourceVersion, .items[].metadata.creationTimestamp, .items[].metadata.managedFields, .items[].status, .items[].spec.clusterIP, .items[].spec.clusterIPs, .items[].spec.ipFamilies, .items[].spec.ipFamilyPolicy, .items[].spec.internalTrafficPolicy, .items[].spec.healthCheckNodePort)' > /home/opsadmin/backup/mysqld-exporter-workload-$BACKUP_DATE.json
# 在 ops-bastion 执行:导出业务 PrometheusRule。
kubectl -n monitor get prometheusrule zzyl-application-alerts -o yaml > /home/opsadmin/backup/zzyl-alert-rules-$BACKUP_DATE.yaml
# 在 ops-bastion 执行:导出 monitor PVC 和对应 PV,删除不可恢复的 UID、resourceVersion、状态与 claimRef UID。
kubectl -n monitor get pvc -o json | jq 'del(.items[].metadata.uid, .items[].metadata.resourceVersion, .items[].metadata.creationTimestamp, .items[].metadata.managedFields, .items[].status)' > /home/opsadmin/backup/monitor-pvc-$BACKUP_DATE.json
# 在 ops-bastion 执行:仅导出 claimRef.namespace 为 monitor 的 PV 并清理运行态字段。
kubectl get pv -o json | jq '{apiVersion: .apiVersion, kind: .kind, items: [.items[] | select(.spec.claimRef.namespace == "monitor") | del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .metadata.managedFields, .status, .spec.claimRef.uid, .spec.claimRef.resourceVersion)]}' > /home/opsadmin/backup/monitor-pv-$BACKUP_DATE.json
# 在 ops-bastion 执行:验证独立备份盘不是 NFS 在线数据盘后创建当天目录。
test "$(readlink -f /srv/backup/monitoring)" != "$(readlink -f /srv/nfs/k8s)" && sudo install -d -m 0750 -o opsadmin -g opsadmin /srv/backup/monitoring/$BACKUP_DATE
# 在 ops-bastion 执行:保留数值 UID、ACL、扩展属性,从 nfs01 只读复制监控数据到独立备份盘。
sudo rsync -aHAX --numeric-ids -e ssh root@nfs01:/srv/nfs/k8s/ /srv/backup/monitoring/$BACKUP_DATE/nfs-k8s/
# 在 ops-bastion 执行:复制本次 Kubernetes 配置备份到同一恢复点。
sudo rsync -aHAX --numeric-ids /home/opsadmin/backup/ /srv/backup/monitoring/$BACKUP_DATE/config/
# 在 ops-bastion 执行:写入当前恢复点全部文件的 SHA-256 校验清单。
sudo sh -c "cd /srv/backup/monitoring/$BACKUP_DATE && find . -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS"
# 在 ops-bastion 执行:抽样校验并列出恢复点。
sudo sh -c "cd /srv/backup/monitoring/$BACKUP_DATE && sha256sum -c SHA256SUMS | tail -n 3" && sudo ls -lh /srv/backup/monitoring/$BACKUP_DATE
执行结果:
./config/prometheus-monitor-values-2026-08-02.yaml: OK
./config/mysqld-exporter-workload-2026-08-02.json: OK
./monitor-pv-2026-08-02.json: OK
./monitor-pvc-2026-08-02.json: OK
./nfs-k8s/pvc-6b7f.../chunks_head/000001: OK
-rw-r----- 1 opsadmin opsadmin 3.2M Aug 02 20:34 /srv/backup/monitoring/2026-08-02/SHA256SUMS
注意事项:必看:rsync 目标已通过 readlink -f 限制为独立备份盘,禁止对在线 /srv/nfs/k8s 使用 --delete;备份不等于恢复,至少每季度在隔离环境演练一次。
通过条件:config、mysqld-exporter-workload JSON、monitor-pv JSON、monitor-pvc JSON、nfs-k8s 和 SHA256SUMS 均在同一日期目录,抽样 sha256sum -c 输出 OK。
失败排查:
# 在 ops-bastion 执行:检查独立备份盘容量、inode 与可清理的日期目录;不操作在线 NFS 数据目录。
df -h /srv/backup/monitoring && df -i /srv/backup/monitoring && find /srv/backup/monitoring -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort
# 在 ops-bastion 执行:以批处理模式验证到 nfs01 的受控 SSH 连通性。
ssh -o BatchMode=yes -o ConnectTimeout=5 root@nfs01 'hostnamectl --static && test -d /srv/nfs/k8s'
# 在校验失败的恢复点目录执行:复现校验失败并检查备份盘内核 I/O 错误;失败时停止恢复流程。
sudo sh -c 'cd /srv/backup/monitoring/2026-08-02 && sha256sum -c SHA256SUMS' && dmesg --level=err,warn | tail -n 50
6.3.2 恢复演练
执行机器:ops-bastion 的 opsadmin 使用 sudo;nfs01 root 仅接收已校验的恢复数据。仅在批准的灾难恢复变更中执行。先按第 1—3.1 节重建节点、API VIP、Kubernetes、Cilium、NFSv4 与 nfs-storage;本节会单独恢复 Traefik,避免把 3.2 节的密管 Secret 创建与恢复流程混在一起。在恢复业务采集对象前,业务团队必须按批准的业务恢复手册恢复 zzyl-admin、MySQL 及带 9100 JMX Agent 的业务镜像;本节仅验证该前置状态,不替代业务系统恢复。恢复期间禁止提前安装 prometheus-monitor,避免新 PVC 先写入空目录。
# 在 ops-bastion 执行:使用第 0 节固定的已批准恢复点。
RESTORE_DATE=2026-08-02
# 在 ops-bastion 执行:确认恢复点存在并先验证全部备份完整性。
test -d /srv/backup/monitoring/$RESTORE_DATE && sudo sh -c "cd /srv/backup/monitoring/$RESTORE_DATE && sha256sum -c SHA256SUMS"
# 在 ops-bastion 执行:确认密管已重新下发证书、Cloud Alert、Grafana、镜像拉取与 MySQL Exporter 凭据;不从备份盘恢复敏感 Secret 内容。
test -s /etc/ops-pki/monitoring/tls.crt && test -s /etc/ops-pki/monitoring/tls.key && test -s /etc/ops-secrets/monitoring/cloudalert-alertmanager.yaml && test -s /etc/ops-secrets/monitoring/grafana-admin-user && test -s /etc/ops-secrets/monitoring/grafana-admin-password && test -s /etc/ops-secrets/zzyl/registry-ops-pull.json && test -s /etc/ops-secrets/zzyl/mysqld-exporter.cnf
# 在 ops-bastion 执行:在恢复 Helm 与业务采集对象前创建所需命名空间。
kubectl create namespace monitor --dry-run=client -o yaml | kubectl apply -f - && kubectl create namespace zzyl --dry-run=client -o yaml | kubectl apply -f -
# 在 ops-bastion 执行:从密管重新创建监控 TLS、Cloud Alert 与 Grafana Secret。
kubectl -n monitor create secret tls monitoring-ops-tls --cert=/etc/ops-pki/monitoring/tls.crt --key=/etc/ops-pki/monitoring/tls.key --dry-run=client -o yaml | kubectl apply -f - && kubectl -n monitor create secret generic cloudalert-alertmanager-config --from-file=alertmanager.yaml=/etc/ops-secrets/monitoring/cloudalert-alertmanager.yaml --dry-run=client -o yaml | kubectl apply -f - && kubectl -n monitor create secret generic grafana-admin --from-file=admin-user=/etc/ops-secrets/monitoring/grafana-admin-user --from-file=admin-password=/etc/ops-secrets/monitoring/grafana-admin-password --dry-run=client -o yaml | kubectl apply -f -
# 在 ops-bastion 执行:从密管重新创建业务镜像拉取与 MySQL Exporter 凭据 Secret,并只校验五个 Secret 的类型。
kubectl -n zzyl create secret generic registry-ops-pull --from-file=.dockerconfigjson=/etc/ops-secrets/zzyl/registry-ops-pull.json --type=kubernetes.io/dockerconfigjson --dry-run=client -o yaml | kubectl apply -f - && kubectl -n zzyl create secret generic mysqld-exporter-auth --from-file=.my.cnf=/etc/ops-secrets/zzyl/mysqld-exporter.cnf --dry-run=client -o yaml | kubectl apply -f - && kubectl -n monitor get secret monitoring-ops-tls -o jsonpath='{.type}{"\n"}' && kubectl -n monitor get secret cloudalert-alertmanager-config -o jsonpath='{.type}{"\n"}' && kubectl -n monitor get secret grafana-admin -o jsonpath='{.type}{"\n"}' && kubectl -n zzyl get secret registry-ops-pull -o jsonpath='{.type}{"\n"}' && kubectl -n zzyl get secret mysqld-exporter-auth -o jsonpath='{.type}{"\n"}'
# 在 ops-bastion 执行:恢复固定版本 Traefik DaemonSet,并确认三个工作节点的 80/443 入口已就绪;监控栈的 HTTPS Ingress 依赖此入口。
helm repo add traefik https://traefik.github.io/charts --force-update && helm repo update && helm upgrade --install traefik traefik/traefik --version 28.2.0 --namespace traefik --create-namespace --set deployment.kind=DaemonSet --set service.enabled=false --set ports.web.hostPort=80 --set ports.websecure.hostPort=443 --wait --timeout 10m && kubectl -n traefik rollout status daemonset/traefik --timeout=180s && kubectl -n traefik get pods -o wide
# 在 nfs01 执行:确认在线 NFS 路径正确后,清空并从已校验恢复点精确回灌历史数据。
ssh root@nfs01 'test "$(readlink -f /srv/nfs/k8s)" = /srv/nfs/k8s && install -d -m 0770 -o nobody -g nobody /srv/nfs/k8s'
# 在 ops-bastion 执行:仅向已验证的 nfs01 在线数据目录回灌备份,--delete 使目录精确匹配恢复点。
sudo rsync -aHAX --numeric-ids --delete -e ssh /srv/backup/monitoring/$RESTORE_DATE/nfs-k8s/ root@nfs01:/srv/nfs/k8s/
# 在 ops-bastion 执行:先恢复原 PV、PVC 绑定,再安装固定 Chart,确保新 Pod 挂载原 NFS 路径。
kubectl apply -f /srv/backup/monitoring/$RESTORE_DATE/config/monitor-pv-$RESTORE_DATE.json
# 在 ops-bastion 执行:恢复 monitor 命名空间中的 PVC 定义。
kubectl apply -f /srv/backup/monitoring/$RESTORE_DATE/config/monitor-pvc-$RESTORE_DATE.json
# 在 ops-bastion 执行:使用备份 values 和固定 Chart 版本恢复监控栈。
helm upgrade --install prometheus-monitor prometheus-community/kube-prometheus-stack --version 76.4.1 --namespace monitor --create-namespace --values /srv/backup/monitoring/$RESTORE_DATE/config/prometheus-monitor-values-$RESTORE_DATE.yaml --wait --timeout 15m
# 在 ops-bastion 执行:在已由密管恢复的 Exporter 凭据基础上,恢复 MySQL Exporter Deployment 与集群内部 Service,并等待 Pod 就绪。
kubectl apply -f /srv/backup/monitoring/$RESTORE_DATE/config/mysqld-exporter-workload-$RESTORE_DATE.json && kubectl -n zzyl rollout status deployment/mysqld-exporter --timeout=180s
# 在 ops-bastion 执行:确认业务系统已由其批准手册恢复,MySQL 有后端,zzyl-admin Pod 模板保留 JMX 9100 端口;任一项失败即停止,不创建 ServiceMonitor。
kubectl -n zzyl get deployment zzyl-admin && kubectl -n zzyl get service mysql && kubectl -n zzyl get endpointslice -l kubernetes.io/service-name=mysql -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\n"}{end}' && kubectl -n zzyl get deployment zzyl-admin -o jsonpath='{.spec.template.spec.containers[?(@.name=="zzyl-admin")].ports}{"\n"}' | grep -w 9100
# 在 ops-bastion 执行:业务前置状态已通过后恢复 MySQL 与 JMX 两个 ServiceMonitor。
kubectl apply -f /srv/backup/monitoring/$RESTORE_DATE/config/service-monitors-$RESTORE_DATE.yaml
# 在 ops-bastion 执行:恢复业务 PrometheusRule。
kubectl apply -f /srv/backup/monitoring/$RESTORE_DATE/config/zzyl-alert-rules-$RESTORE_DATE.yaml
# 在 ops-bastion 执行:验证 PVC 重新绑定、监控栈就绪、基础 Target 和业务采集指标恢复。
kubectl -n monitor get pvc,pods && helm -n monitor status prometheus-monitor && curl --fail --silent --show-error https://prometheus.ops.example.internal/-/healthy && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(up%3D%3D1)' | jq '.data.result[0].value[1]' && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(mysql_up%3D%3D1)' | jq '.data.result[0].value[1]' && curl --fail --silent --show-error 'https://prometheus.ops.example.internal/api/v1/query?query=count(jvm_memory_heap_used_bytes)' | jq '.data.result[0].value[1]'
执行结果:
./config/monitor-pv-2026-08-02.json: OK
./nfs-k8s/pvc-6b7f.../chunks_head/000001: OK
secret/monitoring-ops-tls configured
secret/cloudalert-alertmanager-config configured
secret/grafana-admin configured
secret/registry-ops-pull configured
secret/mysqld-exporter-auth configured
kubernetes.io/tls
Opaque
Opaque
kubernetes.io/dockerconfigjson
Opaque
Release "traefik" has been upgraded. Happy Helming!
daemon set "traefik" successfully rolled out
NAME READY STATUS RESTARTS AGE IP NODE
traefik-7c9d6d77d8-9d4jv 1/1 Running 0 2m 192.168.88.173 worker01
persistentvolume/pvc-6b7f... created
persistentvolumeclaim/prometheus-monitor-kube-prometheus-prometheus-db-prometheus-monitor-kube-prometheus-prometheus-0 created
prometheus-prometheus-monitor-kube-prometheus-prometheus-db-prometheus-monitor-kube-prometheus-prometheus-0 Bound pvc-6b7f... 200Gi RWO nfs-storage
STATUS: deployed
deployment "mysqld-exporter" successfully rolled out
NAME READY UP-TO-DATE AVAILABLE AGE
zzyl-admin 2/2 2 2 12d
10.100.2.41
[{"containerPort":9100,"protocol":"TCP"}]
Prometheus Server is Healthy.
"18"
"2"
"2"
注意事项:必看:RESTORE_DATE 只能由批准变更单改为一个已校验目录,不能自动选择“最新”;恢复命令会覆盖 /srv/nfs/k8s,必须完成变更审批、业务停写和备份保全后执行。
通过条件:所有 SHA256 校验通过,五个密管 Secret 类型正确,Traefik DaemonSet 已就绪,业务 zzyl-admin、MySQL Endpoint 与 JMX 9100 前置检查通过,monitor PVC 均为 Bound,Helm 为 deployed,MySQL Exporter 已滚动就绪,Prometheus healthy,且第 4.2、5.1、5.2、5.3 的网页与 Target 验收全部再次通过。
失败排查:
# 在 ops-bastion 执行:逐项比对恢复后 PVC 与 PV 的绑定关系、命名空间和存储类。
kubectl -n monitor get pvc -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,CLASS:.spec.storageClassName && kubectl get pv -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,CLAIM:.spec.claimRef.namespace/.spec.claimRef.name,CLASS:.spec.storageClassName,NFS:.spec.nfs.path
# 在 nfs01 执行:检查导出、SELinux 标签和 NFSv4 TCP/2049 防火墙规则。
exportfs -v && ls -Zd /srv/nfs/k8s && firewall-cmd --list-rich-rules | grep 'port="2049"'
# 在 Pod 挂载失败的工作节点执行:检查到 NFS 服务器的 TCP/2049 连通性。
nc -vz 192.168.88.180 2049
# 在 ops-bastion 执行:保留 Helm 和 Kubernetes 事件;失败时不重复恢复,回到批准的恢复点评审。
helm -n monitor status prometheus-monitor && kubectl -n monitor get events --sort-by=.lastTimestamp | tail -n 100
# 在 ops-bastion 执行:检查 Traefik 是否已恢复,以及业务系统是否满足恢复采集对象的前置条件。
kubectl -n traefik get daemonset,pods -o wide && kubectl -n zzyl get deployment zzyl-admin && kubectl -n zzyl get endpointslice -l kubernetes.io/service-name=mysql -o wide && kubectl -n zzyl get deployment zzyl-admin -o jsonpath='{.spec.template.spec.containers[?(@.name=="zzyl-admin")].ports}{"\n"}'
# 在 ops-bastion 执行:密管文件缺失或 Secret 类型不正确时停止恢复,并回到密管下发记录核对版本与权限。
kubectl -n monitor get secret monitoring-ops-tls cloudalert-alertmanager-config grafana-admin -o custom-columns=NAME:.metadata.name,TYPE:.type && kubectl -n zzyl get secret registry-ops-pull mysqld-exporter-auth -o custom-columns=NAME:.metadata.name,TYPE:.type
7. 最终核查
- [ ] 7 台服务器名称、时间、解析和安全策略符合第 0 节。
- [ ] 3 控制面 HAProxy/Keepalived 正常,VIP 唯一,6 个 K8s 节点 Ready。
- [ ] Cilium、NFS provisioner、Traefik 均运行,nfs-storage 可绑定 PVC。
- [ ] 私钥、数据库口令、告警鉴权、仓库凭据只存在于受限文件/Secret,未进入本文、Git 或镜像历史。
- [ ] prometheus-monitor 为 deployed;Prometheus 两副本、Alertmanager 三副本、Grafana 均就绪。
- [ ] 基础、MySQL、JMX Targets 均 UP;Prometheus、Grafana、Alertmanager 网页验收通过。
- [ ] 两条业务告警规则可见,测试通知具有审计记录。
- [ ] Helm、CR 清单和 NFS 数据已备份,恢复顺序已演练。
- [ ] 已确认 ELK 属于后续独立项目,本文没有部署或伪造 ELK 内容。
以上全部满足,才可在变更单中标记“生产环境监控告警与可观测性体系建设”完成。