
👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Kubernetes这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- Kubernetes — 从 Docker 到 K8s,容器技术的进阶与协同逻辑 🚀
-
- 一、起点:Docker 是什么?为什么 Java 工程师必须懂它? 🐳
-
- ✅ 示例:用 Dockerfile 构建一个 Spring Boot Web 应用
- 二、跃迁:为什么 Docker 不足以支撑生产级微服务? 📉
- 三、基石:Pod —— Kubernetes 的原子调度单元 🧱
-
- ✅ 示例:Sidecar 模式注入日志采集与指标暴露
- 四、规模化:Deployment —— 声明式副本控制器 🔄
-
- ✅ 示例:定义一个高可用 Spring Boot Deployment
- 🔁 滚动升级实战:从 v1.0 升级到 v1.1
- 五、通信:Service —— 集群内服务发现与负载均衡 🌐
-
- ✅ 示例:为 demo-app 创建 ClusterIP Service
- 六、入口:Ingress —— 统一七层流量网关 🌐➡️📦
-
- ✅ 示例:用 Nginx Ingress Controller 暴露 demo-app
- 七、协同逻辑全景图:从 Docker 到 K8s 的分层抽象 🧩
- 八、Java 工程师的 K8s 实战 Checklist ✅
- 九、结语:容器技术的终局不是取代,而是升维 🌈
Kubernetes — 从 Docker 到 K8s,容器技术的进阶与协同逻辑 🚀
“Docker 让容器可运行,Kubernetes 让容器可信赖。” —— 这不是一句口号,而是一场分布式系统范式的静默革命 🔁
在微服务架构席卷全球的今天,Java 工程师早已不再满足于 java -jar app.jar 的单机部署时代。我们写完 Spring Boot 应用、打好 JAR 包、配置好 application.yml,却常卡在最后一公里:如何让服务在 3 台机器上自动负载均衡?如何在节点宕机时 5 秒内自愈?如何灰度发布新版本而不影响老用户?如何按 CPU 使用率动态扩缩容?
这些问题的答案,不在 Nginx 配置里,不在 Ansible 脚本中,也不在 Jenkins 流水线深处——它们统一指向一个名字:Kubernetes(简称 K8s) 🌐。
但 Kubernetes 并非凭空而降。它生长于 Docker 的土壤之上,承袭其“一次构建、处处运行”的哲学,又以声明式编排、自治控制平面和面向终态的设计,将容器技术推向工程化、生产级的新高度。本文将带你从 Java 开发者的视角出发,以真实可运行的代码为锚点,层层解构 Docker → Containerd → Pod → Deployment → Service → Ingress 的演进链条,揭示容器技术从“能跑”到“稳跑”、“智跑”的底层协同逻辑。全程无概念堆砌,只有可验证的逻辑、可调试的代码、可落地的 YAML 和可渲染的 Mermaid 图表 🧩。
一、起点:Docker 是什么?为什么 Java 工程师必须懂它? 🐳
Docker 不是虚拟机,也不是编程语言,而是一个标准化的软件交付单元封装与运行时平台。它通过 Linux 内核的 Namespaces(隔离) 和 Cgroups(限制) 技术,在宿主机上创建轻量、独立、可移植的运行环境——即容器(Container)。
对 Java 开发者而言,Docker 的核心价值在于:终结“在我机器上能跑”(It works on my machine)的千年魔咒 ✨。
设想这样一个场景:你的 Spring Boot 项目依赖 JDK 17 + MySQL 8.0.33 + Redis 7.2,本地开发用的是 macOS,测试环境是 CentOS 7,生产是 Ubuntu 22.04。每换一个环境,你都要手动安装 JDK、配置 JAVA_HOME、下载 MySQL RPM 包、处理 SELinux 策略……直到某天运维告诉你:“线上不能装 Oracle JDK,必须用 OpenJDK 17.0.2”。崩溃?不,是时候拥抱 Docker 了。
✅ 示例:用 Dockerfile 构建一个 Spring Boot Web 应用
我们从一个极简的 Spring Boot REST API 开始(基于 Spring Boot 3.2.x + Jakarta EE 9+):
// src/main/java/com/example/demo/HelloController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/api/hello")
public String sayHello() {
return "Hello from Spring Boot in Docker! 🐳✨";
}
}
对应的 pom.xml(关键片段):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.7</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
构建完成后,生成 target/demo-0.0.1-SNAPSHOT.jar。接下来,编写 Dockerfile:
# 使用官方 OpenJDK 运行时作为基础镜像(多阶段构建推荐)
FROM registry.access.redhat.com/ubi8/openjdk-17:latest
# 设置工作目录
WORKDIR /app
# 复制 JAR 文件(注意:Spring Boot fat jar 自带嵌入式 Tomcat)
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
# 暴露端口(仅声明,实际由 Spring Boot server.port 决定)
EXPOSE 8080
# 启动命令(使用 jvm args 优化容器内存感知)
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
💡 关键说明:
- -XX:+UseContainerSupport 启用 JVM 对容器 cgroup 内存限制的感知(JDK 10+ 默认启用,但仍建议显式声明);
- -XX:MaxRAMPercentage=75.0 让 JVM 自动根据容器内存 limit 分配堆大小,避免 OOMKill;
- registry.access.redhat.com/ubi8/openjdk-17 是 Red Hat Universal Base Image,安全合规、生产就绪,官方文档 可查。
构建并运行:
# 构建镜像(tag 为 demo-app:1.0)
$ mvn clean package
$ docker build -t demo-app:1.0 .
# 启动容器(映射 8080→8080,后台运行)
$ docker run -d –name demo-ctr -p 8080:8080 demo-app:1.0
# 验证
$ curl http://localhost:8080/api/hello
# 输出:Hello from Spring Boot in Docker! 🐳✨
✅ 成功!你已将 Java 应用封装为不可变镜像(Immutable Image)——内容哈希固定、环境完全一致、启动秒级完成。
但请注意:这只是单机单容器。当业务增长,你需要:
- 启动 3 个实例做负载均衡;
- 某个实例 OOM 崩溃后自动重启;
- 升级时先停 1 个、再启 1 个(滚动更新);
- 所有实例共用同一个数据库连接串,且该串需安全注入;
👉 Docker CLI 或 docker-compose.yml 可以部分解决,但缺乏跨主机调度、健康探针、服务发现、策略驱动的自愈能力。这正是 Kubernetes 登场的时刻 ⏳。
二、跃迁:为什么 Docker 不足以支撑生产级微服务? 📉
Docker 是伟大的,但它本质是一个单机容器引擎(Container Runtime)。它的设计哲学是:“给我一个镜像,还你一个进程”。它不关心:
| 多主机编排 | ❌ 仅限本机 docker run | ✅ 自动选择最优 Node 调度 Pod |
| 服务发现 | ❌ –link 已废弃,docker network 仅限单机 | ✅ 内置 DNS(service-name.namespace.svc.cluster.local) |
| 健康自愈 | ✅ –restart=always(仅进程级) | ✅ Liveness/Readiness 探针 + 控制器闭环 |
| 配置与密钥管理 | ❌ docker run -e 明文暴露,–mount 无生命周期管理 | ✅ ConfigMap / Secret 声明式挂载,热更新支持 |
| 滚动升级 & 回滚 | ❌ 需手动 stop/rm/run | ✅ kubectl rollout undo 一键回退 |
更关键的是:Docker daemon 本身已成为历史。自 Kubernetes v1.20 起,Dockershim 已被正式移除。K8s 不再直接调用 Docker Engine,而是通过 Container Runtime Interface (CRI) 与符合标准的运行时交互(如 containerd、CRI-O)。
这意味着: 🔹 你在 kubectl get nodes 看到的节点,底层可能运行的是 containerd(Docker Desktop 内置)、Podman(Red Hat)、或 CRI-O(OpenShift); 🔹 docker ps 看到的容器,只是 containerd 托管的其中一种 workload; 🔹 kubectl run nginx –image=nginx 创建的 Pod,其容器由 containerd 拉取、解压、运行,与 dockerd 无关。
所以,请放下执念:Kubernetes ≠ Docker 的上层封装,而是以容器为基本单元的分布式操作系统 🧠。它抽象出比“进程”更高一层的原语——Pod。
三、基石:Pod —— Kubernetes 的原子调度单元 🧱
在 K8s 中,最小、最不可分割的可调度对象是 Pod,而非容器。一个 Pod 封装了一个或多个紧密耦合、共享网络与存储的容器。它们总是被调度到同一台 Node 上,共享 IP、端口空间、IPC、UTS 和网络命名空间。
🌟 类比理解:
- Docker 容器 ≈ Linux 进程(独立 PID namespace)
- Kubernetes Pod ≈ 一个“逻辑主机”(共享 network namespace + volumes)
为什么 Java 微服务需要 Pod?看这个典型场景:
✅ 示例:Sidecar 模式注入日志采集与指标暴露
你的 Spring Boot 应用需要:
- 输出结构化 JSON 日志(供 ELK 收集);
- 暴露 /actuator/prometheus 指标(供 Prometheus 抓取);
- 但你不希望在业务代码里集成 Logstash 或 Prometheus Client 的复杂配置。
解决方案:用 Sidecar 容器解耦关注点。
# pod-with-sidecar.yaml
apiVersion: v1
kind: Pod
metadata:
name: springboot–pod
labels:
app: demo–springboot
spec:
containers:
# 主应用容器(Java)
– name: app
image: demo–app:1.0
ports:
– containerPort: 8080
env:
– name: SPRING_PROFILES_ACTIVE
value: "prod"
# Readiness 探针:HTTP GET /actuator/health/readiness
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
# Liveness 探针:HTTP GET /actuator/health/liveness
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
# Sidecar:日志转发(Fluent Bit)
– name: fluent–bit
image: cr.fluentbit.io/fluent/fluent–bit:2.2.0
volumeMounts:
– name: varlog
mountPath: /var/log
– name: fluent–bit–config
mountPath: /fluent–bit/etc/
resources:
limits:
memory: "64Mi"
cpu: "100m"
# Sidecar:Prometheus Exporter(可选)
– name: jmx–exporter
image: bitnami/jmx–exporter:0.18.0
args:
– ––config=/config/jmx–exporter.yaml
volumeMounts:
– name: jmx–config
mountPath: /config
ports:
– containerPort: 5556
# 共享卷:业务容器写日志,Fluent Bit 读取
volumes:
– name: varlog
emptyDir: {}
– name: fluent–bit–config
configMap:
name: fluent–bit–configmap
– name: jmx–config
configMap:
name: jmx–exporter–config
# DNS 策略:启用集群内服务发现
dnsPolicy: ClusterFirst
这个 Pod 启动后,三个容器共享:
- 同一个 IP 地址(kubectl get pod springboot-pod -o wide 查看);
- 同一个 /var/log 目录(业务容器写日志到 /var/log/app.log,Fluent Bit 从中读取);
- 同一个 127.0.0.1(app 容器可通过 http://localhost:5556/metrics 访问 JMX Exporter)。
这是 Docker 单容器永远无法实现的协作粒度。而这一切,由 K8s 的 Pod Controller(如 ReplicaSet) 自动保障:如果 Node 故障,整个 Pod 被驱逐,控制器会在另一台健康 Node 上重建它(含所有容器与卷)。
下面这张 Mermaid 图清晰展示了 Pod 内部容器间的协同关系:
#mermaid-svg-2DTwvN9lT6SwfC32{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2DTwvN9lT6SwfC32 .error-icon{fill:#552222;}#mermaid-svg-2DTwvN9lT6SwfC32 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2DTwvN9lT6SwfC32 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2DTwvN9lT6SwfC32 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2DTwvN9lT6SwfC32 .marker.cross{stroke:#333333;}#mermaid-svg-2DTwvN9lT6SwfC32 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2DTwvN9lT6SwfC32 p{margin:0;}#mermaid-svg-2DTwvN9lT6SwfC32 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-2DTwvN9lT6SwfC32 .cluster-label text{fill:#333;}#mermaid-svg-2DTwvN9lT6SwfC32 .cluster-label span{color:#333;}#mermaid-svg-2DTwvN9lT6SwfC32 .cluster-label span p{background-color:transparent;}#mermaid-svg-2DTwvN9lT6SwfC32 .label text,#mermaid-svg-2DTwvN9lT6SwfC32 span{fill:#333;color:#333;}#mermaid-svg-2DTwvN9lT6SwfC32 .node rect,#mermaid-svg-2DTwvN9lT6SwfC32 .node circle,#mermaid-svg-2DTwvN9lT6SwfC32 .node ellipse,#mermaid-svg-2DTwvN9lT6SwfC32 .node polygon,#mermaid-svg-2DTwvN9lT6SwfC32 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2DTwvN9lT6SwfC32 .rough-node .label text,#mermaid-svg-2DTwvN9lT6SwfC32 .node .label text,#mermaid-svg-2DTwvN9lT6SwfC32 .image-shape .label,#mermaid-svg-2DTwvN9lT6SwfC32 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2DTwvN9lT6SwfC32 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2DTwvN9lT6SwfC32 .rough-node .label,#mermaid-svg-2DTwvN9lT6SwfC32 .node .label,#mermaid-svg-2DTwvN9lT6SwfC32 .image-shape .label,#mermaid-svg-2DTwvN9lT6SwfC32 .icon-shape .label{text-align:center;}#mermaid-svg-2DTwvN9lT6SwfC32 .node.clickable{cursor:pointer;}#mermaid-svg-2DTwvN9lT6SwfC32 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2DTwvN9lT6SwfC32 .arrowheadPath{fill:#333333;}#mermaid-svg-2DTwvN9lT6SwfC32 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2DTwvN9lT6SwfC32 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2DTwvN9lT6SwfC32 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2DTwvN9lT6SwfC32 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2DTwvN9lT6SwfC32 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2DTwvN9lT6SwfC32 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2DTwvN9lT6SwfC32 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2DTwvN9lT6SwfC32 .cluster text{fill:#333;}#mermaid-svg-2DTwvN9lT6SwfC32 .cluster span{color:#333;}#mermaid-svg-2DTwvN9lT6SwfC32 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2DTwvN9lT6SwfC32 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2DTwvN9lT6SwfC32 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2DTwvN9lT6SwfC32 .icon-shape,#mermaid-svg-2DTwvN9lT6SwfC32 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2DTwvN9lT6SwfC32 .icon-shape p,#mermaid-svg-2DTwvN9lT6SwfC32 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2DTwvN9lT6SwfC32 .icon-shape .label rect,#mermaid-svg-2DTwvN9lT6SwfC32 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2DTwvN9lT6SwfC32 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2DTwvN9lT6SwfC32 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2DTwvN9lT6SwfC32 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Pod: springboot-pod
Shared Network Namespace
Shared Volume /var/log
HTTP POST
HTTP GET
Container: app- Spring Boot- Port 8080
Container: fluent-bit- Reads /var/log- Exports to Loki
Container: jmx-exporter- Scrapes JVM metrics- Port 5556
Loki Logging
Prometheus
📌 注意:上述 fluent-bit 和 jmx-exporter 都是社区成熟镜像,无需自行构建。Fluent Bit 官网 提供详尽文档;JMX Exporter GitHub 仓库(虽不放 GitHub 地址,但此链接可正常访问并查看配置示例)。
四、规模化:Deployment —— 声明式副本控制器 🔄
Pod 是原子单元,但生产环境绝不允许只跑一个 Pod。你需要:
- 至少 3 个副本抵御单点故障;
- 升级时逐个替换,保证可用性 > 99.9%;
- 版本回滚能力,应对上线事故。
这就是 Deployment 的使命:它是一个声明式控制器,负责确保任意时刻,集群中运行着指定数量、指定镜像版本的 Pod 副本。
✅ 示例:定义一个高可用 Spring Boot Deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo–app–deployment
labels:
app: demo–app
spec:
# 声明期望状态:3 个副本
replicas: 3
# Pod 模板(与前面 Pod YAML 的 spec.containers 部分一致)
selector:
matchLabels:
app: demo–app
template:
metadata:
labels:
app: demo–app
spec:
containers:
– name: app
image: demo–app:1.0 # ← 关键:镜像版本在此声明
ports:
– containerPort: 8080
env:
– name: SPRING_PROFILES_ACTIVE
value: "k8s"
– name: DATABASE_URL
valueFrom:
configMapKeyRef:
name: app–config
key: database–url
– name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db–secret
key: password
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
# 安全上下文:非 root 用户运行(最佳实践)
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 2001
# 使用 ConfigMap 和 Secret 注入配置(解耦代码与环境)
volumes:
– name: config–volume
configMap:
name: app–config
– name: secret–volume
secret:
secretName: db–secret
配套的 ConfigMap 和 Secret:
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app–config
data:
database-url: "jdbc:postgresql://postgres-svc:5432/mydb"
redis-host: "redis-svc"
# secret.yaml(base64 编码后)
apiVersion: v1
kind: Secret
metadata:
name: db–secret
type: Opaque
data:
password: cGFzc3dvcmQxMjM= # "password123" 的 base64
部署并验证:
$ kubectl apply -f configmap.yaml
$ kubectl apply -f secret.yaml
$ kubectl apply -f deployment.yaml
# 查看 Deployment 状态
$ kubectl get deploy
# NAME READY UP-TO-DATE AVAILABLE AGE
# demo-app-deployment 3/3 3 3 10s
# 查看底层 ReplicaSet 和 Pods
$ kubectl get rs,po -l app=demo-app
# NAME DESIRED CURRENT READY AGE
# replicaset.apps/demo-app-deployment-7c8b9d5f4 3 3 3 20s
#
# NAME READY STATUS RESTARTS AGE
# pod/demo-app-deployment-7c8b9d5f4-2xq8w 1/1 Running 0 15s
# pod/demo-app-deployment-7c8b9d5f4-5vz9p 1/1 Running 0 15s
# pod/demo-app-deployment-7c8b9d5f4-wk7t4 1/1 Running 0 15s
🔁 滚动升级实战:从 v1.0 升级到 v1.1
假设你已构建新镜像 demo-app:1.1(含新功能),只需更新 Deployment 中的 image 字段:
# 方式1:直接 patch(推荐用于 CI/CD)
$ kubectl set image deploy/demo-app-deployment app=demo-app:1.1
# 方式2:修改 YAML 后 apply(适合 GitOps)
# 修改 deployment.yaml 中 image: demo-app:1.0 → demo-app:1.1
$ kubectl apply -f deployment.yaml
K8s 会自动执行滚动更新:
- 创建新的 ReplicaSet(demo-app-deployment-8d9a2b1c3),启动 1 个新 Pod;
- 等待新 Pod ReadinessProbe 成功(HTTP 200);
- 删除 1 个旧 Pod;
- 重复直到所有 3 个 Pod 均为新版本。
全过程零停机,且可通过 kubectl rollout status deploy/demo-app-deployment 实时跟踪。
若发现问题,立即回滚:
$ kubectl rollout undo deploy/demo-app-deployment
# 回滚到上一个 revision(v1.0)
这就是 声明式 API 的力量:你只说“我要 3 个 demo-app:1.1”,K8s 控制器负责计算差异、执行动作、报告状态——你无需写一行 Bash 脚本去 SSH 登录每台机器。
五、通信:Service —— 集群内服务发现与负载均衡 🌐
现在你有 3 个 demo-app Pod 在运行,每个都有独立 IP(如 10.244.1.12, 10.244.2.8, 10.244.1.15)。但这些 IP:
- 由 K8s 动态分配,随时可能变化(Pod 重建);
- 位于 Pod 网络,外部不可路由;
- 无法被其他服务稳定引用。
解决方案:Service —— 一个抽象的、稳定的网络端点,为一组 Pod 提供统一 VIP(ClusterIP)和 DNS 名称。
✅ 示例:为 demo-app 创建 ClusterIP Service
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: demo–app–svc
labels:
app: demo–app
spec:
type: ClusterIP # 默认类型:仅集群内可访问
selector:
app: demo–app # 匹配 label 为 app=demo-app 的 Pod
ports:
– protocol: TCP
port: 80 # Service 暴露的端口(集群内访问)
targetPort: 8080 # Pod 容器的实际端口
name: http
部署后,神奇的事情发生了:
$ kubectl apply -f service.yaml
# 查看 Service 信息
$ kubectl get svc demo-app-svc
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# demo-app-svc ClusterIP 10.96.123.45 <none> 80/TCP 10s
# 在任意 Pod 内访问(DNS 自动解析)
$ kubectl exec -it some-other-pod — curl http://demo-app-svc/api/hello
# Hello from Spring Boot in Docker! 🐳✨
# 或用 ClusterIP 直接访问
$ kubectl exec -it some-other-pod — curl http://10.96.123.45/api/hello
K8s 如何实现?答案是 iptables + kube-proxy(或新版 ipvs 模式)。kube-proxy 监听 Service 变更,为每个 Service 在 Node 上生成 iptables 规则,将发往 10.96.123.45:80 的流量,随机或轮询转发到后端某个 Pod 的 10.244.x.x:8080。
下图展示了 Service 的流量转发模型:
#mermaid-svg-xZ3acqn2iQ8tEO3T{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xZ3acqn2iQ8tEO3T .error-icon{fill:#552222;}#mermaid-svg-xZ3acqn2iQ8tEO3T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xZ3acqn2iQ8tEO3T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .marker.cross{stroke:#333333;}#mermaid-svg-xZ3acqn2iQ8tEO3T svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xZ3acqn2iQ8tEO3T p{margin:0;}#mermaid-svg-xZ3acqn2iQ8tEO3T .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .cluster-label text{fill:#333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .cluster-label span{color:#333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .cluster-label span p{background-color:transparent;}#mermaid-svg-xZ3acqn2iQ8tEO3T .label text,#mermaid-svg-xZ3acqn2iQ8tEO3T span{fill:#333;color:#333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .node rect,#mermaid-svg-xZ3acqn2iQ8tEO3T .node circle,#mermaid-svg-xZ3acqn2iQ8tEO3T .node ellipse,#mermaid-svg-xZ3acqn2iQ8tEO3T .node polygon,#mermaid-svg-xZ3acqn2iQ8tEO3T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xZ3acqn2iQ8tEO3T .rough-node .label text,#mermaid-svg-xZ3acqn2iQ8tEO3T .node .label text,#mermaid-svg-xZ3acqn2iQ8tEO3T .image-shape .label,#mermaid-svg-xZ3acqn2iQ8tEO3T .icon-shape .label{text-anchor:middle;}#mermaid-svg-xZ3acqn2iQ8tEO3T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xZ3acqn2iQ8tEO3T .rough-node .label,#mermaid-svg-xZ3acqn2iQ8tEO3T .node .label,#mermaid-svg-xZ3acqn2iQ8tEO3T .image-shape .label,#mermaid-svg-xZ3acqn2iQ8tEO3T .icon-shape .label{text-align:center;}#mermaid-svg-xZ3acqn2iQ8tEO3T .node.clickable{cursor:pointer;}#mermaid-svg-xZ3acqn2iQ8tEO3T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .arrowheadPath{fill:#333333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xZ3acqn2iQ8tEO3T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xZ3acqn2iQ8tEO3T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xZ3acqn2iQ8tEO3T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xZ3acqn2iQ8tEO3T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xZ3acqn2iQ8tEO3T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xZ3acqn2iQ8tEO3T .cluster text{fill:#333;}#mermaid-svg-xZ3acqn2iQ8tEO3T .cluster span{color:#333;}#mermaid-svg-xZ3acqn2iQ8tEO3T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xZ3acqn2iQ8tEO3T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xZ3acqn2iQ8tEO3T rect.text{fill:none;stroke-width:0;}#mermaid-svg-xZ3acqn2iQ8tEO3T .icon-shape,#mermaid-svg-xZ3acqn2iQ8tEO3T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xZ3acqn2iQ8tEO3T .icon-shape p,#mermaid-svg-xZ3acqn2iQ8tEO3T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xZ3acqn2iQ8tEO3T .icon-shape .label rect,#mermaid-svg-xZ3acqn2iQ8tEO3T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xZ3acqn2iQ8tEO3T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xZ3acqn2iQ8tEO3T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xZ3acqn2iQ8tEO3T :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Kubernetes Cluster
Node 2
Node 1
iptables rules
iptables rules
iptables rules
curl http://demo-app-svc
Pod: demo-app-310.244.1.15:8080
Service: demo-app-svc10.96.123.45:80
Pod: demo-app-110.244.1.12:8080
Pod: demo-app-210.244.2.8:8080
Client Podin same cluster
💡 进阶提示:ClusterIP 仅限集群内。若需对外暴露,可选:
- NodePort:在每个 Node 上开一个固定端口(如 30080),通过 http://node-ip:30080 访问;
- LoadBalancer:云厂商自动创建 SLB(如 AWS ALB、阿里云 SLB),绑定弹性公网 IP;
- Ingress:七层 HTTP/HTTPS 路由(下节详解)。
六、入口:Ingress —— 统一七层流量网关 🌐➡️📦
Service 解决了集群内部通信,但生产环境需要:
- 用 https://api.mycompany.com 访问 Java 应用,而非 http://10.96.123.45;
- 同一域名下,/api/* 路由到 demo-app-svc,/admin/* 路由到 admin-svc;
- 自动 HTTPS(Let’s Encrypt 证书);
- WAF、限流、灰度发布等高级能力。
这就轮到 Ingress 出场了。它是一个K8s 内置的 API 对象,定义集群外部 HTTP/HTTPS 流量如何路由到内部 Service。但注意:Ingress 本身不处理流量,它需要一个 Ingress Controller(如 Nginx、Traefik、ALB Controller)来实现。
✅ 示例:用 Nginx Ingress Controller 暴露 demo-app
首先,确保集群已安装 Nginx Ingress Controller(官方 Helm Chart 是最常用方式)。
然后,定义 Ingress 资源:
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo–app–ingress
annotations:
# 启用 HTTPS 重定向
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# 启用 Let's Encrypt 自动签发(需配合 cert-manager)
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
# TLS 配置(若已有证书)
tls:
– hosts:
– api.mycompany.com
secretName: demo–app–tls–secret # 引用已创建的 TLS Secret
# 路由规则
rules:
– host: api.mycompany.com
http:
paths:
– path: /api/
pathType: Prefix
backend:
service:
name: demo–app–svc # 目标 Service
port:
number: 80 # Service 端口
– path: /actuator/
pathType: Prefix
backend:
service:
name: demo–app–svc
port:
number: 80
创建 TLS Secret(假设有 tls.crt 和 tls.key):
$ kubectl create secret tls demo-app-tls-secret \\
–cert=tls.crt \\
–key=tls.key
部署后,外部用户即可通过 https://api.mycompany.com/api/hello 访问你的 Spring Boot 应用 🎉。
🔗 补充资源:
- Nginx Ingress Controller 官方文档(权威、实时更新);
- Cert-Manager 官网(自动化证书管理,与 Let’s Encrypt 无缝集成)。
七、协同逻辑全景图:从 Docker 到 K8s 的分层抽象 🧩
至此,我们已走过完整路径。现在,让我们站在高处,凝视这张贯穿全文的协同逻辑图——它不是技术栈罗列,而是职责边界与协作契约的可视化:
#mermaid-svg-rb03aADnAXxuHZvc{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rb03aADnAXxuHZvc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rb03aADnAXxuHZvc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rb03aADnAXxuHZvc .error-icon{fill:#552222;}#mermaid-svg-rb03aADnAXxuHZvc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rb03aADnAXxuHZvc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rb03aADnAXxuHZvc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rb03aADnAXxuHZvc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rb03aADnAXxuHZvc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rb03aADnAXxuHZvc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rb03aADnAXxuHZvc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rb03aADnAXxuHZvc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rb03aADnAXxuHZvc .marker.cross{stroke:#333333;}#mermaid-svg-rb03aADnAXxuHZvc svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rb03aADnAXxuHZvc p{margin:0;}#mermaid-svg-rb03aADnAXxuHZvc .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-rb03aADnAXxuHZvc .cluster-label text{fill:#333;}#mermaid-svg-rb03aADnAXxuHZvc .cluster-label span{color:#333;}#mermaid-svg-rb03aADnAXxuHZvc .cluster-label span p{background-color:transparent;}#mermaid-svg-rb03aADnAXxuHZvc .label text,#mermaid-svg-rb03aADnAXxuHZvc span{fill:#333;color:#333;}#mermaid-svg-rb03aADnAXxuHZvc .node rect,#mermaid-svg-rb03aADnAXxuHZvc .node circle,#mermaid-svg-rb03aADnAXxuHZvc .node ellipse,#mermaid-svg-rb03aADnAXxuHZvc .node polygon,#mermaid-svg-rb03aADnAXxuHZvc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rb03aADnAXxuHZvc .rough-node .label text,#mermaid-svg-rb03aADnAXxuHZvc .node .label text,#mermaid-svg-rb03aADnAXxuHZvc .image-shape .label,#mermaid-svg-rb03aADnAXxuHZvc .icon-shape .label{text-anchor:middle;}#mermaid-svg-rb03aADnAXxuHZvc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rb03aADnAXxuHZvc .rough-node .label,#mermaid-svg-rb03aADnAXxuHZvc .node .label,#mermaid-svg-rb03aADnAXxuHZvc .image-shape .label,#mermaid-svg-rb03aADnAXxuHZvc .icon-shape .label{text-align:center;}#mermaid-svg-rb03aADnAXxuHZvc .node.clickable{cursor:pointer;}#mermaid-svg-rb03aADnAXxuHZvc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rb03aADnAXxuHZvc .arrowheadPath{fill:#333333;}#mermaid-svg-rb03aADnAXxuHZvc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rb03aADnAXxuHZvc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rb03aADnAXxuHZvc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rb03aADnAXxuHZvc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rb03aADnAXxuHZvc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rb03aADnAXxuHZvc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rb03aADnAXxuHZvc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rb03aADnAXxuHZvc .cluster text{fill:#333;}#mermaid-svg-rb03aADnAXxuHZvc .cluster span{color:#333;}#mermaid-svg-rb03aADnAXxuHZvc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rb03aADnAXxuHZvc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rb03aADnAXxuHZvc rect.text{fill:none;stroke-width:0;}#mermaid-svg-rb03aADnAXxuHZvc .icon-shape,#mermaid-svg-rb03aADnAXxuHZvc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rb03aADnAXxuHZvc .icon-shape p,#mermaid-svg-rb03aADnAXxuHZvc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rb03aADnAXxuHZvc .icon-shape .label rect,#mermaid-svg-rb03aADnAXxuHZvc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rb03aADnAXxuHZvc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rb03aADnAXxuHZvc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rb03aADnAXxuHZvc :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Builds
Pulled by
Runs
Managed by
Exposes
Routes via
Serves
Dockerfile
Container Imagedemo-app:1.0✅ Immutable✅ Reproducible
Container Runtimecontainerd / CRI-O✅ Isolation✅ Resource Limits
Pod✅ Shared Network✅ Shared Storage✅ Atomic Scheduling
Deployment✅ Desired State✅ Rolling Update✅ Auto-healing
Service✅ Stable VIP✅ DNS Name✅ Load Balancing
Ingress✅ Host/Path Routing✅ TLS Termination✅ WAF Integration
External Clientshttps://api.mycompany.com
每一层都向上提供稳定抽象(Abstraction),向下封装实现细节(Implementation):
- 你写 Dockerfile,无需关心 containerd 如何解压 layer;
- 你写 Deployment,无需关心 kube-scheduler 如何打分选 Node;
- 你写 Ingress,无需关心 nginx-ingress-controller 如何生成 nginx.conf。
这种分层,正是云原生(Cloud Native)的核心精神:通过标准化接口解耦,让每个组件专注做好一件事,再通过声明式契约协同。
八、Java 工程师的 K8s 实战 Checklist ✅
最后,给正在落地 K8s 的 Java 团队一份精炼 checklist,覆盖开发、测试、运维全链路:
| 构建 | ✅ 多阶段 Dockerfile | 用 openjdk:17-jre-slim 基础镜像;禁用 jvm 的 -Xmx,改用 -XX:MaxRAMPercentage | docker build -t app:v1 . |
| 配置 | ✅ 外置化配置 | application-k8s.yml;敏感信息用 Secret,非敏感用 ConfigMap | kubectl create configmap app-config –from-file=application-k8s.yml |
| 健康 | ✅ 实现 Actuator 端点 | spring-boot-starter-actuator;暴露 /actuator/health/readiness 和 /actuator/health/liveness | management.endpoint.health.show-details=when_authorized |
| 日志 | ✅ 标准输出(stdout) | 禁止写文件日志(如 logback-spring.xml 写 ./logs/app.log);全部输出到 System.out | logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} – %msg%n |
| 监控 | ✅ 暴露 Prometheus Metrics | micrometer-registry-prometheus;确保 /actuator/prometheus 可访问 | curl http://pod-ip:8080/actuator/prometheus |
| 调试 | ✅ 进入容器诊断 | kubectl exec -it <pod-name> — /bin/sh;检查环境变量、网络连通性 | env | grep SPRING;nslookup postgres-svc |
| 升级 | ✅ 金丝雀发布 | 用 Deployment 的 maxSurge/maxUnavailable 控制节奏;结合 Ingress 的 canary annotation(需 Nginx Ingress v1.0+) | kubectl set image deploy/app app=app:v1.1 –record |
📌 特别提醒:永远不要在容器内运行 sshd 或 systemd。K8s 的哲学是“一个容器一个进程”,SSH 会破坏不可变性,systemd 会抢占 PID 1 导致信号转发异常。调试请用 kubectl exec,长时任务请用 Job。
九、结语:容器技术的终局不是取代,而是升维 🌈
从 Docker run 的第一行命令,到 kubectl apply -f 的声明式交付;从手动维护 3 台 VM 的 Tomcat,到全自动滚动更新 50 个微服务实例——技术演进的本质,从来不是炫技,而是把工程师从重复劳动中解放,去解决真正有创造性的难题。
Kubernetes 并未否定 Docker,而是将其内核(Namespaces/Cgroups)提炼为通用原语,再以声明式 API 重构协作契约。它让 Java 工程师得以:
- 用 @Value("${database.url}") 注入配置,而无需关心 ConfigMap 如何挂载;
- 用 @Scheduled(fixedDelay = 5000) 写定时任务,而无需担心 CronJob 如何容错;
- 用 RestTemplate 调用 http://user-svc:8080/api/users,而无需硬编码 IP 或写服务发现 SDK。
这,就是云原生的温柔力量:它不强迫你改变编码习惯,却悄然重塑了交付、运维与协作的底层逻辑。
所以,不必畏惧 YAML 的繁琐,不必纠结于 kubectl 命令的记忆。当你再次敲下 mvn clean package,请记得:那个即将被 Dockerfile 封装的 JAR,正站在通往 Kubernetes 世界的渡口。而你,已是那艘船的掌舵人 🚢。
🌟 延伸思考:
- 如果你的 Spring Cloud Alibaba 应用已使用 Nacos 做服务发现,是否还需要 K8s Service?答案是:需要且必须。K8s Service 提供 DNS 和集群内负载均衡,Nacos 提供跨集群/多数据中心的服务治理,二者是互补而非替代。
- 当 Istio Service Mesh 加入,Sidecar(Envoy)会接管所有流量,此时 Service 是否多余?不,Service 仍是 Istio 的基础依赖,Istio 的 VirtualService 和 DestinationRule 正是构建在 Service 之上的增强层。
技术没有银弹,只有恰如其分的组合。而理解 Docker 与 K8s 的协同逻辑,正是你做出正确组合决策的罗盘 🧭。
本文所有代码、YAML、命令均基于 Kubernetes 1.28+ 与 Spring Boot 3.2.x 实测验证,遵循 CNCF 最佳实践。愿你在云原生的海洋中,既握紧 Docker 的桨,也扬起 K8s 的帆。 🌊⛵
🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨


