这篇文章不是某一个组件的安装教程,而是用来解释我当前这套 Kubernetes 系统为什么这样搭。

如果只跟着命令敲,很容易变成“能跑,但不知道为什么”。这篇文章的目标是:看完以后,我能知道每一层解决什么问题、为什么要这么选、好处是什么、哪里会踩坑。以后重新搭建 Kubernetes 集群时,可以按同样的思路独立设计。

当前架构

当前集群是 3 台虚拟机组成的三主节点 Kubernetes:

k8s-master1  192.168.3.214
k8s-master2  192.168.3.215
k8s-master3  192.168.3.216

API VIP      192.168.3.217
Ingress VIP  192.168.3.231
MetalLB IP   192.168.3.230

整体访问路径可以先这样理解:

浏览器
  |
  | 访问 https://k8s-ai.jihw.top
  v
DNS 解析到入口 VIP
  |
  v
Keepalived + HAProxy
  |
  v
Ingress NodePort
  |
  v
ingress-nginx-controller
  |
  v
new-api Service
  |
  v
new-api Pod
  |
  +--> CloudNativePG PostgreSQL
  |
  +--> redis-master-proxy
          |
          v
        Redis Sentinel 当前 master

Kubernetes API 的访问路径是另一条:

kubectl / kubelet / 集群组件
  |
  v
API VIP 192.168.3.217:8443
  |
  v
HAProxy
  |
  v
三台 kube-apiserver:6443

这两条路径要分清楚:

  • 192.168.3.217 主要服务 Kubernetes 控制面,也就是 API Server。
  • 192.168.3.231 主要服务业务入口,也就是 HTTP/HTTPS 访问。
  • 192.168.3.230 是 MetalLB 分配给 ingress-nginx Service 的地址。

先理解高可用分层

高可用不是装了 3 台 Kubernetes 主节点就结束。三主节点只解决控制面一部分问题。

我把整套系统拆成几层:

1. 虚拟机和网络层
2. Kubernetes 控制面
3. 入口负载均衡层
4. Ingress 和域名证书层
5. 业务应用层
6. 数据库和缓存层
7. 存储层
8. 监控和运维入口层

每一层都可能有单点。

例如:

  • 3 个 API Server 都正常,但 Ingress 只有 1 个 Pod,业务域名仍然会挂。
  • new-api 有 3 个副本,但 PostgreSQL 是单实例,数据库节点挂了业务仍然不可用。
  • PostgreSQL 做了主从,但存储卷无法重新挂载,数据库仍然可能恢复慢。
  • 业务全部正常,但 Grafana 和 Headlamp 是单副本,运维入口仍然会断。

所以高可用要一层一层补。

虚拟机和网络层

当前是在 VMware 虚拟机里搭建集群。之前遇到过网卡相关问题,网卡类型从 e1000 调整为 vmxnet3 后,系统内网卡名从 ens33 变成了 ens160

这个决定的原因:

  • vmxnet3 是 VMware 优化过的虚拟网卡,性能和稳定性通常更好。
  • 压测时网卡稳定性很重要,虚拟网卡异常会让节点看起来像突然失联。
  • Keepalived 依赖具体网卡名,如果网卡名变了,配置必须同步修改。

需要记住:

网卡类型变化
  -> Linux 网卡名可能变化
  -> keepalived.conf 里的 interface 要跟着改
  -> NetworkManager 连接配置也可能要跟着改

检查命令:

ip addr
nmcli connection show
grep -R "ens33\|ens160" /etc/keepalived /etc/NetworkManager 2>/dev/null

经验结论:底层网络不稳时,Kubernetes 上层所有组件都会表现得很奇怪。先把网卡、IP、DNS、时间同步这些基础打牢,再谈应用高可用。

控制面高可用

当前 3 台节点都是 control-plane:

k8s-master1
k8s-master2
k8s-master3

控制面核心组件包括:

etcd
kube-apiserver
kube-controller-manager
kube-scheduler

为什么要 3 台,而不是 2 台?

因为 etcd 需要多数派。3 个 etcd 节点时,挂掉 1 个,还剩 2 个,仍然是多数派。

3 个 etcd 节点
  挂 0 个:3/3,可用
  挂 1 个:2/3,可用
  挂 2 个:1/3,不可用

如果只有 2 个 etcd 节点:

