
这篇是「技术演进」总结讲我一路做下来的两个时代的方案传统LVS-DR Keepalived和K8s 集群版Deployment / StatefulSet / Service / Ingress / MetalLB。 不重复上一篇的完整搭建细节搭建过程见《LVS_DR高可用集群实战》《K8s高可用集群搭建-完整实战》《K8s-MySQL主从集群-实战复盘博客》这篇聚焦演进逻辑、配置方式、流量转发过程、踩坑对比。一、为什么要演进先讲动机维度传统高可用K8s 集群版谁管高可用人写 keepalived.conf、ipvsadm手工维护平台控制器自动管声明式 yaml故障处理健康检查脚本 VIP 漂移人工介入多自愈容器挂了 kubelet 重启副本少了控制器补发布手动改配置、重启服务滚动更新kubectl apply即可扩容加机器 改 keepalived 规则改副本数 / HPA自动调度学习/运维成本每套组件都要单独维护一个 K8s 统一yaml 即基础设施即代码一句话传统是靠配置文件 脚本把高可用写死K8s 是把节点、副本、网络、存储变成声明式的对象让平台按你想要的状态去收敛。二、传统技术栈LVS-DR Keepalived2.1 流量转发过程完整链路DNS 解析 → VIP 入口 → LVS 转发传统这套不是客户端直接找 LVS而是一整条链路——域名先过自建的 DNSDNS 再把域名解析成 Web 的 VIP最后才到 LVS客户端 ──访问 www.chengke.com── DNS 服务VIP .200BIND 主从 dns01/.61、dns02/.62 │ ① 解析出 www.chengke.com 192.168.211.100Web VIP .100 ▼ 客户端 ──② 请求 Web VIP .100── LVS Director(.71/.72) ──③ 只改 MAC不动 IP── web01(.81) / web02(.82) │ 后端直接用真实 IP 回包给客户端 │ 回包不经过 Director所以性能最高这里 DNS 本身也是高可用的一环记忆高可用的 DNS 高可用的 Web两条都高可用才叫全程高可用DNS 高可用BIND 主从dns01 主 / dns02 从 LVS/Keepalived 给 DNS 服务做 VIP .200。www.chengke.com解析到 web 的 VIP .100。Web 高可用Keepalived LVS 给 Web 服务做 VIP .100转发到 web01/.81、web02/.82。所以传统高可用要把入口域名 → DNS 解析 → LVS 转发 → 后端这一整条链路都做成高可用客户端全程只需记忆一个域名其余全部无感知。关键设计DR 模式三件套记忆口诀藏家门、挂门牌、别抢答挂门牌后端把 VIP 绑到回环口ip addr add 192.168.211.100/32 dev lo只用于收包。别抢答后端开 ARP 抑制arp_ignore1、arp_announce2防止后端抢答 VIP 的 ARP外界只认 Director。藏家门VIP 挂lo而不是物理网卡不参与对外通告。为什么选 DR因为回包不经过 DirectorDirector 只做转发入口吞吐最高对比 NAT 模式回包必须绕回 Director会成瓶颈。2.2 配置方式在 lb-master 的 keepalived.conf 里# Web VIP 实例VRID 51 vrrp_instance VI_WEB { state MASTER # backup 上是 BACKUP interface ens160 virtual_router_id 51 # 同接口上必须唯一否则脑裂 priority 100 # backup 设 80 virtual_ipaddress { 192.168.211.100 } # VIP 由 Keepalived 管理、负责漂移 } # Web 虚拟服务wrr 加权轮询 virtual_server 192.168.211.100 80 { lb_algo wrr # 加权轮询web012 web021 lb_kind DR # 直接路由 protocol TCP real_server 192.168.211.81 80 { weight 2; TCP_CHECK { connect_timeout 3; retry 3; } } real_server 192.168.211.82 80 { weight 1; TCP_CHECK { connect_timeout 3; retry 3; } } }这套配置里同时干了两件事KeepalivedVRRP管 VIP 漂移。MASTER 挂了 → BACKUP 收不到 VRRP 通告 → 自动升 MASTER → VIP 漂过去客户端无感。LVS / ipvsadm把virtual_server写进 keepalived.confkeepalived 会自动加载成 ipvsadm 规则做四层负载均衡。健康检查TCP_CHECKweb探 TCP 端口和MISC_CHECKDNS跑checkdns.sh用dig查 TXT 记录见上一篇。后端挂了自动从 ipvsadm 剔除。2.3 代表作LVS-DR 高可用集群Web DNS NFS 共享存储一套 Director Keepalived 主备扛 Web 和 DNS 两个 VIP.100 / .200Web 后端挂 NFS 共享一份网页内容。三、K8s 集群版技术栈升级后的时代3.1 流量转发过程七层Ingress 在四层之上又叠了一层浏览器 ──域名── Ingress-nginxIngress 按 host/路径路由 │ ▼ ServiceClusterIPkube-proxy 负载均衡 │ ▼ Endpoints后端 Pod 列表── 真实 Pod和传统的本质区别层传统LVS-DRK8s转发四层只认 IP:端口七层能按域名 / 路径路由负载均衡ipvsadm 的 rr/wrrkube-proxy 自动做 Service 的负载均衡入口暴露Director 绑 VIPMetalLB给 LoadBalancer Service 分配外部 IP路由一个端口对一个 service一个端口 域名路由到多个 Service一个 ingress 全搞定我这次实操的关键K8s 集群里没有云厂商 LB直接用MetalLB给ingress-nginx-controller这个 LoadBalancer 类型的 Service 分配了一个内网 IP192.168.211.220于是http://grafana.k8s.local这种无端口域名就能直接访问了在此之前只能靠 NodePort 的:31196兜底。3.2 配置方式声明式 yaml从 master 上kubectl apply先说清搭这套的三个先后顺序我实际就是这么做的先装 ingress-nginx入口控制器不然业务访问不进来再装 MetalLB给 LoadBalancer 分配外部 IP否则入口只走 NodePort最后装 kube-prometheus-stack监控站本体① ingress-nginx 入口控制器helm 装关键点镜像走 harbor、关闭镜像 digest 校验# 关键镜像是从 daocloud 代理站拉下来再推到 harborhelm 里指向 harbor docker pull m.daocloud.io/registry.k8s.io/ingress-nginx/controller:v1.11.3 docker tag m.daocloud.io/registry.k8s.io/ingress-nginx/controller:v1.11.3 hb.reg.com/k8s/ingress-nginx-controller:v1.11.3 docker push hb.reg.com/k8s/ingress-nginx-controller:v1.11.3 # certgen 同理docker pull/tag/push hb.reg.com/k8s/ingress-nginx-kube-webhook-certgen:v1.4.4 helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx --create-namespace \ --set controller.image.repositoryhb.reg.com/k8s/ingress-nginx-controller \ --set controller.image.tagv1.11.3 \ --set controller.image.digest \ # ⚠️ 关掉 chart 默认 pin 的 digest否则拉Harbor 上不存在的 digest报 not found --set controller.admissionWebhooks.patch.image.repositoryhb.reg.com/k8s/ingress-nginx-kube-webhook-certgen \ --set controller.admissionWebhooks.patch.image.tagv1.4.4 \ --set controller.admissionWebhooks.patch.image.digest \ --set controller.replicaCount1 \ --set controller.resources.requests.cpu100m --set controller.resources.requests.memory128Mi \ --set controller.resources.limits.cpu200m --set controller.resources.limits.memory256Mi⚠️ 这里有个必须记住的坑ingress-nginx chart 默认会给镜像 pin 一个sha256:...digest。你自己docker pull原镜像 →docker tag→ push 到 harbor 后Harbor 上那个 tag 的 digest ≠ chart 里 pin 的 digestkubelet 拉「tag与你不符的digest」必然not found。所以必须--set controller.image.digest让 helm 只用 tag 拉。排障先手kubectl describe pod -n ingress-nginx controller看 EventsFailed to pull image ...sha256:...not found就是 digest 不匹配。② 高可用入口MetalLB 给 LoadBalancer 发 IP 先用metallb-native.yaml装 controller speaker我用的 v0.16.1再 apply IPAddressPool。# metallb-native.yaml 里引入的两个镜像speaker 是 DaemonSet 要全节点都有 # quay.io/metallb/controller:v0.16.1 (Deployment) # quay.io/metallb/speaker:v0.16.1 (DaemonSet)kubectl apply -f metallb-native.yaml # 等 controller Ready 再 apply 地址池否则 webhook connection refused kubectl wait --forconditionready pod -l appmetallb,componentcontroller -n metallb-system kubectl apply -f ipaddresspool.yaml/root/ipaddresspool.yaml我实际用的netease 段 220-240apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: { name: k8s-pool, namespace: metallb-system } spec: addresses: - 192.168.211.220-192.168.211.240 # ⚠️ 必须避开节点 IP(.202/.203/.204) Service网段(10.10.0.0/12) Pod网段(10.244.0.0/16) --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: { name: k8s-l2, namespace: metallb-system } spec: ipAddressPools: [k8s-pool]装完kubectl get ipaddresspool -n metallb-system看到assignedIPv4: 1且ingress-nginx-controller的 EXTERNAL-IP 从pending变成192.168.211.220 成功。这时候入口才真正能从「无端口域名」访问此前只能 NodePort:31196兜底。③ 无状态应用pythonDeployment 多副本 Service Ingress# Deployment5 副本容器挂掉 kubelet 自动重启 apiVersion: apps/v1 kind: Deployment metadata: { name: python, namespace: python } spec: replicas: 5 selector: { matchLabels: { app: python } } template: metadata: { labels: { app: python } } spec: containers: - name: python-cicd image: hb.reg.com/project/python-cicd:v1.0.11 # 走内网 harbor快又稳 ports: [ { containerPort: 5000 } ]# ServiceClusterIP把流量负载均衡到 5 个副本 apiVersion: v1 kind: Service metadata: { name: python, namespace: python } spec: type: ClusterIP selector: { app: python } ports: - port: 80 targetPort: 5000 # 对外 80 → 容器 5000# Ingress一个入口按域名路由到 Service apiVersion: networking.k8s.io/v1 kind: Ingress metadata: { name: python-ingress, namespace: python } spec: ingressClassName: nginx rules: - host: python.k8s.local http: paths: - path: / pathType: Prefix backend: service: name: python port: { number: 80 }④ 有状态应用MySQL 主从StatefulSet 无头服务 NFS PV/PVC这个走的组件多但每层解决一个具体问题见《K8s-MySQL主从集群-实战复盘博客》需求K8s 概念解决什么主从要有固定身份StatefulSet稳定 Pod 名 mysql-0/1Deployment 的 Pod 名是随机哈希无法区分角色从库要精确连主库无头服务 HeadlessclusterIP: None普通 Service 会轮询破坏主从关系数据不能丢PV/PVC NFS数据落共享盘Pod 没了数据还在一 Pod 一磁盘volumeClaimTemplates每起一个 Pod 自动建一个 PVC主从用不同配置ConfigMap initContainer启动前按序号把 primary.cnf / replica.cnf 放进去⑤ 监控体系kube-prometheus-stackhelm 一键监控我用kube-prometheus-stackhelm chart一键装一个 release 带齐 Prometheus / Grafana / Alertmanager / node-exporter / kube-state-metrics / operator。实际版本chart 89.2.2 / app v0.93.1。helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update kubectl create ns monitoring helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --namespace monitoring关键点这 8 个镜像必须能拉到node-exporter是 DaemonSet3 节点都要有quay.io/prometheus/node-exporter:v1.12.1、quay.io/prometheus/prometheus:v3.14.0-distroless、quay.io/prometheus/alertmanager:v0.34.0、quay.io/grafana/grafana:13.2.1-distroless、quay.io/prometheus-operator/prometheus-operator:v0.93.1、registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.20.0、quay.io/kiwigrid/k8s-sidecar:2.11.2、ghcr.io/jkroepke/kube-webhook-certgen:1.8.8。因为registry.k8s.io被你网络环境的墙卡死manifest 被 307 重定向到 google 欧洲 GCP 存储连不通、docker hub 的 registry-mirror 加速站缓存又损坏只有k8s.m.daocloud.iodaocloud 专门代理 k8s.io 镜像的站能拉 kube-state-metrics所以它的镜像我单独推到 harbor 再指过去docker login hb.reg.com -u admin -p Harbor12345 docker pull k8s.m.daocloud.io/kube-state-metrics/kube-state-metrics:v2.20.0 docker tag k8s.m.daocloud.io/kube-state-metrics/kube-state-metrics:v2.20.0 hb.reg.com/k8s/kube-state-metrics:v2.20.0 docker push hb.reg.com/k8s/kube-state-metrics:v2.20.0 # ⚠️ 改镜像地址必须把 registry 和 repository 拆开否则拼成 registry.k8s.io/hb.reg.com/... helm upgrade kube-prometheus-stack prometheus-community/kube-prometheus-stack \ --namespace monitoring --reuse-values \ --set kube-state-metrics.image.registryhb.reg.com \ --set kube-state-metrics.image.repositoryk8s/kube-state-metrics \ --set kube-state-metrics.image.tagv2.20.0 kubectl rollout restart deployment/kube-prometheus-stack-kube-state-metrics -n monitoring装完kubectl get pods -n monitoring全 Running 监控体系就位。三个 Web UIgrafana/prometheus/alertmanager python 共用一个 ingress-nginx 入口靠 Ingress 里不同host域名分流地址全走 MetalLB 的192.168.211.220。四、流量转发 配置方式对比重点4.1 流量转发对比传统 LVS-DR完整链路 客户端 ──访问域名── DNS服务(高可用 VIP .200, BIND主从) ──解析出域名的IP── 请求 Web VIP .100 │ ▼ LVS Director(只改MAC) ── 后端(直接回包,不经过Director) ↑ 两层都要高可用DNS(.200) Web(.100)四层、只认IP:端口、一个端口对一个服务 K8s Ingress 浏览器 ──域名── Ingress-nginx(按host/path) ── Service(ClusterIP) ── Pod ↑ 七层、按域名路径路由、一个入口多个服务一个端口多个服务怎么做到的传统 LVS 是一个端口 一个 virtual_server想暴露 3 个服务得开 3 个端口K8s 是一个 ingress-nginx 入口80靠 Ingress 里不同的host域名把流量分给不同的 Service——这是我这次体会最深的一点。4.2 高可用机制对比服务挂了怎么办 / 升级怎么更场景传统LVS-DR KeepalivedK8s 集群版服务实例挂了健康检查剔除TCP_CHECK / MISC_CHECK→ 但后端服务本身还得手动重启 / 脚本重启不会自动拉起故障自愈容器挂了 kubelet 自动重启副本少了控制器自动补实例没就绪自动从 Endpoints 摘掉全程高可用靠 Keepalived 主备漂移 VIP 后端脚本兜底切换要一点时间且要看脚本健壮性平台兜底kubectl apply后自动收敛到期望状态几乎无需人工升级 / 发布改配置 重启服务需要宕机一段时间停机窗滚动更新逐个替换旧副本新副本就绪再删旧副本全程不中断服务一句话传统是挂了等人修手动/脚本重启 停机窗K8s 是挂了平台自愈自动重启 滚动更新不停机。4.3 配置方式对比传统K8s改/etc/keepalived/keepalived.confipvsadm-save写 yaml kubectl apply改完keepalived -t校验 systemctl restartapply 后控制器自动收敛无需手动重启加节点要改 keepalived.conf 加 real_server加副本改replicas或打 label 扩节点配置错误可能整台 Direct 起不来坑yaml 有 schema 校验 dry-run错在 apply 前就能发现五、踩坑记录两个时代都踩过都值得记5.1 传统 LVS 时代的坑坑现象根因解决VRID 冲突keepalived 启动失败退出码 2两个vrrp_instance用了同一个virtual_router_id 51改成 52同接口 VRID 必须唯一关键字拼写keepalived -t报Unknown keywordtrack_scrip应为track_scriptTCP_CHECK里没有nb_get_retry应是retryMISC_CHECK里应是misc_timeout逐个纠正关键字后端同时绑 VIP 会不会脑裂web01/web02 的 lo:0 都有 .100DR 模式后端绑 VIP 是正常的为了收包只要 ARP 抑制对就不会脑裂判断标准客户端arp -n看 VIP 的 MAC 必须指向 Director负载均衡 vs 高可用混为一谈以为开 ipvs 就能外部访问ClusterIP/NodePort 是对内/外部入口和 lb_algo 负载均衡是两回事拆开看LVS 管转发负载均衡Keepalived 管入口 VIP 主备5.2 K8s 版本的坑坑现象根因解决镜像拉取registry.k8s.iopod ImagePullBackOffregistry.k8s.io的 manifest 被 307 重定向到 google 欧洲 GCP 存储国内连不通走 daocloud 代理站k8s.m.daocloud.io或推到本地 harbor 再用helm 改子 chart 镜像改了地址 pod 还是拉旧的kubeStateMetrics大驼峰不生效正确是kube-state-metrics子 chart 名小写连字符且registry和repository要拆开否则拼成registry.k8s.io/hb.reg.com/...用kube-state-metrics.image.registryhb.reg.comrepositoryk8s/kube-state-metricsIngress 域名不匹配访问不到 / 跳到别的页面浏览器敲的域名必须和 Ingress 里rules.host完全一致hosts 里chengke.com和.k8s.local两套域名体系别混用chengke.com和 LVS 时代那台 .91 冲突统一用*.k8s.localhosts 加192.168.211.220 x.k8s.localMetalLB 分配 IP 后 LoadBalancer 仍是 pendingEXTERNAL-IP 一直pending集群没有 LoadBalancer 实现MetalLB 的 IPAddressPool 网段要和节点/Service/Pod 网段避开装 MetalLBpool 用 192.168.211.220-240再 applyService 的 selector / namespace 不匹配kubectl get endpoints为空selector 没命中 pod或 Service 和 Deployment 不在一个 ns跨 ns selector 不生效查 endpoints改 selector / 对齐 ns六、后续展望还没做先记一笔之后单独展开现在这套 K8s 监控用的是kube-prometheus-stack自带的告警规则已经比较完善。后续要加自定义告警规则和更细的监控时方向大致是自定义告警规则kube-prometheus-stack 用PrometheusRuleCRD的spec.groups[].rules[]写告警表达式yaml 里alert:/expr:/for:/labels.severity配好会自动被 operator 收集。对应传统时代想在 Prometheus 里加 rule_files 的思路但这里用 CRD 声明式不用改配置文件给容器加探针在 Deployment/StatefulSet 的容器上写livenessProbe存活探针挂了重启和readinessProbe就绪探针就绪了才接流量让自愈更及时。七、一句话总结传统高可用靠配置文件 脚本把 VIP 漂移 / 负载均衡写死K8s 靠声明式对象 控制器让平台把副本、网络、存储、路由按你想要的收敛是从手动运维到平台自愈的升级。四层LVS管转发 负载均衡七层Ingress管域名 路径路由MetalLB 给外部门开锁——三者合作一个入口进多服务。