Featured image of post K8s 容器编排:Java 微服务团队的踩坑实录与收益账

K8s 容器编排:Java 微服务团队的踩坑实录与收益账

K8s 容器编排:Java 微服务团队的踩坑实录与收益账

Java Web 微服务系列 · 第 3 篇 · K8s 容器编排 阅读时长:约 35 分钟 本文写于 2026 年 6 月

引子:凌晨 3 点的告警电话

2021 年某日深夜,监控大屏突然飘红——某物流公司的订单服务挂了一台机器。值班同学爬起来 SSH 上去,看了一眼:进程没了。systemctl restart order-service 一把梭,服务起来了。然后第二台挂了,第三台挂了。值班同学对着黑漆漆的屏幕,骂了一句脏话。

第二天复盘,发现是部署脚本的 bug——新版本包没拷全,旧进程在内存里撑了 2 个小时才崩。2 个小时里这个服务有 6 个实例在跑,但其中 4 个已经"假死"——能 ping 通但 HTTP 503。更要命的是,值班同学是按 IP 一台台 SSH 上去重启的,光是重启就花了一个半小时。

“这日子没法过了。”

半年后,这家公司把所有 Java 微服务全部搬到了 K8s 集群。三年过去,再没人在凌晨 3 点因为"进程没了"被叫起来过。

但 K8s 也不是银弹。上 K8s 本身就是一个大坑——只是它把你从一个深渊里捞出来,又把你推进了另一个深渊。本文就按"为什么要上 / 解决什么痛点 / 收益是什么 / 有什么坑"四问,把这条路的全貌讲清楚。

一、为什么要上 K8s 集群:业务驱动的演进

1.1 Java 微服务部署的 4 大历史形态

从 2010 年到现在,Java 微服务的部署方式大概经历了 4 个阶段。每个阶段都不是"上一个错了",而是"业务规模逼着你升级"

阶段部署方式代表工具适用规模典型痛点
单机直跑物理机 / 虚拟机 + Tomcat / jarscp、shell 脚本1-5 个服务、< 50 QPS部署靠人、环境不一致、扩容靠买机器
脚本化自动化脚本 + 进程管理Ansible、SaltStack、Supervisor5-20 个服务、< 500 QPS脚本维护成本高、回滚慢、灰度靠脚本拼
容器化(无编排)Docker + docker-composeDocker、docker-compose20-50 个服务、< 2000 QPS跨主机调度、容器自愈、滚动升级都靠人
容器编排K8s / Swarm / MesosK8s、Docker Swarm、Mesos、Nomad50+ 服务、2000+ QPS集群本身复杂度、运维门槛陡升

💡 原理:为什么"容器"不等于"K8s"

Docker 只解决了"打包一致"——开发机、测试机、生产机跑同一个 image。但 Docker 解决不了"50 个服务怎么调度到 30 台机器"“某台机器挂了容器怎么办"“滚动升级时如何不停机”。这些是"容器编排"要解决的问题。K8s 是容器编排的事实标准,不是因为它最好,而是因为它最早做大、生态最全

1.2 业务量爆炸的拐点

什么时候"必须上 K8s”?没有标准答案,但有几个硬指标

  • 服务数 > 30:人工维护部署脚本开始失控——一个新人改一行配置要培训 1 周
  • 实例数 > 100:单 IP 重启模式失效——光 SSH 串行重启就要 1 小时
  • QPS > 1000 + 长尾延迟敏感:必须做弹性扩缩容——大促前临时扩容 50%,大促后缩回去
  • 机房数 > 1:跨机房调度、流量切换需要平台化——脚本扛不住

上述任一指标突破,都应该认真评估 K8s。不要"为了上 K8s 而上 K8s"——K8s 本身的运维成本比裸 Docker 高 5-10 倍。

1.3 选型对比:K8s vs Swarm vs Mesos vs Nomad

“K8s 是唯一的答案吗?”不是,但事实上是。看个对比表:

维度K8sDocker SwarmMesosNomad
生态⭐⭐⭐⭐⭐ CNCF 一哥⭐⭐ Docker 官方但已停摆⭐⭐⭐ 老牌但社区萎缩⭐⭐ HashiCorp 出品,小而美
学习曲线陡(概念多)平(Docker 用户秒上手)极陡(论文级)中(HashiCorp 一贯简洁)
适用规模数千节点、数万 Pod数百节点、几千容器数万节点数千节点
自愈能力强(多种控制器)弱(基本靠 restart)中(依赖 Marathon)中(基础功能)
服务网格Istio / Linkerd 成熟Consul Connect
存储编排强(CSI 标准)强(Mesos + 各种 framework)中(CSI 支持中)
运维成本高(要专职团队)极高
招聘难度容易(人才多)容易

