云原生改造实战:Spring Boot 应用的 Kubernetes 迁移全指南

发布于:2025-09-15 ⋅ 阅读:(20) ⋅ 点赞:(0)

引言:从传统部署到云原生的必然之路

在数字化转型加速的今天,企业应用架构正经历着从传统单体到云原生的深刻变革。根据 CNCF(Cloud Native Computing Foundation)2024 年调查报告,全球已有超过 96% 的组织正在使用或评估 Kubernetes,云原生技术已成为企业级应用部署的标准选择。

Spring Boot 作为 Java 生态中最流行的应用开发框架,其应用的云原生改造成为许多企业的迫切需求。将 Spring Boot 应用迁移到 Kubernetes(简称 K8s)不仅能实现弹性伸缩、自愈能力等云原生特性,还能显著提升资源利用率、简化运维流程、加速迭代速度。

本文将带你走完 Spring Boot 应用云原生改造的完整旅程,从架构分析到最终部署,涵盖容器化、配置管理、服务发现、监控告警等核心环节,通过可落地的实战案例,让你真正掌握 Java 应用的 K8s 迁移技术。

一、云原生基础与改造前准备

1.1 云原生核心概念解析

云原生应用架构具有以下核心特征:

  • 容器化:应用及其依赖被打包在容器中,保证环境一致性
  • 弹性伸缩:根据负载自动调整实例数量
  • 自愈能力:自动检测并替换故障实例
  • 基础设施即代码:环境配置通过代码管理
  • 微服务架构:应用拆分为小型、自治的服务
  • 持续交付:自动化构建、测试和部署流程

1.2 改造前的评估与准备

在开始迁移前,需要对现有 Spring Boot 应用进行全面评估:

  1. 应用架构评估

    • 是否存在状态(如本地缓存、文件存储)
    • 外部依赖情况(数据库、消息队列等)
    • 会话管理方式(是否使用本地会话)
  2. 技术栈兼容性

    • Spring Boot 版本(建议 2.7.x 以上或 3.x)
    • JDK 版本(推荐 JDK 17,支持容器感知)
    • 第三方库兼容性
  3. 团队技能准备

    • Kubernetes 基础概念与操作
    • 容器化技术(Docker)
    • 云原生监控方案
  4. 环境准备

    • Kubernetes 集群(最小化推荐 3 节点)
    • 容器仓库(Docker Hub 或私有仓库)
    • CI/CD 流水线工具(Jenkins/GitLab CI)

1.3 环境搭建

1.3.1 安装 Docker
# 安装Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# 启动Docker服务
sudo systemctl start docker
sudo systemctl enable docker

# 将当前用户添加到docker组
sudo usermod -aG docker $USER
1.3.2 搭建 Kubernetes 集群

使用 k3d 快速搭建本地 K8s 集群:

# 安装k3d
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash

# 创建包含3个节点的集群
k3d cluster create springboot-cluster \
  --agents 2 \
  --port "8080:80@loadbalancer" \
  --port "8443:443@loadbalancer"

# 验证集群状态
kubectl cluster-info
kubectl get nodes
1.3.3 安装必要工具
# 安装kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/

# 安装Helm
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

二、Spring Boot 应用容器化改造

2.1 容器化改造原则

将 Spring Boot 应用容器化时,应遵循以下原则:

  1. 单一职责:一个容器只运行一个应用进程
  2. 无状态设计:应用不应依赖本地存储的状态
  3. 非 root 用户运行:增强容器安全性
  4. 优雅启动与关闭:实现健康检查和优雅退出
  5. 日志处理:日志输出到标准输出流
  6. 适当的基础镜像:平衡安全性和镜像大小

2.2 准备示例应用

我们将以一个简单的用户管理系统为例,展示完整的改造过程。

2.2.1 项目结构

plaintext

springboot-k8s-demo/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── ken/
│   │   │           └── demo/
│   │   │               ├── controller/
│   │   │               ├── entity/
│   │   │               ├── mapper/
│   │   │               ├── service/
│   │   │               └── SpringbootK8sDemoApplication.java
│   │   └── resources/
│   │       ├── application.yml
│   │       └── mapper/
│   └── test/
├── pom.xml
└── Dockerfile
2.2.2 Maven 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.6</version>
        <relativePath/>
    </parent>
    
    <groupId>com.ken.demo</groupId>
    <artifactId>springboot-k8s-demo</artifactId>
    <version>1.0.0</version>
    <name>Spring Boot K8s Demo</name>
    <description>Spring Boot应用云原生改造示例</description>
    
    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <mybatis-plus.version>3.5.6</mybatis-plus.version>
        <lombok.version>1.18.30</lombok.version>
        <fastjson2.version>2.0.47</fastjson2.version>
        <knife4j.version>4.4.0</knife4j.version>
        <mysql.version>8.0.36</mysql.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <!-- Fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        
        <!-- Knife4j (Swagger3) -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Spring Boot Maven插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                    <!-- 构建分层镜像支持 -->
                    <layers>
                        <enabled>true</enabled>
                    </layers>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            
            <!-- 编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
