1. 从单机容器到企业级编排,中间到底隔了什么
1.1 Docker解决了“单机”问题,却留下了“集群”的烂摊子
先说个很现实的场景。很多团队从 Docker 入门,docker run、docker compose 用得飞起,单个应用容器化改造做完后,所有的镜像、数据卷、网络配置都堆在一台物理机上。一开始人少、流量小,感觉一切都很顺。但等业务量上来,或者要上多套环境、多套应用互相调用的时候,问题就藏不住了:机器宕机了怎么办?资源不够了怎么办?多个副本怎么调度?配置怎么统一管理?版本怎么平滑升级?
这些问题的本质是:Docker 解决的是“在一台机器上把应用跑起来”的问题,但企业级应用集群需要的是“在多台机器上把一组应用管起来”的能力。容器只是打包和运行的基本单元,编排才是让几十上百个容器协同工作的关键。
1.2 Kubernetes 就是为解决多机编排而生的
Kubernetes 的核心价值可以用四个字概括:声明式编排。你告诉它“我要跑 3 个副本,每个副本需要 1 核 2G 内存,暴露 8080 端口”,剩下的副本有多少、运行在哪台机器、网络怎么通、挂了怎么拉起、流量怎么分发,全部由控制平面来保证。
这与传统运维的“命令式”操作有本质区别。以前我们用脚本批量启动进程、检查存活、手工摘流量,每台机器都是一个孤岛;K8s 把整个集群抽象成一台“巨型机器”,所有节点只是资源池。这对企业应用的意义非常直接:应用不需要关心自己跑在哪台机器上,只要声明需求,集群负责兜底。
1.3 从Docker到K8s不是替换,而是分层协作
有同学经常问:有了 Kubernetes 是不是就不需要 Docker 了?答案是不对。
Kubernetes 本身不运行容器,它依赖容器运行时来干活。业界最常见的方式仍然是通过 containerd(或者早期版本的 dockerd)拉起容器。K8s 做的是生命周期管理、网络打通、存储挂载、滚动发布,Docker 负责的是镜像构建、格式标准、底层运行时隔离。两者是上下层关系,不是替代关系。
所以章节标题才会叫“Docker容器化部署企业级应用集群”,而不是叫“用K8s抛弃Docker”。正确理解是:先有 Docker 做镜像标准化,再通过 K8s 把镜像变成真正可靠的分布式服务。
2. 集群规划与基础环境准备,这一步省不了
2.1 硬件资源怎么评估,别拍脑袋
很多第一次搭 K8s 集群的同学,上来就按“三台机器 + 每台 8 核 16G”的标准抄作业,结果资源大量浪费,或者反过来,单台机器配置不够导致部署失败。做企业级集群规划,第一步不是安装,而是算账。
先从工作负载反推。假设你的业务应用有 A、B、C 三个服务,A 需要 2 副本、每副本 2C4G,B 需要 3 副本、每副本 1C2G,C 无状态但需要 3 副本、每副本 4C8G。加上管理组件本身的消耗(etcd、API Server、Scheduler、Controller Manager、kubelet、网络插件),以及 20% 左右的余量,才能得出节点规格。
这里有一个我实践下来比较稳的分配逻辑:
- 控制平面节点建议至少 2 台,小规模集群 4C8G 起步,etcd 尽量放在独立的磁盘上(SSD 是硬性要求),I/O 瓶颈是 etcd 最常见的故障来源
- 工作节点按“可用内存 = 总内存 - 系统预留 - K8s 组件占用”来估,不要把节点打满,要留出 Pod 调度和系统进程的缓冲
- 如果应用里有 Elasticsearch、ClickHouse 这类重存储的服务,节点必须挂独立数据盘,系统盘和数据盘分开是底线
提示:很多初学者以为 Kubernetes 集群节点越多越好,其实小规模(3-10台)核数高一点反而更稳。节点太多会导致 etcd 的 watch 请求和 API Server 的列表压力变大,管理面开销反而吃掉业务资源。
2.2 操作系统与内核参数,这些坑跳过一个是一个
生产环境我建议统一使用较新的 Linux 发行版(版本不要太激进,选稳定维护期内的即可)。为什么?Kubernetes 对内核版本有要求,太老的内核会导致 ipvs 模式、overlay 存储驱动、cgroup v2 支持不完整,踩起来很难受。
初始化时一定要检查几个关键内核参数,推荐直接写成配置:
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 vm.swappiness = 0 vm.overcommit_memory = 1 fs.inotify.max_user_instances = 8192 fs.inotify.max_user_watches = 524288 EOF sysctl -p /etc/sysctl.d/k8s.conf解释两个容易忽略的:
fs.inotify.max_user_watches这个参数,如果你跑的应用里有大量文件监听(比如 Java 应用、Node 应用的 watch 模式),默认值很容易被打爆,导致容器内报“too many open files”之类的问题,很多人误以为是文件描述符限制,其实是 inotify 不够net.netfilter.nf_conntrack_max也要调大一点,否则你在高并发场景下出网或入网会莫名丢包,尤其是大量短连接的企业应用特别明显
还有,记得关闭交换分区。K8s 从 1.24 之后可以支持 swap,但我个人不推荐生产环境开。原因很简单:内存回收策略和 Pod 的 QoS 等级会发生冲突,你没法准确判断容器何时被杀死。机房里的机器内存不够就加内存,比开 swap 拿系统稳定性换性能要明智得多。
2.3 版本选择与服务发现组件,确定性比追新重要
版本选择这块,我强烈建议不要踩最新版本,而是选择当前社区维护的稳定版本。原因很实际:Kubernetes 迭代速度很快,大版本的小版本更新频繁,最新的次版本往往存在一些回归问题,而企业环境追求的是可预测性。
举个我自己踩过的教训:某次部署用了某个较新的 K8s 小版本,结果集群运行一段时间后 kube-proxy 出现异常,conntrack 表刷新逻辑跟内核版本有兼容性问题,排查了整整两天,最后回退一个补丁版本就好了。所以选版本,别选最潮的,选最多人用的。
容器运行时,如果是从 Docker 时代过来的人,天然会用 dockerd 作为运行时。现在情况变了,K8s 已经废弃了 dockershim,推荐直接用 containerd 作为运行时。这个切换本身对上面的应用影响很小,因为你构建镜像时打出来的还是 OCI 镜像,底层运行方式一致。
镜像仓库也需要提前规划。生产集群拉取镜像经常遇到网络不稳定的问题,建议提前把用到的系统组件镜像(pause、coredns、metrics-server、flannel/calico 等)推到私有的镜像仓库里面,这样集群初始化的时候速度会快非常多,也避免部署过程中因为镜像拉取失败导致初始化中断。
3. 从零搭建高可用Kubernetes集群,完整实操记录
3.1 控制平面初始化流程拆解
我以 kubeadm 工具为例(它也是当前最主流的搭建方式),三步走:安装组件 -> 初始化控制平面 -> 配置 kubeconfig。
先装好 kubeadm、kubelet 和 kubectl。注意安装时需要指定版本,三条命令必须用同一个版本号,否则版本不一致会出现 API 版本兼容问题:
sudo apt-get install -y kubelet=1.28.2-1.1 kubeadm=1.28.2-1.1 kubectl=1.28.2-1.1然后写一个 init 配置文件,比直接命令行传参清楚得多。这里我给出一个精力验证过的配置:
cat <<EOF > /etc/kubernetes/kubeadm-init.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 10.0.0.11 bindPort: 6443 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 controlPlaneEndpoint: 10.0.0.100:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 dnsDomain: cluster.local --- apiVersion: kubeadm.k8s.io/v1beta3 kind: KubeletConfiguration cgroupDriver: systemd EOF kubeadm init --config=/etc/kubernetes/kubeadm-init.yaml这里有个非常关键的概念要拆开讲:controlPlaneEndpoint。单机测试集群可以不用它,但企业级多控制节点集群必须配。它本质上是给整个控制平面做了一个虚拟入口,所有 kubelet、kubectl 组件都通过这个地址访问 API Server,而不是直连某一个 Master IP。有了它,后续控制节点挂了才能飘走,才能真正实现高可用。
pods 网段和服务网段的选择也有讲究。Pod 网段决定了集群里每个 Pod 拿到的 IP 范围,服务网段是给 Service 分配 ClusterIP 用的。这两个网段不能和宿主机网段重叠,否则会出现路由冲突。我踩过的坑就是 podSubnet 选得跟公司办公网段后段重合,结果 Pod 访问外部服务时经常无响应。
初始化完成后把 kubeconfig 拷贝出来:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config然后查看节点状态,你会发现控制节点状态是 NotReady,这是正常的,因为还没有装网络插件。
3.2 工作节点加入集群,没那么神秘
生成加入令牌:
kubeadm token create --print-join-command输出类似这样的一条命令,在工作节点上直接执行即可:
kubeadm join 10.0.0.100:6443 --token 你的token --discovery-token-ca-cert-hash sha256:你的hash这条命令背后做的事情,值得理解一下:
- 工作节点通过 controlPlaneEndpoint 访问 API Server
- 用 token 完成临时身份认证
- 通过 CA 证书 hash 验证控制平面的真实性,防止中间人攻击
- 节点启动 kubelet,向控制平面注册自己,并保持心跳
注意:token 默认有效期只有 24 小时,如果加入节点时提示 token 过期,用
kubeadm token create重新生成即可。另外如果你的控制节点是 HA 模式,join 命令里的地址必须是 load balancer 的地址,不能是某一个 Master 的地址,否则某台 Master 挂了,这个节点也连不上了。
加入后查看节点状态:
kubectl get nodes如果节点一直显示 NotReady,大概率是网络插件没装好,或者 kubelet 日志里有证据。现在立刻干活:看 kubelet 日志。
journalctl -u kubelet -f日志指向哪里就排查哪里,最常见的是容器运行时 cgroup driver 不一致。前面 init 配置文件里已经写了cgroupDriver: systemd,节点上的容器运行时也需要是 systemd,两者一致才行。
3.3 网络插件与存储插件怎么选,这次一次说清
网络这块我只推荐两个路线,双栈环境或严格性能场景用 Calico,通用场景用 Flannel。别在这上面反复横跳,网络插件的选择和迁移成本比表面看到的要大得多。
Flannel 的部署很简单:
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml但我更建议在生产环境走 Calico。理由不是 Flannel 不行,而是企业级应用经常需要 NetworkPolicy(比如只允许某个服务访问另一个服务),Flannel 本身不提供网络策略能力,要再加一层插件,复杂度就上去了。Calico 原生支持 BGP 路由模式,性能好,策略能力强。
Calico 部署后,注意检查它的 Pod 是否能正常运行:
kubectl get pods -n calico-system如果 Calico Pod 反复重启,最常见的两个原因:一是IP_AUTODETECTION_METHOD没有指定,Calico 自动选择了错误的网卡,导致路由宣告失败;二是主机的net.ipv4.ip_forward没有开启。第二个问题在你执行前面 sysctl 配置时已经解决,第一个问题解决办法是在 Calico 的 DaemonSet 环境变量里手动指定网卡:
env: - name: IP_AUTODETECTION_METHOD value: interface=eth.*存储插件方面,如果跑的是有状态应用(数据库、消息队列等),建议在集群初始化阶段就把本地存储或网络存储接入。最省事的方案是使用local-path-provisioner这类本地存储方案,注意这是本地方案,不支持跨节点漂移,只适合测试环境。
生产环境要么走企业级存储后端(需要看具体存储设备),要么走开源分布式存储方案。这里只提醒一件事:创建 StorageClass 时,reclaimPolicy一定要设置为Retain,不要用默认的Delete。否则你删除 PVC 的时候,里面的数据会直接被回收,那种“删了个测试卷,结果把生产数据库同时删了”的事故我见过不止一次。
4. 企业应用容器化改造与镜像落地实践
4.1 镜像分层设计与体积瘦身,这些细节直接影响交付效率
企业应用容器化,第一步是写好 Dockerfile。一个看起来不大的问题——镜像体积——往往在集群部署时变成大问题。想象一下,你有 50 个工作节点,每次发布拉取的镜像从 1.5GB 变成 300MB,发布速度会快多少?拉取失败率会降多少?
瘦身的核心思路是分层缓存与构建分离。以 Java 应用为例,不推荐把构建好的 fat jar 直接塞进镜像里,而是用多阶段构建:
# 构建阶段 FROM maven:3.8-openjdk-11 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行时阶段 FROM openjdk:11-jre-slim WORKDIR /app COPY --from=builder /build/target/app.jar . EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]这里有一个很关键的分层技巧:先把 pom.xml 拷进去,单独跑一次依赖下载,再把源码拷进去编译。因为 Docker 的缓存机制是基于每一条指令的输入变化来判断的,只要 pom.xml 没变,mvn dependency:go-offline这一层就直接命中缓存,编译阶段只需要重新编译源码改动,整个构建时间大幅缩短。
运行时阶段选jre-slim而不是jdk,这一点能省很多空间。Java 应用在容器里跑,不需要编译器,只需要 JRE。这个事情说起来简单,但我在交付经验里见过太多团队直接把 JDK 镜像塞进去,白白多出两百多兆。
4.2 从 Compose 到 K8s 清单的映射,别一行行翻译
很多团队是先有 docker-compose.yml,再往 Kubernetes 迁移。在迁移过程中最容易犯的错误,是试图把 Compose 文件里的每个字段“一对一”翻译成 K8s 的 YAML。这么做的结果通常是 YAML 越来越厚,维护成本越来越高。
正确姿势是理解两者的抽象层级区别:
- Compose 里的 service,映射到 K8s 里大概率不是一个 Deployment 而是多个资源:Deployment(控制副本)+ Service(提供稳定访问入口)+ ConfigMap(保存配置)+ Secret(保存密钥)
- Compose 里的 depends_on 依赖关系,在 K8s 里不应该用任何机制去模仿。K8s 中 Service 本身就提供了负载均衡和 DNS 服务发现,只要服务名能被解析,启动顺序就不再是关键问题,你不需要关心数据库先启动还是应用先启动
- Compose 里的 volumes,在 K8s 里要区分是临时卷还是持久卷。代码目录用 emptyDir 或临时卷即可,数据库目录用 PVC
这里放一个简单的映射参考表:
| Compose 概念 | Kubernetes 对应资源 | 说明 |
|---|---|---|
| service | Service + Deployment | Service 只负责流量路由,Deployment 负责副本管理 |
| environment | ConfigMap / Secret | 非敏感用 ConfigMap,敏感用 Secret |
| volumes | PVC / emptyDir | 持久化用 PVC,临时缓存用 emptyDir |
| depends_on | 无需处理 | 通过服务发现自动完成依赖解析 |
| restart: always | 无需设置 | Kubelet 会自动重启异常退出的 Pod |
| ports | Service 的 port/targetPort | 不需要再用宿主机端口映射 |
| networks | 无需处理 | CNI 插件自动打通容器网络 |
4.3 配置、密钥与镜像拉取策略,生产环境不能偷懒
企业应用一定会有配置文件和密码。把这些东西直接写进镜像,或者塞在环境变量里,都是风险极高的做法。
配置用 ConfigMap,密钥用 Secret。两者本质都是 etcd 里保存的对象,区别只在于 Secret 做了 Base64 编码。
但是有一个很常见的安全误解:Secret 的 Base64 编码并不等于加密。任何能访问 K8s API 的人都能轻松解码。要真正保护敏感信息,需要配合外部密钥管理系统或启用 K8s 的静态加密配置,在 API Server 的配置里指定加密 Provider。
镜像拉取策略同样讲个经验值:
- 测试环境直接
imagePullPolicy: Always,保证每次部署都是最新代码 - 生产环境建议
IfNotPresent,前提是你的 CI 给镜像打的 tag 是唯一的(比如 git commit sha),否则“同名 tag + 本地有旧镜像”的问题会悄悄出现
注意:两个不同团队共用同一个镜像仓库,如果你随便把 tag 命名成 latest、v1.0,那么生产环境上的版本就不可控了。我们后来统一成
/应用名/镜像名:GitCommitSha的规则,一个问题立刻消失:再也不需要深夜登录服务器上去看“这个 Pod 到底跑的哪个版本”。
5. 生产环境应用部署与发布策略
5.1 Deployment 还是 StatefulSet,判断标准就三条
很多新手在选择工作负载类型时犹豫不定,其实就三条判断标准:
- 应用是否需要稳定的网络标识(Pod 名称固定不变)
- 应用是否有持久化状态,并且每个副本必须挂载不同数据卷
- 应用是否需要有序启动、有序销毁(比如主从架构的数据库)
如果以上三条都是否,无脑选 Deployment。
大部分企业后台服务属于这一类:无状态,可以随时杀掉重建,可以随意横向扩容。这类服务用 Deployment 最合适,因为 Deployment 自带滚动更新和回滚机制,部署、升级都是顺手的事。
如果应用确实需要固定网络标识和有状态存储,比如一套三节点的中间件集群(类似 ZooKeeper、Kafka 这类),用 StatefulSet。每一行 Pod 有唯一名称,例如zk-0、zk-1、zk-2,通过无头服务(headless service)解决相互发现的问题。
让我说一句大实话:如果业务没有明确要求 StatefulSet,不要因为它看起来更高级就选它。StatefulSet 的滚动更新、缩容都有限制,处理起来要小心得多。它不会自动清理 PVC,如果你删除一个 StatefulSet,PVC 还在,你需要自己决定是否删除,这个设计是有意为之——防止数据误删。
5.2 Service、Ingress 与负载均衡,流量进入集群的三层关卡
传统的单一应用部署,好比只有一个入口门,人来了直接进。在 K8s 里访问内部服务,需要经过这样一条路径:
客户端 -> Ingress Controller -> Service -> Pod
先讲 Service。Service 给一组 Pod 提供统一的虚拟 IP(ClusterIP)和 DNS 名字,比如你在 Pod 里访问http://order-service,就会解析到 ClusterIP,再由 kube-proxy 或相关代理转发到后端的某个 Pod。
这里有个细节很多人不了解:Service 选择后端 Pod 的方式是标签选择器,也就是selector。如果你的 Service 定义的 selector 不匹配任何 Pod 的标签,Service 就相当于一个空壳,没有后端可用。排查问题时第一站永远是它。
Ingress 解决了“外部怎么访问集群内服务”的问题。它其实是一个反向代理规则集合,具体实现由 Ingress Controller 负载(比如 Nginx Ingress Controller)。
写 Ingress 时注意几个配置点:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress annotations: nginx.ingress.kubernetes.io/proxy-body-size: 50m spec: rules: - host: app.example.local http: paths: - path: /api pathType: Prefix backend: service: name: backend-service port: number: 8080关键点:pathType有Exact和Prefix的区别。Prefix匹配 /api 以及 /api/anything,Exact只精确匹配 /api,注意这个精准细节相当影响路由规则。另外,proxy-body-size注解如果不配置,Nginx 默认只允许 1MB 的请求体,文件上传接口就报 413,这属于上线才会发现的坑。
5.3 滚动更新、回滚与优雅停服,发布上线不再慌
滚动更新是 Deployment 最有价值的能力之一。默认策略是 RollingUpdate,它不会把旧版本 Pod 全部杀掉再启动新版本,而是逐批替换,保证服务不中断。
默认参数: maxSurge: 25% maxUnavailable: 25%意思是:更新时最多允许多出 25% 的新 Pod,同时最多允许 25% 的旧 Pod 不可用。比如你当前有 4 个副本,更新时会先启动 1 个新 Pod,就绪后杀掉 1 个旧 Pod,然后循环。
这个默认参数在大多数场景下够用,但如果你的应用是重量级服务(启动要 2 分钟,内存占用高),我建议把maxSurge调成 1、maxUnavailable调成 0,即保证至少有一个新 Pod 完全就绪后再杀旧 Pod。代价是发布会多占用一点资源,但稳定性强很多。
优雅终止是发布时另一个容易忽略的点。K8s 在需要终止一个 Pod 时,会先发送 SIGTERM 信号给主进程,等待一个宽限期(默认 30 秒),宽限期过了再发 SIGKILL 强制杀死。
Java 应用要特别注意一点:默认的 shutdown 钩子如果没有做逻辑处理,收到 SIGTERM 就直接退出了,此时如果有请求还在处理中,就会出现“发布时偶发 502”。解决方式是在应用代码里监听 SIGTERM,先停止接受新请求,再等待当前请求处理完,然后再退出。
Spring Boot 项目加一行配置就行:
server: shutdown: graceful配合spring.lifecycle.timeout-per-shutdown-phase: 30s控制等待时间。Node.js 应用也有类似机制,监听 SIGTERM 后先关闭 HTTP server,再退出进程。
6. 集群运维、监控与常见故障排查
6.1 资源配额、HPA 与集群自动伸缩
K8s 的资源模型是 metrics-server 提供基础指标,HPA 根据 CPU 和内存使用率自动调整副本数。
HPA 的配置看起来很简单:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: app-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: app minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60这个配置的意思是:当 CPU 平均使用率超过 60% 时扩容,低于 60% 持续一段时间后缩容。
但我要提醒一个经验:HPA 不是银弹。如果你的应用没有做合理的资源请求(requests),HPA 可能根本不会触发,或者触发后新 Pod 因为节点资源不够而调度失败。HPA 能否工作,完全取决于 Deployment 里有没有写resources.requests。没有 requests 的 Pod,metrics-server 拿不到准确的基础数据,或者在计算 delta 时无从比较。
建议每一条生产 Deployment 必须写资源请求和限制:
resources: requests: cpu: 500m memory: 512Mi limits: cpu: "1" memory: 1Girequests 是调度依据,limits 是运行上限。两者的差值代表了 Pod 可以突发使用的资源。我一直的建议是:requests 写真实需要量,limits 写安全上限的 1.5~2 倍。不要抠门,更不要超卖太多,否则节点内存打爆后,内核大概率直接杀 Pod,表现为莫名的 OOMKilled。
6.2 日志采集、监控告警体系,别等服务挂了才发现
容器原生日志写在哪里?一般情况下,应用如果把日志写到 stdout/stderr,K8s 会将其重定向到宿主机文件里,Pod 被删除后日志也随之消失。这不符合企业级需求,必须有集中的日志平台。
业界的主流做法是:Filebeat(或 Fluent Bit)以 DaemonSet 方式部署在每个节点,采集容器日志,发到日志存储系统,再通过 Kibana 查询。
这套链路搭起来不难,但要注意三个小坑:
- 容器内应用必须把日志输出到 stdout/stderr,不要自己写入文件。你可能会问:我想按天切分日志文件怎么办?采集层会帮你做索引和时间戳归类,不需要应用层自己切文件
- Filebeat 采集中经常遇到模块冲突,多行日志(比如 Java 的异常栈)会被拆成多条。需要配置 multiline 规则合并以
Exception或时间戳开头的多行内容 - 控制台日志默认是不带容器元数据的,采集时必须通过 Kubernetes 的 metadata(namespace、pod_name、container_name)来关联上下文,否则出了问题根本定位不到是哪台机器的哪个 Pod
监控告警方面,我的建议是最小可用的方案是 Prometheus + Grafana + Alertmanager。核心指标要先关注四个层面:
- 节点层:CPU、内存、磁盘、网络
- 控制平面层:API Server 的错误率与延迟、etcd 的 fsync 耗时、Scheduler 的调度延迟
- 工作负载层:Pod 重启次数、容器 OOMKilled 次数、HPA 扩缩容记录
- 业务层:请求 QPS、错误率、P99 延迟
P99 延迟这个指标,企业应用尤其关注。线上问题往往不是“系统不可用”,而是“接口变得慢”,没有业务层指标就发现不了。建议在应用里打印请求耗时日志,并采集到 Prometheus 自定义指标中。
6.3 高频故障复盘与排查命令速查
把我这些年群里被问到最多的故障按频率排个序,顺便给出排查路径。
第一个:镜像拉取失败 ImagePullBackOff。这个真的太常见了。先kubectl describe pod xxx,看 Events 里的具体报错。如果提示unauthorized,去查 imagePullSecret 和镜像仓库权限。如果提示no such host,检查镜像地址有没有拼错以及私有仓库的 DNS 是否可达。如果提示 manifest unknown,说明你这个 tag 不存在或者跨架构拉取(比如想在 ARM 机器上跑 x86 镜像,必须指定--platform或用 manifest list 方式构建多架构镜像)。
第二个:Pod 一直 Pending。原因通常是调度不成功。kubectl describe pod xxx里的 Events 会写得很明确:0/3 nodes are available: 1 node(s) had untolerated taint...,表示节点上有污点但 Pod 没有容忍;1 Insufficient cpu表示节点资源不够;0/3 nodes are available: 3 node(s) had volume node affinity conflict表示 PVC 的可用区(zone)跟节点对不上,这是云环境里很经典的坑。有状态服务的 PVC 绑定了某个可用区的云硬盘,调度器没法把 Pod 调度到没有该可用区存储的节点上。
第三个:服务之间调用不通。先测 DNS:kubectl run curl-test --image=curlimages/curl -it --rm -- sh,在测试 Pod 里直接 curl 对方服务的 Service 名。如果 DNS 能解析但超时,检查 Service 的 endpoint:kubectl get endpoints 服务名,如果 endpoints 列表为空,说明 selector 匹配不到任何 Pod,查 Deployment 的 Pod 标签和 Service 的 selector 是否一致。如果 endpoints 正常但访问不通,再查网络策略是否放行,以及节点安全组通信规则。
第四个:CrashLoopBackOff。Pod 起来后立刻崩、反复重启。看日志一眼就懂:kubectl logs pod名。如果日志为空,用--previous参数看上一次退出时的日志。有一种情况比较隐蔽:健康检查失败导致不断重启。看就绪探针或存活探针的返回码,如果应用启动比较慢,探针失败会导致 K8s 杀掉进程,启动时间超过initialDelaySeconds就会形成一个“永远起不来”的循环。此时把initialDelaySeconds调大一点就能解决。
7. 我的一些经验总结与忠告
走到这里,一套企业级应用集群已经能从零搭起来,并能稳定承载业务。但有几句话我还是想单独拿出来说,这些话不带具体技术命令,更多是踩坑换来的。
第一句:Kubernetes 解决的是“单点故障”问题,不是“水平无限扩展”的银弹。很多团队把服务部署到集群里,就以为高枕无忧了。实际上数据库这种有状态组件才是整个架构中最难处理的部分,往往还需要依赖外部存储和中间件,这部分编排能力短板明显。规划时一定要分清哪些适合跑在 K8s 上,哪些依然需要独立部署。
第二句:不要追求“全家桶”。一个刚建好的集群,今天装个 Service Mesh,明天加个 Operator 框架,后天上一套 GitOps 流水线,表面上是搞现代化,实际上是把很多不成熟的组件引入了生产环境。每引入一个新组件,就多一个故障域。我见过把集群搞垮的案例,没有一次是核心组件挂掉的,基本都是生态组件之间的兼容性问题。
第三句:记录操作、写好文档,比“能解决问题”更重要。K8s 集群是团队协作的产物,一个人能熟练操作不代表团队能维护。任何一个变更操作,先写变更单,再做备份,再执行,执行后立刻验证。这套流程听起来繁琐,但真到了凌晨两点集群出问题的时候,你会感谢当初那个认真写文档的自己。
最后说一个我这些年一直在用的习惯:搭好集群后,第一时间做一次“破坏性演练”。手动 kill 一个节点上的 kubelet,或者直接停掉某台机器,看看业务是否真的无感知。如果演练结果不是无感知,那说明高可用只是写在 PPT 上的,该补的架构短板赶紧补。这是我认为企业级集群交付验收里最重要的一步,没有之一。