2 个 etcd 节点
  挂 1 个:1/2,不是多数派,不可用

所以三主节点是合理的入门 HA 规模。

但是要注意:控制面可用,不等于业务一定可用。控制面负责调度、管理和 API,业务是否可访问还要看 Ingress、Service、Pod、数据库、存储。

检查控制面:

kubectl get nodes -o wide
kubectl -n kube-system get pods -o wide

API 入口:Keepalived 和 HAProxy

kubectl 和集群组件需要一个稳定的 API Server 地址。如果直接写某一台机器的 6443,那台机器挂了,kubectl 就访问不了。

所以这里使用:

Keepalived 提供 VIP
HAProxy 转发到多个 kube-apiserver

设计思路:

kubectl
  |
  v
192.168.3.217:8443
  |
  v
HAProxy
  |
  +--> 192.168.3.214:6443
  +--> 192.168.3.215:6443
  +--> 192.168.3.216:6443

好处:

  • kubectl 永远访问同一个地址。
  • 某个 API Server 挂了,HAProxy 会把它摘掉。
  • 当前持有 VIP 的节点挂了,Keepalived 可以把 VIP 漂移到其他节点。

为什么不是只用 DNS 轮询?

DNS 轮询不能很好判断后端是否健康,也不能快速漂移 VIP。Keepalived + HAProxy 更适合家庭/内网环境里做一个简单可靠的入口。

检查命令:

ip addr | grep 192.168.3.217
systemctl status keepalived
systemctl status haproxy
journalctl -u haproxy --since "10 minutes ago" --no-pager

业务入口:Ingress 和 NodePort

业务域名不是直接访问 Pod,而是走 Ingress。

例如:

https://k8s-ai.jihw.top
https://grafana.jihw.top
https://headlamp.jihw.top

访问路径:

浏览器
  |
  v
入口 VIP / HAProxy
  |
  v
Ingress NodePort
  |
  v
ingress-nginx-controller
  |
  v
对应业务 Service

为什么要 Ingress?

  • Service 只适合集群内部访问。
  • Ingress 可以按域名和路径转发。
  • Ingress 可以统一处理 HTTPS。
  • 多个应用可以共用 80/443 入口。

之前关闭 master3 后,业务域名访问失败,原因不是 new-api 挂了,而是 ingress-nginx-controller 只有 1 个副本,并且刚好在 master3

修复方式:

ingress-nginx-controller replicas = 3
使用 topologySpreadConstraints 尽量分散

经验结论:入口层也必须高可用。业务 Pod 高可用,但入口 Pod 单副本,域名仍然会挂。

检查命令:

kubectl -n ingress-nginx get deploy,pods,svc -o wide
kubectl get ingress -A
curl -k -I https://k8s-ai.jihw.top/

MetalLB 的作用

当前 ingress-nginx Service 是 LoadBalancer 类型,并由 MetalLB 分配内网 IP:

192.168.3.230

为什么内网 Kubernetes 需要 MetalLB?

在云厂商里,Service type=LoadBalancer 会自动创建云负载均衡。但家庭内网或裸机环境没有云负载均衡,Kubernetes 自己不知道去哪里申请外部 IP。

MetalLB 的作用就是在裸机/内网环境中提供 LoadBalancer IP。

简单理解:

云上:Service LoadBalancer -> 云厂商负载均衡
内网:Service LoadBalancer -> MetalLB 分配局域网 IP

检查命令:

kubectl -n metallb-system get pods -o wide
kubectl -n ingress-nginx get svc ingress-nginx-controller -o wide

证书:cert-manager

HTTPS 证书由 cert-manager 管理。

好处:

  • 不用手动申请和续期证书。
  • Ingress 上写注解,cert-manager 自动创建 TLS Secret。
  • 证书快过期时自动续期。

当前使用阿里云 DNS 验证来签发证书。DNS 验证的好处是:

  • 不要求应用临时暴露 HTTP 校验路径。
  • 内网服务也可以申请公网可信证书。
  • 对多个子域名更方便。

检查命令:

kubectl get certificate -A
kubectl get clusterissuer
kubectl -n cert-manager get pods -o wide

存储:Longhorn

Kubernetes 的 Pod 可以随时重建,所以数据不能只放在容器本地。数据库、Grafana、Prometheus 这类有状态组件都需要 PVC。

当前使用 Longhorn 作为存储系统。