2.2.3 核心代码实现

实体类:

package com.ken.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 用户实体类
 */
@Data
@TableName("user")
@Schema(description = "用户实体")
public class User {
    
    @TableId(type = IdType.AUTO)
    @Schema(description = "用户ID", example = "1")
    private Long id;
    
    @Schema(description = "用户名", example = "zhangsan")
    private String username;
    
    @Schema(description = "密码", example = "123456")
    private String password;
    
    @Schema(description = "姓名", example = "张三")
    private String name;
    
    @Schema(description = "邮箱", example = "zhangsan@example.com")
    private String email;
    
    @Schema(description = "手机号", example = "13800138000")
    private String phone;
    
    @Schema(description = "状态:0-禁用,1-正常", example = "1")
    private Integer status;
    
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    
    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
}

Mapper 接口:

package com.ken.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * 用户Mapper接口
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

Service 接口:

package com.ken.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.ken.demo.entity.User;
import com.ken.demo.result.Result;

/**
 * 用户服务接口
 */
public interface UserService extends IService<User> {
    
    /**
     * 根据ID查询用户
     *
     * @param id 用户ID
     * @return 用户信息
     */
    Result<User> getUserById(Long id);
    
    /**
     * 创建用户
     *
     * @param user 用户信息
     * @return 创建结果
     */
    Result<Long> createUser(User user);
    
    /**
     * 更新用户
     *
     * @param user 用户信息
     * @return 更新结果
     */
    Result<Boolean> updateUser(User user);
    
    /**
     * 删除用户
     *
     * @param id 用户ID
     * @return 删除结果
     */
    Result<Boolean> deleteUser(Long id);
}

Service 实现类:

package com.ken.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ken.demo.entity.User;
import com.ken.demo.mapper.UserMapper;
import com.ken.demo.result.Result;
import com.ken.demo.result.ResultCode;
import com.ken.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * 用户服务实现类
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    private final UserMapper userMapper;
    
    @Override
    public Result<User> getUserById(Long id) {
        log.info("查询用户信息,用户ID:{}", id);
        
        if (id == null || id <= 0) {
            log.warn("查询用户信息失败,用户ID不合法:{}", id);
            return Result.fail(ResultCode.PARAM_ERROR, "用户ID不合法");
        }
        
        User user = userMapper.selectById(id);
        if (user == null) {
            log.warn("查询用户信息失败,用户不存在,用户ID:{}", id);
            return Result.fail(ResultCode.RESOURCE_NOT_FOUND, "用户不存在");
        }
        
        log.info("查询用户信息成功,用户ID:{}", id);
        return Result.success(user);
    }
    
    @Override
    public Result<Long> createUser(User user) {
        log.info("创建用户,用户信息:{}", user);
        
        if (user == null) {
            log.warn("创建用户失败,用户信息为空");
            return Result.fail(ResultCode.PARAM_ERROR, "用户信息不能为空");
        }
        
        if (!StringUtils.hasText(user.getUsername())) {
            log.warn("创建用户失败,用户名为空");
            return Result.fail(ResultCode.PARAM_ERROR, "用户名不能为空");
        }
        
        // 检查用户名是否已存在
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, user.getUsername());
        long count = userMapper.selectCount(queryWrapper);
        if (count > 0) {
            log.warn("创建用户失败,用户名已存在:{}", user.getUsername());
            return Result.fail(ResultCode.PARAM_ERROR, "用户名已存在");
        }
        
        int rows = userMapper.insert(user);
        if (rows <= 0) {
            log.error("创建用户失败,数据库操作失败");
            return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, "创建用户失败");
        }
        
        log.info("创建用户成功,用户ID:{}", user.getId());
        return Result.success(user.getId());
    }
    
    @Override
    public Result<Boolean> updateUser(User user) {
        log.info("更新用户,用户信息:{}", user);
        
        if (user == null || user.getId() == null) {
            log.warn("更新用户失败,用户ID为空");
            return Result.fail(ResultCode.PARAM_ERROR, "用户ID不能为空");
        }
        
        // 检查用户是否存在
        User existingUser = userMapper.selectById(user.getId());
        if (existingUser == null) {
            log.warn("更新用户失败,用户不存在,用户ID:{}", user.getId());
            return Result.fail(ResultCode.RESOURCE_NOT_FOUND, "用户不存在");
        }
        
        int rows = userMapper.updateById(user);
        if (rows <= 0) {
            log.error("更新用户失败,数据库操作失败,用户ID:{}", user.getId());
            return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, "更新用户失败");
        }
        
        log.info("更新用户成功,用户ID:{}", user.getId());
        return Result.success(true);
    }
    
    @Override
    public Result<Boolean> deleteUser(Long id) {
        log.info("删除用户,用户ID:{}", id);
        
        if (id == null || id <= 0) {
            log.warn("删除用户失败,用户ID不合法:{}", id);
            return Result.fail(ResultCode.PARAM_ERROR, "用户ID不合法");
        }
        
        // 检查用户是否存在
        User user = userMapper.selectById(id);
        if (user == null) {
            log.warn("删除用户失败,用户不存在,用户ID:{}", id);
            return Result.fail(ResultCode.RESOURCE_NOT_FOUND, "用户不存在");
        }
        
        int rows = userMapper.deleteById(id);
        if (rows <= 0) {
            log.error("删除用户失败,数据库操作失败,用户ID:{}", id);
            return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, "删除用户失败");
        }
        
        log.info("删除用户成功,用户ID:{}", id);
        return Result.success(true);
    }
}

