引言

你是否经历过这样的「部署噩梦」?本地跑的好好的Spring Boot服务,一上测试环境就报「ClassNotFoundException」;为了扩一个实例,运维小哥在三台服务器上手动拷贝JAR包,结果端口冲突到凌晨;更离谱的是,生产环境突然崩溃,查了半天才发现是服务器A装了JDK8,服务器B还在用JDK11……

传统微服务部署就像「手工作坊」,依赖人工配置、环境碎片化、扩缩容靠「人肉」,效率低到令人发指。而Docker+K8s的组合,就像给部署流程装了「智能流水线」——Docker解决环境一致性,K8s搞定自动化运维。今天我们就从0到1,用一个Spring Boot项目实战,带你体验从「地狱模式」到「丝滑部署」的蜕变。


一、先聊痛点:传统微服务部署到底「坑」在哪?

在开始技术实操前,我们先「扎心」一下——如果你还在用传统方式部署微服务,大概率踩过这些坑:

1. 环境「薛定谔的一致性」

开发:「我本地用Redis 6.2,绝对没问题!」
测试:「我们环境装的是Redis 5.0,报连接超时!」
生产:「服务器上的Nginx版本是1.16,配置文件格式和开发给的不一样……」
环境不一致就像「盲盒」,你永远不知道部署时会开出什么错误。

2. 扩缩容全靠「人肉复制粘贴」

业务高峰要扩3个实例?运维小哥得:
① 登录3台新服务器;
② 安装JDK、Maven;
③ 拷贝JAR包;
④ 配置端口(避免冲突);
⑤ 启动服务;
⑥ 改Nginx负载均衡配置。
一套操作下来,半小时没了,业务高峰都过了。

3. 故障恢复靠「玄学」

某实例突然挂了?传统方式只能:
① 收到监控报警;
② 人工登录服务器查日志;
③ 重启服务(可能再次崩溃);
④ 如果反复崩溃,得重新排查依赖问题。
整个过程像「破案」,效率低且容易二次故障。

4. 资源利用率「惨不忍睹」

有的服务器CPU跑满,有的却闲置30%;
JVM内存参数全靠经验值,内存溢出问题频发;
服务之间端口打架,不得不浪费IP资源开新服务器。

总结:传统部署的核心问题是「人工操作多、环境不可控、运维效率低」。而Docker+K8s的组合,正是为了解决这些问题而生。


二、Docker容器化:给微服务套上「标准化外壳」

Docker的核心是「容器化」——把应用、依赖、配置打包成一个「可移植的标准化单元」,实现「一次构建,到处运行」。我们以一个简单的Spring Boot项目(user-service)为例,演示Docker化全流程。

2.1 准备工作:你的第一个Spring Boot微服务

先写一个最基础的Spring Boot服务,提供/user/{id}接口返回用户信息。项目结构如下:

user-service/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── UserServiceApplication.java
│   │   │           └── controller/
│   │   │               └── UserController.java
│   └── resources/
│       └── application.yml
├── mvnw
├── mvnw.cmd
└── pom.xml

UserController.java代码:

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        // 模拟数据库查询
        User user = new User(id, "用户" + id, "user" + id + "@example.com");
        return ResponseEntity.ok(user);
    }
}

@Data
@AllArgsConstructor
class User {
    private Long id;
    private String name;
    private String email;
}

application.yml配置(注意端口设为8080):

server:
  port: 8080

2.2 编写Dockerfile:给应用「定制包装盒」

Dockerfile是构建镜像的「说明书」,我们需要告诉Docker:用什么基础镜像?怎么拷贝代码?怎么启动服务?

这里推荐「多阶段构建」(Multi-stage Build),可以显著减小镜像体积(比如从1.2GB降到200MB)。 新建Dockerfile(和pom.xml同级):

# 第一阶段:编译构建JAR包(使用Maven镜像)
FROM maven:3.8.6-openjdk-17 AS builder
WORKDIR /app  # 设置工作目录
COPY pom.xml .  # 拷贝Maven依赖文件
RUN mvn dependency:go-offline  # 预下载依赖(加速后续构建)
COPY src ./src  # 拷贝源代码
RUN mvn package -DskipTests  # 打包JAR(跳过测试)

# 第二阶段:运行环境(使用更小的OpenJDK镜像)
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=builder /app/target/user-service-0.0.1-SNAPSHOT.jar app.jar  # 从第一阶段拷贝构建好的JAR包
EXPOSE 8080  # 声明容器暴露8080端口(仅文档作用,实际映射需运行时指定)
ENTRYPOINT ["java", "-jar", "app.jar"]  # 容器启动时执行的命令

关键指令解析

  • FROM:指定基础镜像(第一阶段用Maven编译,第二阶段用轻量JDK运行)。
  • COPY --from=builder:多阶段构建核心,只保留最终运行需要的JAR包,丢弃编译工具。
  • EXPOSE:声明容器对外暴露的端口,方便阅读但不强制映射。
  • ENTRYPOINT:容器启动命令,这里直接运行JAR包。