📌 实践:为什么 99% 的团队最终选 K8s

  1. 生态绑死:所有云厂商默认托管 K8s(EKS / AKS / GKE / 阿里 ACK),自建也有 kubeadm / kOps
  2. 招聘市场最大:K8s 工程师池子 10 倍于 Mesos/Nomad
  3. 学习成本虽然高但只付一次:学会 K8s 概念后,5 年内不会被淘汰
  4. 二次开发友好:CRD + Operator 让任何复杂业务都能抽象

选 K8s 不是"技术最优",是"综合 ROI 最高"。

二、解决了什么痛点:5 大类问题

K8s 解决的不是"一两个痛点",而是Java 微服务规模化后的一整片痛。下面按"团队最痛的"到"运营层面的"排序。

2.1 部署自动化:告别凌晨 3 点的发布

传统部署的核心问题是"人工介入太多"——scp 包、kill 进程、start 进程,串行、慢、易错。K8s 把这一切抽象成一份 YAML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    spec:
      containers:
      - name: order-service
        image: registry.example.com/order-service:v1.2.3
        ports:
        - containerPort: 8080

一次 kubectl apply 完成的事

  1. 拉镜像(v1.2.3)
  2. 按 maxSurge/maxUnavailable 策略滚动升级(每次只停 1 个,起来 1 个)
  3. 等 readinessProbe 通过才继续下一个
  4. 任意一步失败自动回滚
  5. 全程无需 SSH 任何机器
场景传统方式K8s 方式
滚动升级写脚本、串行执行、人工盯日志改 YAML、kubectl apply、自动滚
灰度发布维护两个集群、路由脚本切流量改 replicas 比例、调整 Service selector
回滚找回老包、停止服务、重新部署kubectl rollout undo(秒级)
扩缩容买机器 → 装机 → 部署服务kubectl scale / HPA 自动(分钟级)

2.2 自愈能力:进程挂了不用爬起来重启

K8s 的核心思想是"声明式期望状态"——你告诉它"我想要 3 个 order-service 实例在跑",它永远会努力维持这个状态。

  • Pod 挂了 → kubelet 立刻重启(同节点)
  • 节点挂了 → controller-manager 30s 内发现,在其他节点重新拉起
  • 健康检查失败 → readinessProbe 不通过 → 流量切走 → livenessProbe 失败 → 重启

💡 原理:K8s 自愈的三个层次

  1. Pod 级别:kubelet 监控容器,挂了就在同节点重启
  2. 节点级别:kube-controller-manager 的 NodeController 30s 心跳,丢失后重新调度
  3. 集群级别:kube-scheduler 在 Pod 不可调度时(如资源不够)找其他节点

传统方式下"凌晨 3 点 SSH 上去重启"的事,K8s 在 30 秒内自动完成。

2.3 服务发现与负载均衡:告别 IP 漂移

Java 微服务最头疼的事之一:A 服务调用 B 服务,B 的 IP 变了,调用失败。K8s 用 Service + DNS 解决:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
  - port: 80
    targetPort: 8080

部署后,集群 DNS(CoreDNS)会自动注册 order-service.default.svc.cluster.local → 后端 Pod 的虚拟 IP。Java 代码用 order-service:80 调用就行,IP 漂移全自动屏蔽

维度传统方式(Eureka / Nacos)K8s 方式
注册中心独立部署 Eureka / Nacos 集群内置(Service + kube-proxy)
客户端依赖Spring Cloud 组件 + SDK无(DNS 解析即可)
健康检查Spring Boot Actuator 主动上报kubelet 探针(被动)
跨语言支持Java 友好,其他语言要写 SDK任何语言都行
配置复杂度中(要维护注册中心集群)低(YAML 写一遍就行)

📌 实践:K8s DNS 与 Nacos 的取舍

简单服务用 K8s 内置 DNS 足够。但有状态服务(配置中心、服务治理、可观测)Nacos 仍是首选——它能管理服务元数据、配置、流量规则,K8s DNS 只能做"名字→IP"。

2.4 配置与密钥管理:告别 .properties 走天下