Controller:

package com.ken.demo.controller;

import com.ken.demo.entity.User;
import com.ken.demo.result.Result;
import com.ken.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * 用户控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户CRUD接口")
public class UserController {
    
    private final UserService userService;
    
    @Operation(summary = "根据ID查询用户", description = "通过用户ID获取用户详细信息")
    @GetMapping("/{id}")
    public Result<User> getUserById(
            @Parameter(description = "用户ID", required = true, example = "1")
            @PathVariable Long id) {
        return userService.getUserById(id);
    }
    
    @Operation(summary = "创建用户", description = "新增用户信息")
    @PostMapping
    public Result<Long> createUser(
            @Parameter(description = "用户信息", required = true)
            @RequestBody User user) {
        return userService.createUser(user);
    }
    
    @Operation(summary = "更新用户", description = "修改用户信息")
    @PutMapping
    public Result<Boolean> updateUser(
            @Parameter(description = "用户信息", required = true)
            @RequestBody User user) {
        return userService.updateUser(user);
    }
    
    @Operation(summary = "删除用户", description = "根据ID删除用户")
    @DeleteMapping("/{id}")
    public Result<Boolean> deleteUser(
            @Parameter(description = "用户ID", required = true, example = "1")
            @PathVariable Long id) {
        return userService.deleteUser(id);
    }
}

统一结果类:

package com.ken.demo.result;

import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.annotation.JSONField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 统一响应结果
 *
 * @param <T> 响应数据类型
 */
@Data
@Schema(description = "统一响应结果")
public class Result<T> {
    
    @Schema(description = "状态码", example = "200")
    private int code;
    
    @Schema(description = "消息", example = "操作成功")
    private String msg;
    
    @Schema(description = "响应数据")
    private T data;
    
    @JSONField(serialize = false)
    private boolean success;
    
    /**
     * 私有构造方法,防止直接实例化
     */
    private Result() {
    }
    
    /**
     * 成功响应
     *
     * @param <T> 数据类型
     * @return 成功响应结果
     */
    public static <T> Result<T> success() {
        return success(null);
    }
    
    /**
     * 成功响应
     *
     * @param data 响应数据
     * @param <T>  数据类型
     * @return 成功响应结果
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMsg(ResultCode.SUCCESS.getMsg());
        result.setData(data);
        result.setSuccess(true);
        return result;
    }
    
    /**
     * 失败响应
     *
     * @param code 错误码
     * @param msg  错误消息
     * @param <T>  数据类型
     * @return 失败响应结果
     */
    public static <T> Result<T> fail(int code, String msg) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(null);
        result.setSuccess(false);
        return result;
    }
    
    /**
     * 失败响应
     *
     * @param resultCode 错误码枚举
     * @param <T>        数据类型
     * @return 失败响应结果
     */
    public static <T> Result<T> fail(ResultCode resultCode) {
        return fail(resultCode.getCode(), resultCode.getMsg());
    }
    
    /**
     * 转为JSON字符串
     *
     * @return JSON字符串
     */
    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }
}

结果状态码:

package com.ken.demo.result;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 响应状态码枚举
 */
@Getter
@AllArgsConstructor
@Schema(description = "响应状态码枚举")
public enum ResultCode {
    
    /**
     * 成功
     */
    SUCCESS(200, "操作成功"),
    
    /**
     * 服务器内部错误
     */
    INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
    
    /**
     * 请求参数错误
     */
    PARAM_ERROR(400, "请求参数错误"),
    
    /**
     * 未授权
     */
    UNAUTHORIZED(401, "未授权"),
    
    /**
     * 资源不存在
     */
    RESOURCE_NOT_FOUND(404, "资源不存在");
    