2.3 构建镜像:把「说明书」变成「可运行的包裹」

Dockerfile所在目录执行构建命令:

docker build -t user-service:v1 .
  • -t:指定镜像名称和标签(user-service是名称,v1是版本)。
  • .:构建上下文路径(当前目录)。

构建过程解析

  1. Docker读取Dockerfile,按顺序执行指令。
  2. 第一阶段下载Maven镜像,拷贝代码,编译生成JAR包。
  3. 第二阶段下载轻量JDK镜像,仅拷贝JAR包,最终生成一个小体积镜像。

构建完成后,用docker images查看镜像:

$ docker images
REPOSITORY      TAG       IMAGE ID       CREATED          SIZE
user-service    v1        a1b2c3d4e5f6   5 minutes ago    212MB  # 比直接用Maven镜像小很多!

2.4 运行容器:让「包裹」真正「活起来」

镜像构建完成后,用docker run启动容器:

docker run -d \
  --name user-service-container \
  -p 8080:8080 \
  user-service:v1

参数解析

  • -d:后台运行(detached模式)。
  • --name:给容器起个名字,方便管理。
  • -p:端口映射(宿主机8080端口指向容器8080端口)。

验证服务是否启动:

curl http://localhost:8080/user/1
# 输出:{"id":1,"name":"用户1","email":"user1@example.com"}

2.5 容器管理:常用命令「工具箱」

  • 查看运行中的容器:docker ps
  • 查看容器日志:docker logs user-service-container
  • 进入容器内部:docker exec -it user-service-container bash(如果容器有bash)
  • 停止容器:docker stop user-service-container
  • 删除容器:docker rm user-service-container

小技巧:如果修改了代码,只需重新构建镜像(docker build -t user-service:v2 .),然后用新镜像启动容器即可,环境完全一致!


三、K8s登场:从「单机容器」到「集群化运维」

Docker解决了「单个应用的环境一致性」,但微服务通常是多实例、多服务的集群(比如user-service需要3个实例,order-service需要2个实例)。这时候就需要K8s(Kubernetes)来管理这些容器,实现自动化部署、扩缩容、故障恢复。

3.1 K8s核心组件:理解「集群大脑」的结构

K8s的核心组件就像一个「智能工厂」的各个部门,分工明确又协同工作。我们重点关注3个核心组件:Pod、Deployment、Service

3.1.1 Pod:最小的「运行单元」

Pod是K8s中最小的可部署单元,一个Pod可以包含1个或多个紧密关联的容器(比如应用容器+日志收集容器)。这些容器共享网络和存储,就像「合租的室友」——共用一个客厅(网络命名空间),但各自有卧室(容器独立文件系统)。

关键特点

  • Pod是短暂的(可能被删除后重新创建)。
  • Pod的IP是集群内部的(节点重启后IP会变)。
3.1.2 Deployment:Pod的「智能管家」

Deployment是K8s中管理Pod的「控制器」,它负责:

  • 确保当前运行的Pod数量等于指定的副本数(replicas)。
  • 支持滚动更新(Rolling Update)和回滚(Rollback)。
  • 监控Pod状态,自动替换故障Pod。

可以把Deployment理解为「Pod的老板」——它不直接干活(Pod干活),但负责管理和调度。

3.1.3 Service:Pod的「流量入口」

Pod的IP会动态变化(比如重启后),直接通过IP访问不可靠。Service的作用是为一组Pod提供「稳定的访问入口」,支持负载均衡。

Service类型

  • ClusterIP(默认):集群内部访问(仅集群内可见)。
  • NodePort:通过节点IP+端口暴露(外部可访问)。
  • LoadBalancer:结合云厂商负载均衡器(生产环境常用)。

总结:Pod是「打工人」,Deployment是「包工头」(管理打工人数量),Service是「前台接待」(对外提供稳定访问地址)。


四、K8s部署实战:从YAML配置到服务暴露

K8s的一切操作都基于YAML配置文件(声明式API)。我们以部署user-service为例,演示完整流程。

4.1 准备K8s环境

本地测试可以用minikubekind搭建单节点集群。这里假设你已搭建好K8s集群(Master节点IP:192.168.1.100)。

4.2 编写Deployment YAML:定义「Pod生产规则」

新建user-service-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service-deployment
  labels:
    app: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service-container
        image: user-service:v1
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "100m"
            memory: "256Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

关键配置解析

  • replicas:指定Pod副本数,K8s会自动维持这个数量(比如某Pod挂了,会新建一个)。
  • strategy:滚动更新策略,maxSurgemaxUnavailable控制更新时的实例数量波动。
  • resources:资源请求和限制,确保容器不会占用过多资源(避免节点崩溃)。
  • livenessProbereadinessProbe:存活探针和就绪探针,K8s会根据探针结果重启容器(存活探针失败)或停止分发流量(就绪探针失败)。