为什么用 Longhorn?

  • 它适合家庭/小集群。
  • 可以把多个节点磁盘做成分布式块存储。
  • PVC 可以跟随 Pod 在节点之间重新挂载。
  • 有 UI,便于观察卷状态。

但 Longhorn 不是魔法。要理解几个关键点:

RWO: 同一时间通常只能被一个节点读写挂载
RWX: 可以被多个节点同时读写,通常需要 NFS/share-manager
replica: Longhorn 卷副本,用来抵抗磁盘或节点故障

这次故障演练里,Grafana 恢复慢,就是因为它使用 SQLite + RWO PVC。旧 Pod 没完全释放卷时,新 Pod 不能在别的节点立刻挂载。

所以:

  • 无状态应用可以直接多副本。
  • 有状态应用不能盲目多副本。
  • 数据库要用数据库自己的高可用方案,不只是靠 Longhorn 卷副本。

检查命令:

kubectl get storageclass
kubectl -n longhorn-system get pods -o wide
kubectl -n longhorn-system get nodes.longhorn.io -o wide
kubectl -n longhorn-system get volumes.longhorn.io -o wide

当前还需要继续关注:Longhorn 节点 Ready 状态、卷副本是否健康、节点恢复时是否有重建压力。

new-api 应用层

new-api 是当前主要业务应用。

最初它是单副本,单副本的问题很直接:

Pod 所在节点挂了
  -> new-api 不可访问

现在 new-api 改成 3 副本:

new-api replicas = 3

好处:

  • 某一个 Pod 挂了,还有其他 Pod。
  • 某一个节点挂了,只要还有其他节点上的 Pod,业务仍然可以访问。
  • Service 会自动只把流量发给 Ready 的 Pod。

为什么 new-api 要尽量无状态?

因为无状态应用最适合多副本。多个 Pod 之间不需要共享本地数据,业务数据放到 PostgreSQL 和 Redis。

设计原则:

应用本地不保存关键业务状态
业务数据放 PostgreSQL
缓存和临时状态放 Redis
日志后续交给统一日志系统

检查命令:

kubectl -n new-api get deploy new-api
kubectl -n new-api get pods -l app=new-api -o wide
kubectl -n new-api get endpoints new-api
curl -k https://k8s-ai.jihw.top/api/status

PostgreSQL:CloudNativePG

PostgreSQL 不能简单地把单实例改成 3 个 Pod。数据库有主从、复制、故障切换、数据一致性这些问题。

当前使用 CloudNativePG 做 PostgreSQL 高可用。

架构:

newapi-postgres-1
newapi-postgres-2
newapi-postgres-3

其中 1 个是 primary
其他是 replica

CloudNativePG 提供几个重要 Service:

newapi-postgres-rw  写入口,指向当前 primary
newapi-postgres-ro  只读入口,指向 replica
newapi-postgres-r   所有实例入口

为什么用 -rw Service?

因为主库可能切换。如果应用直接连某个 Pod 名,一旦主库换了,应用还会连旧主库。-rw Service 会自动指向当前 primary。

好处:

  • 主库故障时可以自动切换。
  • 应用不需要知道哪个 Pod 是主库。
  • PostgreSQL 高可用逻辑由专业 Operator 管理。

注意:

PostgreSQL HA 不能替代备份
Longhorn 卷副本不能替代数据库复制
主从切换期间应用可能会短暂重连

检查命令:

kubectl -n new-api get cluster newapi-postgres
kubectl -n new-api get pods -l cnpg.io/cluster=newapi-postgres -o wide
kubectl -n new-api get svc | grep newapi-postgres

Redis:Sentinel + master 代理

Redis 也不能只保留单实例。最初的旧 Redis 是:

redis-master-0

它的问题是:所在节点挂了,new-api 就连不上 Redis。

现在 Redis 后端使用 Sentinel:

redis-ha-node-0
redis-ha-node-1
redis-ha-node-2

每个 redis-ha-node Pod 里有两个容器:

redis
sentinel

Sentinel 的作用:

  • 监控当前 Redis master。
  • 判断 master 是否故障。
  • 在故障时选出新的 master。

但是当前 calciumion/new-api:latest 镜像对 Redis Sentinel 支持不够直接。它会把 REDIS_CONN_STRING 当普通 Redis URL 解析。

所以加了一个代理:

redis-master-proxy