    /**
     * 状态码
     */
    private final int code;
    
    /**
     * 状态描述
     */
    private final String msg;
}

启动类:

package com.ken.demo;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

/**
 * Spring Boot应用启动类
 */
@SpringBootApplication
@MapperScan("com.ken.demo.mapper")
@OpenAPIDefinition(
        info = @Info(
                title = "用户管理API",
                version = "1.0.0",
                description = "用户管理系统的API文档"
        )
)
public class SpringbootK8sDemoApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SpringbootK8sDemoApplication.class, args);
    }
}

2.3 编写 Dockerfile 实现容器化

为 Spring Boot 应用编写优化的 Dockerfile:

# 构建阶段
FROM maven:3.9.6-eclipse-temurin-17 AS builder

# 设置工作目录
WORKDIR /app

# 复制pom.xml和源代码
COPY pom.xml .
COPY src ./src

# 构建应用
RUN mvn clean package -DskipTests

# 运行阶段,使用轻量级JRE镜像
FROM eclipse-temurin:17-jre-alpine

# 添加非root用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# 设置工作目录
WORKDIR /app

# 从构建阶段复制jar包
COPY --from=builder /app/target/springboot-k8s-demo.jar app.jar

# 赋予执行权限
RUN chmod 644 app.jar && chown -R appuser:appgroup /app

# 切换到非root用户
USER appuser

# JVM参数优化,适配容器环境
ENV JAVA_OPTS="\
  -XX:+UseContainerSupport \
  -XX:MaxRAMPercentage=75.0 \
  -XX:+UseG1GC \
  -XX:+ExitOnOutOfMemoryError \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/app/logs/heapdump.hprof"

# 暴露端口
EXPOSE 8080

# 健康检查脚本
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -q --spider http://localhost:8080/actuator/health || exit 1

# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

这个 Dockerfile 具有以下特点:

  1. 多阶段构建:减小最终镜像大小
  2. 使用轻量级基础镜像:alpine 版本体积更小
  3. 非 root 用户运行:提高容器安全性
  4. JVM 容器感知:-XX:+UseContainerSupport 让 JVM 适应容器环境
  5. 资源限制优化:-XX:MaxRAMPercentage=75.0 设置 JVM 最大内存为容器内存的 75%
  6. 健康检查:集成健康检查命令
  7. OOM 处理:配置 OOM 时退出并生成堆转储

2.4 构建并测试容器镜像

# 构建镜像
docker build -t springboot-k8s-demo:1.0.0 .

# 查看构建的镜像
docker images | grep springboot-k8s-demo

# 本地运行容器测试
docker run -d \
  --name springboot-app \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=dev \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3306/user_db \
  -e SPRING_DATASOURCE_USERNAME=root \
  -e SPRING_DATASOURCE_PASSWORD=root \
  springboot-k8s-demo:1.0.0

# 查看容器日志
docker logs -f springboot-app

# 测试应用是否正常运行
curl http://localhost:8080/actuator/health

# 停止并删除测试容器
docker stop springboot-app
docker rm springboot-app

2.5 推送镜像到仓库

# 登录Docker Hub
docker login

# 为镜像打标签
docker tag springboot-k8s-demo:1.0.0 yourusername/springboot-k8s-demo:1.0.0

# 推送镜像
docker push yourusername/springboot-k8s-demo:1.0.0

# 如果使用私有仓库
# docker tag springboot-k8s-demo:1.0.0 your-registry-url/springboot-k8s-demo:1.0.0
# docker push your-registry-url/springboot-k8s-demo:1.0.0

三、Kubernetes 资源配置与部署

3.1 应用配置外部化

在 K8s 环境中,应用配置不应硬编码在镜像中,而应通过 ConfigMap 和 Secret 管理。

3.1.1 创建 ConfigMap
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: springboot-app-config
  namespace: default
data:
  # 应用名称
  application.name: "springboot-k8s-demo"
  # 日志级别
  logging.level.com.ken.demo: "info"
  # 服务器端口
  server.port: "8080"
  # 应用环境
  spring.profiles.active: "prod"
  # 数据库驱动
  spring.datasource.driver-class-name: "com.mysql.cj.jdbc.Driver"
  # MyBatis配置
  mybatis-plus.configuration.map-underscore-to-camel-case: "true"
  #  actuator配置
  management.endpoints.web.exposure.include: "health,info,prometheus,metrics"
  management.endpoint.health.show-details: "always"
3.1.2 创建 Secret
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: springboot-app-secret
  namespace: default
type: Opaque
data:
  # 数据库连接信息(需要Base64编码)
  spring.datasource.url: amRiYzpteXNxbDovL215c3FsOi84MDM2L3VzZXJfZGJ8dXNlZV9kYg==
  spring.datasource.username: cm9vdA==
  spring.datasource.password: cm9vdA==