注意:Spring Boot需要引入actuator依赖以暴露/actuator/health接口。在pom.xml中添加:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

4.3 编写Service YAML:定义「流量入口」

新建user-service-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  type: NodePort
  selector:
    app: user-service
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30080

关键配置解析

  • type: NodePort:将Service暴露到集群节点的固定端口(范围30000-32767)。
  • selector:通过标签选择要代理的Pod(必须和Deployment中Pod的标签一致)。
  • port:Service在集群内部的访问端口(比如集群内其他服务可以通过http://user-service:80访问)。
  • nodePort:节点上的端口,外部用户通过http://节点IP:30080访问。

4.4 部署到K8s集群

4.4.1 应用YAML配置
# 先部署Deployment(创建Pod)
kubectl apply -f user-service-deployment.yaml
# 再部署Service(暴露Pod)
kubectl apply -f user-service-service.yaml
4.4.2 验证部署状态
  • 查看Deployment状态:

    kubectl get deployments
    # 输出:
    # NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
    # user-service-deployment  3/3     3            3           2m
    

    READY 3/3表示3个副本都已就绪。

  • 查看Pod状态:

    kubectl get pods -l app=user-service  # -l 按标签过滤
    # 输出:
    # NAME                                      READY   STATUS    RESTARTS   AGE
    # user-service-deployment-5f78d994-4q8k2   1/1     Running   0          2m
    # user-service-deployment-5f78d994-6z9v5   1/1     Running   0          2m
    # user-service-deployment-5f78d994-8w7j3   1/1     Running   0          2m
    

    所有Pod状态为Running,表示正常运行。

  • 验证服务访问:

    curl http://192.168.1.100:30080/user/1
    # 输出:{"id":1,"name":"用户1","email":"user1@example.com"}
    

4.5 弹性扩缩容:应对流量高峰

业务高峰时,只需修改Deployment的replicas值即可自动扩缩容:

# 扩容到5个实例
kubectl scale deployment user-service-deployment --replicas=5
# 验证扩容结果(等待1分钟后)
kubectl get pods -l app=user-service
# 输出5个Running状态的Pod

# 缩容到2个实例
kubectl scale deployment user-service-deployment --replicas=2

4.6 滚动更新与回滚:安全发布新版本

当需要更新服务(比如发布v2版本),只需修改Deployment的镜像标签并应用:

# 修改user-service-deployment.yaml中的image为user-service:v2
kubectl apply -f user-service-deployment.yaml

K8s会自动执行滚动更新:

  1. 创建1个新Pod(maxSurge=25%,原3个实例,最多新增1个)。
  2. 新Pod通过就绪探针后,旧Pod逐步被替换。
  3. 最终所有实例升级为v2版本,全程无服务中断。

如果发现新版本有问题,可回滚到上一版本:

kubectl rollout undo deployment/user-service-deployment

五、优势与挑战:Docker+K8s的「双面性」

5.1 优势:为什么说它是「微服务部署神器」?

  • 环境一致性:Docker镜像打包了应用、依赖、配置,彻底解决「本地能跑,线上不行」的问题。
  • 自动化运维:K8s自动管理Pod生命周期,故障自动恢复,扩缩容一键完成,运维效率提升10倍。
  • 资源高效利用:通过资源限制和调度算法,K8s能将服务器资源利用率从30%提升到70%以上。
  • 高可用性:多实例部署+负载均衡+健康检查,确保服务99.99%可用(生产环境可配置更严格的SLA)。

5.2 挑战:这些坑你需要提前知道

  • 镜像管理复杂度:镜像版本易混乱(比如v1v1.1latest),需要配套镜像仓库(如Harbor)和版本规范。
  • 学习曲线陡峭:K8s的概念(如Service、Ingress、ConfigMap)和YAML配置对新手不友好,需要系统学习。
  • 资源开销大:K8s集群本身需要至少3台服务器(Master+2个Node),小项目可能「大材小用」。
  • 网络复杂性:K8s的网络模型(如Flannel、Calico)需要额外配置,跨节点通信可能出现延迟问题。

六、总结:微服务部署的「未来已来」

从传统的「手工作坊」到Docker+K8s的「智能工厂」,微服务部署的进化本质是「从人工到自动化、从不可控到可观测」的转变。Docker解决了「环境一致性」的根基问题,K8s则提供了「自动化运维」的强大引擎。

当然,技术没有「银弹」——小项目可能用Docker单机部署就够了,大项目才需要K8s集群。但无论如何,掌握Docker+K8s已经成为Java开发者的「必备技能」。

下一次部署微服务时,不妨试试这套组合拳——你会发现,原来「丝滑部署」真的可以很简单!

Logo

全面兼容主流 AI 模型,支持本地及云端双模式

更多推荐