搞Kubernetes部署,很多人被卡在“集群起来了,但流量不知道怎么进”这一步。我这次把 k8s 多节点集群、Service 的负载均衡、Nginx 反向代理三层放在一起做,从 init 到验证走了一遍,踩了不少坑,也把原理摸透了。文章适合刚把 Kubernetes 跑起来、想在多节点上真正接业务的同学,也适合想把 NodePort、Ingress、Nginx 这几个概念一次性捋清楚的人。全文不讲虚的,直接按部署顺序说。
1. 整体架构与多节点部署思路
1.1 节点角色和流量路径
Kubernetes 多节点部署,第一步先把角色分清。常规做法是一个或多个控制平面节点,也就是常说的 Master,负责调度、保存集群状态、运行 apiserver 等控制面组件;另外一批工作节点,也就是 Worker,负责真正跑业务 Pod。为了演示和中小规模环境,我这次用的是一台控制节点加两台工作节点,生产环境建议控制节点至少三台组成 HA,避免 apiserver 单点。
整个流量链路需要提前在脑子里画清楚:客户端请求先进到边缘入口,我在这边用 Nginx 反向代理统一收口;Nginx 把请求转发到 Kubernetes 任意工作节点的 NodePort 端口;NodePort 端口上的流量经由 Service 的负载均衡规则,转发到后端的真实 Pod。这个链路里有两个负载均衡点:一个是 K8s 内部 Service 的负载均衡,另一个是 Nginx 这边配置的上游节点负载均衡。两者职责不同,后面会分别说。
1.2 为什么需要多节点
有人会问:我单机跑个 Minikube 或 K3s 不也能部署吗?单节点可以学习和测试,但生产上多节点解决的问题很实际:一是容错,一个节点宕了,Pod 自动调度到其它节点,业务不至于直接停摆;二是横向扩展,CPU、内存不够时加节点就行,不需要整体重构;三是负载分散,Ingress 流量不会打爆一台机器。多节点部署还有一个隐藏收益,网络、存储、跨机调度这些东西都会被逼着提前考虑,等真正上规模时不至于手忙脚乱。
1.3 部署工具选型
多节点部署方案常见的有 kubeadm、二进制部署、云厂商托管等。我建议优先用 kubeadm,倒不是因为它最“高级”,而是它的证书、组件、kubeconfig 文件基本都是声明式管理,出问题好排查。二进制部署能让人把每个组件怎么跑、怎么配都摸透,但手工维护证书和 service 文件实在太痛苦,试错成本也高。云托管适合不想管控制面的场景,但如果你要学原理、要自己在裸机上交付一套系统,kubeadm 是把完整控制权和学习曲线平衡得最好的选择。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| kubeadm | 安装快,结构规整,证书自动生成 | 仍需手动搭 CNI 和高可用 | 自建集群、学习、生产基础 |
| 二进制部署 | 组件细节完全透明 | 证书、互相调用关系手动配置,难度大 | 深入原理、特殊定制 |
| 云托管 | 控制面托管,免运维 | 厂商绑定,黑盒化 | 极速上线、不想管基础设施 |
选型不是越复杂越好,关键是团队能维护哪种方案。能快速定位问题,比一开始搞得特别“硬核”更重要。
2. 多节点集群初始化与网络方案选型
2.1 环境准备与版本选择
我用的环境是三台 CentOS 7.9 虚拟机,内存每台至少 2G,Master 建议 4G 以上,磁盘 20G 以上。操作系统内核版本不要太老,配置 Kubernetes 之前先把几个基础项处理掉:关 swap、加载 br_netfilter 模块、打开 IPv4 转发,这些都是 kubelet 和网络组件的硬前提。
版本选择上建议选一个当前较新的稳定版,比如 1.28 或 1.29。Kubernetes 的版本节奏是三个月一个大版本,但真上生产没必要追新,选一个离最新版本落后一个小版本左右的稳定发行版,周边生态兼容性会更稳。容器运行时我用的 containerd,比 Docker Engine 配置更直接,kubeadm 原生支持,不需要额外装 docker-shim。
2.2 kubeadm init 和 Worker 节点加入
Master 上安装完 kubeadm、kubelet、kubectl 之后,用下面的命令初始化。--apiserver-advertise-address 填 Master 的内网 IP,--pod-network-cidr 要按后续选择的 CNI 插件来定。如果选 Calico,通常用 192.168.0.0/16;如果选 Flannel,通常用 10.244.0.0/16。这个值不能乱填,CNI 插件会按这个网段分配 Pod IP。
kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --pod-network-cidr=192.168.0.0/16 \ --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers初始化完成后,kubeadm 会输出一段加入集群的命令,把 Worker 节点通过kubeadm join加进来。如果当时没保存,可以用kubeadm token create --print-join-command重新生成。这里有个容易踩的坑:token 默认有效期只有 24 小时,如果隔天再添加节点,必须重新生成。
kubeadm token create --print-join-command2.3 Pod 网络的选择和配置要点
Pod 网络必须装,不然节点状态一直是 NotReady,CoreDNS 也起不来。我用的 Calico,原因是它对 NetworkPolicy 支持好,BGP 模式在大规模集群下也能控制路由数量。安装方式很简单:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/release-v3.27/manifests/calico.yaml但这里要提醒一句:如果机器是多网卡环境,Calico 的 IP 自动检测可能会选错网卡,导致跨节点 Pod 通信异常。出现这种情况时,需要在 calico.yaml 里设置IP_AUTODETECTION_METHOD=interface=ens192,把网卡名改成实际主网卡。我当时就因为这个折腾了半天,Pod 都在 Running 了,跨节点 ping 不通,后来查看 Calico 日志才发现是 IP 检测走到了 eth1。
网络插件装完后,用kubectl get nodes看状态。全部 Ready 再继续,不要急着往下走,节点没就绪时部署业务,后续排查非常痛苦。
3. 服务暴露方式:NodePort、Ingress 与负载均衡的取舍
3.1 Service 类型到底在负载均衡什么
Kubernetes 里的 Service 不是传统意义上那种摆在机房的负载均衡器,它更像一个抽象层:把一组 Pod 的 IP 聚合成一个稳定的虚拟 IP。Service 有三种常见类型:ClusterIP 只能在集群内部访问,NodePort 会在每个节点上开一个端口,外部流量访问任意节点 IP 加端口就能打到 Service,LoadBalancer 结节云厂商的负载均衡器来自动创建入口。
很多人以为 NodePort 就是负载均衡,其实它本身不算完整的负载均衡器。它的实现逻辑是:kube-proxy 在每个节点上都写入一套转发规则,外部请求无论到达哪个节点,都能被转发到任意一个后端 Pod。这个“任意一个”,靠的是转发规则里的随机或者轮询选择机制。
3.2 kube-proxy 的负载均衡原理解读
kube-proxy 支持 userspace、iptables、IPVS 三种模式。早期版本在大规模集群里会碰到一个现象:Service 后端 Pod 很多时,iptables 规则爆炸式增长,性能下降明显。IPVS 模式本质上是内核态的四层负载均衡,转发效率高,规则管理也更清晰。kubeadm 默认在检测到 IPVS 内核模块时就会启用,因此新集群基本上可以不用动。
从流量视角看,当一个请求进入某个节点的 NodePort,kube-proxy 会把它转到 Service 的 ClusterIP,再由 IPVS 或 iptables 规则转发到一个具体 Pod。这里要明白:Service 的负载均衡只限于四层,它不关心 HTTP 路径,也不关心域名。如果多个应用都想通过 80 端口暴露,用 NodePort 直连就不合适了。这时必须引入七层入口,也就是 Ingress。
3.3 为什么不直接用 NodePort 接业务
NodePort 直接接业务能跑,但问题很多:端口范围默认 30000-32767,客户端必须访问莫名其妙的端口;无法按域名或 URL 路径分流;没有统一证书和 Header 处理的地方;NodePort 端口被大量占用后,维护成本高。生产上一般只把 NodePort 当作接入层的“后门”,真正给用户访问的还是通过 80/443 端口进来,再转发到内部服务。
所以 Nginx 反向代理在这里的价值就很明显:用户访问的是标准的 80/443,Nginx 根据 Host 或路径选择不同后端,转发到 K8s 工作节点的 NodePort,后面再交给 Service 做四层负载均衡。两层各管一段,互不干扰。
4. Nginx 反向代理与入口层的落地配置
4.1 Ingress Controller 和边缘 Nginx 谁来做
Kubernetes 官方生态里有个 Ingress 概念,配一个 Ingress Controller,比如 ingress-nginx,就能把 Ingress 对象转换成实际的 Nginx 配置,并且自动监听 Service 和后端 Pod 的变化。那是不是就不需要自己额外部署 Nginx 了?分情况:如果集群入口直接暴露给公网,用 ingress-nginx 做一个 Service 是 NodePort,前面再接云负载均衡器,没问题。
但如果你和我这次一样,环境里已经有一套独立的 Nginx 网关机器,想用它统一收口多个系统的流量,那么边缘 Nginx 和集群内 Ingress 是可以共存的。边缘 Nginx 负责对外暴露、域名转发、证书卸载,集群内部再用 ingress-nginx 处理更贴近业务的路由规则。两个 Nginx 不是重复建设,而是分层:边缘 Nginx 更“传统”,ingress-nginx 更“云原生”,和 K8s 生命周期深度绑定。
4.2 独立 Nginx 反向代理配置参考
边缘 Nginx 的配置核心是 upstream 和 proxy_pass。Upstream 里写 K8s 工作节点的 NodePort,我这里有两个 Worker 节点,端口假设分别是 30080 和 30080。先安装好 NodePort 类型的 Service,再用下面的配置转发:
upstream k8s_backend { least_conn; server 192.168.1.11:30080 max_fails=0 fail_timeout=0; server 192.168.1.12:30080 max_fails=0 fail_timeout=0; } server { listen 80; server_name demo.example.com; location / { proxy_pass http://k8s_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }这里有个设计细节:upstream 里的max_fails=0 fail_timeout=0是刻意关掉主动健康检查的。为什么?因为如果 Kubernetes 里某个 NodePort 暂时没有可用 Pod,Nginx 的主动探测会把该节点标记为失败,但实际这个节点的 Service 转发规则只是暂时不通,Pod 调度回来后又能恢复。如果 Nginx 健康检查过于激进,会把一个本来健康的节点临时拉黑,导致流量倾斜。真正的 Pod 存活与否,应由 Kubernetes 的 ReadinessProbe 和 Service Endpoints 管理,Nginx 只需要做连接转发。
4.3 集群内 Ingress 配置参考
如果业务本身也有域名或路径拆分需求,建议在集群内部再部署一个 ingress-nginx,然后创建一个 Ingress 对象。NodePort Service 之前,第一步可以是:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.5/deploy/static/provider/cloud/deploy.yaml等 Ingress Controller 的 Pod 起来后,创建下面的 Ingress 资源:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress annotations: nginx.ingress.kubernetes.io/proxy-body-size: 20m spec: ingressClassName: nginx rules: - host: demo.example.com http: paths: - path: /api pathType: Prefix backend: service: name: backend-svc port: number: 80ingress-nginx 的工作模式是“边监听边更新”:它盯着 Ingress、Service、Endpoints 的变化,一旦后端 Pod IP 变化,会重新生成 Nginx 配置并 reload。这就是它比独立 Nginx 更适合放在集群内的原因——Pod 重建、IP 变化完全不需要人去手动改配置文件。
4.4 边缘 Nginx 的会话保持
如果业务是有状态的,比如用户登录状态存在本地 Session,那么负载均衡必须保证同一个用户的请求总是落到同一台 Pod,或者至少同一组 Session 可共享。Nginx 可以选择 ip_hash,也可以基于 Cookie 做会话保持。我实际更推荐 Cookie 方式,因为用 IP 哈希在用户出口 IP 变化的情况下容易丢失会话。
upstream k8s_backend { ip_hash; server 192.168.1.11:30080; server 192.168.1.12:30080; }但话说回来,业务若已经上了 K8s,最好的做法还是把 Session 外置到 Redis 或数据库,让 Pod 保持无状态。这样 Nginx 就不用操心会话保持,滚动发布时也不会因为 Pod 重启导致用户掉线。我在部署时经常对业务说一句:负载均衡能做粘性会话,但不如把业务改成无状态来得干净。
5. 部署验证与流量走向实测
5.1 先部署一个测试应用
验证整个链路前,先在集群里跑一个简单的 Nginx 应用,副本数设 3,让 Pod 分布到不同节点:
kubectl create deployment nginx-demo --image=nginx --replicas=3 kubectl expose deployment nginx-demo --port=80 --target-port=80 --type=NodePort --name=nginx-demo-svc然后查看 Service 暴露的端口:
kubectl get svc nginx-demo-svc假设输出显示 30080,那么两个 Worker 节点上的 30080 端口都会转发到这个 Service。为了测试负载均衡是否真的生效,先直接访问任意一个节点的 NodePort:
curl -H "Host: demo.example.com" http://192.168.1.11:30080/这里能看到 Nginx 默认首页,说明 K8s 内部链路是通的。
5.2 从客户端到 Pod 的完整链路验证
接着配置好边缘 Nginx,并在本机 hosts 里把 demo.example.com 指向边缘 Nginx 的 IP。多次执行:
curl -s http://demo.example.com/ | grep -i "server address"如果你的 Nginx 镜像里没有 server address 信息,也可以用下面的方式确认是哪个 Pod 在响应:
kubectl get pods -o wide -l app=nginx-demo验证时我习惯循环请求,并同时 watch Pod 列表,观察请求是否会随机落到不同 Pod。这个操作可以很好地验证“边缘 Nginx 负载均衡 + K8s Service 负载均衡”两层是否都生效。如果发现请求永远集中在一个 Pod,那肯定有问题,需要从 readiness、kube-proxy 规则、externalTrafficPolicy 三方面排查。
再深入一点,测一下边缘 Nginx 的转发链路是否把源 IP 带过去了。在 Pod 内看访问日志,如果能看到X-Real-IP或X-Forwarded-For字段,说明 Nginx 的 header 设置生效。如果看不到,客户端真实 IP 会全部显示为节点 IP,后面做访问控制和日志分析会很麻烦。
5.3 外部访问后端的字段观测
我在实际操作时会同时打开两个终端,一个终端请求,另一个终端运行:
kubectl logs -f deployment/nginx-demo观察日志里$remote_addr和$http_x_forwarded_for_member。边缘 Nginx 做了转发后,K8s 内部收到的源 IP 实际是边缘 Nginx 的 IP,真正客户端 IP 在X-Forwarded-For里。因此接入层一定把这两个字段都设置好,否则 nginx-demo 的访问日志只能看到“边缘 Nginx IP”,丢掉了真实来源。
6. 常见问题与排查技巧实录
6.1 节点一直 NotReady
节点 NotReady 最常见原因是 CNI 网络插件没装好。先看kubectl get nodes,再看 Pod 状态,如果是 Calico 的 Pod CrashLoopBackOff,大概率是网络端口冲突或者 IP 检测错误。检查节点上是否有其他网络插件残留:ip a里如果有 cni0、flannel.1 之类的网卡,就要清理干净再装 Calico。
如果需要彻底重置节点,比较快的办法是:
kubeadm reset rm -rf /etc/cni/net.d rm -rf /var/lib/calico但记得这是用来排障的重置命令,生产环境慎用。
6.2 NodePort 在外面访问不通
NodePort 本身分配成功,边缘 Nginx 也解析到节点 IP,但就是不通。排查顺序很关键:先在本机 curl 节点 IP 加 NodePort,如果不通,看防火墙和安全组是否放行 30080 端口;如果本机通、外部 Nginx 通不过,则要看 Nginx 所在机器到 K8s 节点的网络路由。还有一点容易被忽略:如果 Service 使用了externalTrafficPolicy: Local,那么某个节点上没有对应 Pod 时,到达该节点的 NodePort 流量会直接被丢弃。这时要么切换为 Cluster 模式,要么确保每个节点上都跑着同类 Pod。
6.3 Ingress Controller 一直没就绪
ingress-nginx 的 Pod 起来了,但 Ingress 对象的 Address 一直为空,多半是控制器没有识别到这个 IngressClass。检查一下是否安装了ingressClassName: nginx对应的 IngressClass 资源,或者 ingress-nginx 启动参数里有没有配置默认 IngressClass。很多新手把 Deployment 装好就以为 Ingress 一定能通,其实 IngressClass 和 Controller 的关联必须存在。
6.4 连接池和 max_fails 的坑
独立 Nginx 配置 upstream 时,如果默认 enable 健康检查,那么节点短暂检测失败就会摘掉节点。K8s 的 Pod 重建、秒级重启、节点维护都会造成一些瞬时连接失败,这些不一定会让 Service 本身不可用。所以我的经验是边缘 Nginx 里把主动健康检查的失败判定设置得宽松一些,真正的前端存活检查交给 K8s 做,Nginx 只负责转发。
6.5 会话一致性和负载倾斜
如果发现负载明显不均,先确认后端 Pod 的 CPU/内存是否接近一致,再看有没有开启会话保持。ip_hash在节点数量少、请求来源 IP 分布不均时,会出现倾斜。更高阶的做法是用一致性哈希并配合加权,让每个节点的权重和实际处理能力匹配。还有一点,K8s Service 默认的转发算法是轮询或随机,和 Nginx 侧的权重加载逻辑要放在一起想,不要只看一层。
6.6 一定要留的排查命令
最后把我常用的排查命令整理成一份速查表,遇到问题可以按顺序走:
| 现象 | 第一步检查 | 常用命令 |
|---|---|---|
| 节点 NotReady | CNI 状态 | kubectl get pods -n kube-system |
| 集群内部域名解析失败 | CoreDNS 状态 | kubectl logs -n kube-system -l k8s-app=kube-dns |
| Service 不通 | Endpoints 状态 | kubectl get endpoints |
| NodePort 不通 | 防火墙 + externalTrafficPolicy | ss -lntp; kubectl get svc -o wide |
| ingress 不通 | IngressClass 关联 | kubectl get ingressclass; kubectl get svc -n ingress-nginx |
| 流量集中 | 后端资源水位 | kubectl top pod |
在一个多节点集群里,网络故障、端口冲突、路由混乱往往混在一起,日志和状态是定位问题的唯一依据。不要一上来就重启节点,先看 Endpoints、看 kube-proxy、看 Nginx 的 error.log,大多数问题都能按图索骥查出来。
这套部署做完,我最大的感受是:Kubernetes 多节点本身并不难,难的是把多节点、Service、Ingress、Nginx 这几层的关系理顺。把每一层的职责搞清楚,流量链路会非常清晰。后续如果再加监控、日志、CI/CD,也只是给这条链路补上更多可观测性和自动化能力。最后再分享一个小经验:所有配置改动尽量用 Git 管理,我曾在一次 Nginx 配置调整后忘了备份,结果回滚全靠记忆,非常被动。生产环境没有后悔药,每一条 upstream、每一个 ingress rule,都值得被当成交付物来对待。