这篇是「技术演进」总结,讲我一路做下来的两个时代的方案:传统(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 + Keepalived
2.1 流量转发过程(完整链路:DNS 解析 → VIP 入口 → LVS 转发)
传统这套不是"客户端直接找 LVS",而是一整条链路——域名先过自建的 DNS,DNS 再把域名解析成 Web 的 VIP,最后才到 LVS:
客户端 ──访问 www.chengke.com──> DNS 服务(VIP .200,BIND 主从 dns01/.61、dns02/.62) │ ① 解析出 www.chengke.com = 192.168.211.100(Web 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_ignore=1、arp_announce=2,防止后端抢答 VIP 的 ARP,外界只认 Director。藏家门:VIP 挂
lo而不是物理网卡,不参与对外通告。
为什么选 DR?因为回包不经过 Director,Director 只做转发入口,吞吐最高(对比 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 # 加权轮询:web01=2 web02=1 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; } } }这套配置里同时干了两件事:
Keepalived(VRRP):管 VIP 漂移。MASTER 挂了 → BACKUP 收不到 VRRP 通告 → 自动升 MASTER → VIP 漂过去,客户端无感。
LVS / ipvsadm:把
virtual_server写进 keepalived.conf,keepalived 会自动加载成 ipvsadm 规则,做四层负载均衡。
健康检查:TCP_CHECK(web,探 TCP 端口)和MISC_CHECK(DNS,跑checkdns.sh用dig查 TXT 记录,见上一篇)。后端挂了自动从 ipvsadm 剔除。
2.3 代表作
LVS-DR 高可用集群(Web + DNS + NFS 共享存储):一套 Director + Keepalived 主备,扛 Web 和 DNS 两个 VIP(.100 / .200),Web 后端挂 NFS 共享一份网页内容。
三、K8s 集群版技术栈(升级后的时代)
3.1 流量转发过程(七层,Ingress 在四层之上又叠了一层)
浏览器 ──域名──> Ingress-nginx(Ingress 按 host/路径路由) │ ▼ Service(ClusterIP,kube-proxy 负载均衡) │ ▼ Endpoints(后端 Pod 列表)──> 真实 Pod
和传统的本质区别:
| 层 | 传统(LVS-DR) | K8s |
|---|---|---|
| 转发 | 四层(只认 IP:端口) | 七层(能按域名 / 路径路由) |
| 负载均衡 | ipvsadm 的 rr/wrr | kube-proxy 自动做 Service 的负载均衡 |
| 入口暴露 | Director 绑 VIP | MetalLB给 LoadBalancer Service 分配外部 IP |
| 路由 | 一个端口对一个 service | 一个端口 + 域名路由到多个 Service(一个 ingress 全搞定) |
我这次实操的关键:K8s 集群里没有云厂商 LB,直接用MetalLB给ingress-nginx-controller这个 LoadBalancer 类型的 Service 分配了一个内网 IP(192.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 代理站拉下来,再推到 harbor,helm 里指向 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.repository=hb.reg.com/k8s/ingress-nginx-controller \ --set controller.image.tag=v1.11.3 \ --set controller.image.digest="" \ # ⚠️ 关掉 chart 默认 pin 的 digest,否则拉"Harbor 上不存在的 digest"报 not found --set controller.admissionWebhooks.patch.image.repository=hb.reg.com/k8s/ingress-nginx-kube-webhook-certgen \ --set controller.admissionWebhooks.patch.image.tag=v1.4.4 \ --set controller.admissionWebhooks.patch.image.digest="" \ --set controller.replicaCount=1 \ --set controller.resources.requests.cpu=100m --set controller.resources.requests.memory=128Mi \ --set controller.resources.limits.cpu=200m --set controller.resources.limits.memory=256Mi
⚠️ 这里有个必须记住的坑:ingress-nginx chart 默认会给镜像 pin 一个
@sha256:...digest。你自己docker pull原镜像 →docker tag→ push 到 harbor 后,Harbor 上那个 tag 的 digest ≠ chart 里 pin 的 digest,kubelet 拉「tag@与你不符的digest」必然not found。所以必须--set controller.image.digest=""让 helm 只用 tag 拉。排障先手:kubectl describe pod -n ingress-nginx <controller>看 Events,Failed 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 --for=condition=ready pod -l app=metallb,component=controller -n metallb-system kubectl apply -f ipaddresspool.yaml
/root/ipaddresspool.yaml(我实际用的,netease 段 220-240):
apiVersion: 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兜底)。
③ 无状态应用(python,Deployment 多副本 + Service + Ingress):
# Deployment:5 副本,容器挂掉 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 } ]# Service:ClusterIP,把流量负载均衡到 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/1) | Deployment 的 Pod 名是随机哈希,无法区分角色 |
| 从库要"精确"连主库 | 无头服务 Headless(clusterIP: None) | 普通 Service 会轮询,破坏主从关系 |
| 数据不能丢 | PV/PVC + NFS | 数据落共享盘,Pod 没了数据还在 |
| 一 Pod 一磁盘 | volumeClaimTemplates | 每起一个 Pod 自动建一个 PVC |
| 主从用不同配置 | ConfigMap + initContainer | 启动前按序号把 primary.cnf / replica.cnf 放进去 |
⑤ 监控体系(kube-prometheus-stack,helm 一键):
监控我用kube-prometheus-stack(helm 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是 DaemonSet,3 节点都要有):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.io(daocloud 专门代理 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.registry=hb.reg.com \ --set kube-state-metrics.image.repository=k8s/kube-state-metrics \ --set kube-state-metrics.image.tag=v2.20.0 kubectl rollout restart deployment/kube-prometheus-stack-kube-state-metrics -n monitoring
装完kubectl get pods -n monitoring全 Running = 监控体系就位。三个 Web UI(grafana/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 + Keepalived) | K8s 集群版 |
|---|---|---|
| 服务实例挂了 | 健康检查剔除(TCP_CHECK / MISC_CHECK)→ 但后端服务本身还得手动重启 / 脚本重启,不会自动拉起 | 故障自愈:容器挂了 kubelet 自动重启,副本少了控制器自动补,实例没就绪自动从 Endpoints 摘掉 |
| 全程高可用 | 靠 Keepalived 主备漂移 VIP + 后端脚本兜底,切换要一点时间,且要看脚本健壮性 | 平台兜底,kubectl apply后自动收敛到期望状态,几乎无需人工 |
| 升级 / 发布 | 改配置 + 重启服务,需要宕机一段时间(停机窗) | 滚动更新:逐个替换旧副本,新副本就绪再删旧副本,全程不中断服务 |
一句话:传统是"挂了等人修"(手动/脚本重启 + 停机窗),K8s 是"挂了平台自愈"(自动重启 + 滚动更新不停机)。
4.3 配置方式对比
| 传统 | K8s |
|---|---|
改/etc/keepalived/keepalived.conf+ipvsadm-save | 写 yaml +kubectl apply |
改完keepalived -t校验 +systemctl restart | apply 后控制器自动收敛,无需手动重启 |
| 加节点要改 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 keyword | track_scrip应为track_script;TCP_CHECK里没有nb_get_retry,应是retry;MISC_CHECK里应是misc_timeout | 逐个纠正关键字 |
| 后端同时绑 VIP 会不会脑裂 | web01/web02 的 lo:0 都有 .100 | DR 模式后端绑 VIP 是正常的(为了收包),只要 ARP 抑制对就不会脑裂 | 判断标准:客户端arp -n看 VIP 的 MAC 必须指向 Director |
| 负载均衡 vs 高可用混为一谈 | 以为开 ipvs 就能外部访问 | ClusterIP/NodePort 是"对内/外部入口",和 lb_algo 负载均衡是两回事 | 拆开看:LVS 管"转发+负载均衡",Keepalived 管"入口 VIP + 主备" |
5.2 K8s 版本的坑
| 坑 | 现象 | 根因 | 解决 |
|---|---|---|---|
镜像拉取(registry.k8s.io) | pod ImagePullBackOff | registry.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.registry=hb.reg.com+repository=k8s/kube-state-metrics |
| Ingress 域名不匹配 | 访问不到 / 跳到别的页面 | 浏览器敲的域名必须和 Ingress 里rules.host完全一致;hosts 里chengke.com和.k8s.local两套域名体系别混用(chengke.com和 LVS 时代那台 .91 冲突) | 统一用*.k8s.local,hosts 加192.168.211.220 x.k8s.local |
| MetalLB 分配 IP 后 LoadBalancer 仍是 pending | EXTERNAL-IP 一直<pending> | 集群没有 LoadBalancer 实现;MetalLB 的 IPAddressPool 网段要和节点/Service/Pod 网段避开 | 装 MetalLB,pool 用 192.168.211.220-240,再 apply |
| Service 的 selector / namespace 不匹配 | kubectl get endpoints为空 | selector 没命中 pod,或 Service 和 Deployment 不在一个 ns(跨 ns selector 不生效) | 查 endpoints,改 selector / 对齐 ns |
六、后续展望(还没做,先记一笔,之后单独展开)
现在这套 K8s 监控用的是kube-prometheus-stack自带的告警规则,已经比较完善。后续要加自定义告警规则和更细的监控时,方向大致是:
自定义告警规则:kube-prometheus-stack 用
PrometheusRule(CRD)的spec.groups[].rules[]写告警表达式,yaml 里alert:/expr:/for:/labels.severity,配好会自动被 operator 收集。(对应传统时代想在 Prometheus 里加 rule_files 的思路,但这里用 CRD 声明式,不用改配置文件)给容器加探针:在 Deployment/StatefulSet 的容器上写
livenessProbe(存活探针,挂了重启)和readinessProbe(就绪探针,就绪了才接流量),让"自愈"更及时。
七、一句话总结
传统高可用靠"配置文件 + 脚本"把 VIP 漂移 / 负载均衡写死;K8s 靠"声明式对象 + 控制器"让平台把副本、网络、存储、路由按你想要的收敛,是"从手动运维到平台自愈"的升级。四层(LVS)管"转发 + 负载均衡",七层(Ingress)管"域名 + 路径路由",MetalLB 给外部门开锁——三者合作,一个入口进多服务。