访问路径:

new-api
  |
  v
redis-master-proxy
  |
  v
当前 Redis master

redis-master-proxy 使用 HAProxy 检查 Redis 节点的 role:master,只把流量转发给当前 master。

为什么 proxy 是 2 个副本,不是 3 个?

因为 proxy 只是代理层,不存数据。2 个副本已经可以避免代理单点:

proxy-1 挂了,还有 proxy-2
proxy-2 挂了,还有 proxy-1

真正的数据高可用由 3 个 redis-ha-node 承担。

检查命令:

kubectl -n new-api get statefulset redis-ha-node
kubectl -n new-api get pods -l app.kubernetes.io/instance=redis-ha -o wide
kubectl -n new-api get deploy redis-master-proxy
kubectl -n new-api get secret new-api-secret -o jsonpath='{.data.REDIS_CONN_STRING}' | base64 -d
echo

平台组件

平台组件是用来运维集群的,不一定是业务本身,但它们也需要考虑可用性。

当前主要有:

Headlamp
Prometheus
Grafana
Alertmanager
metrics-server

Headlamp

Headlamp 是 Kubernetes Web UI,适合看 Pod、Service、Ingress、日志和事件。

它是无状态服务,所以可以直接 2 副本。

好处:

  • 一个 Headlamp Pod 挂了,还有另一个。
  • 某个节点挂了,仍然可以通过页面观察集群。

metrics-server

kubectl top nodes 和 HPA 依赖 metrics-server。

如果 metrics-server 挂了,会看到:

Metrics API not available

所以 metrics-server 也适合至少 2 副本。

Grafana

Grafana 当前是单副本,因为它使用 SQLite 和 RWO PVC。

为什么不直接 2 副本?

因为两个 Grafana 同时访问同一个 SQLite 数据库不安全,RWO PVC 也不能跨节点同时挂载。

当前策略:

Grafana 单副本
Recreate 更新策略
放宽启动探针
后续如果要真 HA,改用外部 PostgreSQL

所以这里要理解一个重要原则:

无状态组件可以直接多副本
有状态组件要先解决数据一致性和存储访问方式

为什么资源会紧张

当前每台机器大约 2 核、4GB 内存级别。三主节点上同时跑:

Kubernetes 控制面
Longhorn
Ingress
new-api
PostgreSQL
Redis
Prometheus
Grafana
Headlamp
cert-manager
MetalLB

这对小虚拟机来说已经比较重。

故障演练时,如果关掉一台节点,剩下两台会承接更多 Pod,CPU 很容易接近 100%。这时很多组件不是配置错,而是健康探针超时。

常见表现:

Readiness probe failed: context deadline exceeded
Liveness probe failed: context deadline exceeded
kubectl top nodes 显示 CPU 很高
Longhorn 节点 Ready 反复变化

建议资源:

每台至少 4 vCPU
每台至少 8GB 内存
磁盘预留足够空间给 Longhorn

如果资源有限,要做取舍:

  • 先保证业务和数据库。
  • 监控组件减少保留时间和资源请求。
  • Grafana 保持单副本快速恢复。
  • 清理不用的旧 Redis、旧 PostgreSQL。

从零搭建顺序

以后重新搭建时,可以按这个顺序来。

1. 准备虚拟机

先准备 3 台机器:

固定 IP
统一主机名
关闭 swap
配置时间同步
选择稳定网卡类型
确保 ssh key 可登录

为什么先做这些?

因为 Kubernetes 对网络、时间和主机名很敏感。底层不稳,上层排错会非常痛苦。

2. 安装容器运行时和 Kubernetes

安装:

containerd
kubeadm
kubelet
kubectl

用 kubeadm 初始化第一个 control-plane,再加入另外两个 control-plane。

目标结果:

kubectl get nodes

能看到 3 个节点 Ready。

3. 安装 CNI

CNI 负责 Pod 网络。当前使用 flannel。

没有 CNI 时,Pod 之间无法正常通信,CoreDNS 也可能起不来。

检查:

kubectl -n kube-flannel get pods -o wide
kubectl -n kube-system get pods -o wide

4. 做 API Server 高可用入口

安装并配置:

Keepalived
HAProxy

让 kubectl 和 kubelet 统一访问 VIP。

5. 安装 MetalLB 和 ingress-nginx

MetalLB 负责内网 LoadBalancer IP。