注意:Secret 中的值需要进行 Base64 编码,可以使用以下命令生成:
echo -n "jdbc:mysql://mysql:3306/user_db" | base64

3.2 部署 MySQL 数据库

在 K8s 中部署 MySQL 作为应用的数据库:

# mysql-deployment.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0.36
        ports:
        - containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "root"
        - name: MYSQL_DATABASE
          value: "user_db"
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        readinessProbe:
          exec:
            command: ["mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
          initialDelaySeconds: 30
          periodSeconds: 10
        livenessProbe:
          exec:
            command: ["mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
          initialDelaySeconds: 60
          periodSeconds: 30
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  selector:
    app: mysql
  ports:
  - port: 3306
    targetPort: 3306
  clusterIP: None  # 无头服务,适合StatefulSet,但这里简化使用

创建数据库表结构:

-- 创建用户表
CREATE TABLE IF NOT EXISTS `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

可以通过临时 Pod 连接到 MySQL 执行上述 SQL:

# 创建临时MySQL客户端Pod
kubectl run -it --rm --image=mysql:8.0.36 mysql-client -- mysql -h mysql -u root -p

# 输入密码root后,执行上述SQL语句

3.3 部署 Spring Boot 应用

创建 Deployment 资源配置:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-app
  labels:
    app: springboot-app
spec:
  replicas: 3  # 3个副本,实现高可用
  selector:
    matchLabels:
      app: springboot-app
  strategy:
    rollingUpdate:
      maxSurge: 1        # 滚动更新时最大可超出期望副本数的数量
      maxUnavailable: 0  # 滚动更新时最大不可用的副本数
    type: RollingUpdate  # 滚动更新策略
  template:
    metadata:
      labels:
        app: springboot-app
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/path: "/actuator/prometheus"
        prometheus.io/port: "8080"
    spec:
      containers:
      - name: springboot-app
        image: yourusername/springboot-k8s-demo:1.0.0  # 替换为你的镜像地址
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
        env:
        # 从ConfigMap获取配置
        - name: SPRING_APPLICATION_NAME
          valueFrom:
            configMapKeyRef:
              name: springboot-app-config
              key: application.name
        - name: LOGGING_LEVEL_COM_KEN_DEMO
          valueFrom:
            configMapKeyRef:
              name: springboot-app-config
              key: logging.level.com.ken.demo
        - name: SERVER_PORT
          valueFrom:
            configMapKeyRef:
              name: springboot-app-config
              key: server.port
        - name: SPRING_PROFILES_ACTIVE
          valueFrom:
            configMapKeyRef:
              name: springboot-app-config
              key: spring.profiles.active
        - name: SPRING_DATASOURCE_DRIVER_CLASS_NAME
          valueFrom:
            configMapKeyRef:
              name: springboot-app-config
              key: spring.datasource.driver-class-name
        - name: MYBATIS_PLUS_CONFIGURATION_MAP_UNDERSCORE_TO_CAMEL_CASE
          valueFrom:
            configMapKeyRef:
              name: springboot-app-config
              key: mybatis-plus.configuration.map-underscore-to-camel-case
        # 从Secret获取敏感配置
        - name: SPRING_DATASOURCE_URL
          valueFrom:
            secretKeyRef:
              name: springboot-app-secret
              key: spring.datasource.url
        - name: SPRING_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: springboot-app-secret
              key: spring.datasource.username
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: springboot-app-secret
              key: spring.datasource.password
        # 健康检查
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
          timeoutSeconds: 3
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 5
          timeoutSeconds: 3
        # 优雅关闭
        lifecycle:
          preStop:
            exec:
              command: ["sh", "-c", "curl -X POST http://localhost:8080/actuator/shutdown"]

创建 Service 资源:

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: springboot-app-service
spec:
  selector:
    app: springboot-app
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP  # 集群内部访问

创建 Ingress 资源(需要集群已安装 Ingress 控制器):

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: springboot-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - host: springboot-app.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: springboot-app-service
            port:
              number: 80

3.4 执行部署

# 创建命名空间
kubectl create namespace springboot-app

# 部署配置
kubectl apply -f configmap.yaml -n springboot-app
kubectl apply -f secret.yaml -n springboot-app

# 部署MySQL
kubectl apply -f mysql-deployment.yaml -n springboot-app

# 部署应用
kubectl apply -f deployment.yaml -n springboot-app
kubectl apply -f service.yaml -n springboot-app
kubectl apply -f ingress.yaml -n springboot-app

# 查看部署状态
kubectl get pods -n springboot-app
kubectl get deployments -n springboot-app
kubectl get services -n springboot-app
kubectl get ingress -n springboot-app

# 查看应用日志
kubectl logs -f <pod-name> -n springboot-app

3.5 验证部署结果

# 查看服务访问地址
kubectl get ingress -n springboot-app

# 添加hosts解析(替换为实际的IP地址)
echo "127.0.0.1 springboot-app.local" | sudo tee -a /etc/hosts

# 测试应用健康状态
curl http://springboot-app.local/actuator/health

# 测试API接口
# 创建用户
curl -X POST http://springboot-app.local/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"123456","name":"测试用户","email":"test@example.com","phone":"13800138000"}'

# 查询用户
curl http://springboot-app.local/api/v1/users/1

四、云原生特性增强

4.1 健康检查与优雅关闭

Spring Boot Actuator 提供了健康检查和优雅关闭的支持,需要在 application.yml 中配置:

# application.yml
management:
  endpoint:
    health:
      probes:
        enabled: true  # 启用K8s探针支持
      group:
        liveness:
          include: livenessState
        readiness:
          include: readinessState
    shutdown:
      enabled: true  # 启用优雅关闭
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,shutdown
  health:
    livenessState:
      enabled: true
    readinessState:
      enabled: true

实现自定义健康检查指示器:

package com.ken.demo.health;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

/**
 * 自定义健康检查指示器
 */
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    private final DatabaseConnectionChecker connectionChecker;
    
    public DatabaseHealthIndicator(DatabaseConnectionChecker connectionChecker) {
        this.connectionChecker = connectionChecker;
    }
    
    @Override
    public Health health() {
        if (connectionChecker.isDatabaseConnected()) {
            return Health.up()
                    .withDetail("database", "MySQL")
                    .withDetail("status", "connected")
                    .build();
        } else {
            return Health.down()
                    .withDetail("database", "MySQL")
                    .withDetail("status", "disconnected")
                    .withDetail("error", "无法连接到数据库")
                    .build();
        }
    }
    
    /**
     * 数据库连接检查器
     */
    @Component
    public static class DatabaseConnectionChecker {
        
        private final javax.sql.DataSource dataSource;
        
        public DatabaseConnectionChecker(javax.sql.DataSource dataSource) {
            this.dataSource = dataSource;
        }
        
        /**
         * 检查数据库连接是否正常
         *
         * @return 连接正常返回true,否则返回false
         */
        public boolean isDatabaseConnected() {
            try (var connection = dataSource.getConnection()) {
                return connection.isValid(5);
            } catch (Exception e) {
                return false;
            }
        }
    }
}

4.2 配置动态刷新

使用 Spring Cloud Kubernetes 实现配置的动态刷新:

  1. 添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-config</artifactId>
    <version>3.1.3</version>
</dependency>
  1. 创建 bootstrap.yml 配置:
spring:
  application:
    name: springboot-k8s-demo
  cloud:
    kubernetes:
      config:
        name: springboot-app-config  # 对应ConfigMap的名称
        namespace: springboot-app
      secrets:
        name: springboot-app-secret  # 对应Secret的名称
        namespace: springboot-app
  1. 在需要动态刷新配置的类上添加 @RefreshScope 注解:
package com.ken.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

import lombok.Data;

/**
 * 应用配置类,支持动态刷新
 */
@Data
@Component
@ConfigurationProperties(prefix = "app")
@RefreshScope
public class AppConfig {
    
    /**
     * 缓存启用开关
     */
    private boolean cacheEnabled = false;
    
    /**
     * 缓存过期时间(秒)
     */
    private int cacheExpireSeconds = 300;
    
    /**
     * 最大并发数
     */
    private int maxConcurrentRequests = 100;
}
  1. 更新 ConfigMap 后触发配置刷新:
# 编辑ConfigMap
kubectl edit configmap springboot-app-config -n springboot-app

# 查看配置是否已更新
kubectl describe configmap springboot-app-config -n springboot-app

# 配置会自动刷新,无需重启应用

4.3 服务发现与注册

在 K8s 环境中,可以使用 Spring Cloud Kubernetes 实现服务发现:

  1. 添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-discovery</artifactId>
    <version>3.1.3</version>
</dependency>
  1. 启用服务发现:
@SpringBootApplication
@EnableDiscoveryClient  // 启用服务发现
public class SpringbootK8sDemoApplication {
    // ...
}
  1. 使用服务发现调用其他服务:
package com.ken.demo.service;

import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.ken.demo.result.Result;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 服务调用示例
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ServiceInvocationService {
    
    private final DiscoveryClient discoveryClient;
    private final RestTemplate restTemplate;
    
    /**
     * 调用其他服务
     *
     * @param serviceName 服务名称
     * @param path 接口路径
     * @return 服务返回结果
     */
    public Result<Object> invokeService(String serviceName, String path) {
        log.info("调用服务:{},路径:{}", serviceName, path);
        
        // 获取服务实例
        var instances = discoveryClient.getInstances(serviceName);
        if (org.springframework.util.CollectionUtils.isEmpty(instances)) {
            log.error("服务{}未找到", serviceName);
            return Result.fail(com.ken.demo.result.ResultCode.RESOURCE_NOT_FOUND, "服务未找到");
        }
        
        // 简单负载均衡:选择第一个实例
        var instance = instances.get(0);
        String url = String.format("http://%s:%d%s", 
                instance.getHost(), instance.getPort(), path);
        
        log.info("服务调用URL:{}", url);
        
        try {
            return restTemplate.getForObject(url, Result.class);
        } catch (Exception e) {
            log.error("调用服务{}失败", serviceName, e);
            return Result.fail(com.ken.demo.result.ResultCode.INTERNAL_SERVER_ERROR, "服务调用失败");
        }
    }
}
  1. 创建 RestTemplate Bean:
package com.ken.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * Web配置类
 */
@Configuration
public class WebConfig {
    
    /**
     * 创建RestTemplate实例
     *
     * @return RestTemplate对象
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

4.4 自动伸缩配置

配置 HPA(Horizontal Pod Autoscaler)实现基于 CPU 和内存使用率的自动伸缩:

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: springboot-app-hpa
  namespace: springboot-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: springboot-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70  # CPU使用率超过70%时扩容
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80  # 内存使用率超过80%时扩容
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60  # 扩容稳定窗口
      policies:
      - type: Percent
        value: 50  # 每次扩容50%
        periodSeconds: 60  # 至少60秒才能再次扩容
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容稳定窗口(5分钟)
      policies:
      - type: Percent
        value: 30  # 每次缩容30%
        periodSeconds: 120  # 至少120秒才能再次缩容

应用 HPA 配置:

kubectl apply -f hpa.yaml -n springboot-app

# 查看HPA状态
kubectl get hpa -n springboot-app

五、监控与日志

5.1 集成 Prometheus 和 Grafana

  1. 使用 Helm 安装 Prometheus 和 Grafana:
# 添加Helm仓库
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# 安装Prometheus和Grafana
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.service.type=NodePort \
  --set grafana.service.nodePort=30000
  1. 配置 Prometheus 监控 Spring Boot 应用:

Prometheus 会自动发现带有以下注解的 Pod:

prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "8080"
  1. 访问 Grafana 并导入 Spring Boot 监控面板:
# 获取Grafana管理员密码
kubectl get secret prometheus-grafana -n monitoring -o jsonpath="{.data.admin-password}" | base64 --decode

# 访问Grafana
# 地址:http://<node-ip>:30000
# 用户名:admin
# 密码:上述命令获取的密码

导入 ID 为 12856 的 Spring Boot 监控面板,即可查看 JVM、请求量、响应时间等指标。

5.2 日志收集与分析

  1. 部署 ELK Stack 或 EFK Stack:
# 使用Helm安装ELK
helm repo add elastic https://helm.elastic.co
helm repo update

# 安装Elasticsearch
helm install elasticsearch elastic/elasticsearch \
  --namespace logging \
  --create-namespace \
  --set replicas=1 \
  --set minimumMasterNodes=1

# 安装Kibana
helm install kibana elastic/kibana \
  --namespace logging \
  --set service.type=NodePort \
  --set service.nodePort=30001

# 安装Filebeat
helm install filebeat elastic/filebeat \
  --namespace logging \
  --set filebeat.inputs[0].type=container \
  --set filebeat.inputs[0].paths=[/var/log/containers/*.log] \
  --set output.elasticsearch.hosts[0]=elasticsearch-master:9200
  1. 配置 Spring Boot 日志输出格式:
# 在application.yml中添加
logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
  level:
    root: info
    com.ken.demo: info
  1. 在 Kibana 中查看日志:

访问 Kibana(http://<node-ip>:30001),创建索引模式filebeat-*,即可在 Discover 页面查看和搜索日志。

5.3 分布式追踪

集成 Spring Cloud Sleuth 和 Zipkin 实现分布式追踪:

  1. 添加依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
    <version>3.1.8</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
    <version>3.1.8</version>
</dependency>
  1. 配置 Zipkin:
# application.yml
spring:
  sleuth:
    sampler:
      probability: 1.0  # 开发环境采样率100%,生产环境可调整为0.1
  zipkin:
    base-url: http://zipkin:9411  # Zipkin服务地址
  1. 部署 Zipkin:
# zipkin-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: zipkin
  namespace: springboot-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: zipkin
  template:
    metadata:
      labels:
        app: zipkin
    spec:
      containers:
      - name: zipkin
        image: openzipkin/zipkin:2.24.3
        ports:
        - containerPort: 9411
---
apiVersion: v1
kind: Service
metadata:
  name: zipkin
  namespace: springboot-app
spec:
  selector:
    app: zipkin
  ports:
  - port: 9411
    targetPort: 9411
  type: NodePort
  nodePort: 30002
  1. 部署 Zipkin:
kubectl apply -f zipkin-deployment.yaml
  1. 访问 Zipkin 控制台(http://<node-ip>:30002),查看分布式追踪信息。

六、CI/CD 流水线实现

使用 GitLab CI/CD 实现自动化构建、测试和部署:

  1. 创建.gitlab-ci.yml文件:
stages:
  - build
  - test
  - package
  - deploy

variables:
  PROJECT_NAME: "springboot-k8s-demo"
  VERSION: "1.0.0"
  DOCKER_REGISTRY: "your-registry-url"
  K8S_NAMESPACE: "springboot-app"

# 构建阶段
build:
  stage: build
  image: maven:3.9.6-eclipse-temurin-17
  script:
    - mvn clean compile
  artifacts:
    paths:
      - target/classes/
    expire_in: 1 hour

# 测试阶段
test:
  stage: test
  image: maven:3.9.6-eclipse-temurin-17
  script:
    - mvn test
  artifacts:
    paths:
      - target/surefire-reports/
    expire_in: 1 day

# 打包镜像阶段
package:
  stage: package
  image: docker:26.1.4
  services:
    - docker:26.1.4-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY
    - docker build -t $DOCKER_REGISTRY/$PROJECT_NAME:$VERSION .
    - docker push $DOCKER_REGISTRY/$PROJECT_NAME:$VERSION
  only:
    - main

# 部署阶段
deploy:
  stage: deploy
  image: bitnami/kubectl:1.28
  script:
    - kubectl config use-context your-k8s-context
    - kubectl set image deployment/$PROJECT_NAME $PROJECT_NAME=$DOCKER_REGISTRY/$PROJECT_NAME:$VERSION -n $K8S_NAMESPACE
    - kubectl rollout status deployment/$PROJECT_NAME -n $K8S_NAMESPACE
  only:
    - main
  1. 流水线执行流程:

七、最佳实践与常见问题

7.1 云原生改造最佳实践

  1. 容器化最佳实践

    • 采用多阶段构建减小镜像大小
    • 避免在容器中运行多个进程
    • 不要使用 latest 标签,始终使用固定版本
    • 配置适当的健康检查和就绪探针
    • 以非 root 用户运行容器
  2. 资源管理最佳实践

    • 为所有容器设置资源请求和限制
    • 合理设置 JVM 内存参数,避免内存溢出
    • 根据应用特性调整 HPA 参数
    • 对有状态应用使用 StatefulSet
  3. 配置管理最佳实践

    • 敏感配置使用 Secret 存储
    • 非敏感配置使用 ConfigMap 存储
    • 利用 Spring Cloud Kubernetes 实现配置动态刷新
    • 避免在代码中硬编码配置
  4. 监控与日志最佳实践

    • 实现全面的健康检查
    • 规范日志格式,便于集中收集和分析
    • 暴露关键业务指标
    • 配置适当的告警阈值

7.2 常见问题及解决方案

  1. JVM 内存溢出

    • 原因:容器环境中 JVM 默认不会感知容器内存限制
    • 解决方案:使用 - XX:+UseContainerSupport 和 - XX:MaxRAMPercentage 参数
  2. 应用启动缓慢

    • 原因:资源限制不足或初始化逻辑复杂
    • 解决方案:调整资源请求、优化初始化逻辑、延长就绪探针初始延迟
  3. 配置更新不生效

    • 原因:未使用 @RefreshScope 注解或配置映射错误
    • 解决方案:在配置类上添加 @RefreshScope、检查配置映射是否正确
  4. 服务发现失败

    • 原因:Service 配置错误或网络策略限制
    • 解决方案:检查 Service 标签选择器、检查网络策略
  5. 自动伸缩不触发

    • 原因:HPA 配置错误或指标采集问题
    • 解决方案:检查 HPA 配置、确认 metrics-server 正常运行

八、总结与展望

将 Spring Boot 应用迁移到 Kubernetes 是一个系统性工程,涉及容器化改造、配置管理、服务发现、监控告警等多个方面。通过本文的实战指南,我们了解了从应用容器化到最终在 K8s 集群中部署和运维的完整流程。

附录:参考资源

  1. Spring Boot 官方文档
  2. Kubernetes 官方文档
  3. Spring Cloud Kubernetes 文档
  4. Docker 官方文档
  5. CNCF 云原生定义
  6. Spring Boot 应用的 Docker 最佳实践
  7. Prometheus 监控 Spring Boot 应用
  8. GitLab CI/CD 文档