传统 Java 应用的配置问题:

  • 改一个数据库地址 → 改 30 台机器的 application.properties → 重启服务
  • 密码明文写在配置文件里 → 提交到 Git → 安全事故

K8s 用 ConfigMap + Secret 抽象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
data:
  application.properties: |
    server.port=8080
    spring.datasource.url=jdbc:mysql://mysql:3306/order
---
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secret
type: Opaque
stringData:
  DB_PASSWORD: <db-password>

热更新:用 Reloader 之类的 Operator,配置变更自动触发滚动重启。密钥管理:Secret 配合 etcd 加密 + RBAC 权限控制。

🎯 避坑点:Secret 默认是 base64 编码,不是加密!

任何能 kubectl get secret 的人都能解出明文。生产环境必须:① 启用 etcd 加密;② 用外部密钥管理(Vault / 阿里云 KMS);③ RBAC 收紧到"运维 + 必要开发"。

2.5 资源隔离与调度:告别"那个服务又把内存吃光了"

传统物理机/虚拟机的痛:A 服务 OOM 杀掉了同机器的 B 服务。K8s 用 cgroup + namespace 做硬隔离:

1
2
3
4
5
6
7
resources:
  requests:
    cpu: "500m"      # 0.5 核(保证)
    memory: "512Mi"  # 512MB(保证)
  limits:
    cpu: "2"         # 最多 2 核(超了就节流)
    memory: "1Gi"    # 最多 1GB(超了就 OOM Kill)
QoS 等级设置条件行为
Guaranteedrequests == limits最高优先级,最后被驱逐
Burstablerequests < limits(有 request)中等优先级,资源紧张时被驱逐
BestEffort没设 requests/limits最低优先级,资源紧张时最先被杀

🛑 误区警示:limits 设太低 = 性能不稳

JVM 堆内存默认会"贪"到 limit 才 GC。memory.limits=512Mi 但 JVM -Xmx 没设 → JVM 启动后申请 1GB 内存 → OOM Kill。Java 容器必须显式设 -XX:MaxRAMPercentage=70.0-Xmx(详见第四章 4.7)。

三、收益是什么:可量化的账

老板最关心"花了多少钱,赚回多少"。这一节给能写进年度预算答辩的数字

3.1 资源利用率:从 20% 到 60%

传统物理机部署:

  • 平均 CPU 利用率 15-25%(高峰要预留 3 倍容量)
  • 平均内存利用率 40%(JVM 堆固定分配)

K8s 部署:

  • 平均 CPU 利用率 50-65%(requests/limits + 混部 + HPA)
  • 平均内存利用率 55-70%(动态堆 + 超卖)

📌 实践:10 台物理机 vs 30 台物理机的故事

某电商公司搬 K8s 前用 30 台物理机跑 200 个微服务实例(平均 6.7 实例/机)。搬 K8s 后用 10 台物理机(多核高配)+ 充分利用 requests/limits + HPA,实例数提升到 350 个(平均 35 实例/机)。节省 20 台物理机 × 5 万/年 = 100 万/年

3.2 发布效率:从 30 分钟到 3 分钟

步骤传统K8s收益
打镜像 + 推仓库5 分钟1 分钟(Maven + Jib/Dockerfile)4 分钟
传包到 10 台机器10 分钟0(Pod 拉镜像)10 分钟
重启服务(滚动)10 分钟1 分钟(K8s 自动)9 分钟
验证 + 监控5 分钟1 分钟4 分钟
合计30 分钟3 分钟节省 27 分钟

按每天发布 10 次算 = 节省 4.5 小时/天。开发体验直接拉满。

3.3 故障恢复时间:MTTR 从小时级到秒级