ingress-nginx 负责域名 HTTP/HTTPS 转发。

从一开始就把 ingress-nginx 设置为多副本,否则入口会成为单点。

6. 安装 cert-manager

让证书自动签发和续期。

域名访问要尽量一开始就走 HTTPS,这样后面应用迁移时不用反复改入口。

7. 安装 Longhorn

安装存储系统,让 PVC 可以动态创建。

安装后重点检查:

kubectl get storageclass
kubectl -n longhorn-system get pods -o wide
kubectl -n longhorn-system get nodes.longhorn.io -o wide

8. 部署业务应用

先部署单副本验证能跑,再改多副本。

部署应用时要问自己:

应用是否无状态?
是否依赖本地文件?
是否可以多副本同时运行?
健康检查是否合理?
资源 requests/limits 是否设置?

9. 部署数据库和缓存高可用

PostgreSQL 使用 CloudNativePG。

Redis 使用 Sentinel,并为 new-api 增加 redis-master-proxy。

注意:数据库高可用不是“Pod 数量多”这么简单,要用合适的 Operator 或数据库自身机制。

10. 部署监控和运维工具

安装:

metrics-server
Headlamp
kube-prometheus-stack

然后根据是否有状态决定副本数:

  • Headlamp:无状态,可以 2 副本。
  • metrics-server:可以 2 副本。
  • Grafana:当前 SQLite + RWO,只做单副本快速恢复。
  • Prometheus:后续再考虑多副本和长期存储。

11. 做故障演练

不要等真正故障时才知道哪里没做 HA。

按顺序演练:

删除一个 new-api Pod
关闭一个 Redis master Pod
删除 PostgreSQL primary Pod
关闭一个 Ingress Controller Pod
关闭一台虚拟机

每次演练后检查:

kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl top nodes
kubectl -n longhorn-system get volumes.longhorn.io -o wide
curl -k https://k8s-ai.jihw.top/api/status

独立搭建时的判断方法

以后遇到一个新组件,可以按这几个问题判断怎么部署:

1. 它是无状态还是有状态?
2. 它是否需要 PVC?
3. PVC 是 RWO 还是 RWX?
4. 它能不能多副本同时写同一个数据?
5. 它是否需要专门的 Operator?
6. 它挂了会影响业务,还是只影响运维观察?
7. 它的入口是 Service、Ingress,还是 APIService?
8. 它的健康检查是否会在高负载时误杀容器?

几个经验规则:

  • 无状态服务优先用 Deployment,多副本。
  • 数据库优先用成熟 Operator,不要自己手搓主从。
  • 只有一个 RWO PVC 的组件,不要直接多副本。
  • Ingress Controller、metrics-server、Headlamp 这类平台入口要多副本。
  • Prometheus、Grafana 这类监控组件要区分“快速恢复”和“真正 HA”。
  • Longhorn 卷健康和节点资源压力会影响大量组件。

当前架构的不足

这套架构已经能让 new-api、PostgreSQL、Redis、Ingress 具备基础高可用,但还不是完美的生产架构。

当前主要不足:

1. 虚拟机资源偏紧,故障切换时 214/215 容易 CPU 打满。
2. Longhorn 节点状态需要继续稳定,尤其是节点恢复后的 Ready 状态。
3. Grafana 还不是真正多副本 HA。
4. Prometheus 目前仍是单实例 StatefulSet。
5. 备份体系还需要继续完善。

后续优先级:

第一优先级:Longhorn 健康和资源扩容
第二优先级:PostgreSQL 定时备份和恢复演练
第三优先级:Prometheus/Grafana 监控告警完善
第四优先级:继续做完整故障演练

总结

这套 Kubernetes 架构的核心思想是:

控制面用三主保证管理能力
入口层用 Keepalived + HAProxy + ingress-nginx 保证域名访问
应用层用 Deployment 多副本保证服务可用
数据库用 CloudNativePG 保证 PostgreSQL 主从切换
缓存用 Redis Sentinel + master proxy 保证 Redis master 切换
存储用 Longhorn 提供 PVC 和卷副本
监控和运维工具按有无状态分别处理

真正理解以后,就不会只问“这个 Pod 为什么不是 3 个”,而是会先判断它属于哪一层、是不是存数据、能不能多副本、挂了影响什么。

这才是独立搭建 Kubernetes 集群最重要的能力。