故障场景传统 MTTRK8s MTTR
进程崩溃5-30 分钟(人工发现 + 重启)< 30 秒(自动重启)
节点宕机10-60 分钟(人工迁移)< 2 分钟(自动迁移)
机房故障30-120 分钟(脚本切流量)< 5 分钟(DNS/Ingress 切)
配置错误5-15 分钟(人工改文件 + 重启)< 1 分钟(ConfigMap + Reloader)
版本回滚10-30 分钟(找回老包 + 部署)< 10 秒(kubectl rollout undo

🎯 避坑点:MTTR < 30s 不是无脑的

K8s 自动重启 / 调度是默认的,但自动迁移不等于自动恢复“Pod 在新节点起来了” ≠ “流量切过来了”——Service selector 要对、readinessProbe 要过、DNS 缓存要刷新。生产环境要配 PodDisruptionBudget + 反亲和 + 健康检查三件套,K8s 的"自愈"才能真正救你

3.4 团队协作:从"运维是瓶颈"到"开发自服务"

传统流程:

1
开发提工单 → 运维评审(3 天)→ 运维操作(2 小时)→ 通知开发

K8s 流程:

1
开发写 YAML → kubectl apply(5 分钟)→ 跑起来

开发自服务带来的收益无法直接量化,但让运维从"操作工"变成"平台工程师",团队效率指数级提升。

3.5 业务连续性:机房级故障 RTO < 30s

承接系列第 1 篇「异地多活」的话题:K8s 是异地多活的基础设施底座

  • 多集群联邦(Karmada / Kubefed)→ 跨机房调度
  • Ingress + Global DNS → 跨机房流量切
  • ArgoCD / GitOps → 配置漂移检测与自动修复
  • Velero → 集群级备份与恢复

💡 原理:K8s 是"数据中心即一台机器"的抽象

上一篇文章讲异地多活是"把系统分散到多机房"。K8s 把这件事的成本降到 1/10——你不再需要写机房切换脚本,集群联邦 + DNS 切流 + 健康检查就能在 30 秒内完成 RTO < 30s 的机房级故障切换。

四、有什么坑:真实踩雷清单

这一节是全文最长的部分——因为"坑"是 K8s 学习曲线最陡的部分。不讲坑,上 K8s = 找死。下面 8 节按"踩坑顺序"展开。

4.1 二进制部署的复杂度:6 大子系统

K8s 集群不是"装一个软件"——它有 6 大核心组件要装、要配、要互相通信

组件角色装在哪启动参数数量
etcd分布式 KV 存储(集群大脑)3 个 master3-5 个
kube-apiserverAPI 网关(唯一对外入口)3 个 master28+ 个
kube-controller-manager控制器集合(保 reconcile 循环)3 个 master18+ 个
kube-scheduler调度器(决定 Pod 放哪)3 个 master8+ 个
kubelet节点 agent(管容器)所有节点30+ 个
kube-proxy网络代理(管 Service)所有节点15+ 个

装完这 6 大件只是开始——还要装 CNI 网络插件(Calico/Flannel)、CoreDNS、Ingress Controller、Metrics Server、Helm、Dashboard……一套完整生产集群有 20+ 组件

🛑 误区警示:kubeadm 不是"一键安装"

很多人以为 kubeadm init 就是装好 K8s 了。真相是:kubeadm 只装 4 大件(apiserver / controller-manager / scheduler / kubelet),etcd 还得自己装,CNI 还得自己装,Ingress 还得自己装,所有生产级配置都得自己加。kubeadm 是"半成品"——它给你"会跑"的环境,不给你"能生产"的环境。

4.2 节点规划的血泪

K8s 集群"装在哪"是个大问题。生产集群一般分 3 种:

模式节点组成适用规模复杂度
all-in-one1 master + N worker(小集群)< 50 Pod
3 master + N worker3 master(HA)+ N worker50-500 Pod
多 master 多机房9+ master 跨机房500+ Pod极高

🛑 坑:异构硬件混部是大忌

见过某公司用 5 台 IBM 老服务器(E5-2660 v2,20 核,64GB)+ 5 台消费级 i7/i9(64GB)+ 1 台 Xeon Gold 80 核 64GB。混部后:

  • 资源利用率算不清:i9 内存 64G 是 2 根 32G,老服务器是 8 根 8G,节点间 OOM 行为不一致
  • 调度不公平:80 核机器被大 Pod 吃掉,20 核机器闲着
  • 故障域混乱:i7/i9 单点故障率高于服务器级 CPU

生产集群应该同构硬件(同型号、同 CPU 架构、同内存配置),最多分 2 档(master vs worker)。别想着"反正都是 x86,能跑就行"——K8s scheduler 会按资源调度,异构会让它的判断出 bug。

端口范围也是坑:默认 K8s Service NodePort 范围是 30000-32767,很多公司的安全策略会拦这个段。要么改范围(--service-node-port-range=8000-9000),要么提前和安全团队对齐。

4.3 系统优化的 20 个坑

K8s 对底层 Linux 有 20+ 项硬性要求。每一项不满足都可能让集群"跑不起来"或"跑着跑着崩"

4.3.1 swap 必须关

1
2
swapoff -a
sed -ri 's/.*swap.*/#&/' /etc/fstab

不关会怎样:kubelet 默认要求关闭 swap(--fail-swap-on=true),开了 swap 容器会性能骤降(swap 拖慢 GC),且 kubelet 启动失败。

4.3.2 防火墙必须关

1
2
sudo systemctl stop ufw
sudo systemctl disable ufw

不关会怎样:iptables 规则混乱,Pod 之间通信失败。

4.3.3 systemd-resolved 必须删

1
2
3
4
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
sudo rm -rf /etc/resolv.conf
sudo touch /etc/resolv.conf

🎯 避坑点:systemd-resolved 是 Ubuntu 上 K8s 的大坑

Ubuntu 22.04 默认启用 systemd-resolved,它会拦截 53 端口。CoreDNS 装上后起不来(端口冲突),表现为 coredns Pod 一直 CrashLoopBackOff,但 kubectl logs 看不到明显错误——因为 coredns 进程根本启动失败。99% 的"CoreDNS 装不上"都是这个原因

4.3.4 ulimit 必须调大

1
2
3
4
5
6
cat >> /etc/security/limits.conf << "EOF"
* soft nofile 655360
* hard nofile 131072
* soft nproc 655350
* hard nproc 655350
EOF

不调会怎样:高并发下 Java 进程的"too many open files"错误。

4.3.5 ipvs 模块必须加载

1
2
3
4
5
6
7
cat >> /etc/modules-load.d/ipvs.conf << "EOF"
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
EOF

不加载会怎样:kube-proxy 退化为 iptables 模式,1000 个 Service 就会让 iptables 规则爆炸(性能 O(n²))。

4.3.6 内核参数 20+ 项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cat > /etc/sysctl.d/k8s.conf << "EOF"
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.core.somaxconn = 16384
net.ipv4.tcp_max_syn_backlog = 16384
vm.overcommit_memory=1
fs.file-max=52706963
# ... 还有 15+ 项
EOF
sysctl --system

这些参数少一个都可能在高并发下出怪事——比如"Syn_sent 状态堆积"、“nf_conntrack table full”。

生产建议用 ansible 一键配置 20+ 项 sysctl + 加载 9 个内核模块,不要手敲。我在这上面栽过 3 次——每次都是少一个参数导致"线上跑得好好的,新机器扩上去就抖"。

4.4 证书管理的麻烦

K8s 集群里有 3 套独立的 CA(证书颁发机构)

CA 用途证书数量过期
etcd-caetcd 集群内部通信1 CA + 3 server cert默认 1 年
kubernetes-caapiserver / kubelet / controller-manager / scheduler1 CA + 10+ cert默认 1 年
front-proxy-caapiserver 聚合层(metrics-server 等用)1 CA + 1 cert默认 1 年

生成工具:cfssl 三件套(cfssl + cfssljson + cfssl-certinfo)。

🛑 坑:证书 hostname 漏一个 = 集群起不来

生成 apiserver 证书时,hostname 列表必须包含

  • 所有 master 的 hostname(master1, master2, master3)
  • 所有 master 的 IP(10.0.0.x 格式)
  • VIP(虚拟 IP,对外服务的)
  • 集群内部地址(kubernetes, kubernetes.default, kubernetes.default.svc, kubernetes.default.svc.cluster.local)
  • 127.0.0.1、localhost

漏任何一个,etcd 报 “x509: certificate is valid for …, not …”,表现是 apiserver 反复重启但 kubectl 连不上。

证书自动续期(1 年期限):生产集群必须用 cert-manager 之类的工具自动化,手动续期等于给自己挖坑

TLS Bootstrapping:新 worker 节点加入集群时,自动签发 kubelet 客户端证书(避免把 CA 私钥分发到所有节点):

1
2
3
4
5
# bootstrap.secret.yaml
stringData:
  token-id: <token-id>
  token-secret: <token-secret>
  # ...

🛑 坑:token-id 和 token-secret 暴露 = 任意节点能加入集群

这个 token 实际是"加入集群的入场券"。生产环境必须:① 缩短 token 有效期(默认 24h);② 审计 token 使用记录;③ 上线后立即吊销。不要把 token 提交到 Git 仓库(记忆库里有真实事故:某 bootstrap token 在私人笔记里登记,已泄露到 git 历史——前车之鉴)。

4.5 组件配置的 28+ 个参数

kube-apiserver 一个组件,启动参数就有 28+ 个:

类别参数示例作用
网络--bind-address--secure-port--advertise-address监听地址
etcd--etcd-servers--etcd-cafile--etcd-certfile后端存储
认证--client-ca-file--tls-cert-fileTLS 双向认证
授权--authorization-mode=Node,RBACRBAC 权限控制
admission--enable-admission-plugins=...准入控制(12+ 种)
聚合--requestheader-*聚合层(apiserver 内部 API 转发)

3 个 master 几乎一样的配置,只有 --advertise-address 不同。手动复制 = 易漏

🎯 避坑点:3 master 配置文件必须用 sed 自动化生成

见过有人手抄 3 份,结果 master2 漏了 --feature-gates=RemoveSelfLink=false,导致 1.28 之后整个集群升级不上去(RemoveSelfLink 已经在 1.16 弃用、1.18 删除)。正确做法:用脚本生成,参数化只有 IP 不同的部分。

controller-manager / scheduler / kubelet / kube-proxy 也有大量参数。生产集群必须有 1 份"配置审计"清单——记录每个参数的原因、默认值、生产值。别"反正跑起来了"——下次升级、扩容、排查时你会感谢现在的自己。

4.6 跑起来之后的坑

集群装好、组件跑起来、Pod 能起——但生产环境会冒出新的问题

4.6.1 kubelet 启动失败

1
failed to run Kubelet: validate service connection: validate CRI v1 runtime API

原因:服务器重启后服务启动顺序乱了——kubelet 比 containerd 启动早,连不上 CRI socket。

解决:加 systemd After 依赖:

1
2
3
[Unit]
After=containerd.service
# 或 cri-dockerd.service

4.6.2 代理后访问不到 Ingress

开发同学用 Charles / Fiddler 抓包时,发现 https://<your-domain> 进不来——Ingress 域名被代理拦截。

解决:代理软件里 bypass 集群内网域名(如 *.cluster.local10.0.0.0/8)。

4.6.3 hostNetwork + hostAliases 解决内网域名

有些服务要访问内网老系统的固定 IP(不是 DNS),Pod 默认隔离网络访问不到。

1
2
3
4
5
spec:
  hostNetwork: true
  hostAliases:
  - ip: "<internal-ip>"
    hostnames: ["legacy-service.internal"]

🛑 坑:hostNetwork = 失去 Pod 网络隔离

开了 hostNetwork,Pod 直接用宿主机网络,Service / DNS / 端口分配全失效能不用就不用——只在调试老系统迁移时用,生产环境应该用 Service + Endpoint 解决

4.6.4 metrics-server 起不来 = HPA 失效

HPA(Horizontal Pod Autoscaler)依赖 metrics-server 提供 Pod 资源数据。metrics-server 装好但kubectl top node 报 “Metrics API not available”——99% 是证书问题:metrics-server 的 --kubelet-certificate-authority 没指对。

4.7 Java 应用特定的坑

Java 跑在 K8s 里有几个"老问题"必须重新理解——不是 K8s 的锅,是 Java 自己的坑:

4.7.1 时区问题

Pod 默认 UTC 时区new Date() 拿到的时间比东八区少 8 小时——日志时间、数据库时间、订单时间全错位。

解决(三选一)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 方案 1:环境变量
env:
- name: TZ
  value: Asia/Shanghai

# 方案 2:挂载时区文件
volumeMounts:
- name: tz-config
  mountPath: /etc/localtime
volumes:
- name: tz-config
  hostPath:
    path: /usr/share/zoneinfo/Asia/Shanghai

# 方案 3:JVM 参数(兼容老应用)
env:
- name: JAVA_OPTS
  value: "-Duser.timezone=Asia/Shanghai"

4.7.2 JVM 内存感知

JVM 早期版本不识别容器 cgroup 内存限制——Runtime.getRuntime().maxMemory() 返回宿主机内存,JVM 启动时按 1/4 算堆,结果容器 OOM Kill

解决:JDK 8u191+ 加上 JDK 10+ 已支持 cgroup v1;JDK 11+ 支持 cgroup v2。但仍需显式声明

1
2
3
4
5
# 推荐:用百分比,让 JVM 自己感知容器内存
JAVA_OPTS="-XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=50.0"

# 不推荐:写死堆大小(容器内存改了 JMX 还按老值)
JAVA_OPTS="-Xmx1g"
模式优点缺点
MaxRAMPercentage自动适配容器内存调优时不可预测峰值
-Xmx 写死行为可预测换机器要重算
OpenJ9 memoryLimit准生产级,OpenJ9 内部感知 cgroup团队要学 OpenJ9

🎯 避坑点:JVM 调优必须和 Pod limits 配合

memory.limits=1Gi + JAVA_OPTS=-Xmx2gJVM 启动时被 K8s OOM Killmemory.limits=1Gi + JAVA_OPTS=-XX:MaxRAMPercentage=70.0 → JVM 自动算 700MB 堆,正合适

4.7.3 探针配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 180   # 给 Spring Boot 启动时间
  periodSeconds: 10
  failureThreshold: 9         # 容忍 90s 启动期
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 180
  periodSeconds: 10
  failureThreshold: 6         # 死 60s 才杀
探针失败后果失败阈值配多少合适
readinessProbe流量切走(不杀进程)9 × 10s = 90s给应用启动时间
livenessProbe重启 Pod6 × 10s = 60s比 readinessProbe 更严格

🛑 坑:livenessProbe 太严 = 雪崩

livenessProbe.failureThreshold=1 → 应用 GC 抖一下就重启。正确做法:livenessProbe 阈值 ≥ 2 次失败(至少 20s),给 GC / 业务高峰一点缓冲。见过某公司 livenessProbe 阈值 1,结果大促时 Java Full GC 1 秒卡顿 → livenessProbe 失败 → Pod 重启 → Full GC 又卡 → 雪崩式重启

4.8 三种工作负载选错 = 数据丢失

K8s 有 3 种核心工作负载控制器,选错 = 业务崩

控制器适用场景关键特性典型应用
Deployment无状态服务副本数随机命名、Pod IP 不固定、可任意扩缩Web 服务、API 服务、网关
StatefulSet有状态服务副本名固定(pod-0/pod-1/…)、稳定的持久卷和 DNS数据库、消息队列、ZooKeeper
DaemonSet节点级守护每个节点跑 1 个 Pod,新节点自动拉起日志收集、监控 agent、存储插件
场景选错结果
MySQL 用 Deployment三个 Pod 各自写不同 PVC → 数据 3 份不一致 → 业务崩
Redis 哨兵用 DeploymentPod 名字带随机后缀 → Sentinel 配置里写的 redis-0 找不到 → 集群不可用
Fluentd 日志收集用 Deployment副本数少于节点数 → 部分节点日志丢失
Fluentd 日志收集用 StatefulSet副本数固定,新节点加入后不自动拉起 → 新节点没日志

🛑 避坑点:状态用 Deployment = 数据全丢

某团队图省事,MySQL 主从用 Deployment 部署。某天 K8s 升级,Deployment 滚动升级 → 旧 Pod 被 kill → 从库没来得及同步 → 数据丢失。正确做法:数据库永远用 StatefulSet + 稳定的 PVC + PodDisruptionBudget 严格控制。

五、给想上 K8s 的 Java 团队忠告

5 条经过血泪教训的务实建议:

  1. 不要为了上 K8s 而上 K8s——服务数 < 30、QPS < 1000 的团队,docker-compose + 1 个老运维就够了,别给团队加复杂度
  2. 从托管 K8s 开始(EKS/AKS/ACK/GKE),别自建——自建 K8s 需要 1 个全职 SRE 团队维护,省下的人工费比托管费多。
  3. Java 应用先做容器化适配再上 K8s——时区、JVM 内存、启动时间、配置外置、探针,每一项都要测
  4. 灰度上 K8s——新业务先上、存量业务后上。一个 200 服务的团队,半年内推 10% 业务到 K8s 已经很快了。
  5. 必须配专职或半专职 SRE——K8s 学习曲线 6-12 个月,让一个开发兼运维的团队会让所有人都痛苦。

💡 最后一条建议:把 K8s 当作"工具"而不是"银弹"

K8s 解决的是"规模化后的部署和运维效率"问题,它不解决业务本身的问题。业务架构差、代码质量差、监控缺失,上 K8s 不会让这些问题消失,只会让它们更复杂

六、下一篇预告

系列第 4 篇(计划)会讲 Spring Cloud Gateway 在 K8s 上的部署与流量治理——Ingress 怎么配置、Service Mesh 要不要上、灰度发布怎么做。敬请期待。


附录:本文资源

📌 延伸阅读

本系列共 16 篇,本文为第 3 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计